summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorJaakko Keränen <jaakko.keranen@iki.fi>2022-01-20 12:06:39 +0200
committerJaakko Keränen <jaakko.keranen@iki.fi>2022-01-20 12:06:39 +0200
commit6da3bdb612e0ead07ff93f4f6dc5aef46c7b9c9e (patch)
tree4378984d744dc67b7453573524d5839b9ce7f9d5 /src
parentd5169339b3454c80a6f2ed5f8cb937e5d5613fc0 (diff)
parent33816278c84fd7ac7e895f4111229c4ff4436b53 (diff)
Merge branch 'dev' of skyjake.fi:gemini/lagrange into dev
Diffstat (limited to 'src')
-rw-r--r--src/app.c397
-rw-r--r--src/app.h3
-rw-r--r--src/audio/player.c25
-rw-r--r--src/bookmarks.c132
-rw-r--r--src/bookmarks.h77
-rw-r--r--src/defs.h29
-rw-r--r--src/feeds.c22
-rw-r--r--src/fontpack.c2
-rw-r--r--src/gmcerts.c29
-rw-r--r--src/gmcerts.h5
-rw-r--r--src/gmdocument.c84
-rw-r--r--src/gmdocument.h2
-rw-r--r--src/gmrequest.c13
-rw-r--r--src/gmutil.c43
-rw-r--r--src/gmutil.h10
-rw-r--r--src/history.c80
-rw-r--r--src/history.h16
-rw-r--r--src/ios.h29
-rw-r--r--src/ios.m319
-rw-r--r--src/macos.h2
-rw-r--r--src/macos.m233
-rw-r--r--src/main.c4
-rw-r--r--src/media.c13
-rw-r--r--src/media.h3
-rw-r--r--src/mimehooks.c3
-rw-r--r--src/periodic.c26
-rw-r--r--src/periodic.h1
-rw-r--r--src/prefs.c16
-rw-r--r--src/prefs.h138
-rw-r--r--src/resources.c12
-rw-r--r--src/sitespec.c83
-rw-r--r--src/sitespec.h20
-rw-r--r--src/ui/banner.c8
-rw-r--r--src/ui/bindingswidget.c6
-rw-r--r--src/ui/certimportwidget.c5
-rw-r--r--src/ui/certlistwidget.c490
-rw-r--r--src/ui/certlistwidget.h40
-rw-r--r--src/ui/color.c6
-rw-r--r--src/ui/color.h2
-rw-r--r--src/ui/command.c77
-rw-r--r--src/ui/documentwidget.c4489
-rw-r--r--src/ui/documentwidget.h6
-rw-r--r--src/ui/inputwidget.c809
-rw-r--r--src/ui/inputwidget.h1
-rw-r--r--src/ui/keys.c1
-rw-r--r--src/ui/labelwidget.c108
-rw-r--r--src/ui/labelwidget.h2
-rw-r--r--src/ui/linkinfo.c176
-rw-r--r--src/ui/linkinfo.h47
-rw-r--r--src/ui/listwidget.c82
-rw-r--r--src/ui/listwidget.h31
-rw-r--r--src/ui/lookupwidget.c28
-rw-r--r--src/ui/mediaui.c52
-rw-r--r--src/ui/mobile.c146
-rw-r--r--src/ui/mobile.h14
-rw-r--r--src/ui/paint.c11
-rw-r--r--src/ui/paint.h1
-rw-r--r--src/ui/root.c511
-rw-r--r--src/ui/root.h8
-rw-r--r--src/ui/scrollwidget.c4
-rw-r--r--src/ui/sidebarwidget.c925
-rw-r--r--src/ui/sidebarwidget.h1
-rw-r--r--src/ui/text.c193
-rw-r--r--src/ui/text.h11
-rw-r--r--src/ui/text_simple.c5
-rw-r--r--src/ui/touch.c55
-rw-r--r--src/ui/touch.h4
-rw-r--r--src/ui/uploadwidget.c200
-rw-r--r--src/ui/uploadwidget.h1
-rw-r--r--src/ui/util.c444
-rw-r--r--src/ui/util.h31
-rw-r--r--src/ui/visbuf.c3
-rw-r--r--src/ui/widget.c102
-rw-r--r--src/ui/widget.h28
-rw-r--r--src/ui/window.c48
-rw-r--r--src/ui/window.h8
-rw-r--r--src/visited.c2
77 files changed, 7543 insertions, 3550 deletions
diff --git a/src/app.c b/src/app.c
index 28b32939..6392e7fa 100644
--- a/src/app.c
+++ b/src/app.c
@@ -55,6 +55,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
55#include <the_Foundation/path.h> 55#include <the_Foundation/path.h>
56#include <the_Foundation/process.h> 56#include <the_Foundation/process.h>
57#include <the_Foundation/sortedarray.h> 57#include <the_Foundation/sortedarray.h>
58#include <the_Foundation/stringset.h>
58#include <the_Foundation/time.h> 59#include <the_Foundation/time.h>
59#include <the_Foundation/version.h> 60#include <the_Foundation/version.h>
60#include <SDL.h> 61#include <SDL.h>
@@ -71,6 +72,9 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
71#if defined (iPlatformAppleMobile) 72#if defined (iPlatformAppleMobile)
72# include "ios.h" 73# include "ios.h"
73#endif 74#endif
75#if defined (iPlatformAndroidMobile)
76#include <SDL_log.h>
77#endif
74#if defined (iPlatformMsys) 78#if defined (iPlatformMsys)
75# include "win32.h" 79# include "win32.h"
76#endif 80#endif
@@ -108,6 +112,7 @@ static const char *defaultDataDir_App_ = "~/config/settings/lagrange";
108static const char *prefsFileName_App_ = "prefs.cfg"; 112static const char *prefsFileName_App_ = "prefs.cfg";
109static const char *oldStateFileName_App_ = "state.binary"; 113static const char *oldStateFileName_App_ = "state.binary";
110static const char *stateFileName_App_ = "state.lgr"; 114static const char *stateFileName_App_ = "state.lgr";
115static const char *tempStateFileName_App_ = "state.lgr.tmp";
111static const char *defaultDownloadDir_App_ = "~/Downloads"; 116static const char *defaultDownloadDir_App_ = "~/Downloads";
112 117
113static const int idleThreshold_App_ = 1000; /* ms */ 118static const int idleThreshold_App_ = 1000; /* ms */
@@ -115,6 +120,7 @@ static const int idleThreshold_App_ = 1000; /* ms */
115struct Impl_App { 120struct Impl_App {
116 iCommandLine args; 121 iCommandLine args;
117 iString * execPath; 122 iString * execPath;
123 iStringSet * tempFilesPendingDeletion;
118 iMimeHooks * mimehooks; 124 iMimeHooks * mimehooks;
119 iGmCerts * certs; 125 iGmCerts * certs;
120 iVisited * visited; 126 iVisited * visited;
@@ -127,6 +133,7 @@ struct Impl_App {
127 iBool isRunning; 133 iBool isRunning;
128 iBool isRunningUnderWindowSystem; 134 iBool isRunningUnderWindowSystem;
129 iBool isDarkSystemTheme; 135 iBool isDarkSystemTheme;
136 iBool isSuspended;
130#if defined (LAGRANGE_ENABLE_IDLE_SLEEP) 137#if defined (LAGRANGE_ENABLE_IDLE_SLEEP)
131 iBool isIdling; 138 iBool isIdling;
132 uint32_t lastEventTime; 139 uint32_t lastEventTime;
@@ -164,7 +171,11 @@ struct Impl_Ticker {
164 171
165static int cmp_Ticker_(const void *a, const void *b) { 172static int cmp_Ticker_(const void *a, const void *b) {
166 const iTicker *elems[2] = { a, b }; 173 const iTicker *elems[2] = { a, b };
167 return iCmp(elems[0]->context, elems[1]->context); 174 const int cmp = iCmp(elems[0]->context, elems[1]->context);
175 if (cmp) {
176 return cmp;
177 }
178 return iCmp((void *) elems[0]->callback, (void *) elems[1]->callback);
168} 179}
169 180
170/*----------------------------------------------------------------------------------------------*/ 181/*----------------------------------------------------------------------------------------------*/
@@ -237,6 +248,13 @@ static iString *serializePrefs_App_(const iApp *d) {
237 appendFormat_String(str, "linewidth.set arg:%d\n", d->prefs.lineWidth); 248 appendFormat_String(str, "linewidth.set arg:%d\n", d->prefs.lineWidth);
238 appendFormat_String(str, "linespacing.set arg:%f\n", d->prefs.lineSpacing); 249 appendFormat_String(str, "linespacing.set arg:%f\n", d->prefs.lineSpacing);
239 appendFormat_String(str, "returnkey.set arg:%d\n", d->prefs.returnKey); 250 appendFormat_String(str, "returnkey.set arg:%d\n", d->prefs.returnKey);
251 for (size_t i = 0; i < iElemCount(d->prefs.navbarActions); i++) {
252 appendFormat_String(str, "navbar.action.set arg:%d button:%d\n", d->prefs.navbarActions[i], i);
253 }
254#if defined (iPlatformMobile)
255 appendFormat_String(str, "toolbar.action.set arg:%d button:0\n", d->prefs.toolbarActions[0]);
256 appendFormat_String(str, "toolbar.action.set arg:%d button:1\n", d->prefs.toolbarActions[1]);
257#endif
240 iConstForEach(StringSet, fp, d->prefs.disabledFontPacks) { 258 iConstForEach(StringSet, fp, d->prefs.disabledFontPacks) {
241 appendFormat_String(str, "fontpack.disable id:%s\n", cstr_String(fp.value)); 259 appendFormat_String(str, "fontpack.disable id:%s\n", cstr_String(fp.value));
242 } 260 }
@@ -263,6 +281,7 @@ static iString *serializePrefs_App_(const iApp *d) {
263 { "prefs.bookmarks.addbottom", &d->prefs.addBookmarksToBottom }, 281 { "prefs.bookmarks.addbottom", &d->prefs.addBookmarksToBottom },
264 { "prefs.archive.openindex", &d->prefs.openArchiveIndexPages }, 282 { "prefs.archive.openindex", &d->prefs.openArchiveIndexPages },
265 { "prefs.font.warnmissing", &d->prefs.warnAboutMissingGlyphs }, 283 { "prefs.font.warnmissing", &d->prefs.warnAboutMissingGlyphs },
284 { "prefs.blink", &d->prefs.blinkingCursor },
266 }; 285 };
267 iForIndices(i, boolPrefs) { 286 iForIndices(i, boolPrefs) {
268 appendFormat_String(str, "%s.changed arg:%d\n", boolPrefs[i].id, *boolPrefs[i].value); 287 appendFormat_String(str, "%s.changed arg:%d\n", boolPrefs[i].id, *boolPrefs[i].value);
@@ -313,6 +332,9 @@ static const char *dataDir_App_(void) {
313} 332}
314 333
315static const char *downloadDir_App_(void) { 334static const char *downloadDir_App_(void) {
335#if defined (iPlatformAndroidMobile)
336 return concatPath_CStr(SDL_AndroidGetInternalStoragePath(), "Downloads");
337#endif
316#if defined (iPlatformLinux) || defined (iPlatformOther) 338#if defined (iPlatformLinux) || defined (iPlatformOther)
317 /* Parse user-dirs.dirs using the `xdg-user-dir` tool. */ 339 /* Parse user-dirs.dirs using the `xdg-user-dir` tool. */
318 iProcess *proc = iClob(new_Process()); 340 iProcess *proc = iClob(new_Process());
@@ -363,7 +385,7 @@ static void loadPrefs_App_(iApp *d) {
363 setUiScale_Window(get_Window(), argf_Command(cmd)); 385 setUiScale_Window(get_Window(), argf_Command(cmd));
364 } 386 }
365 else if (equal_Command(cmd, "uilang")) { 387 else if (equal_Command(cmd, "uilang")) {
366 const char *id = cstr_Rangecc(range_Command(cmd, "id")); 388 const char *id = cstr_Command(cmd, "id");
367 setCStr_String(&d->prefs.strings[uiLanguage_PrefsString], id); 389 setCStr_String(&d->prefs.strings[uiLanguage_PrefsString], id);
368 setCurrent_Lang(id); 390 setCurrent_Lang(id);
369 } 391 }
@@ -385,6 +407,12 @@ static void loadPrefs_App_(iApp *d) {
385 insert_StringSet(d->prefs.disabledFontPacks, 407 insert_StringSet(d->prefs.disabledFontPacks,
386 collect_String(suffix_Command(cmd, "id"))); 408 collect_String(suffix_Command(cmd, "id")));
387 } 409 }
410#if defined (iPlatformAndroidMobile)
411 else if (equal_Command(cmd, "returnkey.set")) {
412 /* Hardcoded to avoid accidental presses of the virtual Return key. */
413 d->prefs.returnKey = default_ReturnKeyBehavior;
414 }
415#endif
388#if !defined (LAGRANGE_ENABLE_DOWNLOAD_EDIT) 416#if !defined (LAGRANGE_ENABLE_DOWNLOAD_EDIT)
389 else if (equal_Command(cmd, "downloads")) { 417 else if (equal_Command(cmd, "downloads")) {
390 continue; /* can't change downloads directory */ 418 continue; /* can't change downloads directory */
@@ -410,11 +438,13 @@ static void loadPrefs_App_(iApp *d) {
410 iRelease(f); 438 iRelease(f);
411 /* Upgrade checks. */ 439 /* Upgrade checks. */
412 if (cmp_Version(&upgradedFromAppVersion, &(iVersion){ 1, 8, 0 }) < 0) { 440 if (cmp_Version(&upgradedFromAppVersion, &(iVersion){ 1, 8, 0 }) < 0) {
441#if !defined (iPlatformAppleMobile) && !defined (iPlatformAndroidMobile)
413 /* When upgrading to v1.8.0, the old hardcoded font library is gone and that means 442 /* When upgrading to v1.8.0, the old hardcoded font library is gone and that means
414 UI strings may not have the right fonts available for the UI to remain 443 UI strings may not have the right fonts available for the UI to remain
415 usable. */ 444 usable. */
416 postCommandf_App("uilang id:en"); 445 postCommandf_App("uilang id:en");
417 postCommand_App("~fontpack.suggest.classic"); 446 postCommand_App("~fontpack.suggest.classic");
447#endif
418 } 448 }
419#if !defined (LAGRANGE_ENABLE_CUSTOM_FRAME) 449#if !defined (LAGRANGE_ENABLE_CUSTOM_FRAME)
420 d->prefs.customFrame = iFalse; 450 d->prefs.customFrame = iFalse;
@@ -505,7 +535,7 @@ static iBool loadState_App_(iApp *d) {
505 if (flags & 8) { 535 if (flags & 8) {
506 postCommand_Widget(sidebar2, "feeds.mode arg:%d", unread_FeedsMode); 536 postCommand_Widget(sidebar2, "feeds.mode arg:%d", unread_FeedsMode);
507 } 537 }
508 if (deviceType_App() != phone_AppDeviceType) { 538 if (deviceType_App() == desktop_AppDeviceType) {
509 setWidth_SidebarWidget(sidebar, widths[0]); 539 setWidth_SidebarWidget(sidebar, widths[0]);
510 setWidth_SidebarWidget(sidebar2, widths[1]); 540 setWidth_SidebarWidget(sidebar2, widths[1]);
511 if (flags & 1) postCommand_Root(root, "sidebar.toggle noanim:1"); 541 if (flags & 1) postCommand_Root(root, "sidebar.toggle noanim:1");
@@ -561,7 +591,7 @@ static void saveState_App_(const iApp *d) {
561 navigation history, cached content) and depends closely on the widget 591 navigation history, cached content) and depends closely on the widget
562 tree. The data is largely not reorderable and should not be modified 592 tree. The data is largely not reorderable and should not be modified
563 by the user manually. */ 593 by the user manually. */
564 iFile *f = newCStr_File(concatPath_CStr(dataDir_App_(), stateFileName_App_)); 594 iFile *f = newCStr_File(concatPath_CStr(dataDir_App_(), tempStateFileName_App_));
565 if (open_File(f, writeOnly_FileMode)) { 595 if (open_File(f, writeOnly_FileMode)) {
566 writeData_File(f, magicState_App_, 4); 596 writeData_File(f, magicState_App_, 4);
567 writeU32_File(f, latest_FileVersion); /* version */ 597 writeU32_File(f, latest_FileVersion); /* version */
@@ -603,11 +633,19 @@ static void saveState_App_(const iApp *d) {
603 write8_File(f, flags); 633 write8_File(f, flags);
604 serializeState_DocumentWidget(i.object, stream_File(f)); 634 serializeState_DocumentWidget(i.object, stream_File(f));
605 } 635 }
636 iRelease(f);
606 } 637 }
607 else { 638 else {
639 iRelease(f);
608 fprintf(stderr, "[App] failed to save state: %s\n", strerror(errno)); 640 fprintf(stderr, "[App] failed to save state: %s\n", strerror(errno));
641 return;
609 } 642 }
610 iRelease(f); 643 /* Copy it over to the real file. This avoids truncation if the app for any reason crashes
644 before the state file is fully written. */
645 const char *tempName = concatPath_CStr(dataDir_App_(), tempStateFileName_App_);
646 const char *finalName = concatPath_CStr(dataDir_App_(), stateFileName_App_);
647 remove(finalName);
648 rename(tempName, finalName);
611} 649}
612 650
613#if defined (LAGRANGE_ENABLE_IDLE_SLEEP) 651#if defined (LAGRANGE_ENABLE_IDLE_SLEEP)
@@ -678,18 +716,22 @@ static void communicateWithRunningInstance_App_(iApp *d, iProcessId instance,
678 appendCStr_String(cmds, "tabs.new\n"); 716 appendCStr_String(cmds, "tabs.new\n");
679 requestRaise = iTrue; 717 requestRaise = iTrue;
680 } 718 }
719 iBool gotResult = iFalse;
681 if (!isEmpty_String(cmds)) { 720 if (!isEmpty_String(cmds)) {
682 iString *result = communicate_Ipc(cmds, requestRaise); 721 iString *result = communicate_Ipc(cmds, requestRaise);
683 if (result) { 722 if (result) {
684 fwrite(cstr_String(result), 1, size_String(result), stdout); 723 fwrite(cstr_String(result), 1, size_String(result), stdout);
685 fflush(stdout); 724 fflush(stdout);
725 if (!isEmpty_String(result)) {
726 gotResult = iTrue;
727 }
686 } 728 }
687 delete_String(result); 729 delete_String(result);
688 } 730 }
689 iUnused(instance); 731 iUnused(instance);
690// else { 732 if (!gotResult) {
691// printf("Lagrange already running (PID %d)\n", instance); 733 printf("Commands sent to Lagrange process %d\n", instance);
692// } 734 }
693 terminate_App_(0); 735 terminate_App_(0);
694} 736}
695#endif /* defined (LAGRANGE_ENABLE_IPC) */ 737#endif /* defined (LAGRANGE_ENABLE_IPC) */
@@ -714,6 +756,8 @@ static void init_App_(iApp *d, int argc, char **argv) {
714 d->isRunningUnderWindowSystem = iTrue; 756 d->isRunningUnderWindowSystem = iTrue;
715#endif 757#endif
716 d->isDarkSystemTheme = iTrue; /* will be updated by system later on, if supported */ 758 d->isDarkSystemTheme = iTrue; /* will be updated by system later on, if supported */
759 d->isSuspended = iFalse;
760 d->tempFilesPendingDeletion = new_StringSet();
717 init_CommandLine(&d->args, argc, argv); 761 init_CommandLine(&d->args, argc, argv);
718 /* Where was the app started from? We ask SDL first because the command line alone 762 /* Where was the app started from? We ask SDL first because the command line alone
719 cannot be relied on (behavior differs depending on OS). */ { 763 cannot be relied on (behavior differs depending on OS). */ {
@@ -920,6 +964,8 @@ static void init_App_(iApp *d, int argc, char **argv) {
920 if (!loadState_App_(d)) { 964 if (!loadState_App_(d)) {
921 postCommand_Root(NULL, "open url:about:help"); 965 postCommand_Root(NULL, "open url:about:help");
922 } 966 }
967 postCommand_App("~navbar.actions.changed");
968 postCommand_App("~toolbar.actions.changed");
923 postCommand_Root(NULL, "~window.unfreeze"); 969 postCommand_Root(NULL, "~window.unfreeze");
924 postCommand_Root(NULL, "font.reset"); 970 postCommand_Root(NULL, "font.reset");
925 d->autoReloadTimer = SDL_AddTimer(60 * 1000, postAutoReloadCommand_App_, NULL); 971 d->autoReloadTimer = SDL_AddTimer(60 * 1000, postAutoReloadCommand_App_, NULL);
@@ -986,6 +1032,11 @@ static void deinit_App(iApp *d) {
986 deinit_Periodic(&d->periodic); 1032 deinit_Periodic(&d->periodic);
987 deinit_Lang(); 1033 deinit_Lang();
988 iRecycle(); 1034 iRecycle();
1035 /* Delete all temporary files created while running. */
1036 iConstForEach(StringSet, tmp, d->tempFilesPendingDeletion) {
1037 remove(cstr_String(tmp.value));
1038}
1039 iRelease(d->tempFilesPendingDeletion);
989} 1040}
990 1041
991const iString *execPath_App(void) { 1042const iString *execPath_App(void) {
@@ -1000,7 +1051,7 @@ const iString *downloadDir_App(void) {
1000 return collect_String(cleaned_Path(&app_.prefs.strings[downloadDir_PrefsString])); 1051 return collect_String(cleaned_Path(&app_.prefs.strings[downloadDir_PrefsString]));
1001} 1052}
1002 1053
1003const iString *downloadPathForUrl_App(const iString *url, const iString *mime) { 1054const iString *fileNameForUrl_App(const iString *url, const iString *mime) {
1004 /* Figure out a file name from the URL. */ 1055 /* Figure out a file name from the URL. */
1005 iUrl parts; 1056 iUrl parts;
1006 init_Url(&parts, url); 1057 init_Url(&parts, url);
@@ -1026,22 +1077,27 @@ const iString *downloadPathForUrl_App(const iString *url, const iString *mime) {
1026 } 1077 }
1027 } 1078 }
1028 if (startsWith_String(name, "~")) { 1079 if (startsWith_String(name, "~")) {
1029 /* This would be interpreted as a reference to a home directory. */ 1080 /* This might be interpreted as a reference to a home directory. */
1030 remove_Block(&name->chars, 0, 1); 1081 remove_Block(&name->chars, 0, 1);
1031 } 1082 }
1032 iString *savePath = concat_Path(downloadDir_App(), name); 1083 if (lastIndexOfCStr_String(name, ".") == iInvalidPos) {
1033 if (lastIndexOfCStr_String(savePath, ".") == iInvalidPos) { 1084 /* TODO: Needs the inverse of `mediaTypeFromFileExtension_String()`. */
1034 /* No extension specified in URL. */ 1085 /* No extension specified in URL. */
1035 if (startsWith_String(mime, "text/gemini")) { 1086 if (startsWith_String(mime, "text/gemini")) {
1036 appendCStr_String(savePath, ".gmi"); 1087 appendCStr_String(name, ".gmi");
1037 } 1088 }
1038 else if (startsWith_String(mime, "text/")) { 1089 else if (startsWith_String(mime, "text/")) {
1039 appendCStr_String(savePath, ".txt"); 1090 appendCStr_String(name, ".txt");
1040 } 1091 }
1041 else if (startsWith_String(mime, "image/")) { 1092 else if (startsWith_String(mime, "image/")) {
1042 appendCStr_String(savePath, cstr_String(mime) + 6); 1093 appendCStr_String(name, cstr_String(mime) + 6);
1043 } 1094 }
1044 } 1095 }
1096 return name;
1097}
1098
1099const iString *downloadPathForUrl_App(const iString *url, const iString *mime) {
1100 iString *savePath = concat_Path(downloadDir_App(), fileNameForUrl_App(url, mime));
1045 if (fileExists_FileInfo(savePath)) { 1101 if (fileExists_FileInfo(savePath)) {
1046 /* Make it unique. */ 1102 /* Make it unique. */
1047 iDate now; 1103 iDate now;
@@ -1056,6 +1112,22 @@ const iString *downloadPathForUrl_App(const iString *url, const iString *mime) {
1056 return collect_String(savePath); 1112 return collect_String(savePath);
1057} 1113}
1058 1114
1115const iString *temporaryPathForUrl_App(const iString *url, const iString *mime) {
1116 iApp *d = &app_;
1117#if defined (P_tmpdir)
1118 iString * tmpPath = collectNew_String();
1119 const iRangecc tmpDir = range_CStr(P_tmpdir);
1120#else
1121 iString * tmpPath = collectNewCStr_String(tmpnam(NULL));
1122 const iRangecc tmpDir = dirName_Path(tmpPath);
1123#endif
1124 set_String(
1125 tmpPath,
1126 collect_String(concat_Path(collectNewRange_String(tmpDir), fileNameForUrl_App(url, mime))));
1127 insert_StringSet(d->tempFilesPendingDeletion, tmpPath); /* deleted in `deinit_App` */
1128 return tmpPath;
1129}
1130
1059const iString *debugInfo_App(void) { 1131const iString *debugInfo_App(void) {
1060 extern char **environ; /* The environment variables. */ 1132 extern char **environ; /* The environment variables. */
1061 iApp *d = &app_; 1133 iApp *d = &app_;
@@ -1175,9 +1247,6 @@ iBool findCachedContent_App(const iString *url, iString *mime_out, iBlock *data_
1175#endif 1247#endif
1176 1248
1177iLocalDef iBool isWaitingAllowed_App_(iApp *d) { 1249iLocalDef iBool isWaitingAllowed_App_(iApp *d) {
1178 if (!isEmpty_Periodic(&d->periodic)) {
1179 return iFalse;
1180 }
1181 if (d->warmupFrames > 0) { 1250 if (d->warmupFrames > 0) {
1182 return iFalse; 1251 return iFalse;
1183 } 1252 }
@@ -1191,16 +1260,19 @@ iLocalDef iBool isWaitingAllowed_App_(iApp *d) {
1191 1260
1192static iBool nextEvent_App_(iApp *d, enum iAppEventMode eventMode, SDL_Event *event) { 1261static iBool nextEvent_App_(iApp *d, enum iAppEventMode eventMode, SDL_Event *event) {
1193 if (eventMode == waitForNewEvents_AppEventMode && isWaitingAllowed_App_(d)) { 1262 if (eventMode == waitForNewEvents_AppEventMode && isWaitingAllowed_App_(d)) {
1194 /* If there are periodic commands pending, wait only for a short while. */
1195 if (!isEmpty_Periodic(&d->periodic)) {
1196 return SDL_WaitEventTimeout(event, 500);
1197 }
1198 /* We may be allowed to block here until an event comes in. */ 1263 /* We may be allowed to block here until an event comes in. */
1199 if (isWaitingAllowed_App_(d)) { 1264 if (isWaitingAllowed_App_(d)) {
1200 return SDL_WaitEvent(event); 1265 return SDL_WaitEvent(event);
1201 } 1266 }
1202 } 1267 }
1268 /* SDL regression circa 2.0.18? SDL_PollEvent() doesn't always return
1269 events posted immediately beforehand. Waiting with a very short timeout
1270 seems to work better. */
1271#if defined (iPlatformLinux) && SDL_VERSION_ATLEAST(2, 0, 18)
1272 return SDL_WaitEventTimeout(event, 1);
1273#else
1203 return SDL_PollEvent(event); 1274 return SDL_PollEvent(event);
1275#endif
1204} 1276}
1205 1277
1206static iPtrArray *listWindows_App_(const iApp *d, iPtrArray *windows) { 1278static iPtrArray *listWindows_App_(const iApp *d, iPtrArray *windows) {
@@ -1239,6 +1311,7 @@ void processEvents_App(enum iAppEventMode eventMode) {
1239 break; 1311 break;
1240 case SDL_APP_WILLENTERFOREGROUND: 1312 case SDL_APP_WILLENTERFOREGROUND:
1241 invalidate_Window(as_Window(d->window)); 1313 invalidate_Window(as_Window(d->window));
1314 d->isSuspended = iFalse;
1242 break; 1315 break;
1243 case SDL_APP_DIDENTERFOREGROUND: 1316 case SDL_APP_DIDENTERFOREGROUND:
1244 gotEvents = iTrue; 1317 gotEvents = iTrue;
@@ -1256,6 +1329,7 @@ void processEvents_App(enum iAppEventMode eventMode) {
1256 setFreezeDraw_MainWindow(d->window, iTrue); 1329 setFreezeDraw_MainWindow(d->window, iTrue);
1257 savePrefs_App_(d); 1330 savePrefs_App_(d);
1258 saveState_App_(d); 1331 saveState_App_(d);
1332 d->isSuspended = iTrue;
1259 break; 1333 break;
1260 case SDL_APP_TERMINATING: 1334 case SDL_APP_TERMINATING:
1261 setFreezeDraw_MainWindow(d->window, iTrue); 1335 setFreezeDraw_MainWindow(d->window, iTrue);
@@ -1284,6 +1358,10 @@ void processEvents_App(enum iAppEventMode eventMode) {
1284 break; 1358 break;
1285 } 1359 }
1286 default: { 1360 default: {
1361 if (ev.type == SDL_USEREVENT && ev.user.code == periodic_UserEventCode) {
1362 dispatchCommands_Periodic(&d->periodic);
1363 continue;
1364 }
1287#if defined (LAGRANGE_ENABLE_IDLE_SLEEP) 1365#if defined (LAGRANGE_ENABLE_IDLE_SLEEP)
1288 if (ev.type == SDL_USEREVENT && ev.user.code == asleep_UserEventCode) { 1366 if (ev.type == SDL_USEREVENT && ev.user.code == asleep_UserEventCode) {
1289 if (SDL_GetTicks() - d->lastEventTime > idleThreshold_App_ && 1367 if (SDL_GetTicks() - d->lastEventTime > idleThreshold_App_ &&
@@ -1323,28 +1401,6 @@ void processEvents_App(enum iAppEventMode eventMode) {
1323#endif 1401#endif
1324 /* Scroll events may be per-pixel or mouse wheel steps. */ 1402 /* Scroll events may be per-pixel or mouse wheel steps. */
1325 if (ev.type == SDL_MOUSEWHEEL) { 1403 if (ev.type == SDL_MOUSEWHEEL) {
1326#if defined (iPlatformAppleDesktop)
1327 /* On macOS, we handle both trackpad and mouse events. We expect SDL to identify
1328 which device is sending the event. */
1329 if (ev.wheel.which == 0) {
1330 /* Trackpad with precise scrolling w/inertia (points). */
1331 setPerPixel_MouseWheelEvent(&ev.wheel, iTrue);
1332 ev.wheel.x *= -d->window->base.pixelRatio;
1333 ev.wheel.y *= d->window->base.pixelRatio;
1334 /* Only scroll on one axis at a time. */
1335 if (iAbs(ev.wheel.x) > iAbs(ev.wheel.y)) {
1336 ev.wheel.y = 0;
1337 }
1338 else {
1339 ev.wheel.x = 0;
1340 }
1341 }
1342 else {
1343 /* Disregard wheel acceleration applied by the OS. */
1344 ev.wheel.x = -ev.wheel.x;
1345 ev.wheel.y = iSign(ev.wheel.y);
1346 }
1347#endif
1348#if defined (iPlatformMsys) 1404#if defined (iPlatformMsys)
1349 ev.wheel.x = -ev.wheel.x; 1405 ev.wheel.x = -ev.wheel.x;
1350#endif 1406#endif
@@ -1460,10 +1516,10 @@ void processEvents_App(enum iAppEventMode eventMode) {
1460 deinit_PtrArray(&windows); 1516 deinit_PtrArray(&windows);
1461#if defined (LAGRANGE_ENABLE_IDLE_SLEEP) 1517#if defined (LAGRANGE_ENABLE_IDLE_SLEEP)
1462 if (d->isIdling && !gotEvents) { 1518 if (d->isIdling && !gotEvents) {
1463 /* This is where we spend most of our time when idle. 60 Hz still quite a lot but we 1519 /* This is where we spend most of our time when idle. 30 Hz still quite a lot but we
1464 can't wait too long after the user tries to interact again with the app. In any 1520 can't wait too long after the user tries to interact again with the app. In any
1465 case, on macOS SDL_WaitEvent() seems to use 10x more CPU time than sleeping. */ 1521 case, on iOS SDL_WaitEvent() seems to use 10x more CPU time than sleeping (2.0.18). */
1466 SDL_Delay(1000 / 60); 1522 SDL_Delay(1000 / 30);
1467 } 1523 }
1468#endif 1524#endif
1469backToMainLoop:; 1525backToMainLoop:;
@@ -1478,6 +1534,12 @@ static void runTickers_App_(iApp *d) {
1478 d->lastTickerTime = 0; 1534 d->lastTickerTime = 0;
1479 return; 1535 return;
1480 } 1536 }
1537 iForIndices(i, d->window->base.roots) {
1538 iRoot *root = d->window->base.roots[i];
1539 if (root) {
1540 root->didAnimateVisualOffsets = iFalse;
1541 }
1542 }
1481 /* Tickers may add themselves again, so we'll run off a copy. */ 1543 /* Tickers may add themselves again, so we'll run off a copy. */
1482 iSortedArray *pending = copy_SortedArray(&d->tickers); 1544 iSortedArray *pending = copy_SortedArray(&d->tickers);
1483 clear_SortedArray(&d->tickers); 1545 clear_SortedArray(&d->tickers);
@@ -1494,6 +1556,12 @@ static void runTickers_App_(iApp *d) {
1494 if (isEmpty_SortedArray(&d->tickers)) { 1556 if (isEmpty_SortedArray(&d->tickers)) {
1495 d->lastTickerTime = 0; 1557 d->lastTickerTime = 0;
1496 } 1558 }
1559// iForIndices(i, d->window->base.roots) {
1560// iRoot *root = d->window->base.roots[i];
1561// if (root) {
1562// notifyVisualOffsetChange_Root(root);
1563// }
1564// }
1497} 1565}
1498 1566
1499static int resizeWatcher_(void *user, SDL_Event *event) { 1567static int resizeWatcher_(void *user, SDL_Event *event) {
@@ -1526,7 +1594,6 @@ static int run_App_(iApp *d) {
1526 SDL_AddEventWatch(resizeWatcher_, d); /* redraw window during resizing */ 1594 SDL_AddEventWatch(resizeWatcher_, d); /* redraw window during resizing */
1527#endif 1595#endif
1528 while (d->isRunning) { 1596 while (d->isRunning) {
1529 dispatchCommands_Periodic(&d->periodic);
1530 processEvents_App(waitForNewEvents_AppEventMode); 1597 processEvents_App(waitForNewEvents_AppEventMode);
1531 runTickers_App_(d); 1598 runTickers_App_(d);
1532 refresh_App(); 1599 refresh_App();
@@ -1680,13 +1747,20 @@ void postCommand_Root(iRoot *d, const char *command) {
1680 ev.user.data1 = strdup(command); 1747 ev.user.data1 = strdup(command);
1681 ev.user.data2 = d; /* all events are root-specific */ 1748 ev.user.data2 = d; /* all events are root-specific */
1682 SDL_PushEvent(&ev); 1749 SDL_PushEvent(&ev);
1750 iWindow *win = get_Window();
1751#if defined (iPlatformAndroid)
1752 SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "%s[command] {%d} %s",
1753 app_.isLoadingPrefs ? "[Prefs] " : "",
1754 (d == NULL || win == NULL ? 0 : d == win->roots[0] ? 1 : 2),
1755 command);
1756#else
1683 if (app_.commandEcho) { 1757 if (app_.commandEcho) {
1684 iWindow *win = get_Window();
1685 printf("%s[command] {%d} %s\n", 1758 printf("%s[command] {%d} %s\n",
1686 app_.isLoadingPrefs ? "[Prefs] " : "", 1759 app_.isLoadingPrefs ? "[Prefs] " : "",
1687 (d == NULL || win == NULL ? 0 : d == win->roots[0] ? 1 : 2), 1760 (d == NULL || win == NULL ? 0 : d == win->roots[0] ? 1 : 2),
1688 command); fflush(stdout); 1761 command); fflush(stdout);
1689 } 1762 }
1763#endif
1690} 1764}
1691 1765
1692void postCommandf_Root(iRoot *d, const char *command, ...) { 1766void postCommandf_Root(iRoot *d, const char *command, ...) {
@@ -1823,6 +1897,12 @@ static void updatePrefsPinSplitButtons_(iWidget *d, int value) {
1823 } 1897 }
1824} 1898}
1825 1899
1900static void updatePrefsToolBarActionButton_(iWidget *prefs, int buttonIndex, int action) {
1901 updateDropdownSelection_LabelWidget(
1902 findChild_Widget(prefs, format_CStr("prefs.toolbaraction%d", buttonIndex + 1)),
1903 format_CStr(" arg:%d button:%d", action, buttonIndex));
1904}
1905
1826static void updateScrollSpeedButtons_(iWidget *d, enum iScrollType type, const int value) { 1906static void updateScrollSpeedButtons_(iWidget *d, enum iScrollType type, const int value) {
1827 const char *typeStr = (type == mouse_ScrollType ? "mouse" : "keyboard"); 1907 const char *typeStr = (type == mouse_ScrollType ? "mouse" : "keyboard");
1828 for (int i = 0; i <= 40; i++) { 1908 for (int i = 0; i <= 40; i++) {
@@ -1913,6 +1993,10 @@ static iBool handlePrefsCommands_(iWidget *d, const char *cmd) {
1913 format_CStr("returnkey.set arg:%d", arg_Command(cmd))); 1993 format_CStr("returnkey.set arg:%d", arg_Command(cmd)));
1914 return iFalse; 1994 return iFalse;
1915 } 1995 }
1996 else if (equal_Command(cmd, "toolbar.action.set")) {
1997 updatePrefsToolBarActionButton_(d, argLabel_Command(cmd, "button"), arg_Command(cmd));
1998 return iFalse;
1999 }
1916 else if (equal_Command(cmd, "pinsplit.set")) { 2000 else if (equal_Command(cmd, "pinsplit.set")) {
1917 updatePrefsPinSplitButtons_(d, arg_Command(cmd)); 2001 updatePrefsPinSplitButtons_(d, arg_Command(cmd));
1918 return iFalse; 2002 return iFalse;
@@ -1957,8 +2041,11 @@ static iBool handlePrefsCommands_(iWidget *d, const char *cmd) {
1957 } 2041 }
1958 } 2042 }
1959 else if (equalWidget_Command(cmd, d, "input.resized")) { 2043 else if (equalWidget_Command(cmd, d, "input.resized")) {
1960 updatePreferencesLayout_Widget(d); 2044 if (!d->root->pendingArrange) {
1961 return iFalse; 2045 d->root->pendingArrange = iTrue;
2046 postCommand_Root(d->root, "root.arrange");
2047 }
2048 return iTrue;
1962 } 2049 }
1963 return iFalse; 2050 return iFalse;
1964} 2051}
@@ -2176,6 +2263,7 @@ iBool handleCommand_App(const char *cmd) {
2176 return iTrue; 2263 return iTrue;
2177 } 2264 }
2178 else if (equal_Command(cmd, "fontpack.suggest.classic")) { 2265 else if (equal_Command(cmd, "fontpack.suggest.classic")) {
2266 /* TODO: Don't use this when system fonts are accessible. */
2179 if (!isInstalled_Fonts("classic-set") && !isInstalled_Fonts("cjk")) { 2267 if (!isInstalled_Fonts("classic-set") && !isInstalled_Fonts("cjk")) {
2180 makeQuestion_Widget( 2268 makeQuestion_Widget(
2181 uiHeading_ColorEscape "${heading.fontpack.classic}", 2269 uiHeading_ColorEscape "${heading.fontpack.classic}",
@@ -2208,6 +2296,22 @@ iBool handleCommand_App(const char *cmd) {
2208 } 2296 }
2209 return iTrue; 2297 return iTrue;
2210 } 2298 }
2299 else if (equal_Command(cmd, "navbar.action.set")) {
2300 d->prefs.navbarActions[iClamp(argLabel_Command(cmd, "button"), 0, maxNavbarActions_Prefs - 1)] =
2301 iClamp(arg_Command(cmd), 0, max_ToolbarAction - 1);
2302 if (!isFrozen) {
2303 postCommand_App("~navbar.actions.changed");
2304 }
2305 return iTrue;
2306 }
2307 else if (equal_Command(cmd, "toolbar.action.set")) {
2308 d->prefs.toolbarActions[iClamp(argLabel_Command(cmd, "button"), 0, 1)] =
2309 iClamp(arg_Command(cmd), 0, max_ToolbarAction - 1);
2310 if (!isFrozen) {
2311 postCommand_App("~toolbar.actions.changed");
2312 }
2313 return iTrue;
2314 }
2211 else if (equal_Command(cmd, "translation.languages")) { 2315 else if (equal_Command(cmd, "translation.languages")) {
2212 d->prefs.langFrom = argLabel_Command(cmd, "from"); 2316 d->prefs.langFrom = argLabel_Command(cmd, "from");
2213 d->prefs.langTo = argLabel_Command(cmd, "to"); 2317 d->prefs.langTo = argLabel_Command(cmd, "to");
@@ -2229,6 +2333,9 @@ iBool handleCommand_App(const char *cmd) {
2229 (argLabel_Command(cmd, "axis") ? vertical_WindowSplit : 0) | (arg_Command(cmd) << 1); 2333 (argLabel_Command(cmd, "axis") ? vertical_WindowSplit : 0) | (arg_Command(cmd) << 1);
2230 const char *url = suffixPtr_Command(cmd, "url"); 2334 const char *url = suffixPtr_Command(cmd, "url");
2231 setCStr_String(d->window->pendingSplitUrl, url ? url : ""); 2335 setCStr_String(d->window->pendingSplitUrl, url ? url : "");
2336 if (hasLabel_Command(cmd, "origin")) {
2337 set_String(d->window->pendingSplitOrigin, string_Command(cmd, "origin"));
2338 }
2232 postRefresh_App(); 2339 postRefresh_App();
2233 return iTrue; 2340 return iTrue;
2234 } 2341 }
@@ -2587,6 +2694,10 @@ iBool handleCommand_App(const char *cmd) {
2587 d->prefs.uiAnimations = arg_Command(cmd) != 0; 2694 d->prefs.uiAnimations = arg_Command(cmd) != 0;
2588 return iTrue; 2695 return iTrue;
2589 } 2696 }
2697 else if (equal_Command(cmd, "prefs.blink.changed")) {
2698 d->prefs.blinkingCursor = arg_Command(cmd) != 0;
2699 return iTrue;
2700 }
2590 else if (equal_Command(cmd, "prefs.time.24h.changed")) { 2701 else if (equal_Command(cmd, "prefs.time.24h.changed")) {
2591 d->prefs.time24h = arg_Command(cmd) != 0; 2702 d->prefs.time24h = arg_Command(cmd) != 0;
2592 return iTrue; 2703 return iTrue;
@@ -2642,7 +2753,9 @@ iBool handleCommand_App(const char *cmd) {
2642 } 2753 }
2643#endif 2754#endif
2644 else if (equal_Command(cmd, "downloads.open")) { 2755 else if (equal_Command(cmd, "downloads.open")) {
2645 postCommandf_App("open url:%s", cstrCollect_String(makeFileUrl_String(downloadDir_App()))); 2756 postCommandf_App("open newtab:%d url:%s",
2757 argLabel_Command(cmd, "newtab"),
2758 cstrCollect_String(makeFileUrl_String(downloadDir_App())));
2646 return iTrue; 2759 return iTrue;
2647 } 2760 }
2648 else if (equal_Command(cmd, "ca.file")) { 2761 else if (equal_Command(cmd, "ca.file")) {
@@ -2673,14 +2786,29 @@ iBool handleCommand_App(const char *cmd) {
2673 } 2786 }
2674 return iTrue; 2787 return iTrue;
2675 } 2788 }
2789 else if (equal_Command(cmd, "reveal")) {
2790 const iString *path = NULL;
2791 if (hasLabel_Command(cmd, "path")) {
2792 path = suffix_Command(cmd, "path");
2793 }
2794 else if (hasLabel_Command(cmd, "url")) {
2795 path = collect_String(localFilePathFromUrl_String(suffix_Command(cmd, "url")));
2796 }
2797 if (path) {
2798 revealPath_App(path);
2799 }
2800 return iTrue;
2801 }
2676 else if (equal_Command(cmd, "open")) { 2802 else if (equal_Command(cmd, "open")) {
2677 const char *urlArg = suffixPtr_Command(cmd, "url"); 2803 const char *urlArg = suffixPtr_Command(cmd, "url");
2678 if (!urlArg) { 2804 if (!urlArg) {
2679 return iTrue; /* invalid command */ 2805 return iTrue; /* invalid command */
2680 } 2806 }
2681 iString *url = collectNewCStr_String(urlArg); 2807 if (findWidget_App("prefs")) {
2682 const iBool noProxy = argLabel_Command(cmd, "noproxy") != 0; 2808 postCommand_App("prefs.dismiss");
2683 const iBool fromSidebar = argLabel_Command(cmd, "fromsidebar") != 0; 2809 }
2810 iString *url = collectNewCStr_String(urlArg);
2811 const iBool noProxy = argLabel_Command(cmd, "noproxy") != 0;
2684 iUrl parts; 2812 iUrl parts;
2685 init_Url(&parts, url); 2813 init_Url(&parts, url);
2686 if (equal_Rangecc(parts.scheme, "about") && equal_Rangecc(parts.path, "command") && 2814 if (equal_Rangecc(parts.scheme, "about") && equal_Rangecc(parts.path, "command") &&
@@ -2711,12 +2839,23 @@ iBool handleCommand_App(const char *cmd) {
2711 openInDefaultBrowser_App(url); 2839 openInDefaultBrowser_App(url);
2712 return iTrue; 2840 return iTrue;
2713 } 2841 }
2842 iDocumentWidget *doc = document_Command(cmd);
2843 iDocumentWidget *origin = doc;
2844 if (hasLabel_Command(cmd, "origin")) {
2845 iDocumentWidget *cmdOrig = findWidget_App(cstr_Command(cmd, "origin"));
2846 if (cmdOrig) {
2847 origin = cmdOrig;
2848 }
2849 }
2714 const int newTab = argLabel_Command(cmd, "newtab"); 2850 const int newTab = argLabel_Command(cmd, "newtab");
2715 if (newTab & otherRoot_OpenTabFlag && numRoots_Window(get_Window()) == 1) { 2851 if (newTab & otherRoot_OpenTabFlag && numRoots_Window(get_Window()) == 1) {
2716 /* Need to split first. */ 2852 /* Need to split first. */
2717 const iInt2 winSize = get_Window()->size; 2853 const iInt2 winSize = get_Window()->size;
2718 postCommandf_App("ui.split arg:3 axis:%d newtab:%d url:%s", 2854 const int splitMode = argLabel_Command(cmd, "splitmode");
2855 postCommandf_App("ui.split arg:%d axis:%d origin:%s newtab:%d url:%s",
2856 splitMode ? splitMode : 3,
2719 (float) winSize.x / (float) winSize.y < 0.7f ? 1 : 0, 2857 (float) winSize.x / (float) winSize.y < 0.7f ? 1 : 0,
2858 cstr_String(id_Widget(as_Widget(origin))),
2720 newTab & ~otherRoot_OpenTabFlag, 2859 newTab & ~otherRoot_OpenTabFlag,
2721 cstr_String(url)); 2860 cstr_String(url));
2722 return iTrue; 2861 return iTrue;
@@ -2727,15 +2866,16 @@ iBool handleCommand_App(const char *cmd) {
2727 root = otherRoot_Window(as_Window(d->window), root); 2866 root = otherRoot_Window(as_Window(d->window), root);
2728 setKeyRoot_Window(as_Window(d->window), root); 2867 setKeyRoot_Window(as_Window(d->window), root);
2729 setCurrent_Root(root); /* need to change for widget creation */ 2868 setCurrent_Root(root); /* need to change for widget creation */
2869 doc = document_Command(cmd); /* may be different */
2730 } 2870 }
2731 iDocumentWidget *doc = document_Command(cmd);
2732 if (newTab & (new_OpenTabFlag | newBackground_OpenTabFlag)) { 2871 if (newTab & (new_OpenTabFlag | newBackground_OpenTabFlag)) {
2733 doc = newTab_App(NULL, (newTab & new_OpenTabFlag) != 0); /* `newtab:2` to open in background */ 2872 doc = newTab_App(NULL, (newTab & new_OpenTabFlag) != 0); /* `newtab:2` to open in background */
2734 } 2873 }
2735 iHistory *history = history_DocumentWidget(doc); 2874 iHistory *history = history_DocumentWidget(doc);
2736 const iBool isHistory = argLabel_Command(cmd, "history") != 0; 2875 const iBool isHistory = argLabel_Command(cmd, "history") != 0;
2737 int redirectCount = argLabel_Command(cmd, "redirect"); 2876 int redirectCount = argLabel_Command(cmd, "redirect");
2738 if (!isHistory) { 2877 if (!isHistory) {
2878 /* TODO: Shouldn't DocumentWidget manage history on its own? */
2739 if (redirectCount) { 2879 if (redirectCount) {
2740 replace_History(history, url); 2880 replace_History(history, url);
2741 } 2881 }
@@ -2745,16 +2885,10 @@ iBool handleCommand_App(const char *cmd) {
2745 } 2885 }
2746 setInitialScroll_DocumentWidget(doc, argfLabel_Command(cmd, "scroll")); 2886 setInitialScroll_DocumentWidget(doc, argfLabel_Command(cmd, "scroll"));
2747 setRedirectCount_DocumentWidget(doc, redirectCount); 2887 setRedirectCount_DocumentWidget(doc, redirectCount);
2888 setOrigin_DocumentWidget(doc, origin);
2748 showCollapsed_Widget(findWidget_App("document.progress"), iFalse); 2889 showCollapsed_Widget(findWidget_App("document.progress"), iFalse);
2749 if (prefs_App()->decodeUserVisibleURLs) {
2750 urlDecodePath_String(url);
2751 }
2752 else {
2753 urlEncodePath_String(url);
2754 }
2755 setUrlFlags_DocumentWidget(doc, url, 2890 setUrlFlags_DocumentWidget(doc, url,
2756 (isHistory ? useCachedContentIfAvailable_DocumentWidgetSetUrlFlag : 0) | 2891 isHistory ? useCachedContentIfAvailable_DocumentWidgetSetUrlFlag : 0);
2757 (fromSidebar ? openedFromSidebar_DocumentWidgetSetUrlFlag : 0));
2758 /* Optionally, jump to a text in the document. This will only work if the document 2892 /* Optionally, jump to a text in the document. This will only work if the document
2759 is already available, e.g., it's from "about:" or restored from cache. */ 2893 is already available, e.g., it's from "about:" or restored from cache. */
2760 const iRangecc gotoHeading = range_Command(cmd, "gotoheading"); 2894 const iRangecc gotoHeading = range_Command(cmd, "gotoheading");
@@ -2895,6 +3029,8 @@ iBool handleCommand_App(const char *cmd) {
2895 iWidget *dlg = makePreferences_Widget(); 3029 iWidget *dlg = makePreferences_Widget();
2896 updatePrefsThemeButtons_(dlg); 3030 updatePrefsThemeButtons_(dlg);
2897 setText_InputWidget(findChild_Widget(dlg, "prefs.downloads"), &d->prefs.strings[downloadDir_PrefsString]); 3031 setText_InputWidget(findChild_Widget(dlg, "prefs.downloads"), &d->prefs.strings[downloadDir_PrefsString]);
3032 /* TODO: Use a common table in Prefs to do this more conviently.
3033 Also see `serializePrefs_App_()`. */
2898 setToggle_Widget(findChild_Widget(dlg, "prefs.hoverlink"), d->prefs.hoverLink); 3034 setToggle_Widget(findChild_Widget(dlg, "prefs.hoverlink"), d->prefs.hoverLink);
2899 setToggle_Widget(findChild_Widget(dlg, "prefs.smoothscroll"), d->prefs.smoothScrolling); 3035 setToggle_Widget(findChild_Widget(dlg, "prefs.smoothscroll"), d->prefs.smoothScrolling);
2900 setToggle_Widget(findChild_Widget(dlg, "prefs.imageloadscroll"), d->prefs.loadImageInsteadOfScrolling); 3036 setToggle_Widget(findChild_Widget(dlg, "prefs.imageloadscroll"), d->prefs.loadImageInsteadOfScrolling);
@@ -2905,7 +3041,7 @@ iBool handleCommand_App(const char *cmd) {
2905 setToggle_Widget(findChild_Widget(dlg, "prefs.ostheme"), d->prefs.useSystemTheme); 3041 setToggle_Widget(findChild_Widget(dlg, "prefs.ostheme"), d->prefs.useSystemTheme);
2906 setToggle_Widget(findChild_Widget(dlg, "prefs.customframe"), d->prefs.customFrame); 3042 setToggle_Widget(findChild_Widget(dlg, "prefs.customframe"), d->prefs.customFrame);
2907 setToggle_Widget(findChild_Widget(dlg, "prefs.animate"), d->prefs.uiAnimations); 3043 setToggle_Widget(findChild_Widget(dlg, "prefs.animate"), d->prefs.uiAnimations);
2908// setText_InputWidget(findChild_Widget(dlg, "prefs.userfont"), &d->prefs.symbolFontPath); 3044 setToggle_Widget(findChild_Widget(dlg, "prefs.blink"), d->prefs.blinkingCursor);
2909 updatePrefsPinSplitButtons_(dlg, d->prefs.pinSplit); 3045 updatePrefsPinSplitButtons_(dlg, d->prefs.pinSplit);
2910 updateScrollSpeedButtons_(dlg, mouse_ScrollType, d->prefs.smoothScrollSpeed[mouse_ScrollType]); 3046 updateScrollSpeedButtons_(dlg, mouse_ScrollType, d->prefs.smoothScrollSpeed[mouse_ScrollType]);
2911 updateScrollSpeedButtons_(dlg, keyboard_ScrollType, d->prefs.smoothScrollSpeed[keyboard_ScrollType]); 3047 updateScrollSpeedButtons_(dlg, keyboard_ScrollType, d->prefs.smoothScrollSpeed[keyboard_ScrollType]);
@@ -2914,16 +3050,11 @@ iBool handleCommand_App(const char *cmd) {
2914 updateDropdownSelection_LabelWidget( 3050 updateDropdownSelection_LabelWidget(
2915 findChild_Widget(dlg, "prefs.returnkey"), 3051 findChild_Widget(dlg, "prefs.returnkey"),
2916 format_CStr("returnkey.set arg:%d", d->prefs.returnKey)); 3052 format_CStr("returnkey.set arg:%d", d->prefs.returnKey));
3053 updatePrefsToolBarActionButton_(dlg, 0, d->prefs.toolbarActions[0]);
3054 updatePrefsToolBarActionButton_(dlg, 1, d->prefs.toolbarActions[1]);
2917 setToggle_Widget(findChild_Widget(dlg, "prefs.retainwindow"), d->prefs.retainWindowSize); 3055 setToggle_Widget(findChild_Widget(dlg, "prefs.retainwindow"), d->prefs.retainWindowSize);
2918 setText_InputWidget(findChild_Widget(dlg, "prefs.uiscale"), 3056 setText_InputWidget(findChild_Widget(dlg, "prefs.uiscale"),
2919 collectNewFormat_String("%g", uiScale_Window(as_Window(d->window)))); 3057 collectNewFormat_String("%g", uiScale_Window(as_Window(d->window))));
2920// setFlags_Widget(findChild_Widget(dlg, format_CStr("prefs.font.%d", d->prefs.font)),
2921// selected_WidgetFlag,
2922// iTrue);
2923// setFlags_Widget(
2924// findChild_Widget(dlg, format_CStr("prefs.headingfont.%d", d->prefs.headingFont)),
2925// selected_WidgetFlag,
2926// iTrue);
2927 setFlags_Widget(findChild_Widget(dlg, "prefs.mono.gemini"), 3058 setFlags_Widget(findChild_Widget(dlg, "prefs.mono.gemini"),
2928 selected_WidgetFlag, 3059 selected_WidgetFlag,
2929 d->prefs.monospaceGemini); 3060 d->prefs.monospaceGemini);
@@ -2990,13 +3121,16 @@ iBool handleCommand_App(const char *cmd) {
2990 showTabPage_Widget(tabs, tabPage_Widget(tabs, d->prefs.dialogTab)); 3121 showTabPage_Widget(tabs, tabPage_Widget(tabs, d->prefs.dialogTab));
2991 } 3122 }
2992 setCommandHandler_Widget(dlg, handlePrefsCommands_); 3123 setCommandHandler_Widget(dlg, handlePrefsCommands_);
3124 if (argLabel_Command(cmd, "idents") && deviceType_App() != desktop_AppDeviceType) {
3125 iWidget *idPanel = panel_Mobile(dlg, 2);
3126 iWidget *button = findUserData_Widget(findChild_Widget(dlg, "panel.top"), idPanel);
3127 postCommand_Widget(button, "panel.open");
3128 }
2993 } 3129 }
2994 else if (equal_Command(cmd, "navigate.home")) { 3130 else if (equal_Command(cmd, "navigate.home")) {
2995 /* Look for bookmarks tagged "homepage". */ 3131 /* Look for bookmarks tagged "homepage". */
2996 iRegExp *pattern = iClob(new_RegExp("\\b" homepage_BookmarkTag "\\b",
2997 caseInsensitive_RegExpOption));
2998 const iPtrArray *homepages = 3132 const iPtrArray *homepages =
2999 list_Bookmarks(d->bookmarks, NULL, filterTagsRegExp_Bookmarks, pattern); 3133 list_Bookmarks(d->bookmarks, NULL, filterHomepage_Bookmark, NULL);
3000 if (isEmpty_PtrArray(homepages)) { 3134 if (isEmpty_PtrArray(homepages)) {
3001 postCommand_Root(get_Root(), "open url:about:lagrange"); 3135 postCommand_Root(get_Root(), "open url:about:lagrange");
3002 } 3136 }
@@ -3147,6 +3281,7 @@ iBool handleCommand_App(const char *cmd) {
3147 d->certs, 3281 d->certs,
3148 findIdentity_GmCerts(d->certs, collect_Block(hexDecode_Rangecc(range_Command(cmd, "ident")))), 3282 findIdentity_GmCerts(d->certs, collect_Block(hexDecode_Rangecc(range_Command(cmd, "ident")))),
3149 url); 3283 url);
3284 postCommand_App("navigate.reload");
3150 postCommand_App("idents.changed"); 3285 postCommand_App("idents.changed");
3151 return iTrue; 3286 return iTrue;
3152 } 3287 }
@@ -3159,9 +3294,29 @@ iBool handleCommand_App(const char *cmd) {
3159 else { 3294 else {
3160 setUse_GmIdentity(ident, collect_String(suffix_Command(cmd, "url")), iFalse); 3295 setUse_GmIdentity(ident, collect_String(suffix_Command(cmd, "url")), iFalse);
3161 } 3296 }
3297 postCommand_App("navigate.reload");
3162 postCommand_App("idents.changed"); 3298 postCommand_App("idents.changed");
3163 return iTrue; 3299 return iTrue;
3164 } 3300 }
3301 else if (equal_Command(cmd, "ident.switch")) {
3302 /* This is different than "ident.signin" in that the currently used identity's activation
3303 URL is used instead of the current one. */
3304 const iString *docUrl = url_DocumentWidget(document_App());
3305 const iGmIdentity *cur = identityForUrl_GmCerts(d->certs, docUrl);
3306 iGmIdentity *dst = findIdentity_GmCerts(
3307 d->certs, collect_Block(hexDecode_Rangecc(range_Command(cmd, "fp"))));
3308 if (dst && cur != dst) {
3309 iString *useUrl = copy_String(findUse_GmIdentity(cur, docUrl));
3310 if (isEmpty_String(useUrl)) {
3311 useUrl = copy_String(docUrl);
3312 }
3313 signIn_GmCerts(d->certs, dst, useUrl);
3314 postCommand_App("idents.changed");
3315 postCommand_App("navigate.reload");
3316 delete_String(useUrl);
3317 }
3318 return iTrue;
3319 }
3165 else if (equal_Command(cmd, "idents.changed")) { 3320 else if (equal_Command(cmd, "idents.changed")) {
3166 saveIdentities_GmCerts(d->certs); 3321 saveIdentities_GmCerts(d->certs);
3167 return iFalse; 3322 return iFalse;
@@ -3259,50 +3414,69 @@ void openInDefaultBrowser_App(const iString *url) {
3259 return; 3414 return;
3260 } 3415 }
3261#endif 3416#endif
3262#if !defined (iPlatformAppleMobile) 3417#if defined (iPlatformAppleMobile)
3418 if (equalCase_Rangecc(urlScheme_String(url), "file")) {
3419 revealPath_App(collect_String(localFilePathFromUrl_String(url)));
3420 }
3421 return;
3422#endif
3263 iProcess *proc = new_Process(); 3423 iProcess *proc = new_Process();
3264 setArguments_Process(proc, 3424 setArguments_Process(proc, iClob(newStringsCStr_StringList(
3265#if defined (iPlatformAppleDesktop) 3425#if defined (iPlatformAppleDesktop)
3266 iClob(newStringsCStr_StringList("/usr/bin/env", "open", cstr_String(url), NULL)) 3426 "/usr/bin/env",
3427 "open",
3428 cstr_String(url),
3267#elif defined (iPlatformLinux) || defined (iPlatformOther) || defined (iPlatformHaiku) 3429#elif defined (iPlatformLinux) || defined (iPlatformOther) || defined (iPlatformHaiku)
3268 iClob(newStringsCStr_StringList("/usr/bin/env", "xdg-open", cstr_String(url), NULL)) 3430 "/usr/bin/env",
3431 "xdg-open",
3432 cstr_String(url),
3269#elif defined (iPlatformMsys) 3433#elif defined (iPlatformMsys)
3270 iClob(newStringsCStr_StringList( 3434 concatPath_CStr(cstr_String(execPath_App()), "../urlopen.bat"),
3271 concatPath_CStr(cstr_String(execPath_App()), "../urlopen.bat"), 3435 cstr_String(url),
3272 cstr_String(url),
3273 NULL))
3274 /* TODO: The prompt window is shown momentarily... */ 3436 /* TODO: The prompt window is shown momentarily... */
3275#endif 3437#endif
3438 NULL))
3276 ); 3439 );
3277 start_Process(proc); 3440 start_Process(proc);
3278 waitForFinished_Process(proc); /* TODO: test on Windows */ 3441 waitForFinished_Process(proc);
3279 iRelease(proc); 3442 iRelease(proc);
3280#endif
3281} 3443}
3282 3444
3445#include <the_Foundation/thread.h>
3446
3283void revealPath_App(const iString *path) { 3447void revealPath_App(const iString *path) {
3284#if defined (iPlatformAppleDesktop) 3448#if defined (iPlatformAppleDesktop)
3285 const char *scriptPath = concatPath_CStr(dataDir_App_(), "revealfile.scpt"); 3449 iProcess *proc = new_Process();
3286 iFile *f = newCStr_File(scriptPath); 3450 setArguments_Process(
3287 if (open_File(f, writeOnly_FileMode | text_FileMode)) { 3451 proc, iClob(newStringsCStr_StringList("/usr/bin/open", "-R", cstr_String(path), NULL)));
3288 /* AppleScript to select a specific file. */ 3452 start_Process(proc);
3289 write_File(f, collect_Block(newCStr_Block("on run argv\n" 3453 iRelease(proc);
3290 " tell application \"Finder\"\n" 3454#elif defined (iPlatformAppleMobile)
3291 " activate\n" 3455 /* Use a share sheet. */
3292 " reveal POSIX file (item 1 of argv) as text\n" 3456 openFileActivityView_iOS(path);
3293 " end tell\n" 3457#elif defined (iPlatformLinux) || defined (iPlatformHaiku)
3294 "end run\n"))); 3458 iProcess *proc = NULL;
3295 close_File(f); 3459 /* Try with `dbus-send` first. */ {
3296 iProcess *proc = new_Process(); 3460 proc = new_Process();
3297 setArguments_Process( 3461 setArguments_Process(
3298 proc, 3462 proc,
3299 iClob(newStringsCStr_StringList( 3463 iClob(newStringsCStr_StringList(
3300 "/usr/bin/osascript", scriptPath, cstr_String(path), NULL))); 3464 "/usr/bin/dbus-send",
3465 "--print-reply",
3466 "--dest=org.freedesktop.FileManager1",
3467 "/org/freedesktop/FileManager1",
3468 "org.freedesktop.FileManager1.ShowItems",
3469 format_CStr("array:string:%s", makeFileUrl_CStr(cstr_String(path))),
3470 "string:",
3471 NULL)));
3301 start_Process(proc); 3472 start_Process(proc);
3473 waitForFinished_Process(proc);
3474 const iBool dbusDidSucceed = (exitStatus_Process(proc) == 0);
3302 iRelease(proc); 3475 iRelease(proc);
3476 if (dbusDidSucceed) {
3477 return;
3478 }
3303 } 3479 }
3304 iRelease(f);
3305#elif defined (iPlatformLinux) || defined (iPlatformHaiku)
3306 iFileInfo *inf = iClob(new_FileInfo(path)); 3480 iFileInfo *inf = iClob(new_FileInfo(path));
3307 iRangecc target; 3481 iRangecc target;
3308 if (isDirectory_FileInfo(inf)) { 3482 if (isDirectory_FileInfo(inf)) {
@@ -3311,7 +3485,7 @@ void revealPath_App(const iString *path) {
3311 else { 3485 else {
3312 target = dirName_Path(path); 3486 target = dirName_Path(path);
3313 } 3487 }
3314 iProcess *proc = new_Process(); 3488 proc = new_Process();
3315 setArguments_Process( 3489 setArguments_Process(
3316 proc, iClob(newStringsCStr_StringList("/usr/bin/env", "xdg-open", cstr_Rangecc(target), NULL))); 3490 proc, iClob(newStringsCStr_StringList("/usr/bin/env", "xdg-open", cstr_Rangecc(target), NULL)));
3317 start_Process(proc); 3491 start_Process(proc);
@@ -3365,8 +3539,21 @@ void closePopups_App(void) {
3365} 3539}
3366 3540
3367#if defined (iPlatformAndroidMobile) 3541#if defined (iPlatformAndroidMobile)
3542
3368float displayDensity_Android(void) { 3543float displayDensity_Android(void) {
3369 iApp *d = &app_; 3544 iApp *d = &app_;
3370 return toFloat_String(at_CommandLine(&d->args, 1)); 3545 return toFloat_String(at_CommandLine(&d->args, 1));
3371} 3546}
3547
3548#include <jni.h>
3549
3550JNIEXPORT void JNICALL Java_fi_skyjake_lagrange_LagrangeActivity_postAppCommand(
3551 JNIEnv* env, jclass jcls,
3552 jstring command)
3553{
3554 const char *cmd = (*env)->GetStringUTFChars(env, command, NULL);
3555 postCommand_Root(NULL, cmd);
3556 (*env)->ReleaseStringUTFChars(env, command, cmd);
3557}
3558
3372#endif 3559#endif
diff --git a/src/app.h b/src/app.h
index 50d3ac6b..5968de0d 100644
--- a/src/app.h
+++ b/src/app.h
@@ -61,6 +61,7 @@ enum iUserEventCode {
61 command_UserEventCode = 1, 61 command_UserEventCode = 1,
62 refresh_UserEventCode, 62 refresh_UserEventCode,
63 asleep_UserEventCode, 63 asleep_UserEventCode,
64 periodic_UserEventCode,
64 /* The start of a potential touch tap event is notified via a custom event because 65 /* The start of a potential touch tap event is notified via a custom event because
65 sending SDL_MOUSEBUTTONDOWN would be premature: we don't know how long the tap will 66 sending SDL_MOUSEBUTTONDOWN would be premature: we don't know how long the tap will
66 take, it could turn into a tap-and-hold for example. */ 67 take, it could turn into a tap-and-hold for example. */
@@ -110,6 +111,8 @@ enum iColorTheme colorTheme_App (void);
110const iString * schemeProxy_App (iRangecc scheme); 111const iString * schemeProxy_App (iRangecc scheme);
111iBool willUseProxy_App (const iRangecc scheme); 112iBool willUseProxy_App (const iRangecc scheme);
112const iString * searchQueryUrl_App (const iString *queryStringUnescaped); 113const iString * searchQueryUrl_App (const iString *queryStringUnescaped);
114const iString * fileNameForUrl_App (const iString *url, const iString *mime);
115const iString * temporaryPathForUrl_App(const iString *url, const iString *mime); /* deleted before quitting */
113const iString * downloadPathForUrl_App(const iString *url, const iString *mime); 116const iString * downloadPathForUrl_App(const iString *url, const iString *mime);
114 117
115typedef void (*iTickerFunc)(iAny *); 118typedef void (*iTickerFunc)(iAny *);
diff --git a/src/audio/player.c b/src/audio/player.c
index 94bcd065..de430b17 100644
--- a/src/audio/player.c
+++ b/src/audio/player.c
@@ -31,6 +31,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
31#include <the_Foundation/thread.h> 31#include <the_Foundation/thread.h>
32#include <SDL_audio.h> 32#include <SDL_audio.h>
33#include <SDL_timer.h> 33#include <SDL_timer.h>
34#include <SDL.h>
34 35
35#if defined (LAGRANGE_ENABLE_MPG123) 36#if defined (LAGRANGE_ENABLE_MPG123)
36# include <mpg123.h> 37# include <mpg123.h>
@@ -191,7 +192,7 @@ static enum iDecoderStatus decodeVorbis_Decoder_(iDecoder *d) {
191 int error; 192 int error;
192 int consumed; 193 int consumed;
193 d->vorbis = stb_vorbis_open_pushdata( 194 d->vorbis = stb_vorbis_open_pushdata(
194 constData_Block(input), size_Block(input), &consumed, &error, NULL); 195 constData_Block(input), (int) size_Block(input), &consumed, &error, NULL);
195 if (!d->vorbis) { 196 if (!d->vorbis) {
196 return needMoreInput_DecoderStatus; 197 return needMoreInput_DecoderStatus;
197 } 198 }
@@ -224,7 +225,7 @@ static enum iDecoderStatus decodeVorbis_Decoder_(iDecoder *d) {
224 lock_Mutex(&d->input->mtx); 225 lock_Mutex(&d->input->mtx);
225 d->totalInputSize = size_Block(input); 226 d->totalInputSize = size_Block(input);
226 int error = 0; 227 int error = 0;
227 stb_vorbis *vrb = stb_vorbis_open_memory(constData_Block(input), size_Block(input), 228 stb_vorbis *vrb = stb_vorbis_open_memory(constData_Block(input), (int) size_Block(input),
228 &error, NULL); 229 &error, NULL);
229 if (vrb) { 230 if (vrb) {
230 d->totalSamples = stb_vorbis_stream_length_in_samples(vrb); 231 d->totalSamples = stb_vorbis_stream_length_in_samples(vrb);
@@ -739,6 +740,22 @@ size_t sourceDataSize_Player(const iPlayer *d) {
739 return size; 740 return size;
740} 741}
741 742
743static iBool setupSDLAudio_(iBool init) {
744 static iBool isAudioInited_ = iFalse;
745 if (init) {
746 if (SDL_InitSubSystem(SDL_INIT_AUDIO)) {
747 fprintf(stderr, "[SDL] audio init failed: %s\n", SDL_GetError());
748 return iFalse;
749 }
750 isAudioInited_ = iTrue;
751 }
752 else if (isAudioInited_) {
753 SDL_QuitSubSystem(SDL_INIT_AUDIO);
754 isAudioInited_ = iFalse;
755 }
756 return isAudioInited_;
757}
758
742iBool start_Player(iPlayer *d) { 759iBool start_Player(iPlayer *d) {
743 if (isStarted_Player(d)) { 760 if (isStarted_Player(d)) {
744 return iFalse; 761 return iFalse;
@@ -757,6 +774,9 @@ iBool start_Player(iPlayer *d) {
757 } 774 }
758 content.output.callback = writeOutputSamples_Player_; 775 content.output.callback = writeOutputSamples_Player_;
759 content.output.userdata = d; 776 content.output.userdata = d;
777 if (!setupSDLAudio_(iTrue)) {
778 return iFalse;
779 }
760 d->device = SDL_OpenAudioDevice(NULL, SDL_FALSE /* playback */, &content.output, &d->spec, 0); 780 d->device = SDL_OpenAudioDevice(NULL, SDL_FALSE /* playback */, &content.output, &d->spec, 0);
761 if (!d->device) { 781 if (!d->device) {
762 return iFalse; 782 return iFalse;
@@ -796,6 +816,7 @@ void stop_Player(iPlayer *d) {
796 d->device = 0; 816 d->device = 0;
797 delete_Decoder(d->decoder); 817 delete_Decoder(d->decoder);
798 d->decoder = NULL; 818 d->decoder = NULL;
819 setupSDLAudio_(iFalse);
799 } 820 }
800} 821}
801 822
diff --git a/src/bookmarks.c b/src/bookmarks.c
index 5e943387..500caa38 100644
--- a/src/bookmarks.c
+++ b/src/bookmarks.c
@@ -37,6 +37,7 @@ void init_Bookmark(iBookmark *d) {
37 init_String(&d->url); 37 init_String(&d->url);
38 init_String(&d->title); 38 init_String(&d->title);
39 init_String(&d->tags); 39 init_String(&d->tags);
40 iZap(d->flags);
40 iZap(d->when); 41 iZap(d->when);
41 d->parentId = 0; 42 d->parentId = 0;
42 d->order = 0; 43 d->order = 0;
@@ -48,6 +49,7 @@ void deinit_Bookmark(iBookmark *d) {
48 deinit_String(&d->url); 49 deinit_String(&d->url);
49} 50}
50 51
52#if 0
51iBool hasTag_Bookmark(const iBookmark *d, const char *tag) { 53iBool hasTag_Bookmark(const iBookmark *d, const char *tag) {
52 if (!d) return iFalse; 54 if (!d) return iFalse;
53 iRegExp *pattern = new_RegExp(format_CStr("\\b%s\\b", tag), caseSensitive_RegExpOption); 55 iRegExp *pattern = new_RegExp(format_CStr("\\b%s\\b", tag), caseSensitive_RegExpOption);
@@ -60,7 +62,7 @@ iBool hasTag_Bookmark(const iBookmark *d, const char *tag) {
60 62
61void addTag_Bookmark(iBookmark *d, const char *tag) { 63void addTag_Bookmark(iBookmark *d, const char *tag) {
62 if (!isEmpty_String(&d->tags)) { 64 if (!isEmpty_String(&d->tags)) {
63 appendChar_String(&d->tags, ' '); 65 appendCStr_String(&d->tags, " ");
64 } 66 }
65 appendCStr_String(&d->tags, tag); 67 appendCStr_String(&d->tags, tag);
66} 68}
@@ -72,6 +74,93 @@ void removeTag_Bookmark(iBookmark *d, const char *tag) {
72 trim_String(&d->tags); 74 trim_String(&d->tags);
73 } 75 }
74} 76}
77#endif
78
79static struct {
80 uint32_t bit;
81 const char *tag;
82 iRegExp * pattern;
83 iRegExp * oldPattern;
84}
85specialTags_[] = {
86 { homepage_BookmarkFlag, ".homepage" },
87 { remoteSource_BookmarkFlag, ".remotesource" },
88 { linkSplit_BookmarkFlag, ".linksplit" },
89 { userIcon_BookmarkFlag, ".usericon" },
90 { subscribed_BookmarkFlag, ".subscribed" },
91 { headings_BookmarkFlag, ".headings" },
92 { ignoreWeb_BookmarkFlag, ".ignoreweb" },
93 /* `remote_BookmarkFlag` not included because it's runtime only */
94};
95
96static void updatePatterns_(size_t index) {
97 if (!specialTags_[index].pattern) {
98 specialTags_[index].pattern = new_RegExp(format_CStr("(?<!\\w)\\%s\\b(?!\\w)",
99 specialTags_[index].tag),
100 caseSensitive_RegExpOption); /* never released */
101 }
102 if (!specialTags_[index].oldPattern) {
103 /* TODO: Get rid of these when compatibility with v1.9 or older is not important. */
104 specialTags_[index].oldPattern =
105 new_RegExp(format_CStr("\\b%s\\b", specialTags_[index].tag + 1), /* dotless */
106 caseSensitive_RegExpOption); /* never released */
107 }
108}
109
110static void normalizeSpacesInTags_(iString *tags) {
111 iBool wasSpace = iFalse;
112 iString out;
113 init_String(&out);
114 for (const char *ch = constBegin_String(tags); ch != constEnd_String(tags); ch++) {
115 if (*ch == ' ') {
116 if (!wasSpace) {
117 wasSpace = iTrue;
118 }
119 else {
120 continue;
121 }
122 }
123 else {
124 wasSpace = iFalse;
125 }
126 appendData_Block(&out.chars, ch, 1);
127 }
128 trim_String(&out);
129 set_String(tags, &out);
130 deinit_String(&out);
131}
132
133static void unpackDotTags_Bookmark_(iBookmark *d) {
134 iZap(d->flags);
135 iForIndices(i, specialTags_) {
136 updatePatterns_(i);
137 iRegExpMatch m;
138 init_RegExpMatch(&m);
139 iBool isSet = matchString_RegExp(specialTags_[i].pattern, &d->tags, &m);
140 if (!isSet) {
141 init_RegExpMatch(&m);
142 isSet = matchString_RegExp(specialTags_[i].oldPattern, &d->tags, &m);
143 }
144 iChangeFlags(d->flags, specialTags_[i].bit, isSet);
145 if (isSet) {
146 remove_Block(&d->tags.chars, m.range.start, size_Range(&m.range));
147 }
148 }
149 normalizeSpacesInTags_(&d->tags);
150}
151
152static iString *packedDotTags_Bookmark_(const iBookmark *d) {
153 iString *withDot = copy_String(&d->tags);
154 iForIndices(i, specialTags_) {
155 if (d->flags & specialTags_[i].bit) {
156 if (!isEmpty_String(withDot)) {
157 appendCStr_String(withDot, " ");
158 }
159 appendCStr_String(withDot, specialTags_[i].tag);
160 }
161 }
162 return withDot;
163}
75 164
76iDefineTypeConstruction(Bookmark) 165iDefineTypeConstruction(Bookmark)
77 166
@@ -176,6 +265,7 @@ static void loadOldFormat_Bookmarks(iBookmarks *d, const char *dirPath) {
176 setRange_String(&bm->title, line); 265 setRange_String(&bm->title, line);
177 nextSplit_Rangecc(src, "\n", &line); 266 nextSplit_Rangecc(src, "\n", &line);
178 setRange_String(&bm->tags, line); 267 setRange_String(&bm->tags, line);
268 unpackDotTags_Bookmark_(bm);
179 insert_Bookmarks_(d, bm); 269 insert_Bookmarks_(d, bm);
180 } 270 }
181 } 271 }
@@ -220,6 +310,7 @@ static void handleKeyValue_BookmarkLoader_(void *context, const iString *table,
220 } 310 }
221 else if (!cmp_String(key, "tags") && tv->type == string_TomlType) { 311 else if (!cmp_String(key, "tags") && tv->type == string_TomlType) {
222 set_String(&bm->tags, tv->value.string); 312 set_String(&bm->tags, tv->value.string);
313 unpackDotTags_Bookmark_(bm);
223 } 314 }
224 else if (!cmp_String(key, "icon") && tv->type == int64_TomlType) { 315 else if (!cmp_String(key, "icon") && tv->type == int64_TomlType) {
225 bm->icon = (iChar) tv->value.int64; 316 bm->icon = (iChar) tv->value.int64;
@@ -292,7 +383,6 @@ void load_Bookmarks(iBookmarks *d, const char *dirPath) {
292 383
293void save_Bookmarks(const iBookmarks *d, const char *dirPath) { 384void save_Bookmarks(const iBookmarks *d, const char *dirPath) {
294 lock_Mutex(d->mtx); 385 lock_Mutex(d->mtx);
295 iRegExp *remotePattern = iClob(new_RegExp("\\bremote\\b", caseSensitive_RegExpOption));
296 iFile *f = newCStr_File(concatPath_CStr(dirPath, fileName_Bookmarks_)); 386 iFile *f = newCStr_File(concatPath_CStr(dirPath, fileName_Bookmarks_));
297 if (open_File(f, writeOnly_FileMode | text_FileMode)) { 387 if (open_File(f, writeOnly_FileMode | text_FileMode)) {
298 iString *str = collectNew_String(); 388 iString *str = collectNew_String();
@@ -300,13 +390,12 @@ void save_Bookmarks(const iBookmarks *d, const char *dirPath) {
300 writeData_File(f, cstr_String(str), size_String(str)); 390 writeData_File(f, cstr_String(str), size_String(str));
301 iConstForEach(Hash, i, &d->bookmarks) { 391 iConstForEach(Hash, i, &d->bookmarks) {
302 const iBookmark *bm = (const iBookmark *) i.value; 392 const iBookmark *bm = (const iBookmark *) i.value;
303 iRegExpMatch m; 393 if (bm->flags & remote_BookmarkFlag) {
304 init_RegExpMatch(&m);
305 if (matchString_RegExp(remotePattern, &bm->tags, &m)) {
306 /* Remote bookmarks are not saved. */ 394 /* Remote bookmarks are not saved. */
307 continue; 395 continue;
308 } 396 }
309 iBeginCollect(); 397 iBeginCollect();
398 const iString *packedTags = collect_String(packedDotTags_Bookmark_(bm));
310 format_String(str, 399 format_String(str,
311 "[%d]\n" 400 "[%d]\n"
312 "url = \"%s\"\n" 401 "url = \"%s\"\n"
@@ -317,7 +406,7 @@ void save_Bookmarks(const iBookmarks *d, const char *dirPath) {
317 id_Bookmark(bm), 406 id_Bookmark(bm),
318 cstrCollect_String(quote_String(&bm->url, iFalse)), 407 cstrCollect_String(quote_String(&bm->url, iFalse)),
319 cstrCollect_String(quote_String(&bm->title, iFalse)), 408 cstrCollect_String(quote_String(&bm->title, iFalse)),
320 cstrCollect_String(quote_String(&bm->tags, iFalse)), 409 cstrCollect_String(quote_String(packedTags, iFalse)),
321 bm->icon, 410 bm->icon,
322 seconds_Time(&bm->when), 411 seconds_Time(&bm->when),
323 cstrCollect_String(format_Time(&bm->when, "%Y-%m-%d"))); 412 cstrCollect_String(format_Time(&bm->when, "%Y-%m-%d")));
@@ -397,7 +486,7 @@ iBool updateBookmarkIcon_Bookmarks(iBookmarks *d, const iString *url, iChar icon
397 const uint32_t id = findUrl_Bookmarks(d, url); 486 const uint32_t id = findUrl_Bookmarks(d, url);
398 if (id) { 487 if (id) {
399 iBookmark *bm = get_Bookmarks(d, id); 488 iBookmark *bm = get_Bookmarks(d, id);
400 if (!hasTag_Bookmark(bm, remote_BookmarkTag) && !hasTag_Bookmark(bm, userIcon_BookmarkTag)) { 489 if (~bm->flags & remote_BookmarkFlag && ~bm->flags & userIcon_BookmarkFlag) {
401 if (icon != bm->icon) { 490 if (icon != bm->icon) {
402 bm->icon = icon; 491 bm->icon = icon;
403 changed = iTrue; 492 changed = iTrue;
@@ -422,19 +511,13 @@ iChar siteIcon_Bookmarks(const iBookmarks *d, const iString *url) {
422 if (isEmpty_String(url)) { 511 if (isEmpty_String(url)) {
423 return 0; 512 return 0;
424 } 513 }
425 static iRegExp *tagPattern_;
426 if (!tagPattern_) {
427 tagPattern_ = new_RegExp("\\b" userIcon_BookmarkTag "\\b", caseSensitive_RegExpOption);
428 }
429 const iRangecc urlRoot = urlRoot_String(url); 514 const iRangecc urlRoot = urlRoot_String(url);
430 size_t matchingSize = iInvalidSize; /* we'll pick the shortest matching */ 515 size_t matchingSize = iInvalidSize; /* we'll pick the shortest matching */
431 iChar icon = 0; 516 iChar icon = 0;
432 lock_Mutex(d->mtx); 517 lock_Mutex(d->mtx);
433 iConstForEach(Hash, i, &d->bookmarks) { 518 iConstForEach(Hash, i, &d->bookmarks) {
434 const iBookmark *bm = (const iBookmark *) i.value; 519 const iBookmark *bm = (const iBookmark *) i.value;
435 iRegExpMatch m; 520 if (bm->icon && bm->flags & userIcon_BookmarkFlag) {
436 init_RegExpMatch(&m);
437 if (bm->icon && matchString_RegExp(tagPattern_, &bm->tags, &m)) {
438 const iRangecc bmRoot = urlRoot_String(&bm->url); 521 const iRangecc bmRoot = urlRoot_String(&bm->url);
439 if (equalRangeCase_Rangecc(urlRoot, bmRoot)) { 522 if (equalRangeCase_Rangecc(urlRoot, bmRoot)) {
440 const size_t n = size_String(&bm->url); 523 const size_t n = size_String(&bm->url);
@@ -467,10 +550,15 @@ void reorder_Bookmarks(iBookmarks *d, uint32_t id, int newOrder) {
467 unlock_Mutex(d->mtx); 550 unlock_Mutex(d->mtx);
468} 551}
469 552
470iBool filterTagsRegExp_Bookmarks(void *regExp, const iBookmark *bm) { 553//iBool filterTagsRegExp_Bookmarks(void *regExp, const iBookmark *bm) {
471 iRegExpMatch m; 554// iRegExpMatch m;
472 init_RegExpMatch(&m); 555// init_RegExpMatch(&m);
473 return matchString_RegExp(regExp, &bm->tags, &m); 556// return matchString_RegExp(regExp, &bm->tags, &m);
557//}
558
559iBool filterHomepage_Bookmark(void *d, const iBookmark *bm) {
560 iUnused(d);
561 return (bm->flags & homepage_BookmarkFlag) != 0;
474} 562}
475 563
476static iBool matchUrl_(void *url, const iBookmark *bm) { 564static iBool matchUrl_(void *url, const iBookmark *bm) {
@@ -618,7 +706,7 @@ const iString *bookmarkListPage_Bookmarks(const iBookmarks *d, enum iBookmarkLis
618 706
619static iBool isRemoteSource_Bookmark_(void *context, const iBookmark *d) { 707static iBool isRemoteSource_Bookmark_(void *context, const iBookmark *d) {
620 iUnused(context); 708 iUnused(context);
621 return hasTag_Bookmark(d, remoteSource_BookmarkTag); 709 return (d->flags & remoteSource_BookmarkFlag) != 0;
622} 710}
623 711
624void remoteRequestFinished_Bookmarks_(iBookmarks *d, iGmRequest *req) { 712void remoteRequestFinished_Bookmarks_(iBookmarks *d, iGmRequest *req) {
@@ -642,7 +730,6 @@ void requestFinished_Bookmarks(iBookmarks *d, iGmRequest *req) {
642 initCurrent_Time(&now); 730 initCurrent_Time(&now);
643 iRegExp *linkPattern = new_RegExp("^=>\\s*([^\\s]+)(\\s+(.*))?", 0); 731 iRegExp *linkPattern = new_RegExp("^=>\\s*([^\\s]+)(\\s+(.*))?", 0);
644 iString src; 732 iString src;
645 const iString *remoteTag = collectNewCStr_String("remote");
646 initBlock_String(&src, body_GmRequest(req)); 733 initBlock_String(&src, body_GmRequest(req));
647 iRangecc srcLine = iNullRange; 734 iRangecc srcLine = iNullRange;
648 while (nextSplit_Rangecc(range_String(&src), "\n", &srcLine)) { 735 while (nextSplit_Rangecc(range_String(&src), "\n", &srcLine)) {
@@ -660,8 +747,9 @@ void requestFinished_Bookmarks(iBookmarks *d, iGmRequest *req) {
660 if (isEmpty_String(titleStr)) { 747 if (isEmpty_String(titleStr)) {
661 setRange_String(titleStr, urlHost_String(urlStr)); 748 setRange_String(titleStr, urlHost_String(urlStr));
662 } 749 }
663 const uint32_t bmId = add_Bookmarks(d, absUrl, titleStr, remoteTag, 0x2913); 750 const uint32_t bmId = add_Bookmarks(d, absUrl, titleStr, NULL, 0x2913);
664 iBookmark *bm = get_Bookmarks(d, bmId); 751 iBookmark *bm = get_Bookmarks(d, bmId);
752 bm->flags |= remote_BookmarkFlag;
665 bm->parentId = *(uint32_t *) userData_Object(req); 753 bm->parentId = *(uint32_t *) userData_Object(req);
666 delete_String(titleStr); 754 delete_String(titleStr);
667 } 755 }
@@ -690,7 +778,7 @@ void fetchRemote_Bookmarks(iBookmarks *d) {
690 size_t numRemoved = 0; 778 size_t numRemoved = 0;
691 iForEach(Hash, i, &d->bookmarks) { 779 iForEach(Hash, i, &d->bookmarks) {
692 iBookmark *bm = (iBookmark *) i.value; 780 iBookmark *bm = (iBookmark *) i.value;
693 if (hasTag_Bookmark(bm, remote_BookmarkTag)) { 781 if (bm->flags & remote_BookmarkFlag) {
694 remove_HashIterator(&i); 782 remove_HashIterator(&i);
695 delete_Bookmark(bm); 783 delete_Bookmark(bm);
696 numRemoved++; 784 numRemoved++;
diff --git a/src/bookmarks.h b/src/bookmarks.h
index 6cb5c8a9..08afdd8b 100644
--- a/src/bookmarks.h
+++ b/src/bookmarks.h
@@ -31,27 +31,30 @@ iDeclareType(GmRequest)
31 31
32iDeclareType(Bookmark) 32iDeclareType(Bookmark)
33iDeclareTypeConstruction(Bookmark) 33iDeclareTypeConstruction(Bookmark)
34 34
35/* TODO: Make the special internal tags a bitfield, separate from user's tags. */ 35/* These values are not serialized as-is in bookmarks.ini. Instead, they are included in `tags`
36 36 with a dot prefix. This helps retain backwards and forwards compatibility. */
37#define headings_BookmarkTag "headings" 37enum iBookmarkFlags {
38#define ignoreWeb_BookmarkTag "ignoreweb" 38 homepage_BookmarkFlag = iBit(1),
39#define homepage_BookmarkTag "homepage" 39 remoteSource_BookmarkFlag = iBit(2),
40#define linkSplit_BookmarkTag "linksplit" 40 linkSplit_BookmarkFlag = iBit(3),
41#define remote_BookmarkTag "remote" 41 userIcon_BookmarkFlag = iBit(4),
42#define remoteSource_BookmarkTag "remotesource" 42 subscribed_BookmarkFlag = iBit(17),
43#define subscribed_BookmarkTag "subscribed" 43 headings_BookmarkFlag = iBit(18),
44#define userIcon_BookmarkTag "usericon" 44 ignoreWeb_BookmarkFlag = iBit(19),
45 remote_BookmarkFlag = iBit(31),
46};
45 47
46struct Impl_Bookmark { 48struct Impl_Bookmark {
47 iHashNode node; 49 iHashNode node;
48 iString url; 50 iString url;
49 iString title; 51 iString title;
50 iString tags; 52 iString tags;
51 iChar icon; 53 uint32_t flags;
52 iTime when; 54 iChar icon;
53 uint32_t parentId; /* remote source or folder */ 55 iTime when;
54 int order; /* sort order */ 56 uint32_t parentId; /* remote source or folder */
57 int order; /* sort order */
55}; 58};
56 59
57iLocalDef uint32_t id_Bookmark (const iBookmark *d) { return d->node.key; } 60iLocalDef uint32_t id_Bookmark (const iBookmark *d) { return d->node.key; }
@@ -59,23 +62,24 @@ iLocalDef iBool isFolder_Bookmark (const iBookmark *d) { return isEmpty_St
59 62
60iBool hasParent_Bookmark (const iBookmark *, uint32_t parentId); 63iBool hasParent_Bookmark (const iBookmark *, uint32_t parentId);
61int depth_Bookmark (const iBookmark *); 64int depth_Bookmark (const iBookmark *);
62iBool hasTag_Bookmark (const iBookmark *, const char *tag); 65
63void addTag_Bookmark (iBookmark *, const char *tag); 66//iBool hasTag_Bookmark (const iBookmark *, const char *tag);
64void removeTag_Bookmark (iBookmark *, const char *tag); 67//void addTag_Bookmark (iBookmark *, const char *tag);
65 68//void removeTag_Bookmark (iBookmark *, const char *tag);
66iLocalDef void addTagIfMissing_Bookmark(iBookmark *d, const char *tag) { 69
67 if (!hasTag_Bookmark(d, tag)) { 70//iLocalDef void addTagIfMissing_Bookmark(iBookmark *d, const char *tag) {
68 addTag_Bookmark(d, tag); 71// if (!hasTag_Bookmark(d, tag)) {
69 } 72// addTag_Bookmark(d, tag);
70} 73// }
71iLocalDef void addOrRemoveTag_Bookmark(iBookmark *d, const char *tag, iBool add) { 74//}
72 if (add) { 75//iLocalDef void addOrRemoveTag_Bookmark(iBookmark *d, const char *tag, iBool add) {
73 addTagIfMissing_Bookmark(d, tag); 76// if (add) {
74 } 77// addTagIfMissing_Bookmark(d, tag);
75 else { 78// }
76 removeTag_Bookmark(d, tag); 79// else {
77 } 80// removeTag_Bookmark(d, tag);
78} 81// }
82//}
79 83
80int cmpTitleAscending_Bookmark (const iBookmark **, const iBookmark **); 84int cmpTitleAscending_Bookmark (const iBookmark **, const iBookmark **);
81int cmpTree_Bookmark (const iBookmark **, const iBookmark **); 85int cmpTree_Bookmark (const iBookmark **, const iBookmark **);
@@ -109,7 +113,8 @@ iChar siteIcon_Bookmarks (const iBookmarks *, const iString *url)
109uint32_t findUrl_Bookmarks (const iBookmarks *, const iString *url); /* O(n) */ 113uint32_t findUrl_Bookmarks (const iBookmarks *, const iString *url); /* O(n) */
110uint32_t recentFolder_Bookmarks (const iBookmarks *); 114uint32_t recentFolder_Bookmarks (const iBookmarks *);
111 115
112iBool filterTagsRegExp_Bookmarks (void *regExp, const iBookmark *); 116//iBool filterTagsRegExp_Bookmarks (void *regExp, const iBookmark *);
117iBool filterHomepage_Bookmark (void *, const iBookmark *);
113 118
114/** 119/**
115 * Lists all or a subset of the bookmarks in a sorted array of Bookmark pointers. 120 * Lists all or a subset of the bookmarks in a sorted array of Bookmark pointers.
diff --git a/src/defs.h b/src/defs.h
index 9a466674..e2edd100 100644
--- a/src/defs.h
+++ b/src/defs.h
@@ -66,10 +66,28 @@ enum iReturnKeyFlag {
66 accept_ReturnKeyFlag = 4, /* shift */ 66 accept_ReturnKeyFlag = 4, /* shift */
67}; 67};
68 68
69enum iToolbarAction {
70 back_ToolbarAction = 0,
71 forward_ToolbarAction = 1,
72 home_ToolbarAction = 2,
73 parent_ToolbarAction = 3,
74 reload_ToolbarAction = 4,
75 newTab_ToolbarAction = 5,
76 closeTab_ToolbarAction = 6,
77 addBookmark_ToolbarAction = 7,
78 translate_ToolbarAction = 8,
79 upload_ToolbarAction = 9,
80 editPage_ToolbarAction = 10,
81 findText_ToolbarAction = 11,
82 settings_ToolbarAction = 12,
83 sidebar_ToolbarAction = 13, /* desktop only */
84 max_ToolbarAction
85};
86
69/* Return key behavior is not handled via normal bindings because only certain combinations 87/* Return key behavior is not handled via normal bindings because only certain combinations
70 are valid. */ 88 are valid. */
71enum iReturnKeyBehavior { 89enum iReturnKeyBehavior {
72 default_ReturnKeyBehavior = 90 acceptWithoutMod_ReturnKeyBehavior =
73 shiftReturn_ReturnKeyFlag | (return_ReturnKeyFlag << accept_ReturnKeyFlag), 91 shiftReturn_ReturnKeyFlag | (return_ReturnKeyFlag << accept_ReturnKeyFlag),
74 acceptWithShift_ReturnKeyBehavior = 92 acceptWithShift_ReturnKeyBehavior =
75 return_ReturnKeyFlag | (shiftReturn_ReturnKeyFlag << accept_ReturnKeyFlag), 93 return_ReturnKeyFlag | (shiftReturn_ReturnKeyFlag << accept_ReturnKeyFlag),
@@ -79,6 +97,11 @@ enum iReturnKeyBehavior {
79#else 97#else
80 return_ReturnKeyFlag | (controlReturn_ReturnKeyFlag << accept_ReturnKeyFlag), 98 return_ReturnKeyFlag | (controlReturn_ReturnKeyFlag << accept_ReturnKeyFlag),
81#endif 99#endif
100#if defined (iPlatformAndroidMobile)
101 default_ReturnKeyBehavior = acceptWithShift_ReturnKeyBehavior,
102#else
103 default_ReturnKeyBehavior = acceptWithoutMod_ReturnKeyBehavior,
104#endif
82}; 105};
83 106
84int keyMod_ReturnKeyFlag (int flag); 107int keyMod_ReturnKeyFlag (int flag);
@@ -94,7 +117,7 @@ iLocalDef int acceptKeyMod_ReturnKeyBehavior(int behavior) {
94 117
95#define menu_Icon "\U0001d362" 118#define menu_Icon "\U0001d362"
96#define rightArrowhead_Icon "\u27a4" 119#define rightArrowhead_Icon "\u27a4"
97#define leftArrowhead_Icon "\u27a4" 120#define leftArrowhead_Icon "\u2b9c"
98#define warning_Icon "\u26a0" 121#define warning_Icon "\u26a0"
99#define openLock_Icon "\U0001f513" 122#define openLock_Icon "\U0001f513"
100#define closedLock_Icon "\U0001f512" 123#define closedLock_Icon "\U0001f512"
@@ -102,7 +125,7 @@ iLocalDef int acceptKeyMod_ReturnKeyBehavior(int behavior) {
102#define reload_Icon "\U0001f503" 125#define reload_Icon "\U0001f503"
103#define backArrow_Icon "\U0001f870" 126#define backArrow_Icon "\U0001f870"
104#define forwardArrow_Icon "\U0001f872" 127#define forwardArrow_Icon "\U0001f872"
105#define upArrow_Icon "\u2191" 128#define upArrow_Icon "\U0001f871"
106#define upArrowBar_Icon "\u2912" 129#define upArrowBar_Icon "\u2912"
107#define downArrowBar_Icon "\u2913" 130#define downArrowBar_Icon "\u2913"
108#define rightArrowWhite_Icon "\u21e8" 131#define rightArrowWhite_Icon "\u21e8"
diff --git a/src/feeds.c b/src/feeds.c
index 26b3d6db..a8cbf47a 100644
--- a/src/feeds.c
+++ b/src/feeds.c
@@ -65,7 +65,9 @@ const iString *url_FeedEntry(const iFeedEntry *d) {
65iBool isUnread_FeedEntry(const iFeedEntry *d) { 65iBool isUnread_FeedEntry(const iFeedEntry *d) {
66 const size_t fragPos = indexOf_String(&d->url, '#'); 66 const size_t fragPos = indexOf_String(&d->url, '#');
67 if (fragPos != iInvalidPos) { 67 if (fragPos != iInvalidPos) {
68 /* Check if the entry is newer than the latest visit. */ 68 /* Check if the entry is newer than the latest visit. If the URL has not been visited,
69 `urlVisitTime_Visited` returns a zero timestamp that is always earlier than
70 `posted`. */
69 const iTime visTime = urlVisitTime_Visited(visited_App(), url_FeedEntry(d)); 71 const iTime visTime = urlVisitTime_Visited(visited_App(), url_FeedEntry(d));
70 return cmp_Time(&visTime, &d->posted) < 0; 72 return cmp_Time(&visTime, &d->posted) < 0;
71 } 73 }
@@ -97,8 +99,8 @@ static void init_FeedJob(iFeedJob *d, const iBookmark *bookmark) {
97 init_PtrArray(&d->results); 99 init_PtrArray(&d->results);
98 iZap(d->startTime); 100 iZap(d->startTime);
99 d->isFirstUpdate = iFalse; 101 d->isFirstUpdate = iFalse;
100 d->checkHeadings = hasTag_Bookmark(bookmark, headings_BookmarkTag); 102 d->checkHeadings = (bookmark->flags & headings_BookmarkFlag) != 0;
101 d->ignoreWeb = hasTag_Bookmark(bookmark, ignoreWeb_BookmarkTag); 103 d->ignoreWeb = (bookmark->flags & ignoreWeb_BookmarkFlag) != 0;
102} 104}
103 105
104static void deinit_FeedJob(iFeedJob *d) { 106static void deinit_FeedJob(iFeedJob *d) {
@@ -146,13 +148,7 @@ static void submit_FeedJob_(iFeedJob *d) {
146 148
147static iBool isSubscribed_(void *context, const iBookmark *bm) { 149static iBool isSubscribed_(void *context, const iBookmark *bm) {
148 iUnused(context); 150 iUnused(context);
149 static iRegExp *pattern_ = NULL; 151 return (bm->flags & subscribed_BookmarkFlag) != 0;
150 if (!pattern_) {
151 pattern_ = new_RegExp("\\bsubscribed\\b", caseSensitive_RegExpOption);
152 }
153 iRegExpMatch m;
154 init_RegExpMatch(&m);
155 return matchString_RegExp(pattern_, &bm->tags, &m);
156} 152}
157 153
158static const iPtrArray *listSubscriptions_(void) { 154static const iPtrArray *listSubscriptions_(void) {
@@ -443,7 +439,7 @@ static iThreadResult fetch_Feeds_(iThread *thread) {
443 iZap(work); 439 iZap(work);
444 iBool gotNew = iFalse; 440 iBool gotNew = iFalse;
445 postCommand_App("feeds.update.started"); 441 postCommand_App("feeds.update.started");
446 const int totalJobs = size_PtrArray(&d->jobs); 442 const size_t totalJobs = size_PtrArray(&d->jobs);
447 int numFinishedJobs = 0; 443 int numFinishedJobs = 0;
448 while (!d->stopWorker) { 444 while (!d->stopWorker) {
449 /* Start new jobs. */ 445 /* Start new jobs. */
@@ -482,7 +478,7 @@ static iThreadResult fetch_Feeds_(iThread *thread) {
482 } 478 }
483 } 479 }
484 if (doNotify) { 480 if (doNotify) {
485 postCommandf_App("feeds.update.progress arg:%d total:%d", numFinishedJobs, totalJobs); 481 postCommandf_App("feeds.update.progress arg:%d total:%zu", numFinishedJobs, totalJobs);
486 } 482 }
487 /* Stop if everything has finished. */ 483 /* Stop if everything has finished. */
488 if (ongoing == 0 && isEmpty_PtrArray(&d->jobs)) { 484 if (ongoing == 0 && isEmpty_PtrArray(&d->jobs)) {
@@ -620,7 +616,7 @@ static void load_Feeds_(iFeeds *d) {
620 /* TODO: Cleanup needed... 616 /* TODO: Cleanup needed...
621 All right, this could maybe use a bit more robust, structured format. 617 All right, this could maybe use a bit more robust, structured format.
622 The code below is messy. */ 618 The code below is messy. */
623 const uint32_t feedId = strtoul(line.start, NULL, 16); 619 const uint32_t feedId = (uint32_t) strtoul(line.start, NULL, 16);
624 if (!nextSplit_Rangecc(range_Block(src), "\n", &line)) { 620 if (!nextSplit_Rangecc(range_Block(src), "\n", &line)) {
625 goto aborted; 621 goto aborted;
626 } 622 }
diff --git a/src/fontpack.c b/src/fontpack.c
index 79f35526..a440234e 100644
--- a/src/fontpack.c
+++ b/src/fontpack.c
@@ -818,7 +818,7 @@ const iArray *actions_FontPack(const iFontPack *d, iBool showInstalled) {
818 pushBack_Array( 818 pushBack_Array(
819 items, 819 items,
820 &(iMenuItem){ format_Lang(isEnabled ? close_Icon " ${fontpack.disable}" 820 &(iMenuItem){ format_Lang(isEnabled ? close_Icon " ${fontpack.disable}"
821 : leftArrowhead_Icon " ${fontpack.enable}", 821 : "${fontpack.enable}",
822 fpId), 822 fpId),
823 0, 823 0,
824 0, 824 0,
diff --git a/src/gmcerts.c b/src/gmcerts.c
index f95fea7d..7b05103b 100644
--- a/src/gmcerts.c
+++ b/src/gmcerts.c
@@ -90,7 +90,7 @@ void serialize_GmIdentity(const iGmIdentity *d, iStream *outs) {
90 writeU32_Stream(outs, d->icon); 90 writeU32_Stream(outs, d->icon);
91 serialize_String(&d->notes, outs); 91 serialize_String(&d->notes, outs);
92 write32_Stream(outs, d->flags); 92 write32_Stream(outs, d->flags);
93 writeU32_Stream(outs, size_StringSet(d->useUrls)); 93 writeU32_Stream(outs, (uint32_t) size_StringSet(d->useUrls));
94 iConstForEach(StringSet, i, d->useUrls) { 94 iConstForEach(StringSet, i, d->useUrls) {
95 serialize_String(i.value, outs); 95 serialize_String(i.value, outs);
96 } 96 }
@@ -146,6 +146,7 @@ iBool isUsed_GmIdentity(const iGmIdentity *d) {
146} 146}
147 147
148iBool isUsedOn_GmIdentity(const iGmIdentity *d, const iString *url) { 148iBool isUsedOn_GmIdentity(const iGmIdentity *d, const iString *url) {
149#if 0
149 size_t pos = iInvalidPos; 150 size_t pos = iInvalidPos;
150 locate_StringSet(d->useUrls, url, &pos); 151 locate_StringSet(d->useUrls, url, &pos);
151 if (pos < size_StringSet(d->useUrls)) { 152 if (pos < size_StringSet(d->useUrls)) {
@@ -159,6 +160,12 @@ iBool isUsedOn_GmIdentity(const iGmIdentity *d, const iString *url) {
159 return iTrue; 160 return iTrue;
160 } 161 }
161 } 162 }
163#endif
164 iConstForEach(StringSet, i, d->useUrls) {
165 if (startsWithCase_String(url, cstr_String(i.value))) {
166 return iTrue;
167 }
168 }
162 return iFalse; 169 return iFalse;
163} 170}
164 171
@@ -193,7 +200,13 @@ void setUse_GmIdentity(iGmIdentity *d, const iString *url, iBool use) {
193 iAssert(wasInserted); 200 iAssert(wasInserted);
194 } 201 }
195 else { 202 else {
196 remove_StringSet(d->useUrls, url); 203 iForEach(Array, i, &d->useUrls->strings.values) {
204 iString *used = i.value;
205 if (startsWithCase_String(url, cstr_String(used))) {
206 deinit_String(used);
207 remove_ArrayIterator(&i);
208 }
209 }
197 } 210 }
198} 211}
199 212
@@ -201,6 +214,16 @@ void clearUse_GmIdentity(iGmIdentity *d) {
201 clear_StringSet(d->useUrls); 214 clear_StringSet(d->useUrls);
202} 215}
203 216
217const iString *findUse_GmIdentity(const iGmIdentity *d, const iString *url) {
218 if (!d) return NULL;
219 iConstForEach(StringSet, using, d->useUrls) {
220 if (startsWith_String(url, cstr_String(using.value))) {
221 return using.value;
222 }
223 }
224 return NULL;
225}
226
204const iString *name_GmIdentity(const iGmIdentity *d) { 227const iString *name_GmIdentity(const iGmIdentity *d) {
205 iString *name = collect_String(subject_TlsCertificate(d->cert)); 228 iString *name = collect_String(subject_TlsCertificate(d->cert));
206 if (startsWith_String(name, "CN = ")) { 229 if (startsWith_String(name, "CN = ")) {
@@ -631,7 +654,7 @@ void importIdentity_GmCerts(iGmCerts *d, iTlsCertificate *cert, const iString *n
631} 654}
632 655
633static const char *certPath_GmCerts_(const iGmCerts *d, const iGmIdentity *identity) { 656static const char *certPath_GmCerts_(const iGmCerts *d, const iGmIdentity *identity) {
634 if (!(identity->flags & (temporary_GmIdentityFlag | imported_GmIdentityFlag))) { 657 if (!(identity->flags & temporary_GmIdentityFlag)) {
635 const char *finger = cstrCollect_String(hexEncode_Block(&identity->fingerprint)); 658 const char *finger = cstrCollect_String(hexEncode_Block(&identity->fingerprint));
636 return concatPath_CStr(cstr_String(&d->saveDir), format_CStr("idents/%s", finger)); 659 return concatPath_CStr(cstr_String(&d->saveDir), format_CStr("idents/%s", finger));
637 } 660 }
diff --git a/src/gmcerts.h b/src/gmcerts.h
index 02a41c14..6ece1954 100644
--- a/src/gmcerts.h
+++ b/src/gmcerts.h
@@ -48,8 +48,9 @@ iBool isUsed_GmIdentity (const iGmIdentity *);
48iBool isUsedOn_GmIdentity (const iGmIdentity *, const iString *url); 48iBool isUsedOn_GmIdentity (const iGmIdentity *, const iString *url);
49iBool isUsedOnDomain_GmIdentity (const iGmIdentity *, const iRangecc domain); 49iBool isUsedOnDomain_GmIdentity (const iGmIdentity *, const iRangecc domain);
50 50
51void setUse_GmIdentity (iGmIdentity *, const iString *url, iBool use); 51void setUse_GmIdentity (iGmIdentity *, const iString *url, iBool use);
52void clearUse_GmIdentity (iGmIdentity *); 52void clearUse_GmIdentity (iGmIdentity *);
53const iString *findUse_GmIdentity (const iGmIdentity *, const iString *url);
53 54
54const iString *name_GmIdentity(const iGmIdentity *); 55const iString *name_GmIdentity(const iGmIdentity *);
55 56
diff --git a/src/gmdocument.c b/src/gmdocument.c
index 9d79830b..19230392 100644
--- a/src/gmdocument.c
+++ b/src/gmdocument.c
@@ -37,6 +37,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
37#include <the_Foundation/intset.h> 37#include <the_Foundation/intset.h>
38#include <the_Foundation/ptrarray.h> 38#include <the_Foundation/ptrarray.h>
39#include <the_Foundation/regexp.h> 39#include <the_Foundation/regexp.h>
40#include <the_Foundation/stringarray.h>
40#include <the_Foundation/stringset.h> 41#include <the_Foundation/stringset.h>
41 42
42#include <ctype.h> 43#include <ctype.h>
@@ -163,6 +164,7 @@ struct Impl_GmDocument {
163 iBool enableCommandLinks; /* `about:command?` only allowed on selected pages */ 164 iBool enableCommandLinks; /* `about:command?` only allowed on selected pages */
164 iBool isLayoutInvalidated; 165 iBool isLayoutInvalidated;
165 iArray layout; /* contents of source, laid out in document space */ 166 iArray layout; /* contents of source, laid out in document space */
167 iStringArray auxText; /* generated text that appears on the page but is not part of the source */
166 iPtrArray links; 168 iPtrArray links;
167 iString title; /* the first top-level title */ 169 iString title; /* the first top-level title */
168 iArray headings; 170 iArray headings;
@@ -554,16 +556,22 @@ static const int maxLedeLines_ = 10;
554 556
555static void applyAttributes_RunTypesetter_(iRunTypesetter *d, iTextAttrib attrib) { 557static void applyAttributes_RunTypesetter_(iRunTypesetter *d, iTextAttrib attrib) {
556 /* WARNING: This is duplicated in run_Font_(). Make sure they behave identically. */ 558 /* WARNING: This is duplicated in run_Font_(). Make sure they behave identically. */
557 if (attrib.bold) { 559 if (attrib.monospace) {
558 d->run.font = fontWithStyle_Text(d->baseFont, bold_FontStyle); 560 d->run.font = fontWithFamily_Text(d->baseFont, monospace_FontId);
559 d->run.color = tmFirstParagraph_ColorId; 561 d->run.color = tmPreformatted_ColorId;
560 } 562 }
561 else if (attrib.italic) { 563 else if (attrib.italic) {
562 d->run.font = fontWithStyle_Text(d->baseFont, italic_FontStyle); 564 d->run.font = fontWithStyle_Text(d->baseFont, italic_FontStyle);
563 } 565 }
564 else if (attrib.monospace) { 566 else if (attrib.regular) {
565 d->run.font = fontWithFamily_Text(d->baseFont, monospace_FontId); 567 d->run.font = fontWithStyle_Text(d->baseFont, regular_FontStyle);
566 d->run.color = tmPreformatted_ColorId; 568 }
569 else if (attrib.bold) {
570 d->run.font = fontWithStyle_Text(d->baseFont, bold_FontStyle);
571 d->run.color = tmFirstParagraph_ColorId;
572 }
573 else if (attrib.light) {
574 d->run.font = fontWithStyle_Text(d->baseFont, light_FontStyle);
567 } 575 }
568 else { 576 else {
569 d->run.font = d->baseFont; 577 d->run.font = d->baseFont;
@@ -643,6 +651,7 @@ static void doLayout_GmDocument_(iGmDocument *d) {
643 static const char *uploadArrow = upload_Icon; 651 static const char *uploadArrow = upload_Icon;
644 static const char *image = photo_Icon; 652 static const char *image = photo_Icon;
645 clear_Array(&d->layout); 653 clear_Array(&d->layout);
654 clear_StringArray(&d->auxText);
646 clearLinks_GmDocument_(d); 655 clearLinks_GmDocument_(d);
647 clear_Array(&d->headings); 656 clear_Array(&d->headings);
648 const iArray *oldPreMeta = collect_Array(copy_Array(&d->preMeta)); /* remember fold states */ 657 const iArray *oldPreMeta = collect_Array(copy_Array(&d->preMeta)); /* remember fold states */
@@ -927,7 +936,7 @@ static void doLayout_GmDocument_(iGmDocument *d) {
927 : paragraph_FontId; 936 : paragraph_FontId;
928 alignDecoration_GmRun_(&icon, iFalse); 937 alignDecoration_GmRun_(&icon, iFalse);
929 icon.color = linkColor_GmDocument(d, run.linkId, icon_GmLinkPart); 938 icon.color = linkColor_GmDocument(d, run.linkId, icon_GmLinkPart);
930 icon.flags |= decoration_GmRunFlag; 939 icon.flags |= decoration_GmRunFlag | startOfLine_GmRunFlag;
931 pushBack_Array(&d->layout, &icon); 940 pushBack_Array(&d->layout, &icon);
932 } 941 }
933 run.lineType = type; 942 run.lineType = type;
@@ -1041,7 +1050,12 @@ static void doLayout_GmDocument_(iGmDocument *d) {
1041 deinit_RunTypesetter_(&rts); 1050 deinit_RunTypesetter_(&rts);
1042 } 1051 }
1043 /* Flag the end of line, too. */ 1052 /* Flag the end of line, too. */
1044 ((iGmRun *) back_Array(&d->layout))->flags |= endOfLine_GmRunFlag; 1053 iGmRun *lastRun = back_Array(&d->layout);
1054 lastRun->flags |= endOfLine_GmRunFlag;
1055 if (lastRun->linkId && lastRun->flags & startOfLine_GmRunFlag) {
1056 /* Single-run link: the icon should also be marked endOfLine. */
1057 lastRun[-1].flags |= endOfLine_GmRunFlag;
1058 }
1045 /* Image or audio content. */ 1059 /* Image or audio content. */
1046 if (type == link_GmLineType) { 1060 if (type == link_GmLineType) {
1047 /* TODO: Cleanup here? Move to a function of its own. */ 1061 /* TODO: Cleanup here? Move to a function of its own. */
@@ -1075,7 +1089,9 @@ static void doLayout_GmDocument_(iGmDocument *d) {
1075 run.bounds.pos.x -= d->outsideMargin; 1089 run.bounds.pos.x -= d->outsideMargin;
1076 } 1090 }
1077 run.visBounds = run.bounds; 1091 run.visBounds = run.bounds;
1078 const iInt2 maxSize = mulf_I2(imgSize, get_Window()->pixelRatio); 1092 const iInt2 maxSize = mulf_I2(
1093 imgSize,
1094 get_Window()->pixelRatio * iMax(1.0f, (prefs_App()->zoomPercent / 100.0f)));
1079 if (width_Rect(run.visBounds) > maxSize.x) { 1095 if (width_Rect(run.visBounds) > maxSize.x) {
1080 /* Don't scale the image up. */ 1096 /* Don't scale the image up. */
1081 run.visBounds.size.y = 1097 run.visBounds.size.y =
@@ -1085,6 +1101,34 @@ static void doLayout_GmDocument_(iGmDocument *d) {
1085 run.bounds.size.y = run.visBounds.size.y; 1101 run.bounds.size.y = run.visBounds.size.y;
1086 } 1102 }
1087 pushBack_Array(&d->layout, &run); 1103 pushBack_Array(&d->layout, &run);
1104 pos.y += run.bounds.size.y + margin / 2;
1105 /* Image metadata caption. */ {
1106 run.font = FONT_ID(documentBody_FontId, semiBold_FontStyle, contentSmall_FontSize);
1107 run.color = tmQuoteIcon_ColorId;
1108 run.flags = decoration_GmRunFlag;
1109 run.mediaId = 0;
1110 run.mediaType = 0;
1111 run.visBounds.pos.y = pos.y;
1112 run.visBounds.size.y = lineHeight_Text(run.font);
1113 run.bounds = zero_Rect();
1114 iString caption;
1115 init_String(&caption);
1116 format_String(&caption,
1117 "%s \u2014 %d x %d \u2014 %.1f%s",
1118 info.type,
1119 imgSize.x,
1120 imgSize.y,
1121 info.numBytes / 1.0e6f,
1122 cstr_Lang("mb"));
1123 pushBack_StringArray(&d->auxText, &caption);
1124 run.text = range_String(&caption);
1125 /* Center it. */
1126 run.visBounds.size.x = measureRange_Text(run.font, range_String(&caption)).bounds.size.x;
1127 run.visBounds.pos.x = d->size.x / 2 - run.visBounds.size.x / 2;
1128 deinit_String(&caption);
1129 pushBack_Array(&d->layout, &run);
1130 pos.y += run.visBounds.size.y + margin;
1131 }
1088 break; 1132 break;
1089 } 1133 }
1090 case audio_MediaType: { 1134 case audio_MediaType: {
@@ -1157,6 +1201,7 @@ void init_GmDocument(iGmDocument *d) {
1157 d->enableCommandLinks = iFalse; 1201 d->enableCommandLinks = iFalse;
1158 d->isLayoutInvalidated = iFalse; 1202 d->isLayoutInvalidated = iFalse;
1159 init_Array(&d->layout, sizeof(iGmRun)); 1203 init_Array(&d->layout, sizeof(iGmRun));
1204 init_StringArray(&d->auxText);
1160 init_PtrArray(&d->links); 1205 init_PtrArray(&d->links);
1161 init_String(&d->title); 1206 init_String(&d->title);
1162 init_Array(&d->headings, sizeof(iGmHeading)); 1207 init_Array(&d->headings, sizeof(iGmHeading));
@@ -1178,6 +1223,7 @@ void deinit_GmDocument(iGmDocument *d) {
1178 deinit_PtrArray(&d->links); 1223 deinit_PtrArray(&d->links);
1179 deinit_Array(&d->preMeta); 1224 deinit_Array(&d->preMeta);
1180 deinit_Array(&d->headings); 1225 deinit_Array(&d->headings);
1226 deinit_StringArray(&d->auxText);
1181 deinit_Array(&d->layout); 1227 deinit_Array(&d->layout);
1182 deinit_String(&d->localHost); 1228 deinit_String(&d->localHost);
1183 deinit_String(&d->url); 1229 deinit_String(&d->url);
@@ -1228,8 +1274,8 @@ static void setDerivedThemeColors_(enum iGmDocumentTheme theme) {
1228 mix_Color(get_Color(tmQuoteIcon_ColorId), get_Color(tmBackground_ColorId), 0.4f)); 1274 mix_Color(get_Color(tmQuoteIcon_ColorId), get_Color(tmBackground_ColorId), 0.4f));
1229 set_Color(tmBackgroundOpenLink_ColorId, 1275 set_Color(tmBackgroundOpenLink_ColorId,
1230 mix_Color(get_Color(tmLinkText_ColorId), get_Color(tmBackground_ColorId), 0.90f)); 1276 mix_Color(get_Color(tmLinkText_ColorId), get_Color(tmBackground_ColorId), 0.90f));
1231 set_Color(tmFrameOpenLink_ColorId, 1277 set_Color(tmLinkFeedEntryDate_ColorId,
1232 mix_Color(get_Color(tmLinkText_ColorId), get_Color(tmBackground_ColorId), 0.75f)); 1278 mix_Color(get_Color(tmLinkText_ColorId), get_Color(tmBackground_ColorId), 0.25f));
1233 if (theme == colorfulDark_GmDocumentTheme) { 1279 if (theme == colorfulDark_GmDocumentTheme) {
1234 /* Ensure paragraph text and link text aren't too similarly colored. */ 1280 /* Ensure paragraph text and link text aren't too similarly colored. */
1235 if (delta_Color(get_Color(tmLinkText_ColorId), get_Color(tmParagraph_ColorId)) < 100) { 1281 if (delta_Color(get_Color(tmLinkText_ColorId), get_Color(tmParagraph_ColorId)) < 100) {
@@ -1715,9 +1761,12 @@ void setThemeSeed_GmDocument(iGmDocument *d, const iBlock *seed) {
1715} 1761}
1716 1762
1717void makePaletteGlobal_GmDocument(const iGmDocument *d) { 1763void makePaletteGlobal_GmDocument(const iGmDocument *d) {
1718 if (d->isPaletteValid) { 1764 if (!d->isPaletteValid) {
1719 memcpy(get_Root()->tmPalette, d->palette, sizeof(d->palette)); 1765 /* Recompute the palette since it's needed now. */
1766 setThemeSeed_GmDocument((iGmDocument *) d, urlThemeSeed_String(&d->url));
1720 } 1767 }
1768 iAssert(d->isPaletteValid);
1769 memcpy(get_Root()->tmPalette, d->palette, sizeof(d->palette));
1721} 1770}
1722 1771
1723void invalidatePalette_GmDocument(iGmDocument *d) { 1772void invalidatePalette_GmDocument(iGmDocument *d) {
@@ -1754,6 +1803,7 @@ static void markLinkRunsVisited_GmDocument_(iGmDocument *d, const iIntSet *linkI
1754 iForEach(Array, r, &d->layout) { 1803 iForEach(Array, r, &d->layout) {
1755 iGmRun *run = r.value; 1804 iGmRun *run = r.value;
1756 if (run->linkId && !run->mediaId && contains_IntSet(linkIds, run->linkId)) { 1805 if (run->linkId && !run->mediaId && contains_IntSet(linkIds, run->linkId)) {
1806 /* TODO: Does this even work? The font IDs may be different. */
1757 if (run->font == bold_FontId) { 1807 if (run->font == bold_FontId) {
1758 run->font = paragraph_FontId; 1808 run->font = paragraph_FontId;
1759 } 1809 }
@@ -1890,6 +1940,7 @@ static void normalize_GmDocument(iGmDocument *d) {
1890void setUrl_GmDocument(iGmDocument *d, const iString *url) { 1940void setUrl_GmDocument(iGmDocument *d, const iString *url) {
1891 url = canonicalUrl_String(url); 1941 url = canonicalUrl_String(url);
1892 set_String(&d->url, url); 1942 set_String(&d->url, url);
1943 setThemeSeed_GmDocument(d, urlThemeSeed_String(url));
1893 iUrl parts; 1944 iUrl parts;
1894 init_Url(&parts, url); 1945 init_Url(&parts, url);
1895 setRange_String(&d->localHost, parts.host); 1946 setRange_String(&d->localHost, parts.host);
@@ -1900,9 +1951,9 @@ void setUrl_GmDocument(iGmDocument *d, const iString *url) {
1900 } 1951 }
1901} 1952}
1902 1953
1903static int replaceRegExp_String(iString *d, const iRegExp *regexp, const char *replacement, 1954int replaceRegExp_String(iString *d, const iRegExp *regexp, const char *replacement,
1904 void (*matchHandler)(void *, const iRegExpMatch *), 1955 void (*matchHandler)(void *, const iRegExpMatch *),
1905 void *context) { 1956 void *context) {
1906 iRegExpMatch m; 1957 iRegExpMatch m;
1907 iString result; 1958 iString result;
1908 int numMatches = 0; 1959 int numMatches = 0;
@@ -2103,6 +2154,7 @@ void setSource_GmDocument(iGmDocument *d, const iString *source, int width, int
2103 if (size_String(source) == size_String(&d->unormSource)) { 2154 if (size_String(source) == size_String(&d->unormSource)) {
2104 iAssert(equal_String(source, &d->unormSource)); 2155 iAssert(equal_String(source, &d->unormSource));
2105// printf("[GmDocument] source is unchanged!\n"); 2156// printf("[GmDocument] source is unchanged!\n");
2157 updateWidth_GmDocument(d, width, canvasWidth);
2106 return; /* Nothing to do. */ 2158 return; /* Nothing to do. */
2107 } 2159 }
2108 /* Normalize and convert to Gemtext if needed. */ 2160 /* Normalize and convert to Gemtext if needed. */
diff --git a/src/gmdocument.h b/src/gmdocument.h
index 58fc3db3..eb02a26c 100644
--- a/src/gmdocument.h
+++ b/src/gmdocument.h
@@ -139,7 +139,7 @@ struct Impl_GmRun {
139 139
140 uint32_t font : 14; 140 uint32_t font : 14;
141 uint32_t mediaType : 3; /* note: max_MediaType means preformatted block */ 141 uint32_t mediaType : 3; /* note: max_MediaType means preformatted block */
142 uint32_t mediaId : 11; /* zero if not an image */ 142 uint32_t mediaId : 11;
143 uint32_t lineType : 3; 143 uint32_t lineType : 3;
144 uint32_t isLede : 1; 144 uint32_t isLede : 1;
145 }; 145 };
diff --git a/src/gmrequest.c b/src/gmrequest.c
index 23845475..3d5a4aef 100644
--- a/src/gmrequest.c
+++ b/src/gmrequest.c
@@ -585,8 +585,10 @@ void setUrl_GmRequest(iGmRequest *d, const iString *url) {
585 /* TODO: Gemini spec allows UTF-8 encoded URLs, but still need to percent-encode non-ASCII 585 /* TODO: Gemini spec allows UTF-8 encoded URLs, but still need to percent-encode non-ASCII
586 characters? Could be a server-side issue, e.g., if they're using a URL parser meant for 586 characters? Could be a server-side issue, e.g., if they're using a URL parser meant for
587 the web. */ 587 the web. */
588 urlEncodePath_String(&d->url); 588 /* Encode everything except already-percent encoded characters. */
589 urlEncodeSpaces_String(&d->url); 589 iString *enc = urlEncodeExclude_String(&d->url, "%" URL_RESERVED_CHARS);
590 set_String(&d->url, enc);
591 delete_String(enc);
590 d->identity = identityForUrl_GmCerts(d->certs, &d->url); 592 d->identity = identityForUrl_GmCerts(d->certs, &d->url);
591} 593}
592 594
@@ -692,9 +694,11 @@ void submit_GmRequest(iGmRequest *d) {
692 setCStr_String(&resp->meta, "text/gemini"); 694 setCStr_String(&resp->meta, "text/gemini");
693 iString *page = collectNew_String(); 695 iString *page = collectNew_String();
694 iString *parentDir = collectNewRange_String(dirName_Path(path)); 696 iString *parentDir = collectNewRange_String(dirName_Path(path));
697#if !defined (iPlatformMobile)
695 appendFormat_String(page, "=> %s " upArrow_Icon " %s" iPathSeparator "\n\n", 698 appendFormat_String(page, "=> %s " upArrow_Icon " %s" iPathSeparator "\n\n",
696 cstrCollect_String(makeFileUrl_String(parentDir)), 699 cstrCollect_String(makeFileUrl_String(parentDir)),
697 cstr_String(parentDir)); 700 cstr_String(parentDir));
701#endif
698 appendFormat_String(page, "# %s\n", cstr_Rangecc(baseName_Path(path))); 702 appendFormat_String(page, "# %s\n", cstr_Rangecc(baseName_Path(path)));
699 /* Make a directory index page. */ 703 /* Make a directory index page. */
700 iPtrArray *sortedInfo = collectNew_PtrArray(); 704 iPtrArray *sortedInfo = collectNew_PtrArray();
@@ -790,7 +794,8 @@ void submit_GmRequest(iGmRequest *d) {
790 cstr_String(containerUrl)); 794 cstr_String(containerUrl));
791 appendFormat_String(page, "# %s\n\n", cstr_Rangecc(containerName)); 795 appendFormat_String(page, "# %s\n\n", cstr_Rangecc(containerName));
792 appendFormat_String(page, 796 appendFormat_String(page,
793 cstrCount_Lang("archive.summary.n", numEntries_Archive(arch)), 797 cstrCount_Lang("archive.summary.n",
798 (int) numEntries_Archive(arch)),
794 numEntries_Archive(arch), 799 numEntries_Archive(arch),
795 (double) sourceSize_Archive(arch) / 1.0e6); 800 (double) sourceSize_Archive(arch) / 1.0e6);
796 appendCStr_String(page, "\n\n"); 801 appendCStr_String(page, "\n\n");
@@ -802,7 +807,7 @@ void submit_GmRequest(iGmRequest *d) {
802 } 807 }
803 else if (size_StringSet(contents) > 1) { 808 else if (size_StringSet(contents) > 1) {
804 appendFormat_String(page, cstrCount_Lang("dir.summary.n", 809 appendFormat_String(page, cstrCount_Lang("dir.summary.n",
805 size_StringSet(contents)), 810 (int) size_StringSet(contents)),
806 size_StringSet(contents)); 811 size_StringSet(contents));
807 appendCStr_String(page, "\n\n"); 812 appendCStr_String(page, "\n\n");
808 } 813 }
diff --git a/src/gmutil.c b/src/gmutil.c
index 70a3608e..98e4d4d6 100644
--- a/src/gmutil.c
+++ b/src/gmutil.c
@@ -253,6 +253,17 @@ iRangecc urlRoot_String(const iString *d) {
253 return (iRangecc){ constBegin_String(d), rootEnd }; 253 return (iRangecc){ constBegin_String(d), rootEnd };
254} 254}
255 255
256const iBlock *urlThemeSeed_String(const iString *url) {
257 if (equalCase_Rangecc(urlScheme_String(url), "file")) {
258 return collect_Block(new_Block(0));
259 }
260 const iRangecc user = urlUser_String(url);
261 if (isEmpty_Range(&user)) {
262 return collect_Block(newRange_Block(urlHost_String(url)));
263 }
264 return collect_Block(newRange_Block(user));
265}
266
256static iBool isAbsolutePath_(iRangecc path) { 267static iBool isAbsolutePath_(iRangecc path) {
257 return isAbsolute_Path(collect_String(urlDecode_String(collect_String(newRange_String(path))))); 268 return isAbsolute_Path(collect_String(urlDecode_String(collect_String(newRange_String(path)))));
258} 269}
@@ -319,6 +330,28 @@ void urlEncodePath_String(iString *d) {
319 delete_String(encoded); 330 delete_String(encoded);
320} 331}
321 332
333void urlEncodeQuery_String(iString *d) {
334 iUrl url;
335 init_Url(&url, d);
336 if (isEmpty_Range(&url.query)) {
337 return;
338 }
339 iString encoded;
340 init_String(&encoded);
341 appendRange_String(&encoded, (iRangecc){ constBegin_String(d), url.query.start });
342 iString query;
343 url.query.start++; /* omit the question mark */
344 initRange_String(&query, url.query);
345 iString *encQuery = urlEncode_String(&query); /* fully encoded */
346 appendCStr_String(&encoded, "?");
347 append_String(&encoded, encQuery);
348 delete_String(encQuery);
349 deinit_String(&query);
350 appendRange_String(&encoded, (iRangecc){ url.query.end, constEnd_String(d) });
351 set_String(d, &encoded);
352 deinit_String(&encoded);
353}
354
322iBool isKnownScheme_Rangecc(iRangecc scheme) { 355iBool isKnownScheme_Rangecc(iRangecc scheme) {
323 if (isKnownUrlScheme_Rangecc(scheme)) { 356 if (isKnownUrlScheme_Rangecc(scheme)) {
324 return iTrue; 357 return iTrue;
@@ -651,25 +684,25 @@ const iString *withSpacesEncoded_String(const iString *d) {
651const iString *canonicalUrl_String(const iString *d) { 684const iString *canonicalUrl_String(const iString *d) {
652 /* The "canonical" form, used for internal storage and comparisons, is: 685 /* The "canonical" form, used for internal storage and comparisons, is:
653 - all non-reserved characters decoded (i.e., it's an IRI) 686 - all non-reserved characters decoded (i.e., it's an IRI)
654 - expect for spaces, which are always `%20` 687 - except spaces, which are always `%20`
655 This means a canonical URL can be used on a gemtext link line without modifications. */ 688 This means a canonical URL can be used on a gemtext link line without modifications. */
656 iString *canon = NULL; 689 iString *canon = NULL;
657 iUrl parts; 690 iUrl parts;
658 init_Url(&parts, d); 691 init_Url(&parts, d);
659 /* Colons are in decoded form in the URL path. */ 692 /* Colons (0x3a) are in decoded form in the URL path. */
660 if (iStrStrN(parts.path.start, "%3A", size_Range(&parts.path)) || 693 if (iStrStrN(parts.path.start, "%3A", size_Range(&parts.path)) ||
661 iStrStrN(parts.path.start, "%3a", size_Range(&parts.path))) { 694 iStrStrN(parts.path.start, "%3a", size_Range(&parts.path))) {
662 /* This is done separately to avoid the copy if %3A is not present; it's rare. */ 695 /* This is done separately to avoid the copy if %3A is not present; it's rare. */
663 canon = copy_String(d); 696 canon = copy_String(d);
664 urlDecodePath_String(canon); 697 urlDecodePath_String(canon);
665 iString *dec = maybeUrlDecodeExclude_String(canon, "%/?:;#&+= "); /* decode everything else in all parts */ 698 iString *dec = maybeUrlDecodeExclude_String(canon, "% " URL_RESERVED_CHARS); /* decode everything else in all parts */
666 if (dec) { 699 if (dec) {
667 set_String(canon, dec); 700 set_String(canon, dec);
668 delete_String(dec); 701 delete_String(dec);
669 } 702 }
670 } 703 }
671 else { 704 else {
672 canon = maybeUrlDecodeExclude_String(d, "%/?:;#&+= "); 705 canon = maybeUrlDecodeExclude_String(d, "% " URL_RESERVED_CHARS);
673 } 706 }
674 /* `canon` may now be NULL if nothing was decoded. */ 707 /* `canon` may now be NULL if nothing was decoded. */
675 if (indexOfCStr_String(canon ? canon : d, " ") != iInvalidPos || 708 if (indexOfCStr_String(canon ? canon : d, " ") != iInvalidPos ||
@@ -678,7 +711,7 @@ const iString *canonicalUrl_String(const iString *d) {
678 canon = copy_String(d); 711 canon = copy_String(d);
679 } 712 }
680 urlEncodeSpaces_String(canon); 713 urlEncodeSpaces_String(canon);
681 } 714 }
682 return canon ? collect_String(canon) : d; 715 return canon ? collect_String(canon) : d;
683} 716}
684 717
diff --git a/src/gmutil.h b/src/gmutil.h
index 6b10b444..15bb7b2e 100644
--- a/src/gmutil.h
+++ b/src/gmutil.h
@@ -27,6 +27,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
27 27
28iDeclareType(GmError) 28iDeclareType(GmError)
29iDeclareType(RegExp) 29iDeclareType(RegExp)
30iDeclareType(RegExpMatch)
30iDeclareType(Url) 31iDeclareType(Url)
31 32
32/* Response status codes. */ 33/* Response status codes. */
@@ -99,6 +100,7 @@ iRegExp * newGemtextLink_RegExp (void);
99 100
100#define GEMINI_DEFAULT_PORT ((uint16_t) 1965) 101#define GEMINI_DEFAULT_PORT ((uint16_t) 1965)
101#define GEMINI_DEFAULT_PORT_CSTR "1965" 102#define GEMINI_DEFAULT_PORT_CSTR "1965"
103#define URL_RESERVED_CHARS ":/?#[]@!$&'()*+,;=" /* RFC 3986 */
102 104
103struct Impl_Url { 105struct Impl_Url {
104 iRangecc scheme; 106 iRangecc scheme;
@@ -117,6 +119,8 @@ iRangecc urlHost_String (const iString *);
117uint16_t urlPort_String (const iString *); 119uint16_t urlPort_String (const iString *);
118iRangecc urlUser_String (const iString *); 120iRangecc urlUser_String (const iString *);
119iRangecc urlRoot_String (const iString *); 121iRangecc urlRoot_String (const iString *);
122const iBlock * urlThemeSeed_String (const iString *);
123
120const iString * absoluteUrl_String (const iString *, const iString *urlMaybeRelative); 124const iString * absoluteUrl_String (const iString *, const iString *urlMaybeRelative);
121iBool isLikelyUrl_String (const iString *); 125iBool isLikelyUrl_String (const iString *);
122iBool isKnownScheme_Rangecc (iRangecc scheme); /* any URI scheme */ 126iBool isKnownScheme_Rangecc (iRangecc scheme); /* any URI scheme */
@@ -128,6 +132,7 @@ const iString * urlFragmentStripped_String(const iString *);
128const iString * urlQueryStripped_String (const iString *); 132const iString * urlQueryStripped_String (const iString *);
129void urlDecodePath_String (iString *); 133void urlDecodePath_String (iString *);
130void urlEncodePath_String (iString *); 134void urlEncodePath_String (iString *);
135void urlEncodeQuery_String (iString *);
131iString * makeFileUrl_String (const iString *localFilePath); 136iString * makeFileUrl_String (const iString *localFilePath);
132const char * makeFileUrl_CStr (const char *localFilePath); 137const char * makeFileUrl_CStr (const char *localFilePath);
133iString * localFilePathFromUrl_String(const iString *); 138iString * localFilePathFromUrl_String(const iString *);
@@ -143,3 +148,8 @@ const iString * findContainerArchive_Path (const iString *path);
143 148
144 149
145const iString * feedEntryOpenCommand_String (const iString *url, int newTab); /* checks fragment */ 150const iString * feedEntryOpenCommand_String (const iString *url, int newTab); /* checks fragment */
151
152/* TODO: Consider adding this to the_Foundation. */
153int replaceRegExp_String (iString *, const iRegExp *regexp, const char *replacement,
154 void (*matchHandler)(void *, const iRegExpMatch *),
155 void *context);
diff --git a/src/history.c b/src/history.c
index 7185912f..56454009 100644
--- a/src/history.c
+++ b/src/history.c
@@ -37,7 +37,7 @@ void init_RecentUrl(iRecentUrl *d) {
37 d->normScrollY = 0; 37 d->normScrollY = 0;
38 d->cachedResponse = NULL; 38 d->cachedResponse = NULL;
39 d->cachedDoc = NULL; 39 d->cachedDoc = NULL;
40 d->flags.openedFromSidebar = iFalse; 40 d->flags = 0;
41} 41}
42 42
43void deinit_RecentUrl(iRecentUrl *d) { 43void deinit_RecentUrl(iRecentUrl *d) {
@@ -110,6 +110,14 @@ iHistory *copy_History(const iHistory *d) {
110 return copy; 110 return copy;
111} 111}
112 112
113void lock_History(iHistory *d) {
114 lock_Mutex(d->mtx);
115}
116
117void unlock_History(iHistory *d) {
118 unlock_Mutex(d->mtx);
119}
120
113iMemInfo memoryUsage_History(const iHistory *d) { 121iMemInfo memoryUsage_History(const iHistory *d) {
114 iMemInfo mem = { 0, 0 }; 122 iMemInfo mem = { 0, 0 };
115 iConstForEach(Array, i, &d->recent) { 123 iConstForEach(Array, i, &d->recent) {
@@ -173,7 +181,7 @@ void serialize_History(const iHistory *d, iStream *outs) {
173 const iRecentUrl *item = i.value; 181 const iRecentUrl *item = i.value;
174 serialize_String(&item->url, outs); 182 serialize_String(&item->url, outs);
175 write32_Stream(outs, item->normScrollY * 1.0e6f); 183 write32_Stream(outs, item->normScrollY * 1.0e6f);
176 writeU16_Stream(outs, item->flags.openedFromSidebar ? iBit(1) : 0); 184 writeU16_Stream(outs, item->flags);
177 if (item->cachedResponse) { 185 if (item->cachedResponse) {
178 write8_Stream(outs, 1); 186 write8_Stream(outs, 1);
179 serialize_GmResponse(item->cachedResponse, outs); 187 serialize_GmResponse(item->cachedResponse, outs);
@@ -197,10 +205,7 @@ void deserialize_History(iHistory *d, iStream *ins) {
197 set_String(&item.url, canonicalUrl_String(&item.url)); 205 set_String(&item.url, canonicalUrl_String(&item.url));
198 item.normScrollY = (float) read32_Stream(ins) / 1.0e6f; 206 item.normScrollY = (float) read32_Stream(ins) / 1.0e6f;
199 if (version_Stream(ins) >= addedRecentUrlFlags_FileVersion) { 207 if (version_Stream(ins) >= addedRecentUrlFlags_FileVersion) {
200 uint16_t flags = readU16_Stream(ins); 208 item.flags = readU16_Stream(ins);
201 if (flags & iBit(1)) {
202 item.flags.openedFromSidebar = iTrue;
203 }
204 } 209 }
205 if (read8_Stream(ins)) { 210 if (read8_Stream(ins)) {
206 item.cachedResponse = new_GmResponse(); 211 item.cachedResponse = new_GmResponse();
@@ -246,18 +251,26 @@ const iString *url_History(const iHistory *d, size_t pos) {
246 return collectNew_String(); 251 return collectNew_String();
247} 252}
248 253
249iRecentUrl *findUrl_History(iHistory *d, const iString *url) { 254#if 0
255iRecentUrl *findUrl_History(iHistory *d, const iString *url, int timeDir) {
250 url = canonicalUrl_String(url); 256 url = canonicalUrl_String(url);
257// if (!timeDir) {
258// timeDir = -1;
259// }
251 lock_Mutex(d->mtx); 260 lock_Mutex(d->mtx);
252 iReverseForEach(Array, i, &d->recent) { 261 for (size_t i = size_Array(&d->recent) - 1 - d->recentPos; i < size_Array(&d->recent);
253 if (cmpStringCase_String(url, &((iRecentUrl *) i.value)->url) == 0) { 262 i += timeDir) {
263 iRecentUrl *item = at_Array(&d->recent, i);
264 if (cmpStringCase_String(url, &item->url) == 0) {
254 unlock_Mutex(d->mtx); 265 unlock_Mutex(d->mtx);
255 return i.value; 266 return item; /* FIXME: Returning an internal pointer; should remain locked. */
256 } 267 }
268 if (!timeDir) break;
257 } 269 }
258 unlock_Mutex(d->mtx); 270 unlock_Mutex(d->mtx);
259 return NULL; 271 return NULL;
260} 272}
273#endif
261 274
262void replace_History(iHistory *d, const iString *url) { 275void replace_History(iHistory *d, const iString *url) {
263 url = canonicalUrl_String(url); 276 url = canonicalUrl_String(url);
@@ -297,20 +310,31 @@ void add_History(iHistory *d, const iString *url) {
297 unlock_Mutex(d->mtx); 310 unlock_Mutex(d->mtx);
298} 311}
299 312
300iBool preceding_History(iHistory *d, iRecentUrl *recent_out) { 313void undo_History(iHistory *d) {
301 iBool ok = iFalse;
302 lock_Mutex(d->mtx); 314 lock_Mutex(d->mtx);
303 if (!isEmpty_Array(&d->recent) && d->recentPos < size_Array(&d->recent) - 1) { 315 if (!isEmpty_Array(&d->recent) || d->recentPos != 0) {
304 const iRecentUrl *recent = constAt_Array(&d->recent, size_Array(&d->recent) - 1 - 316 deinit_RecentUrl(back_Array(&d->recent));
305 (d->recentPos + 1)); 317 popBack_Array(&d->recent);
306 set_String(&recent_out->url, &recent->url); 318 }
307 recent_out->normScrollY = recent->normScrollY; 319 unlock_Mutex(d->mtx);
308 iChangeRef(recent_out->cachedDoc, recent->cachedDoc); 320}
321
322iRecentUrl *precedingLocked_History(iHistory *d) {
323 /* NOTE: Manual lock and unlock are required when using this; returning an internal pointer. */
324 iBool ok = iFalse;
325 //lock_Mutex(d->mtx);
326 const size_t lastIndex = size_Array(&d->recent) - 1;
327 if (!isEmpty_Array(&d->recent) && d->recentPos < lastIndex) {
328 return at_Array(&d->recent, lastIndex - (d->recentPos + 1));
329// set_String(&recent_out->url, &recent->url);
330// recent_out->normScrollY = recent->normScrollY;
331// iChangeRef(recent_out->cachedDoc, recent->cachedDoc);
309 /* Cached response is not returned, would involve a deep copy. */ 332 /* Cached response is not returned, would involve a deep copy. */
310 ok = iTrue; 333// ok = iTrue;
311 } 334 }
312 unlock_Mutex(d->mtx); 335 //unlock_Mutex(d->mtx);
313 return ok; 336// return ok;
337 return NULL;
314} 338}
315 339
316#if 0 340#if 0
@@ -360,7 +384,7 @@ iBool goForward_History(iHistory *d) {
360 return iFalse; 384 return iFalse;
361} 385}
362 386
363iBool atLatest_History(const iHistory *d) { 387iBool atNewest_History(const iHistory *d) {
364 iBool isLatest; 388 iBool isLatest;
365 iGuardMutex(d->mtx, isLatest = (d->recentPos == 0)); 389 iGuardMutex(d->mtx, isLatest = (d->recentPos == 0));
366 return isLatest; 390 return isLatest;
@@ -391,12 +415,18 @@ void setCachedResponse_History(iHistory *d, const iGmResponse *response) {
391 unlock_Mutex(d->mtx); 415 unlock_Mutex(d->mtx);
392} 416}
393 417
394void setCachedDocument_History(iHistory *d, iGmDocument *doc, iBool openedFromSidebar) { 418void setCachedDocument_History(iHistory *d, iGmDocument *doc) {
395 lock_Mutex(d->mtx); 419 lock_Mutex(d->mtx);
396 iRecentUrl *item = mostRecentUrl_History(d); 420 iRecentUrl *item = mostRecentUrl_History(d);
421 iAssert(size_GmDocument(doc).x > 0);
397 if (item) { 422 if (item) {
398 iAssert(equal_String(url_GmDocument(doc), &item->url)); 423#if !defined (NDEBUG)
399 item->flags.openedFromSidebar = openedFromSidebar; 424 if (!equal_String(url_GmDocument(doc), &item->url)) {
425 printf("[History] Cache mismatch! Expecting data for item {%s} but document URL is {%s}\n",
426 cstr_String(&item->url),
427 cstr_String(url_GmDocument(doc)));
428 }
429#endif
400 if (item->cachedDoc != doc) { 430 if (item->cachedDoc != doc) {
401 iRelease(item->cachedDoc); 431 iRelease(item->cachedDoc);
402 item->cachedDoc = ref_Object(doc); 432 item->cachedDoc = ref_Object(doc);
diff --git a/src/history.h b/src/history.h
index d3daae80..7959187d 100644
--- a/src/history.h
+++ b/src/history.h
@@ -39,9 +39,7 @@ struct Impl_RecentUrl {
39 float normScrollY; /* normalized to document height */ 39 float normScrollY; /* normalized to document height */
40 iGmResponse *cachedResponse; /* kept in memory for quicker back navigation */ 40 iGmResponse *cachedResponse; /* kept in memory for quicker back navigation */
41 iGmDocument *cachedDoc; /* cached copy of the presentation: layout and media (not serialized) */ 41 iGmDocument *cachedDoc; /* cached copy of the presentation: layout and media (not serialized) */
42 struct { 42 uint16_t flags;
43 uint8_t openedFromSidebar : 1;
44 } flags;
45}; 43};
46 44
47iDeclareType(MemInfo) 45iDeclareType(MemInfo)
@@ -58,19 +56,21 @@ iDeclareTypeConstruction(History)
58iDeclareTypeSerialization(History) 56iDeclareTypeSerialization(History)
59 57
60iHistory * copy_History (const iHistory *); 58iHistory * copy_History (const iHistory *);
59void lock_History (iHistory *);
60void unlock_History (iHistory *);
61 61
62void clear_History (iHistory *); 62void clear_History (iHistory *);
63void add_History (iHistory *, const iString *url); 63void add_History (iHistory *, const iString *url);
64void undo_History (iHistory *); /* removes the most recent URL */
64void replace_History (iHistory *, const iString *url); 65void replace_History (iHistory *, const iString *url);
65void setCachedResponse_History (iHistory *, const iGmResponse *response); 66void setCachedResponse_History (iHistory *, const iGmResponse *response);
66void setCachedDocument_History (iHistory *, iGmDocument *doc, iBool openedFromSidebar); 67void setCachedDocument_History (iHistory *, iGmDocument *doc);
67iBool goBack_History (iHistory *); 68iBool goBack_History (iHistory *);
68iBool goForward_History (iHistory *); 69iBool goForward_History (iHistory *);
69iBool preceding_History (iHistory *d, iRecentUrl *recent_out); 70iRecentUrl *precedingLocked_History (iHistory *); /* requires manual lock/unlock! */
70//iBool following_History (iHistory *d, iRecentUrl *recent_out);
71iRecentUrl *recentUrl_History (iHistory *, size_t pos); 71iRecentUrl *recentUrl_History (iHistory *, size_t pos);
72iRecentUrl *mostRecentUrl_History (iHistory *); 72iRecentUrl *mostRecentUrl_History (iHistory *);
73iRecentUrl *findUrl_History (iHistory *, const iString *url); 73//iRecentUrl *findUrl_History (iHistory *, const iString *url, int timeDir);
74 74
75void clearCache_History (iHistory *); 75void clearCache_History (iHistory *);
76size_t pruneLeastImportant_History (iHistory *); 76size_t pruneLeastImportant_History (iHistory *);
@@ -78,7 +78,7 @@ size_t pruneLeastImportantMemory_History (iHistory *);
78void invalidateTheme_History (iHistory *); /* theme has changed, cached contents need updating */ 78void invalidateTheme_History (iHistory *); /* theme has changed, cached contents need updating */
79void invalidateCachedLayout_History (iHistory *); 79void invalidateCachedLayout_History (iHistory *);
80 80
81iBool atLatest_History (const iHistory *); 81iBool atNewest_History (const iHistory *);
82iBool atOldest_History (const iHistory *); 82iBool atOldest_History (const iHistory *);
83 83
84const iStringArray * searchContents_History (const iHistory *, const iRegExp *pattern); /* chronologically ascending */ 84const iStringArray * searchContents_History (const iHistory *, const iRegExp *pattern); /* chronologically ascending */
diff --git a/src/ios.h b/src/ios.h
index 85177409..9860f7a2 100644
--- a/src/ios.h
+++ b/src/ios.h
@@ -38,6 +38,8 @@ void playHapticEffect_iOS (enum iHapticEffect effect);
38void exportDownloadedFile_iOS(const iString *path); 38void exportDownloadedFile_iOS(const iString *path);
39void pickFileForOpening_iOS (void); 39void pickFileForOpening_iOS (void);
40void pickFile_iOS (const char *command); /* ` path:%s` will be appended */ 40void pickFile_iOS (const char *command); /* ` path:%s` will be appended */
41void openTextActivityView_iOS(const iString *text);
42void openFileActivityView_iOS(const iString *path);
41 43
42iBool isPhone_iOS (void); 44iBool isPhone_iOS (void);
43void safeAreaInsets_iOS (float *left, float *top, float *right, float *bottom); 45void safeAreaInsets_iOS (float *left, float *top, float *right, float *bottom);
@@ -61,3 +63,30 @@ iBool isPaused_AVFAudioPlayer (const iAVFAudioPlayer *);
61 63
62void clearNowPlayingInfo_iOS (void); 64void clearNowPlayingInfo_iOS (void);
63void updateNowPlayingInfo_iOS (void); 65void updateNowPlayingInfo_iOS (void);
66
67/*----------------------------------------------------------------------------------------------*/
68
69enum iSystemTextInputFlags {
70 selectAll_SystemTextInputFlags = iBit(1),
71 multiLine_SystemTextInputFlags = iBit(2),
72 returnGo_SystemTextInputFlags = iBit(3),
73 returnSend_SystemTextInputFlags = iBit(4),
74 disableAutocorrect_SystemTextInputFlag = iBit(5),
75 disableAutocapitalize_SystemTextInputFlag = iBit(6),
76 alignRight_SystemTextInputFlag = iBit(7),
77 insertNewlines_SystemTextInputFlag = iBit(8),
78 extraPadding_SystemTextInputFlag = iBit(9),
79};
80
81iDeclareType(SystemTextInput)
82iDeclareTypeConstructionArgs(SystemTextInput, iRect rect, int flags)
83
84void setRect_SystemTextInput (iSystemTextInput *, iRect rect);
85void setText_SystemTextInput (iSystemTextInput *, const iString *text, iBool allowUndo);
86void setFont_SystemTextInput (iSystemTextInput *, int fontId);
87void setTextChangedFunc_SystemTextInput
88 (iSystemTextInput *, void (*textChangedFunc)(iSystemTextInput *, void *), void *);
89void selectAll_SystemTextInput(iSystemTextInput *);
90
91const iString * text_SystemTextInput (const iSystemTextInput *);
92int preferredHeight_SystemTextInput (const iSystemTextInput *);
diff --git a/src/ios.m b/src/ios.m
index b46fb8dc..82596ffd 100644
--- a/src/ios.m
+++ b/src/ios.m
@@ -25,6 +25,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
25#include "audio/player.h" 25#include "audio/player.h"
26#include "ui/command.h" 26#include "ui/command.h"
27#include "ui/window.h" 27#include "ui/window.h"
28#include "ui/touch.h"
28 29
29#include <the_Foundation/file.h> 30#include <the_Foundation/file.h>
30#include <the_Foundation/fileinfo.h> 31#include <the_Foundation/fileinfo.h>
@@ -60,6 +61,9 @@ static UIViewController *viewController_(iWindow *window) {
60 return NULL; 61 return NULL;
61} 62}
62 63
64static void notifyChange_SystemTextInput_(iSystemTextInput *);
65static BOOL isNewlineAllowed_SystemTextInput_(const iSystemTextInput *);
66
63/*----------------------------------------------------------------------------------------------*/ 67/*----------------------------------------------------------------------------------------------*/
64 68
65API_AVAILABLE(ios(13.0)) 69API_AVAILABLE(ios(13.0))
@@ -159,9 +163,10 @@ API_AVAILABLE(ios(13.0))
159 163
160/*----------------------------------------------------------------------------------------------*/ 164/*----------------------------------------------------------------------------------------------*/
161 165
162@interface AppState : NSObject<UIDocumentPickerDelegate> { 166@interface AppState : NSObject<UIDocumentPickerDelegate, UITextFieldDelegate, UITextViewDelegate> {
163 iString *fileBeingSaved; 167 iString *fileBeingSaved;
164 iString *pickFileCommand; 168 iString *pickFileCommand;
169 iSystemTextInput *sysCtrl;
165} 170}
166@property (nonatomic, assign) BOOL isHapticsAvailable; 171@property (nonatomic, assign) BOOL isHapticsAvailable;
167@property (nonatomic, strong) NSObject *haptic; 172@property (nonatomic, strong) NSObject *haptic;
@@ -175,9 +180,18 @@ static AppState *appState_;
175 self = [super init]; 180 self = [super init];
176 fileBeingSaved = NULL; 181 fileBeingSaved = NULL;
177 pickFileCommand = NULL; 182 pickFileCommand = NULL;
183 sysCtrl = NULL;
178 return self; 184 return self;
179} 185}
180 186
187-(void)setSystemTextInput:(iSystemTextInput *)sys {
188 sysCtrl = sys;
189}
190
191-(iSystemTextInput *)systemTextInput {
192 return sysCtrl;
193}
194
181-(void)setPickFileCommand:(const char *)command { 195-(void)setPickFileCommand:(const char *)command {
182 if (!pickFileCommand) { 196 if (!pickFileCommand) {
183 pickFileCommand = new_String(); 197 pickFileCommand = new_String();
@@ -256,6 +270,46 @@ didPickDocumentsAtURLs:(NSArray<NSURL *> *)urls {
256-(void)keyboardOffScreen:(NSNotification *)notification { 270-(void)keyboardOffScreen:(NSNotification *)notification {
257 setKeyboardHeight_MainWindow(get_MainWindow(), 0); 271 setKeyboardHeight_MainWindow(get_MainWindow(), 0);
258} 272}
273
274static void sendReturnKeyPress_(void) {
275 SDL_Event ev = { .type = SDL_KEYDOWN };
276 ev.key.timestamp = SDL_GetTicks();
277 ev.key.keysym.sym = SDLK_RETURN;
278 ev.key.state = SDL_PRESSED;
279 SDL_PushEvent(&ev);
280 ev.type = SDL_KEYUP;
281 ev.key.state = SDL_RELEASED;
282 SDL_PushEvent(&ev);
283}
284
285- (BOOL)textFieldShouldReturn:(UITextField *)textField {
286 sendReturnKeyPress_();
287 return NO;
288}
289
290- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range
291replacementString:(NSString *)string {
292 iSystemTextInput *sysCtrl = [appState_ systemTextInput];
293 notifyChange_SystemTextInput_(sysCtrl);
294 return YES;
295}
296
297- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range
298 replacementText:(NSString *)text {
299 if ([text isEqualToString:@"\n"]) {
300 if (!isNewlineAllowed_SystemTextInput_([appState_ systemTextInput])) {
301 sendReturnKeyPress_();
302 return NO;
303 }
304 }
305 return YES;
306}
307
308- (void)textViewDidChange:(UITextView *)textView {
309 iSystemTextInput *sysCtrl = [appState_ systemTextInput];
310 notifyChange_SystemTextInput_(sysCtrl);
311}
312
259@end 313@end
260 314
261static void enableMouse_(iBool yes) { 315static void enableMouse_(iBool yes) {
@@ -483,6 +537,35 @@ void pickFile_iOS(const char *command) {
483 [viewController_(get_Window()) presentViewController:picker animated:YES completion:nil]; 537 [viewController_(get_Window()) presentViewController:picker animated:YES completion:nil];
484} 538}
485 539
540static void openActivityView_(NSArray *activityItems) {
541 UIActivityViewController *actView =
542 [[UIActivityViewController alloc]
543 initWithActivityItems:activityItems
544 applicationActivities:nil];
545 iWindow *win = get_Window();
546 UIViewController *viewCtl = viewController_(win);
547 UIPopoverPresentationController *popover = [actView popoverPresentationController];
548 if (popover) {
549 [popover setSourceView:[viewCtl view]];
550 iInt2 tapPos = latestTapPosition_Touch();
551 tapPos.x /= win->pixelRatio;
552 tapPos.y /= win->pixelRatio;
553 [popover setSourceRect:(CGRect){{tapPos.x - 10, tapPos.y - 10}, {20, 20}}];
554 [popover setCanOverlapSourceViewRect:YES];
555 }
556 [viewCtl presentViewController:actView animated:YES completion:nil];
557}
558
559void openTextActivityView_iOS(const iString *text) {
560 openActivityView_(@[[NSString stringWithUTF8String:cstr_String(text)]]);
561}
562
563void openFileActivityView_iOS(const iString *path) {
564 NSURL *url = [NSURL fileURLWithPath:[[NSString alloc] initWithCString:cstr_String(path)
565 encoding:NSUTF8StringEncoding]];
566 openActivityView_(@[url]);
567}
568
486/*----------------------------------------------------------------------------------------------*/ 569/*----------------------------------------------------------------------------------------------*/
487 570
488enum iAVFAudioPlayerState { 571enum iAVFAudioPlayerState {
@@ -511,6 +594,10 @@ void init_AVFAudioPlayer(iAVFAudioPlayer *d) {
511 594
512void deinit_AVFAudioPlayer(iAVFAudioPlayer *d) { 595void deinit_AVFAudioPlayer(iAVFAudioPlayer *d) {
513 setInput_AVFAudioPlayer(d, NULL, NULL); 596 setInput_AVFAudioPlayer(d, NULL, NULL);
597 if (d->player) {
598 CFBridgingRelease(d->player);
599 d->player = nil;
600 }
514} 601}
515 602
516static const char *cacheDir_ = "~/Library/Caches/Audio"; 603static const char *cacheDir_ = "~/Library/Caches/Audio";
@@ -607,3 +694,233 @@ iBool isStarted_AVFAudioPlayer(const iAVFAudioPlayer *d) {
607iBool isPaused_AVFAudioPlayer(const iAVFAudioPlayer *d) { 694iBool isPaused_AVFAudioPlayer(const iAVFAudioPlayer *d) {
608 return d->state == paused_AVFAudioPlayerState; 695 return d->state == paused_AVFAudioPlayerState;
609} 696}
697
698/*----------------------------------------------------------------------------------------------*/
699
700struct Impl_SystemTextInput {
701 int flags;
702 void *field; /* single-line text field */
703 void *view; /* multi-line text view */
704 void (*textChangedFunc)(iSystemTextInput *, void *);
705 void *textChangedContext;
706};
707
708iDefineTypeConstructionArgs(SystemTextInput, (iRect rect, int flags), rect, flags)
709
710#define REF_d_field (__bridge UITextField *)d->field
711#define REF_d_view (__bridge UITextView *)d->view
712
713static CGRect convertToCGRect_(const iRect *rect, iBool expanded) {
714 const iWindow *win = get_Window();
715 CGRect frame;
716 // TODO: Convert coordinates properly!
717 frame.origin.x = rect->pos.x / win->pixelRatio;
718 frame.origin.y = (rect->pos.y - gap_UI + 1) / win->pixelRatio;
719 frame.size.width = rect->size.x / win->pixelRatio;
720 frame.size.height = rect->size.y / win->pixelRatio;
721 /* Some padding to account for insets. If we just zero out the insets, the insertion point
722 may be clipped at the edges. */
723 if (expanded) {
724 const float inset = gap_UI / get_Window()->pixelRatio;
725 frame.origin.x -= inset + 1;
726 frame.origin.y -= inset + 1;
727 frame.size.width += 2 * inset + 2;
728 frame.size.height += inset + 1 + inset;
729 }
730 return frame;
731}
732
733static UIColor *makeUIColor_(enum iColorId colorId) {
734 iColor color = get_Color(colorId);
735 return [UIColor colorWithRed:color.r / 255.0
736 green:color.g / 255.0
737 blue:color.b / 255.0
738 alpha:color.a / 255.0];
739}
740
741void init_SystemTextInput(iSystemTextInput *d, iRect rect, int flags) {
742 d->flags = flags;
743 d->field = NULL;
744 d->view = NULL;
745 CGRect frame = convertToCGRect_(&rect, (flags & multiLine_SystemTextInputFlags) != 0);
746 if (flags & multiLine_SystemTextInputFlags) {
747 d->view = (void *) CFBridgingRetain([[UITextView alloc] initWithFrame:frame textContainer:nil]);
748 [[viewController_(get_Window()) view] addSubview:REF_d_view];
749 }
750 else {
751 d->field = (void *) CFBridgingRetain([[UITextField alloc] initWithFrame:frame]);
752 [[viewController_(get_Window()) view] addSubview:REF_d_field];
753 }
754 UIControl<UITextInputTraits> *traits = (UIControl<UITextInputTraits> *) (d->view ? REF_d_view : REF_d_field);
755 if (~flags & insertNewlines_SystemTextInputFlag) {
756 [traits setReturnKeyType:UIReturnKeyDone];
757 }
758 if (flags & returnGo_SystemTextInputFlags) {
759 [traits setReturnKeyType:UIReturnKeyGo];
760 }
761 if (flags & returnSend_SystemTextInputFlags) {
762 [traits setReturnKeyType:UIReturnKeySend];
763 }
764 if (flags & disableAutocorrect_SystemTextInputFlag) {
765 [traits setAutocorrectionType:UITextAutocorrectionTypeNo];
766 [traits setSpellCheckingType:UITextSpellCheckingTypeNo];
767 }
768 if (flags & disableAutocapitalize_SystemTextInputFlag) {
769 [traits setAutocapitalizationType:UITextAutocapitalizationTypeNone];
770 }
771 if (flags & alignRight_SystemTextInputFlag) {
772 if (d->field) {
773 [REF_d_field setTextAlignment:NSTextAlignmentRight];
774 }
775 if (d->view) {
776 [REF_d_view setTextAlignment:NSTextAlignmentRight];
777 }
778 }
779 UIColor *textColor = makeUIColor_(uiInputTextFocused_ColorId);
780 UIColor *backgroundColor = makeUIColor_(uiInputBackgroundFocused_ColorId);
781 UIColor *tintColor = makeUIColor_(uiInputFrameHover_ColorId); /* use the accent color */ //uiInputCursor_ColorId);
782 [appState_ setSystemTextInput:d];
783 if (d->field) {
784 UITextField *field = REF_d_field;
785 [field setTextColor:textColor];
786 [field setTintColor:tintColor];
787 [field setDelegate:appState_];
788 [field becomeFirstResponder];
789 }
790 else {
791 UITextView *view = REF_d_view;
792 [view setBackgroundColor:[UIColor colorWithWhite:1.0f alpha:0.0f]];
793 [view setTextColor:textColor];
794 [view setTintColor:tintColor];
795 if (flags & extraPadding_SystemTextInputFlag) {
796 [view setContentInset:(UIEdgeInsets){ 0, 0, 3 * gap_UI / get_Window()->pixelRatio, 0}];
797 }
798 [view setEditable:YES];
799 [view setDelegate:appState_];
800 [view becomeFirstResponder];
801 }
802 d->textChangedFunc = NULL;
803 d->textChangedContext = NULL;
804}
805
806void deinit_SystemTextInput(iSystemTextInput *d) {
807 [appState_ setSystemTextInput:nil];
808 if (d->field) {
809 [REF_d_field removeFromSuperview];
810 CFBridgingRelease(d->field);
811 d->field = nil;
812 }
813 if (d->view) {
814 [REF_d_view removeFromSuperview];
815 CFBridgingRelease(d->view);
816 d->view = nil;
817 }
818}
819
820void selectAll_SystemTextInput(iSystemTextInput *d) {
821 if (d->field) {
822 [REF_d_field selectAll:nil];
823 }
824 if (d->view) {
825 [REF_d_view selectAll:nil];
826 }
827}
828
829void setText_SystemTextInput(iSystemTextInput *d, const iString *text, iBool allowUndo) {
830 NSString *str = [NSString stringWithUTF8String:cstr_String(text)];
831 if (d->field) {
832 [REF_d_field setText:str];
833 if (d->flags & selectAll_SystemTextInputFlags) {
834 [REF_d_field selectAll:nil];
835 }
836 }
837 else {
838 UITextView *view = REF_d_view;
839// if (allowUndo) {
840// [view selectAll:nil];
841// if ([view shouldChangeTextInRange:[view selectedTextRange] replacementText:@""]) {
842// [[view textStorage] beginEditing];
843// [[view textStorage] replaceCharactersInRange:[view selectedRange] withString:@""];
844// [[view textStorage] endEditing];
845// }
846// }
847// else {
848 // TODO: How to implement `allowUndo`, given that UITextView does not exist when unfocused?
849 // Maybe keep the UITextStorage (if it has the undo?)?
850 [view setText:str];
851// }
852 if (d->flags & selectAll_SystemTextInputFlags) {
853 [view selectAll:nil];
854 }
855 }
856}
857
858int preferredHeight_SystemTextInput(const iSystemTextInput *d) {
859 if (d->view) {
860 CGRect usedRect = [[REF_d_view layoutManager] usedRectForTextContainer:[REF_d_view textContainer]];
861 return usedRect.size.height * get_Window()->pixelRatio;
862 }
863 return 0;
864}
865
866void setFont_SystemTextInput(iSystemTextInput *d, int fontId) {
867 float height = lineHeight_Text(fontId) / get_Window()->pixelRatio;
868 UIFont *font;
869 // for (NSString *name in [UIFont familyNames]) {
870 // printf("family: %s\n", [name cStringUsingEncoding:NSUTF8StringEncoding]);
871 // }
872 if (fontId / maxVariants_Fonts * maxVariants_Fonts == monospace_FontId) {
873// font = [UIFont monospacedSystemFontOfSize:0.8f * height weight:UIFontWeightRegular];
874// for (NSString *name in [UIFont fontNamesForFamilyName:@"Iosevka Term"]) {
875// printf("fontname: %s\n", [name cStringUsingEncoding:NSUTF8StringEncoding]);
876// }
877 font = [UIFont fontWithName:@"Iosevka-Term-Extended" size:height * 0.82f];
878 }
879 else {
880// font = [UIFont systemFontOfSize:0.65f * height];
881 font = [UIFont fontWithName:@"SourceSans3-Regular" size:height * 0.7f];
882 }
883 if (d->field) {
884 [REF_d_field setFont:font];
885 }
886 if (d->view) {
887 [REF_d_view setFont:font];
888 }
889}
890
891const iString *text_SystemTextInput(const iSystemTextInput *d) {
892 if (d->field) {
893 return collectNewCStr_String([[REF_d_field text] cStringUsingEncoding:NSUTF8StringEncoding]);
894 }
895 if (d->view) {
896 return collectNewCStr_String([[REF_d_view text] cStringUsingEncoding:NSUTF8StringEncoding]);
897 }
898 return NULL;
899}
900
901void setRect_SystemTextInput(iSystemTextInput *d, iRect rect) {
902 CGRect frame = convertToCGRect_(&rect, (d->flags & multiLine_SystemTextInputFlags) != 0);
903 if (d->field) {
904 [REF_d_field setFrame:frame];
905 }
906 else {
907 [REF_d_view setFrame:frame];
908 }
909}
910
911void setTextChangedFunc_SystemTextInput(iSystemTextInput *d,
912 void (*textChangedFunc)(iSystemTextInput *, void *),
913 void *context) {
914 d->textChangedFunc = textChangedFunc;
915 d->textChangedContext = context;
916}
917
918static void notifyChange_SystemTextInput_(iSystemTextInput *d) {
919 if (d && d->textChangedFunc) {
920 d->textChangedFunc(d, d->textChangedContext);
921 }
922}
923
924static BOOL isNewlineAllowed_SystemTextInput_(const iSystemTextInput *d) {
925 return (d->flags & insertNewlines_SystemTextInputFlag) != 0;
926}
diff --git a/src/macos.h b/src/macos.h
index 22a6dfff..10cbba81 100644
--- a/src/macos.h
+++ b/src/macos.h
@@ -39,8 +39,10 @@ void hideTitleBar_MacOS (iWindow *window);
39void insertMenuItems_MacOS (const char *menuLabel, int atIndex, const iMenuItem *items, size_t count); 39void insertMenuItems_MacOS (const char *menuLabel, int atIndex, const iMenuItem *items, size_t count);
40void removeMenu_MacOS (int atIndex); 40void removeMenu_MacOS (int atIndex);
41void enableMenu_MacOS (const char *menuLabel, iBool enable); 41void enableMenu_MacOS (const char *menuLabel, iBool enable);
42void enableMenuIndex_MacOS (int index, iBool enable);
42void enableMenuItem_MacOS (const char *menuItemCommand, iBool enable); 43void enableMenuItem_MacOS (const char *menuItemCommand, iBool enable);
43void enableMenuItemsByKey_MacOS (int key, int kmods, iBool enable); 44void enableMenuItemsByKey_MacOS (int key, int kmods, iBool enable);
45void enableMenuItemsOnHomeRow_MacOS(iBool enable);
44void handleCommand_MacOS (const char *cmd); 46void handleCommand_MacOS (const char *cmd);
45 47
46void showPopupMenu_MacOS (iWidget *source, iInt2 windowCoord, const iMenuItem *items, size_t n); 48void showPopupMenu_MacOS (iWidget *source, iInt2 windowCoord, const iMenuItem *items, size_t n);
diff --git a/src/macos.m b/src/macos.m
index cfbca488..4ad267c1 100644
--- a/src/macos.m
+++ b/src/macos.m
@@ -31,6 +31,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
31 31
32#include <SDL_timer.h> 32#include <SDL_timer.h>
33#include <SDL_syswm.h> 33#include <SDL_syswm.h>
34#include <the_Foundation/stringset.h>
34 35
35#import <AppKit/AppKit.h> 36#import <AppKit/AppKit.h>
36 37
@@ -110,7 +111,7 @@ static void ignoreImmediateKeyDownEvents_(void) {
110- (id)initWithIdentifier:(NSTouchBarItemIdentifier)identifier 111- (id)initWithIdentifier:(NSTouchBarItemIdentifier)identifier
111 title:(NSString *)title 112 title:(NSString *)title
112 command:(NSString *)cmd { 113 command:(NSString *)cmd {
113 [super initWithIdentifier:identifier]; 114 self = [super initWithIdentifier:identifier];
114 self.view = [NSButton buttonWithTitle:title target:self action:@selector(buttonPressed)]; 115 self.view = [NSButton buttonWithTitle:title target:self action:@selector(buttonPressed)];
115 command = cmd; 116 command = cmd;
116 return self; 117 return self;
@@ -120,7 +121,7 @@ static void ignoreImmediateKeyDownEvents_(void) {
120 image:(NSImage *)image 121 image:(NSImage *)image
121 widget:(iWidget *)widget 122 widget:(iWidget *)widget
122 command:(NSString *)cmd { 123 command:(NSString *)cmd {
123 [super initWithIdentifier:identifier]; 124 self = [super initWithIdentifier:identifier];
124 self.view = [NSButton buttonWithImage:image target:self action:@selector(buttonPressed)]; 125 self.view = [NSButton buttonWithImage:image target:self action:@selector(buttonPressed)];
125 command = cmd; 126 command = cmd;
126 return self; 127 return self;
@@ -163,12 +164,13 @@ static void ignoreImmediateKeyDownEvents_(void) {
163@implementation MenuCommands 164@implementation MenuCommands
164 165
165- (id)init { 166- (id)init {
167 self = [super init];
166 commands = [[NSMutableDictionary<NSString *, NSString *> alloc] init]; 168 commands = [[NSMutableDictionary<NSString *, NSString *> alloc] init];
167 source = NULL; 169 source = NULL;
168 return self; 170 return self;
169} 171}
170 172
171- (void)setCommand:(NSString *)command forMenuItem:(NSMenuItem *)menuItem { 173- (void)setCommand:(NSString * __nonnull)command forMenuItem:(NSMenuItem * __nonnull)menuItem {
172 [commands setObject:command forKey:[menuItem title]]; 174 [commands setObject:command forKey:[menuItem title]];
173} 175}
174 176
@@ -220,7 +222,7 @@ static void ignoreImmediateKeyDownEvents_(void) {
220@implementation MyDelegate 222@implementation MyDelegate
221 223
222- (id)initWithSDLDelegate:(NSObject<NSApplicationDelegate> *)sdl { 224- (id)initWithSDLDelegate:(NSObject<NSApplicationDelegate> *)sdl {
223 [super init]; 225 self = [super init];
224 currentAppearanceName = nil; 226 currentAppearanceName = nil;
225 menuCommands = [[MenuCommands alloc] init]; 227 menuCommands = [[MenuCommands alloc] init];
226 touchBarVariant = default_TouchBarVariant; 228 touchBarVariant = default_TouchBarVariant;
@@ -402,6 +404,131 @@ void registerURLHandler_MacOS(void) {
402 [handler release]; 404 [handler release];
403} 405}
404 406
407#if 0
408static iBool isTracking_;
409
410static void trackSwipe_(NSEvent *event) {
411 if (isTracking_) {
412 return;
413 }
414 isTracking_ = iTrue;
415 [event trackSwipeEventWithOptions:NSEventSwipeTrackingLockDirection
416 dampenAmountThresholdMin:-1.0
417 max:1.0
418 usingHandler:^(CGFloat gestureAmount, NSEventPhase phase,
419 BOOL isComplete, BOOL *stop) {
420 printf("TRACK: amount:%f phase:%lu complete:%d\n",
421 gestureAmount, (unsigned long) phase, isComplete);
422 fflush(stdout);
423 if (isComplete) {
424 isTracking_ = iFalse;
425 }
426 }
427 ];
428}
429#endif
430
431static int swipeDir_ = 0;
432static int preventTapGlitch_ = 0;
433
434static iBool processScrollWheelEvent_(NSEvent *event) {
435 const iBool isPerPixel = (event.hasPreciseScrollingDeltas != 0);
436 const iBool isInertia = (event.momentumPhase & (NSEventPhaseBegan | NSEventPhaseChanged)) != 0;
437 const iBool isEnded = event.scrollingDeltaX == 0.0f && event.scrollingDeltaY == 0.0f && !isInertia;
438 const iWindow *win = &get_MainWindow()->base;
439 if (isPerPixel) {
440 /* On macOS 12.1, stopping ongoing inertia scroll with a tap seems to sometimes produce
441 spurious large scroll events. */
442 switch (preventTapGlitch_) {
443 case 0:
444 if (isInertia && event.momentumPhase == NSEventPhaseChanged) {
445 preventTapGlitch_++;
446 }
447 else {
448 preventTapGlitch_ = 0;
449 }
450 break;
451 case 1:
452 if (event.scrollingDeltaY == 0 && event.momentumPhase == NSEventPhaseEnded) {
453 preventTapGlitch_++;
454 }
455 break;
456 case 2:
457 if (event.scrollingDeltaY == 0 && event.momentumPhase == 0 && isEnded) {
458 preventTapGlitch_++;
459 }
460 else {
461 preventTapGlitch_ = 0;
462 }
463 break;
464 case 3:
465 if (event.scrollingDeltaY != 0 && event.momentumPhase == 0 && !isInertia) {
466 preventTapGlitch_ = 0;
467 // printf("SPURIOUS\n"); fflush(stdout);
468 return iTrue;
469 }
470 preventTapGlitch_ = 0;
471 break;
472 }
473 }
474 /* Post corresponding MOUSEWHEEL events. */
475 SDL_MouseWheelEvent e = { .type = SDL_MOUSEWHEEL };
476 e.timestamp = SDL_GetTicks();
477 e.which = isPerPixel ? 0 : 1; /* Distinction between trackpad and regular mouse. TODO: Still needed? */
478 setPerPixel_MouseWheelEvent(&e, isPerPixel);
479 if (isPerPixel) {
480 setInertia_MouseWheelEvent(&e, isInertia);
481 setScrollFinished_MouseWheelEvent(&e, isEnded);
482 e.x = event.scrollingDeltaX * win->pixelRatio;
483 e.y = event.scrollingDeltaY * win->pixelRatio;
484 /* Only scroll on one axis at a time. */
485 if (swipeDir_ == 0) {
486 swipeDir_ = iAbs(e.x) > iAbs(e.y) ? 1 : 2;
487 }
488 if (swipeDir_ == 1) {
489 e.y = 0;
490 }
491 else if (swipeDir_ == 2) {
492 e.x = 0;
493 }
494 if (isEnded) {
495 swipeDir_ = 0;
496 }
497 }
498 else {
499 /* Disregard wheel acceleration applied by the OS. */
500 e.x = -event.scrollingDeltaX;
501 e.y = iSign(event.scrollingDeltaY);
502 }
503 // printf("#### [%d] dx:%d dy:%d phase:%ld inertia:%d end:%d\n", preventTapGlitch_, e.x, e.y, (long) event.momentumPhase,
504 // isInertia, isEnded); fflush(stdout);
505 SDL_PushEvent((SDL_Event *) &e);
506#if 0
507 /* On macOS, we handle both trackpad and mouse events. We expect SDL to identify
508 which device is sending the event. */
509 if (ev.wheel.which == 0) {
510 /* Trackpad with precise scrolling w/inertia (points). */
511 setPerPixel_MouseWheelEvent(&ev.wheel, iTrue);
512 ev.wheel.x *= -d->window->base.pixelRatio;
513 ev.wheel.y *= d->window->base.pixelRatio;
514 /* Only scroll on one axis at a time. */
515 if (iAbs(ev.wheel.x) > iAbs(ev.wheel.y)) {
516 ev.wheel.y = 0;
517 }
518 else {
519 ev.wheel.x = 0;
520 }
521 }
522 else {
523 /* Disregard wheel acceleration applied by the OS. */
524 ev.wheel.x = -ev.wheel.x;
525 ev.wheel.y = iSign(ev.wheel.y);
526 }
527#endif
528
529 return iTrue;
530}
531
405void setupApplication_MacOS(void) { 532void setupApplication_MacOS(void) {
406 NSApplication *app = [NSApplication sharedApplication]; 533 NSApplication *app = [NSApplication sharedApplication];
407 [app setActivationPolicy:NSApplicationActivationPolicyRegular]; 534 [app setActivationPolicy:NSApplicationActivationPolicyRegular];
@@ -423,6 +550,21 @@ void setupApplication_MacOS(void) {
423 NSMenuItem *windowCloseItem = [windowMenu itemWithTitle:@"Close"]; 550 NSMenuItem *windowCloseItem = [windowMenu itemWithTitle:@"Close"];
424 windowCloseItem.target = myDel; 551 windowCloseItem.target = myDel;
425 windowCloseItem.action = @selector(closeTab); 552 windowCloseItem.action = @selector(closeTab);
553 [NSEvent addLocalMonitorForEventsMatchingMask:NSEventMaskScrollWheel
554 handler:^NSEvent*(NSEvent *event){
555// printf("event type: %lu\n", (unsigned long) event.type);
556// fflush(stdout);
557// if (event.type == NSEventTypeGesture) {
558// trackSwipe_(event);
559// printf("GESTURE phase:%lu\n", (unsigned long) event.phase);
560//fflush(stdout);
561// }
562 if (event.type == NSEventTypeScrollWheel &&
563 processScrollWheelEvent_(event)) {
564 return nil; /* was eaten */
565 }
566 return event;
567 }];
426} 568}
427 569
428void hideTitleBar_MacOS(iWindow *window) { 570void hideTitleBar_MacOS(iWindow *window) {
@@ -439,6 +581,13 @@ void enableMenu_MacOS(const char *menuLabel, iBool enable) {
439 [menuItem setEnabled:enable]; 581 [menuItem setEnabled:enable];
440} 582}
441 583
584void enableMenuIndex_MacOS(int index, iBool enable) {
585 NSApplication *app = [NSApplication sharedApplication];
586 NSMenu *appMenu = [app mainMenu];
587 NSMenuItem *menuItem = [appMenu itemAtIndex:index];
588 [menuItem setEnabled:enable];
589}
590
442void enableMenuItem_MacOS(const char *menuItemCommand, iBool enable) { 591void enableMenuItem_MacOS(const char *menuItemCommand, iBool enable) {
443 NSApplication *app = [NSApplication sharedApplication]; 592 NSApplication *app = [NSApplication sharedApplication];
444 NSMenu *appMenu = [app mainMenu]; 593 NSMenu *appMenu = [app mainMenu];
@@ -513,6 +662,47 @@ void enableMenuItemsByKey_MacOS(int key, int kmods, iBool enable) {
513 delete_String(keyEquiv); 662 delete_String(keyEquiv);
514} 663}
515 664
665void enableMenuItemsOnHomeRow_MacOS(iBool enable) {
666 iStringSet *homeRowKeys = new_StringSet();
667 const char *keys[] = { /* Note: another array in documentwidget.c */
668 "f", "d", "s", "a",
669 "j", "k", "l",
670 "r", "e", "w", "q",
671 "u", "i", "o", "p",
672 "v", "c", "x", "z",
673 "m", "n",
674 "g", "h",
675 "b",
676 "t", "y"
677 };
678 iForIndices(i, keys) {
679 iString str;
680 initCStr_String(&str, keys[i]);
681 insert_StringSet(homeRowKeys, &str);
682 deinit_String(&str);
683 }
684 NSApplication *app = [NSApplication sharedApplication];
685 NSMenu *appMenu = [app mainMenu];
686 for (NSMenuItem *mainMenuItem in appMenu.itemArray) {
687 NSMenu *menu = mainMenuItem.submenu;
688 if (menu) {
689 for (NSMenuItem *menuItem in menu.itemArray) {
690 if (menuItem.keyEquivalentModifierMask == 0) {
691 iString equiv;
692 initCStr_String(&equiv, [menuItem.keyEquivalent
693 cStringUsingEncoding:NSUTF8StringEncoding]);
694 if (contains_StringSet(homeRowKeys, &equiv)) {
695 [menuItem setEnabled:enable];
696 [menu setAutoenablesItems:NO];
697 }
698 deinit_String(&equiv);
699 }
700 }
701 }
702 }
703 iRelease(homeRowKeys);
704}
705
516static void setShortcut_NSMenuItem_(NSMenuItem *item, int key, int kmods) { 706static void setShortcut_NSMenuItem_(NSMenuItem *item, int key, int kmods) {
517 NSEventModifierFlags modMask; 707 NSEventModifierFlags modMask;
518 iString *str = composeKeyEquivalent_(key, kmods, &modMask); 708 iString *str = composeKeyEquivalent_(key, kmods, &modMask);
@@ -541,6 +731,29 @@ enum iColorId removeColorEscapes_String(iString *d) {
541 return color; 731 return color;
542} 732}
543 733
734static NSString *cleanString_(const iString *ansiEscapedText) {
735 iString mod;
736 initCopy_String(&mod, ansiEscapedText);
737 iRegExp *ansi = makeAnsiEscapePattern_Text();
738 replaceRegExp_String(&mod, ansi, "", NULL, NULL);
739 iRelease(ansi);
740 NSString *clean = [NSString stringWithUTF8String:cstr_String(&mod)];
741 deinit_String(&mod);
742 return clean;
743}
744
745#if 0
746static NSAttributedString *makeAttributedString_(const iString *ansiEscapedText) {
747 iString mod;
748 initCopy_String(&mod, ansiEscapedText);
749 NSData *data = [NSData dataWithBytesNoCopy:data_Block(&mod.chars) length:size_String(&mod)];
750 NSAttributedString *as = [[NSAttributedString alloc] initWithHTML:data
751 documentAttributes:nil];
752 deinit_String(&mod);
753 return as;
754}
755#endif
756
544/* returns the selected item, if any */ 757/* returns the selected item, if any */
545static NSMenuItem *makeMenuItems_(NSMenu *menu, MenuCommands *commands, const iMenuItem *items, size_t n) { 758static NSMenuItem *makeMenuItems_(NSMenu *menu, MenuCommands *commands, const iMenuItem *items, size_t n) {
546 NSMenuItem *selectedItem = nil; 759 NSMenuItem *selectedItem = nil;
@@ -557,7 +770,7 @@ static NSMenuItem *makeMenuItems_(NSMenu *menu, MenuCommands *commands, const iM
557 isChecked = iTrue; 770 isChecked = iTrue;
558 label += 3; 771 label += 3;
559 } 772 }
560 else if (startsWith_CStr(label, "///")) { 773 else if (startsWith_CStr(label, "///") || startsWith_CStr(label, "```")) {
561 isDisabled = iTrue; 774 isDisabled = iTrue;
562 label += 3; 775 label += 3;
563 } 776 }
@@ -567,9 +780,13 @@ static NSMenuItem *makeMenuItems_(NSMenu *menu, MenuCommands *commands, const iM
567 if (removeColorEscapes_String(&itemTitle) == uiTextCaution_ColorId) { 780 if (removeColorEscapes_String(&itemTitle) == uiTextCaution_ColorId) {
568// prependCStr_String(&itemTitle, "\u26a0\ufe0f "); 781// prependCStr_String(&itemTitle, "\u26a0\ufe0f ");
569 } 782 }
570 NSMenuItem *item = [menu addItemWithTitle:[NSString stringWithUTF8String:cstr_String(&itemTitle)] 783 NSMenuItem *item = [[NSMenuItem alloc] init];
571 action:(hasCommand ? @selector(postMenuItemCommand:) : nil) 784 /* Use attributed string to allow newlines. */
572 keyEquivalent:@""]; 785 NSAttributedString *title = [[NSAttributedString alloc] initWithString:cleanString_(&itemTitle)];
786 item.attributedTitle = title;
787 [title release];
788 item.action = (hasCommand ? @selector(postMenuItemCommand:) : nil);
789 [menu addItem:item];
573 deinit_String(&itemTitle); 790 deinit_String(&itemTitle);
574 [item setTarget:commands]; 791 [item setTarget:commands];
575 if (isChecked) { 792 if (isChecked) {
diff --git a/src/main.c b/src/main.c
index ad69c6df..e18ab065 100644
--- a/src/main.c
+++ b/src/main.c
@@ -67,6 +67,7 @@ int main(int argc, char **argv) {
67 "ECDHE-RSA-AES128-GCM-SHA256:" 67 "ECDHE-RSA-AES128-GCM-SHA256:"
68 "DHE-RSA-AES256-GCM-SHA384"); 68 "DHE-RSA-AES256-GCM-SHA384");
69 SDL_SetHint(SDL_HINT_VIDEO_ALLOW_SCREENSAVER, "1"); 69 SDL_SetHint(SDL_HINT_VIDEO_ALLOW_SCREENSAVER, "1");
70 SDL_EnableScreenSaver();
70 SDL_SetHint(SDL_HINT_MAC_BACKGROUND_APP, "1"); 71 SDL_SetHint(SDL_HINT_MAC_BACKGROUND_APP, "1");
71 SDL_SetHint(SDL_HINT_MAC_CTRL_CLICK_EMULATE_RIGHT_CLICK, "1"); 72 SDL_SetHint(SDL_HINT_MAC_CTRL_CLICK_EMULATE_RIGHT_CLICK, "1");
72#if SDL_VERSION_ATLEAST(2, 0, 8) 73#if SDL_VERSION_ATLEAST(2, 0, 8)
@@ -82,9 +83,6 @@ int main(int argc, char **argv) {
82 fprintf(stderr, "[SDL] init failed: %s\n", SDL_GetError()); 83 fprintf(stderr, "[SDL] init failed: %s\n", SDL_GetError());
83 return -1; 84 return -1;
84 } 85 }
85 if (SDL_Init(SDL_INIT_AUDIO)) {
86 fprintf(stderr, "[SDL] audio init failed: %s\n", SDL_GetError());
87 }
88 init_Updater(); 86 init_Updater();
89 run_App(argc, argv); 87 run_App(argc, argv);
90 SDL_Quit(); 88 SDL_Quit();
diff --git a/src/media.c b/src/media.c
index a3f381ec..4940c13e 100644
--- a/src/media.c
+++ b/src/media.c
@@ -144,7 +144,7 @@ void makeTexture_GmImage(iGmImage *d) {
144 } 144 }
145 else { 145 else {
146 imgData = stbi_load_from_memory( 146 imgData = stbi_load_from_memory(
147 constData_Block(data), size_Block(data), &d->size.x, &d->size.y, NULL, 4); 147 constData_Block(data), (int) size_Block(data), &d->size.x, &d->size.y, NULL, 4);
148 if (!imgData) { 148 if (!imgData) {
149 fprintf(stderr, "[media] image load failed: %s\n", stbi_failure_reason()); 149 fprintf(stderr, "[media] image load failed: %s\n", stbi_failure_reason());
150 } 150 }
@@ -629,6 +629,17 @@ void deinit_MediaRequest(iMediaRequest *d) {
629 iRelease(d->req); 629 iRelease(d->req);
630} 630}
631 631
632iMediaRequest *newReused_MediaRequest(iDocumentWidget *doc, unsigned int linkId,
633 iGmRequest *request) {
634 iMediaRequest *d = new_Object(&Class_MediaRequest);
635 d->doc = doc;
636 d->linkId = linkId;
637 d->req = request; /* takes ownership */
638 iConnect(GmRequest, d->req, updated, d, updated_MediaRequest_);
639 iConnect(GmRequest, d->req, finished, d, finished_MediaRequest_);
640 return d;
641}
642
632iDefineObjectConstructionArgs(MediaRequest, 643iDefineObjectConstructionArgs(MediaRequest,
633 (iDocumentWidget *doc, unsigned int linkId, const iString *url, 644 (iDocumentWidget *doc, unsigned int linkId, const iString *url,
634 iBool enableFilters), 645 iBool enableFilters),
diff --git a/src/media.h b/src/media.h
index 3b329716..584c77eb 100644
--- a/src/media.h
+++ b/src/media.h
@@ -123,3 +123,6 @@ struct Impl_MediaRequest {
123 123
124iDeclareObjectConstructionArgs(MediaRequest, iDocumentWidget *doc, unsigned int linkId, 124iDeclareObjectConstructionArgs(MediaRequest, iDocumentWidget *doc, unsigned int linkId,
125 const iString *url, iBool enableFilters) 125 const iString *url, iBool enableFilters)
126
127iMediaRequest * newReused_MediaRequest (iDocumentWidget *doc, unsigned int linkId,
128 iGmRequest *request);
diff --git a/src/mimehooks.c b/src/mimehooks.c
index c097bd1f..eb379106 100644
--- a/src/mimehooks.c
+++ b/src/mimehooks.c
@@ -142,7 +142,8 @@ static iBlock *translateAtomXmlToGeminiFeed_(const iString *mime, const iBlock *
142 appendCStr_String(&out, cstr_Lang("feeds.atom.translated")); 142 appendCStr_String(&out, cstr_Lang("feeds.atom.translated"));
143 appendCStr_String(&out, "\n\n"); 143 appendCStr_String(&out, "\n\n");
144 iRegExp *datePattern = 144 iRegExp *datePattern =
145 iClob(new_RegExp("^([0-9][0-9][0-9][0-9]-[0-1][0-9]-[0-3][0-9])(T|\\s).*", caseSensitive_RegExpOption)); 145 iClob(new_RegExp("^\\s*([0-9][0-9][0-9][0-9]-[0-1][0-9]-[0-3][0-9])(T|\\s).*",
146 caseSensitive_RegExpOption));
146 iBeginCollect(); 147 iBeginCollect();
147 iConstForEach(PtrArray, i, &feed->children) { 148 iConstForEach(PtrArray, i, &feed->children) {
148 iEndCollect(); 149 iEndCollect();
diff --git a/src/periodic.c b/src/periodic.c
index ef3d8033..b4f51ed3 100644
--- a/src/periodic.c
+++ b/src/periodic.c
@@ -57,6 +57,25 @@ iDefineTypeConstructionArgs(PeriodicCommand, (iAny *ctx, const char *cmd), ctx,
57 57
58static const uint32_t postingInterval_Periodic_ = 500; 58static const uint32_t postingInterval_Periodic_ = 500;
59 59
60static uint32_t postEvent_Periodic_(uint32_t interval, void *context) {
61 iUnused(context);
62 SDL_UserEvent ev = { .type = SDL_USEREVENT,
63 .timestamp = SDL_GetTicks(),
64 .code = periodic_UserEventCode };
65 SDL_PushEvent((SDL_Event *) &ev);
66 return interval;
67}
68
69static void startOrStopWakeupTimer_Periodic_(iPeriodic *d, iBool start) {
70 if (start && !d->wakeupTimer) {
71 d->wakeupTimer = SDL_AddTimer(postingInterval_Periodic_, postEvent_Periodic_, d);
72 }
73 else if (!start && d->wakeupTimer) {
74 SDL_RemoveTimer(d->wakeupTimer);
75 d->wakeupTimer = 0;
76 }
77}
78
60static void removePending_Periodic_(iPeriodic *d) { 79static void removePending_Periodic_(iPeriodic *d) {
61 iForEach(PtrSet, i, &d->pendingRemoval) { 80 iForEach(PtrSet, i, &d->pendingRemoval) {
62 size_t pos; 81 size_t pos;
@@ -68,6 +87,9 @@ static void removePending_Periodic_(iPeriodic *d) {
68 } 87 }
69 } 88 }
70 clear_PtrSet(&d->pendingRemoval); 89 clear_PtrSet(&d->pendingRemoval);
90 if (isEmpty_SortedArray(&d->commands)) {
91 startOrStopWakeupTimer_Periodic_(d, iFalse);
92 }
71} 93}
72 94
73static iBool isDispatching_; 95static iBool isDispatching_;
@@ -109,9 +131,11 @@ void init_Periodic(iPeriodic *d) {
109 init_SortedArray(&d->commands, sizeof(iPeriodicCommand), cmp_PeriodicCommand_); 131 init_SortedArray(&d->commands, sizeof(iPeriodicCommand), cmp_PeriodicCommand_);
110 d->lastPostTime = 0; 132 d->lastPostTime = 0;
111 init_PtrSet(&d->pendingRemoval); 133 init_PtrSet(&d->pendingRemoval);
134 d->wakeupTimer = 0;
112} 135}
113 136
114void deinit_Periodic(iPeriodic *d) { 137void deinit_Periodic(iPeriodic *d) {
138 startOrStopWakeupTimer_Periodic_(d, iFalse);
115 deinit_PtrSet(&d->pendingRemoval); 139 deinit_PtrSet(&d->pendingRemoval);
116 iForEach(Array, i, &d->commands.values) { 140 iForEach(Array, i, &d->commands.values) {
117 deinit_PeriodicCommand(i.value); 141 deinit_PeriodicCommand(i.value);
@@ -121,6 +145,7 @@ void deinit_Periodic(iPeriodic *d) {
121} 145}
122 146
123void add_Periodic(iPeriodic *d, iAny *context, const char *command) { 147void add_Periodic(iPeriodic *d, iAny *context, const char *command) {
148 iAssert(isInstance_Object(context, &Class_Widget));
124 lock_Mutex(d->mutex); 149 lock_Mutex(d->mutex);
125 size_t pos; 150 size_t pos;
126 iPeriodicCommand key = { .context = context }; 151 iPeriodicCommand key = { .context = context };
@@ -133,6 +158,7 @@ void add_Periodic(iPeriodic *d, iAny *context, const char *command) {
133 init_PeriodicCommand(&pc, context, command); 158 init_PeriodicCommand(&pc, context, command);
134 insert_SortedArray(&d->commands, &pc); 159 insert_SortedArray(&d->commands, &pc);
135 } 160 }
161 startOrStopWakeupTimer_Periodic_(d, iTrue);
136 unlock_Mutex(d->mutex); 162 unlock_Mutex(d->mutex);
137} 163}
138 164
diff --git a/src/periodic.h b/src/periodic.h
index a56310a8..f65a4299 100644
--- a/src/periodic.h
+++ b/src/periodic.h
@@ -35,6 +35,7 @@ struct Impl_Periodic {
35 iSortedArray commands; 35 iSortedArray commands;
36 uint32_t lastPostTime; 36 uint32_t lastPostTime;
37 iPtrSet pendingRemoval; /* contexts */ 37 iPtrSet pendingRemoval; /* contexts */
38 int wakeupTimer; /* running while there are pending periodic commands */
38}; 39};
39 40
40void init_Periodic (iPeriodic *); 41void init_Periodic (iPeriodic *);
diff --git a/src/prefs.c b/src/prefs.c
index 10df9ade..6164ca25 100644
--- a/src/prefs.c
+++ b/src/prefs.c
@@ -23,6 +23,10 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
23#include "prefs.h" 23#include "prefs.h"
24 24
25#include <the_Foundation/fileinfo.h> 25#include <the_Foundation/fileinfo.h>
26#include <assert.h>
27
28static_assert(offsetof(iPrefs, plainTextWrap) == offsetof(iPrefs, bools[plainTextWrap_PrefsBool]),
29 "memory layout mismatch (needs struct packing?)");
26 30
27void init_Prefs(iPrefs *d) { 31void init_Prefs(iPrefs *d) {
28 iForIndices(i, d->strings) { 32 iForIndices(i, d->strings) {
@@ -40,8 +44,20 @@ void init_Prefs(iPrefs *d) {
40 d->uiAnimations = iTrue; 44 d->uiAnimations = iTrue;
41 d->uiScale = 1.0f; /* default set elsewhere */ 45 d->uiScale = 1.0f; /* default set elsewhere */
42 d->zoomPercent = 100; 46 d->zoomPercent = 100;
47 d->navbarActions[0] = back_ToolbarAction;
48 d->navbarActions[1] = forward_ToolbarAction;
49 d->navbarActions[2] = sidebar_ToolbarAction;
50 d->navbarActions[3] = home_ToolbarAction;
51#if defined (iPlatformAndroidMobile)
52 /* Android has a system-wide back button so no need to have a duplicate. */
53 d->toolbarActions[0] = closeTab_ToolbarAction;
54#else
55 d->toolbarActions[0] = back_ToolbarAction;
56#endif
57 d->toolbarActions[1] = forward_ToolbarAction;
43 d->sideIcon = iTrue; 58 d->sideIcon = iTrue;
44 d->hideToolbarOnScroll = iTrue; 59 d->hideToolbarOnScroll = iTrue;
60 d->blinkingCursor = iTrue;
45 d->pinSplit = 1; 61 d->pinSplit = 1;
46 d->time24h = iTrue; 62 d->time24h = iTrue;
47 d->returnKey = default_ReturnKeyBehavior; 63 d->returnKey = default_ReturnKeyBehavior;
diff --git a/src/prefs.h b/src/prefs.h
index 2fbff9de..ea864f51 100644
--- a/src/prefs.h
+++ b/src/prefs.h
@@ -33,76 +33,150 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
33iDeclareType(Prefs) 33iDeclareType(Prefs)
34 34
35enum iPrefsString { 35enum iPrefsString {
36 /* General */
36 uiLanguage_PrefsString, 37 uiLanguage_PrefsString,
37 downloadDir_PrefsString, 38 downloadDir_PrefsString,
38 searchUrl_PrefsString, 39 searchUrl_PrefsString,
40
39 /* Network */ 41 /* Network */
40 caFile_PrefsString, 42 caFile_PrefsString,
41 caPath_PrefsString, 43 caPath_PrefsString,
42 geminiProxy_PrefsString, 44 geminiProxy_PrefsString,
43 gopherProxy_PrefsString, 45 gopherProxy_PrefsString,
44 httpProxy_PrefsString, 46 httpProxy_PrefsString,
47
45 /* Style */ 48 /* Style */
46 uiFont_PrefsString, 49 uiFont_PrefsString,
47 headingFont_PrefsString, 50 headingFont_PrefsString,
48 bodyFont_PrefsString, 51 bodyFont_PrefsString,
49 monospaceFont_PrefsString, 52 monospaceFont_PrefsString,
50 monospaceDocumentFont_PrefsString, 53 monospaceDocumentFont_PrefsString,
54
55 /* Meta */
51 max_PrefsString 56 max_PrefsString
52}; 57};
53 58
54/* TODO: Refactor at least the boolean values into an array for easier manipulation. 59/* Note: These match match the array/struct in Prefs. */
55 Then they can be (de)serialized as a group. Need to use a systematic command naming 60enum iPrefsBool {
56 convention for notifications. */ 61 /* Window and User Interface */
62 useSystemTheme_PrefsBool,
63 customFrame_PrefsBool,
64 retainWindowSize_PrefsBool,
65 uiAnimations_PrefsBool,
66 hideToolbarOnScroll_PrefsBool,
67
68 blinkingCursor_PrefsBool,
69
70 /* Document presentation */
71 sideIcon_PrefsBool,
72 time24h_PrefsBool,
73
74 /* Behavior */
75 hoverLink_PrefsBool,
76 smoothScrolling_PrefsBool,
77 loadImageInsteadOfScrolling_PrefsBool,
78 collapsePreOnLoad_PrefsBool,
79 openArchiveIndexPages_PrefsBool,
80
81 addBookmarksToBottom_PrefsBool,
82 warnAboutMissingGlyphs_PrefsBool,
83
84 /* Network */
85 decodeUserVisibleURLs_PrefsBool,
86
87 /* Style */
88 monospaceGemini_PrefsBool,
89 monospaceGopher_PrefsBool,
90 boldLinkVisited_PrefsBool,
91 boldLinkDark_PrefsBool,
92 boldLinkLight_PrefsBool,
93
94 fontSmoothing_PrefsBool,
95 bigFirstParagraph_PrefsBool,
96 quoteIcon_PrefsBool,
97 centerShortDocs_PrefsBool,
98 plainTextWrap_PrefsBool,
99
100 /* Meta */
101 max_PrefsBool
102};
103
104#define maxNavbarActions_Prefs 4
105
106/* TODO: Use a systematic command naming convention for notifications. */
107
57struct Impl_Prefs { 108struct Impl_Prefs {
58 iString strings[max_PrefsString]; 109 iString strings[max_PrefsString];
59 /* UI state */ 110 union {
111 iBool bools[max_PrefsBool];
112 /* For convenience, contents of the array are accessible also via these members. */
113 struct {
114 /* Window and User Interface */
115 iBool useSystemTheme;
116 iBool customFrame; /* when LAGRANGE_ENABLE_CUSTOM_FRAME is defined */
117 iBool retainWindowSize;
118 iBool uiAnimations;
119 iBool hideToolbarOnScroll;
120
121 iBool blinkingCursor;
122
123 /* Document presentation */
124 iBool sideIcon;
125 iBool time24h;
126
127 /* Behavior */
128 iBool hoverLink;
129 iBool smoothScrolling;
130 iBool loadImageInsteadOfScrolling;
131 iBool collapsePreOnLoad;
132 iBool openArchiveIndexPages;
133
134 iBool addBookmarksToBottom;
135 iBool warnAboutMissingGlyphs;
136
137 /* Network */
138 iBool decodeUserVisibleURLs;
139
140 /* Style */
141 iBool monospaceGemini;
142 iBool monospaceGopher;
143 iBool boldLinkVisited;
144 iBool boldLinkDark;
145 iBool boldLinkLight;
146
147 iBool fontSmoothing;
148 iBool bigFirstParagraph;
149 iBool quoteIcon;
150 iBool centerShortDocs;
151 iBool plainTextWrap;
152 };
153 };
154 /* UI state (belongs to state.lgr...) */
60 int dialogTab; 155 int dialogTab;
61 int langFrom; 156 int langFrom;
62 int langTo; 157 int langTo;
63 /* Window */ 158 /* Colors */
64 iBool useSystemTheme;
65 enum iColorTheme systemPreferredColorTheme[2]; /* dark, light */ 159 enum iColorTheme systemPreferredColorTheme[2]; /* dark, light */
66 enum iColorTheme theme; 160 enum iColorTheme theme;
67 enum iColorAccent accent; 161 enum iColorAccent accent;
68 iBool customFrame; /* when LAGRANGE_ENABLE_CUSTOM_FRAME is defined */ 162 /* Window and User Interface */
69 iBool retainWindowSize;
70 iBool uiAnimations;
71 float uiScale; 163 float uiScale;
164 enum iToolbarAction navbarActions[maxNavbarActions_Prefs];
165 enum iToolbarAction toolbarActions[2];
166 /* Document presentation */
72 int zoomPercent; 167 int zoomPercent;
73 iBool sideIcon;
74 iBool hideToolbarOnScroll;
75 int pinSplit; /* 0: no pinning, 1: left doc, 2: right doc */
76 iBool time24h;
77 /* Behavior */ 168 /* Behavior */
169 int pinSplit; /* 0: no pinning, 1: left doc, 2: right doc */
78 int returnKey; 170 int returnKey;
79 iBool hoverLink;
80 iBool smoothScrolling;
81 int smoothScrollSpeed[max_ScrollType]; 171 int smoothScrollSpeed[max_ScrollType];
82 iBool loadImageInsteadOfScrolling;
83 iBool collapsePreOnLoad;
84 iBool openArchiveIndexPages;
85 iBool addBookmarksToBottom;
86 iBool warnAboutMissingGlyphs;
87 /* Network */ 172 /* Network */
88 iBool decodeUserVisibleURLs;
89 int maxCacheSize; /* MB */ 173 int maxCacheSize; /* MB */
90 int maxMemorySize; /* MB */ 174 int maxMemorySize; /* MB */
91 /* Style */ 175 /* Style */
92 iStringSet * disabledFontPacks; 176 iStringSet * disabledFontPacks;
93 iBool fontSmoothing;
94 int gemtextAnsiEscapes; 177 int gemtextAnsiEscapes;
95 iBool monospaceGemini;
96 iBool monospaceGopher;
97 iBool boldLinkVisited;
98 iBool boldLinkDark;
99 iBool boldLinkLight;
100 int lineWidth; 178 int lineWidth;
101 float lineSpacing; 179 float lineSpacing;
102 iBool bigFirstParagraph;
103 iBool quoteIcon;
104 iBool centerShortDocs;
105 iBool plainTextWrap;
106 enum iImageStyle imageStyle; 180 enum iImageStyle imageStyle;
107 /* Colors */ 181 /* Colors */
108 enum iGmDocumentTheme docThemeDark; 182 enum iGmDocumentTheme docThemeDark;
diff --git a/src/resources.c b/src/resources.c
index 03ca7cbb..ae85463a 100644
--- a/src/resources.c
+++ b/src/resources.c
@@ -28,7 +28,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
28#include <SDL_rwops.h> 28#include <SDL_rwops.h>
29 29
30static iArchive *archive_; 30static iArchive *archive_;
31 31
32iBlock blobAbout_Resources; 32iBlock blobAbout_Resources;
33iBlock blobHelp_Resources; 33iBlock blobHelp_Resources;
34iBlock blobLagrange_Resources; 34iBlock blobLagrange_Resources;
@@ -66,10 +66,18 @@ static struct {
66 const char *archivePath; 66 const char *archivePath;
67} entries_[] = { 67} entries_[] = {
68 { &blobAbout_Resources, "about/about.gmi" }, 68 { &blobAbout_Resources, "about/about.gmi" },
69 { &blobHelp_Resources, "about/help.gmi" },
70 { &blobLagrange_Resources, "about/lagrange.gmi" }, 69 { &blobLagrange_Resources, "about/lagrange.gmi" },
71 { &blobLicense_Resources, "about/license.gmi" }, 70 { &blobLicense_Resources, "about/license.gmi" },
71#if defined (iPlatformAppleMobile)
72 { &blobHelp_Resources, "about/ios-help.gmi" },
73 { &blobVersion_Resources, "about/ios-version.gmi" },
74#elif defined (iPlatformAndroidMobile)
75 { &blobHelp_Resources, "about/android-help.gmi" },
76 { &blobVersion_Resources, "about/android-version.gmi" },
77#else
78 { &blobHelp_Resources, "about/help.gmi" },
72 { &blobVersion_Resources, "about/version.gmi" }, 79 { &blobVersion_Resources, "about/version.gmi" },
80#endif
73 { &blobArghelp_Resources, "arg-help.txt" }, 81 { &blobArghelp_Resources, "arg-help.txt" },
74 { &blobCs_Resources, "lang/cs.bin" }, 82 { &blobCs_Resources, "lang/cs.bin" },
75 { &blobDe_Resources, "lang/de.bin" }, 83 { &blobDe_Resources, "lang/de.bin" },
diff --git a/src/sitespec.c b/src/sitespec.c
index 6f4546f0..fe80ad13 100644
--- a/src/sitespec.c
+++ b/src/sitespec.c
@@ -25,6 +25,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
25#include <the_Foundation/file.h> 25#include <the_Foundation/file.h>
26#include <the_Foundation/path.h> 26#include <the_Foundation/path.h>
27#include <the_Foundation/stringhash.h> 27#include <the_Foundation/stringhash.h>
28#include <the_Foundation/stringarray.h>
28#include <the_Foundation/toml.h> 29#include <the_Foundation/toml.h>
29 30
30iDeclareClass(SiteParams) 31iDeclareClass(SiteParams)
@@ -35,6 +36,7 @@ struct Impl_SiteParams {
35 uint16_t titanPort; 36 uint16_t titanPort;
36 iString titanIdentity; /* fingerprint */ 37 iString titanIdentity; /* fingerprint */
37 int dismissWarnings; 38 int dismissWarnings;
39 iStringArray usedIdentities; /* fingerprints; latest ones at the end */
38 /* TODO: theme seed, style settings */ 40 /* TODO: theme seed, style settings */
39}; 41};
40 42
@@ -42,12 +44,23 @@ void init_SiteParams(iSiteParams *d) {
42 d->titanPort = 0; /* undefined */ 44 d->titanPort = 0; /* undefined */
43 init_String(&d->titanIdentity); 45 init_String(&d->titanIdentity);
44 d->dismissWarnings = 0; 46 d->dismissWarnings = 0;
47 init_StringArray(&d->usedIdentities);
45} 48}
46 49
47void deinit_SiteParams(iSiteParams *d) { 50void deinit_SiteParams(iSiteParams *d) {
51 deinit_StringArray(&d->usedIdentities);
48 deinit_String(&d->titanIdentity); 52 deinit_String(&d->titanIdentity);
49} 53}
50 54
55static size_t findUsedIdentity_SiteParams_(const iSiteParams *d, const iString *fingerprint) {
56 iConstForEach(StringArray, i, &d->usedIdentities) {
57 if (equal_String(i.value, fingerprint)) {
58 return index_StringArrayConstIterator(&i);
59 }
60 }
61 return iInvalidPos;
62}
63
51iDefineClass(SiteParams) 64iDefineClass(SiteParams)
52iDefineObjectConstruction(SiteParams) 65iDefineObjectConstruction(SiteParams)
53 66
@@ -128,7 +141,13 @@ static void handleIniKeyValue_SiteSpec_(void *context, const iString *table, con
128 set_String(&d->loadParams->titanIdentity, value->value.string); 141 set_String(&d->loadParams->titanIdentity, value->value.string);
129 } 142 }
130 else if (!cmp_String(key, "dismissWarnings") && value->type == int64_TomlType) { 143 else if (!cmp_String(key, "dismissWarnings") && value->type == int64_TomlType) {
131 d->loadParams->dismissWarnings = value->value.int64; 144 d->loadParams->dismissWarnings = (int) value->value.int64;
145 }
146 else if (!cmp_String(key, "usedIdentities") && value->type == string_TomlType) {
147 iRangecc seg = iNullRange;
148 while (nextSplit_Rangecc(range_String(value->value.string), " ", &seg)) {
149 pushBack_StringArray(&d->loadParams->usedIdentities, collectNewRange_String(seg));
150 }
132 } 151 }
133} 152}
134 153
@@ -151,6 +170,7 @@ static void save_SiteSpec_(iSiteSpec *d) {
151 if (open_File(f, writeOnly_FileMode | text_FileMode)) { 170 if (open_File(f, writeOnly_FileMode | text_FileMode)) {
152 iString *buf = new_String(); 171 iString *buf = new_String();
153 iConstForEach(StringHash, i, &d->sites) { 172 iConstForEach(StringHash, i, &d->sites) {
173 iBeginCollect();
154 const iBlock * key = &i.value->keyBlock; 174 const iBlock * key = &i.value->keyBlock;
155 const iSiteParams *params = i.value->object; 175 const iSiteParams *params = i.value->object;
156 format_String(buf, "[%s]\n", cstr_Block(key)); 176 format_String(buf, "[%s]\n", cstr_Block(key));
@@ -164,8 +184,15 @@ static void save_SiteSpec_(iSiteSpec *d) {
164 if (params->dismissWarnings) { 184 if (params->dismissWarnings) {
165 appendFormat_String(buf, "dismissWarnings = 0x%x\n", params->dismissWarnings); 185 appendFormat_String(buf, "dismissWarnings = 0x%x\n", params->dismissWarnings);
166 } 186 }
187 if (!isEmpty_StringArray(&params->usedIdentities)) {
188 appendFormat_String(
189 buf,
190 "usedIdentities = \"%s\"\n",
191 cstrCollect_String(joinCStr_StringArray(&params->usedIdentities, " ")));
192 }
167 appendCStr_String(buf, "\n"); 193 appendCStr_String(buf, "\n");
168 write_File(f, utf8_String(buf)); 194 write_File(f, utf8_String(buf));
195 iEndCollect();
169 } 196 }
170 delete_String(buf); 197 delete_String(buf);
171 } 198 }
@@ -188,14 +215,19 @@ void deinit_SiteSpec(void) {
188 deinit_String(&d->saveDir); 215 deinit_String(&d->saveDir);
189} 216}
190 217
191void setValue_SiteSpec(const iString *site, enum iSiteSpecKey key, int value) { 218static iSiteParams *findParams_SiteSpec_(iSiteSpec *d, const iString *site) {
192 iSiteSpec *d = &siteSpec_;
193 const iString *hashKey = collect_String(lower_String(site)); 219 const iString *hashKey = collect_String(lower_String(site));
194 iSiteParams *params = value_StringHash(&d->sites, hashKey); 220 iSiteParams *params = value_StringHash(&d->sites, hashKey);
195 if (!params) { 221 if (!params) {
196 params = new_SiteParams(); 222 params = new_SiteParams();
197 insert_StringHash(&d->sites, hashKey, params); 223 insert_StringHash(&d->sites, hashKey, params);
198 } 224 }
225 return params;
226}
227
228void setValue_SiteSpec(const iString *site, enum iSiteSpecKey key, int value) {
229 iSiteSpec *d = &siteSpec_;
230 iSiteParams *params = findParams_SiteSpec_(d, site);
199 iBool needSave = iFalse; 231 iBool needSave = iFalse;
200 switch (key) { 232 switch (key) {
201 case titanPort_SiteSpecKey: 233 case titanPort_SiteSpecKey:
@@ -216,12 +248,7 @@ void setValue_SiteSpec(const iString *site, enum iSiteSpecKey key, int value) {
216 248
217void setValueString_SiteSpec(const iString *site, enum iSiteSpecKey key, const iString *value) { 249void setValueString_SiteSpec(const iString *site, enum iSiteSpecKey key, const iString *value) {
218 iSiteSpec *d = &siteSpec_; 250 iSiteSpec *d = &siteSpec_;
219 const iString *hashKey = collect_String(lower_String(site)); 251 iSiteParams *params = findParams_SiteSpec_(d, site);
220 iSiteParams *params = value_StringHash(&d->sites, hashKey);
221 if (!params) {
222 params = new_SiteParams();
223 insert_StringHash(&d->sites, hashKey, params);
224 }
225 iBool needSave = iFalse; 252 iBool needSave = iFalse;
226 switch (key) { 253 switch (key) {
227 case titanIdentity_SiteSpecKey: 254 case titanIdentity_SiteSpecKey:
@@ -238,6 +265,44 @@ void setValueString_SiteSpec(const iString *site, enum iSiteSpecKey key, const i
238 } 265 }
239} 266}
240 267
268static void insertOrRemoveString_SiteSpec_(iSiteSpec *d, const iString *site, enum iSiteSpecKey key,
269 const iString *value, iBool doInsert) {
270 iSiteParams *params = findParams_SiteSpec_(d, site);
271 iBool needSave = iFalse;
272 switch (key) {
273 case usedIdentities_SiteSpecKey: {
274 const size_t index = findUsedIdentity_SiteParams_(params, value);
275 if (doInsert && index == iInvalidPos) {
276 pushBack_StringArray(&params->usedIdentities, value);
277 needSave = iTrue;
278 }
279 else if (!doInsert && index != iInvalidPos) {
280 remove_StringArray(&params->usedIdentities, index);
281 needSave = iTrue;
282 }
283 break;
284 }
285 default:
286 break;
287 }
288 if (needSave) {
289 save_SiteSpec_(d);
290 }
291}
292
293void insertString_SiteSpec(const iString *site, enum iSiteSpecKey key, const iString *value) {
294 insertOrRemoveString_SiteSpec_(&siteSpec_, site, key, value, iTrue);
295}
296
297void removeString_SiteSpec(const iString *site, enum iSiteSpecKey key, const iString *value) {
298 insertOrRemoveString_SiteSpec_(&siteSpec_, site, key, value, iFalse);
299}
300
301const iStringArray *strings_SiteSpec(const iString *site, enum iSiteSpecKey key) {
302 const iSiteParams *params = findParams_SiteSpec_(&siteSpec_, site);
303 return &params->usedIdentities;
304}
305
241int value_SiteSpec(const iString *site, enum iSiteSpecKey key) { 306int value_SiteSpec(const iString *site, enum iSiteSpecKey key) {
242 iSiteSpec *d = &siteSpec_; 307 iSiteSpec *d = &siteSpec_;
243 const iSiteParams *params = constValue_StringHash(&d->sites, collect_String(lower_String(site))); 308 const iSiteParams *params = constValue_StringHash(&d->sites, collect_String(lower_String(site)));
diff --git a/src/sitespec.h b/src/sitespec.h
index 5adaeb8c..11c40e3c 100644
--- a/src/sitespec.h
+++ b/src/sitespec.h
@@ -22,22 +22,26 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22 22
23#pragma once 23#pragma once
24 24
25#include <the_Foundation/string.h> 25#include <the_Foundation/stringarray.h>
26 26
27iDeclareType(SiteSpec) 27iDeclareType(SiteSpec)
28 28
29enum iSiteSpecKey { 29enum iSiteSpecKey {
30 titanPort_SiteSpecKey, 30 titanPort_SiteSpecKey, /* int */
31 titanIdentity_SiteSpecKey, 31 titanIdentity_SiteSpecKey, /* String */
32 dismissWarnings_SiteSpecKey, 32 dismissWarnings_SiteSpecKey, /* int */
33 usedIdentities_SiteSpecKey, /* StringArray */
33}; 34};
34 35
35void init_SiteSpec (const char *saveDir); 36void init_SiteSpec (const char *saveDir);
36void deinit_SiteSpec (void); 37void deinit_SiteSpec (void);
37 38
38/* changes saved immediately */ 39/* changes saved immediately */
39void setValue_SiteSpec (const iString *site, enum iSiteSpecKey key, int value); 40void setValue_SiteSpec (const iString *site, enum iSiteSpecKey key, int value);
40void setValueString_SiteSpec (const iString *site, enum iSiteSpecKey key, const iString *value); 41void setValueString_SiteSpec (const iString *site, enum iSiteSpecKey key, const iString *value);
42void insertString_SiteSpec (const iString *site, enum iSiteSpecKey key, const iString *value);
43void removeString_SiteSpec (const iString *site, enum iSiteSpecKey key, const iString *value);
41 44
42int value_SiteSpec (const iString *site, enum iSiteSpecKey key); 45int value_SiteSpec (const iString *site, enum iSiteSpecKey key);
43const iString * valueString_SiteSpec (const iString *site, enum iSiteSpecKey key); 46const iString * valueString_SiteSpec (const iString *site, enum iSiteSpecKey key);
47const iStringArray *strings_SiteSpec (const iString *site, enum iSiteSpecKey key);
diff --git a/src/ui/banner.c b/src/ui/banner.c
index 7ec189a4..11ae1574 100644
--- a/src/ui/banner.c
+++ b/src/ui/banner.c
@@ -76,7 +76,6 @@ static void updateHeight_Banner_(iBanner *d) {
76 } 76 }
77 const size_t numItems = size_Array(&d->items); 77 const size_t numItems = size_Array(&d->items);
78 if (numItems) { 78 if (numItems) {
79 const int innerPad = gap_UI;
80 iConstForEach(Array, i, &d->items) { 79 iConstForEach(Array, i, &d->items) {
81 const iBannerItem *item = i.value; 80 const iBannerItem *item = i.value;
82 d->rect.size.y += item->height; 81 d->rect.size.y += item->height;
@@ -161,6 +160,13 @@ void setSite_Banner(iBanner *d, iRangecc site, iChar icon) {
161 160
162void add_Banner(iBanner *d, enum iBannerType type, enum iGmStatusCode code, 161void add_Banner(iBanner *d, enum iBannerType type, enum iGmStatusCode code,
163 const iString *message, const iString *details) { 162 const iString *message, const iString *details) {
163 /* If there already is a matching item, don't add a second one. */
164 iConstForEach(Array, i, &d->items) {
165 const iBannerItem *item = i.value;
166 if (item->type == type && item->code == code) {
167 return;
168 }
169 }
164 iBannerItem item; 170 iBannerItem item;
165 init_BannerItem(&item); 171 init_BannerItem(&item);
166 item.type = type; 172 item.type = type;
diff --git a/src/ui/bindingswidget.c b/src/ui/bindingswidget.c
index 4cf8df8e..13f9434e 100644
--- a/src/ui/bindingswidget.c
+++ b/src/ui/bindingswidget.c
@@ -143,12 +143,16 @@ static void setActiveItem_BindingsWidget_(iBindingsWidget *d, size_t pos) {
143 item->isWaitingForEvent = iTrue; 143 item->isWaitingForEvent = iTrue;
144 invalidateItem_ListWidget(d->list, d->activePos); 144 invalidateItem_ListWidget(d->list, d->activePos);
145 } 145 }
146#if defined (iPlatformAppleDesktop) 146#if defined (iPlatformAppleDesktop) && defined (iHaveNativeContextMenus)
147 /* Native menus must be disabled while grabbing keys so the shortcuts don't trigger. */ 147 /* Native menus must be disabled while grabbing keys so the shortcuts don't trigger. */
148 const iBool enableNativeMenus = (d->activePos == iInvalidPos); 148 const iBool enableNativeMenus = (d->activePos == iInvalidPos);
149 enableMenu_MacOS("${menu.title.file}", enableNativeMenus);
149 enableMenu_MacOS("${menu.title.edit}", enableNativeMenus); 150 enableMenu_MacOS("${menu.title.edit}", enableNativeMenus);
150 enableMenu_MacOS("${menu.title.view}", enableNativeMenus); 151 enableMenu_MacOS("${menu.title.view}", enableNativeMenus);
152 enableMenu_MacOS("${menu.title.bookmarks}", enableNativeMenus);
151 enableMenu_MacOS("${menu.title.identity}", enableNativeMenus); 153 enableMenu_MacOS("${menu.title.identity}", enableNativeMenus);
154 enableMenuIndex_MacOS(6, enableNativeMenus);
155 enableMenuIndex_MacOS(7, enableNativeMenus);
152#endif 156#endif
153} 157}
154 158
diff --git a/src/ui/certimportwidget.c b/src/ui/certimportwidget.c
index f4dfdefa..e4e461e0 100644
--- a/src/ui/certimportwidget.c
+++ b/src/ui/certimportwidget.c
@@ -145,10 +145,7 @@ void init_CertImportWidget(iCertImportWidget *d) {
145 else { 145 else {
146 /* This should behave similar to sheets. */ 146 /* This should behave similar to sheets. */
147 useSheetStyle_Widget(w); 147 useSheetStyle_Widget(w);
148 addChildFlags_Widget( 148 addDialogTitle_Widget(w, "${heading.certimport}", NULL);
149 w,
150 iClob(new_LabelWidget(uiHeading_ColorEscape "${heading.certimport}", NULL)),
151 frameless_WidgetFlag);
152 d->info = addChildFlags_Widget(w, iClob(new_LabelWidget(infoText_, NULL)), frameless_WidgetFlag); 149 d->info = addChildFlags_Widget(w, iClob(new_LabelWidget(infoText_, NULL)), frameless_WidgetFlag);
153 addChild_Widget(w, iClob(makePadding_Widget(gap_UI))); 150 addChild_Widget(w, iClob(makePadding_Widget(gap_UI)));
154 d->crtLabel = new_LabelWidget("", NULL); { 151 d->crtLabel = new_LabelWidget("", NULL); {
diff --git a/src/ui/certlistwidget.c b/src/ui/certlistwidget.c
new file mode 100644
index 00000000..2a7562d8
--- /dev/null
+++ b/src/ui/certlistwidget.c
@@ -0,0 +1,490 @@
1/* Copyright 2021 Jaakko Keränen <jaakko.keranen@iki.fi>
2
3Redistribution and use in source and binary forms, with or without
4modification, are permitted provided that the following conditions are met:
5
61. Redistributions of source code must retain the above copyright notice, this
7 list of conditions and the following disclaimer.
82. Redistributions in binary form must reproduce the above copyright notice,
9 this list of conditions and the following disclaimer in the documentation
10 and/or other materials provided with the distribution.
11
12THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
13ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
14WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
15DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
16ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
17(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
18LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
19ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
20(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
21SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22
23#include "certlistwidget.h"
24
25#include "documentwidget.h"
26#include "command.h"
27#include "labelwidget.h"
28#include "listwidget.h"
29#include "../gmcerts.h"
30#include "../app.h"
31
32#include <SDL_clipboard.h>
33
34iDeclareType(CertItem)
35typedef iListItemClass iCertItemClass;
36
37struct Impl_CertItem {
38 iListItem listItem;
39 uint32_t id;
40 int indent;
41 iChar icon;
42 iBool isBold;
43 iString label;
44 iString meta;
45// iString url;
46};
47
48void init_CertItem(iCertItem *d) {
49 init_ListItem(&d->listItem);
50 d->id = 0;
51 d->indent = 0;
52 d->icon = 0;
53 d->isBold = iFalse;
54 init_String(&d->label);
55 init_String(&d->meta);
56// init_String(&d->url);
57}
58
59void deinit_CertItem(iCertItem *d) {
60// deinit_String(&d->url);
61 deinit_String(&d->meta);
62 deinit_String(&d->label);
63}
64
65static void draw_CertItem_(const iCertItem *d, iPaint *p, iRect itemRect, const iListWidget *list);
66
67iBeginDefineSubclass(CertItem, ListItem)
68 .draw = (iAny *) draw_CertItem_,
69iEndDefineSubclass(CertItem)
70
71iDefineObjectConstruction(CertItem)
72
73/*----------------------------------------------------------------------------------------------*/
74
75struct Impl_CertListWidget {
76 iListWidget list;
77 int itemFonts[2];
78 iWidget *menu; /* context menu for an item */
79 iCertItem *contextItem; /* list item accessed in the context menu */
80 size_t contextIndex; /* index of list item accessed in the context menu */
81};
82
83iDefineObjectConstruction(CertListWidget)
84
85static iGmIdentity *menuIdentity_CertListWidget_(const iCertListWidget *d) {
86 if (d->contextItem) {
87 return identity_GmCerts(certs_App(), d->contextItem->id);
88 }
89 return NULL;
90}
91
92static void updateContextMenu_CertListWidget_(iCertListWidget *d) {
93 iArray *items = collectNew_Array(sizeof(iMenuItem));
94 const iString *docUrl = url_DocumentWidget(document_App());
95 size_t firstIndex = 0;
96 if (deviceType_App() != desktop_AppDeviceType && !isEmpty_String(docUrl)) {
97 pushBack_Array(items, &(iMenuItem){ format_CStr("```%s", cstr_String(docUrl)) });
98 firstIndex = 1;
99 }
100 const iMenuItem ctxItems[] = {
101 { person_Icon " ${ident.use}", 0, 0, "ident.use arg:1" },
102 { close_Icon " ${ident.stopuse}", 0, 0, "ident.use arg:0" },
103 { close_Icon " ${ident.stopuse.all}", 0, 0, "ident.use arg:0 clear:1" },
104 { "---", 0, 0, NULL },
105 { edit_Icon " ${menu.edit.notes}", 0, 0, "ident.edit" },
106 { "${ident.fingerprint}", 0, 0, "ident.fingerprint" },
107#if defined (iPlatformAppleDesktop)
108 { magnifyingGlass_Icon " ${menu.reveal.macos}", 0, 0, "ident.reveal" },
109#endif
110#if defined (iPlatformLinux)
111 { magnifyingGlass_Icon " ${menu.reveal.filemgr}", 0, 0, "ident.reveal" },
112#endif
113 { export_Icon " ${ident.export}", 0, 0, "ident.export" },
114 { "---", 0, 0, NULL },
115 { delete_Icon " " uiTextCaution_ColorEscape "${ident.delete}", 0, 0, "ident.delete confirm:1" },
116 };
117 pushBackN_Array(items, ctxItems, iElemCount(ctxItems));
118 /* Used URLs. */
119 const iGmIdentity *ident = menuIdentity_CertListWidget_(d);
120 if (ident) {
121 size_t insertPos = firstIndex + 3;
122 if (!isEmpty_StringSet(ident->useUrls)) {
123 insert_Array(items, insertPos++, &(iMenuItem){ "---", 0, 0, NULL });
124 }
125 iBool usedOnCurrentPage = iFalse;
126 iConstForEach(StringSet, i, ident->useUrls) {
127 const iString *url = i.value;
128 usedOnCurrentPage |= startsWithCase_String(docUrl, cstr_String(url));
129 iRangecc urlStr = range_String(url);
130 if (startsWith_Rangecc(urlStr, "gemini://")) {
131 urlStr.start += 9; /* omit the default scheme */
132 }
133 insert_Array(items,
134 insertPos++,
135 &(iMenuItem){ format_CStr(globe_Icon " %s", cstr_Rangecc(urlStr)),
136 0,
137 0,
138 format_CStr("!open url:%s", cstr_String(url)) });
139 }
140 if (!usedOnCurrentPage) {
141 remove_Array(items, firstIndex + 1);
142 }
143 else {
144 remove_Array(items, firstIndex);
145 }
146 }
147 destroy_Widget(d->menu);
148 d->menu = makeMenu_Widget(as_Widget(d), data_Array(items), size_Array(items));
149}
150
151static void itemClicked_CertListWidget_(iCertListWidget *d, iCertItem *item, size_t itemIndex) {
152 iWidget *w = as_Widget(d);
153 setFocus_Widget(NULL);
154 d->contextItem = item;
155 if (d->contextIndex != iInvalidPos) {
156 invalidateItem_ListWidget(&d->list, d->contextIndex);
157 }
158 d->contextIndex = itemIndex;
159 if (itemIndex < numItems_ListWidget(&d->list)) {
160 updateContextMenu_CertListWidget_(d);
161 arrange_Widget(d->menu);
162 openMenu_Widget(d->menu,
163 bounds_Widget(w).pos.x < mid_Rect(rect_Root(w->root)).x
164 ? topRight_Rect(itemRect_ListWidget(&d->list, itemIndex))
165 : addX_I2(topLeft_Rect(itemRect_ListWidget(&d->list, itemIndex)),
166 -width_Widget(d->menu)));
167 }
168}
169
170static iBool processEvent_CertListWidget_(iCertListWidget *d, const SDL_Event *ev) {
171 iWidget *w = as_Widget(d);
172 /* Handle commands. */
173 if (ev->type == SDL_USEREVENT && ev->user.code == command_UserEventCode) {
174 const char *cmd = command_UserEvent(ev);
175 if (equal_Command(cmd, "idents.changed")) {
176 updateItems_CertListWidget(d);
177 }
178 else if (isCommand_Widget(w, ev, "list.clicked")) {
179 itemClicked_CertListWidget_(
180 d, pointerLabel_Command(cmd, "item"), argU32Label_Command(cmd, "arg"));
181 return iTrue;
182 }
183 else if (isCommand_Widget(w, ev, "ident.use")) {
184 iGmIdentity *ident = menuIdentity_CertListWidget_(d);
185 const iString *tabUrl = urlQueryStripped_String(url_DocumentWidget(document_App()));
186 if (ident) {
187 if (argLabel_Command(cmd, "clear")) {
188 clearUse_GmIdentity(ident);
189 }
190 else if (arg_Command(cmd)) {
191 signIn_GmCerts(certs_App(), ident, tabUrl);
192 postCommand_App("navigate.reload");
193 }
194 else {
195 signOut_GmCerts(certs_App(), tabUrl);
196 postCommand_App("navigate.reload");
197 }
198 saveIdentities_GmCerts(certs_App());
199 updateItems_CertListWidget(d);
200 }
201 return iTrue;
202 }
203 else if (isCommand_Widget(w, ev, "ident.edit")) {
204 const iGmIdentity *ident = menuIdentity_CertListWidget_(d);
205 if (ident) {
206 makeValueInput_Widget(get_Root()->widget,
207 &ident->notes,
208 uiHeading_ColorEscape "${heading.ident.notes}",
209 format_CStr(cstr_Lang("dlg.ident.notes"), cstr_String(name_GmIdentity(ident))),
210 uiTextAction_ColorEscape "${dlg.default}",
211 format_CStr("!ident.setnotes ident:%p ptr:%p", ident, d));
212 }
213 return iTrue;
214 }
215 else if (isCommand_Widget(w, ev, "ident.fingerprint")) {
216 const iGmIdentity *ident = menuIdentity_CertListWidget_(d);
217 if (ident) {
218 const iString *fps = collect_String(
219 hexEncode_Block(collect_Block(fingerprint_TlsCertificate(ident->cert))));
220 SDL_SetClipboardText(cstr_String(fps));
221 }
222 return iTrue;
223 }
224 else if (isCommand_Widget(w, ev, "ident.export")) {
225 const iGmIdentity *ident = menuIdentity_CertListWidget_(d);
226 if (ident) {
227 iString *pem = collect_String(pem_TlsCertificate(ident->cert));
228 append_String(pem, collect_String(privateKeyPem_TlsCertificate(ident->cert)));
229 iDocumentWidget *expTab = newTab_App(NULL, iTrue);
230 setUrlAndSource_DocumentWidget(
231 expTab,
232 collectNewFormat_String("file:%s.pem", cstr_String(name_GmIdentity(ident))),
233 collectNewCStr_String("text/plain"),
234 utf8_String(pem));
235 }
236 return iTrue;
237 }
238 else if (isCommand_Widget(w, ev, "ident.setnotes")) {
239 iGmIdentity *ident = pointerLabel_Command(cmd, "ident");
240 if (ident) {
241 setCStr_String(&ident->notes, suffixPtr_Command(cmd, "value"));
242 updateItems_CertListWidget(d);
243 }
244 return iTrue;
245 }
246 else if (isCommand_Widget(w, ev, "ident.pickicon")) {
247 return iTrue;
248 }
249 else if (isCommand_Widget(w, ev, "ident.reveal")) {
250 const iGmIdentity *ident = menuIdentity_CertListWidget_(d);
251 if (ident) {
252 const iString *crtPath = certificatePath_GmCerts(certs_App(), ident);
253 if (crtPath) {
254 postCommandf_App("reveal path:%s", cstr_String(crtPath));
255 }
256 }
257 return iTrue;
258 }
259 else if (isCommand_Widget(w, ev, "ident.delete")) {
260 iCertItem *item = d->contextItem;
261 if (argLabel_Command(cmd, "confirm")) {
262 makeQuestion_Widget(
263 uiTextCaution_ColorEscape "${heading.ident.delete}",
264 format_CStr(cstr_Lang("dlg.confirm.ident.delete"),
265 uiTextAction_ColorEscape,
266 cstr_String(&item->label),
267 uiText_ColorEscape),
268 (iMenuItem[]){ { "${cancel}", 0, 0, NULL },
269 { uiTextCaution_ColorEscape "${dlg.ident.delete}",
270 0,
271 0,
272 format_CStr("!ident.delete confirm:0 ptr:%p", d) } },
273 2);
274 return iTrue;
275 }
276 deleteIdentity_GmCerts(certs_App(), menuIdentity_CertListWidget_(d));
277 postCommand_App("idents.changed");
278 return iTrue;
279 }
280 }
281 if (ev->type == SDL_MOUSEMOTION && !isVisible_Widget(d->menu)) {
282 const iInt2 mouse = init_I2(ev->motion.x, ev->motion.y);
283 /* Update cursor. */
284 if (contains_Widget(w, mouse)) {
285 setCursor_Window(get_Window(), SDL_SYSTEM_CURSOR_ARROW);
286 }
287 else if (d->contextIndex != iInvalidPos) {
288 invalidateItem_ListWidget(&d->list, d->contextIndex);
289 d->contextIndex = iInvalidPos;
290 }
291 }
292 /* Update context menu items. */
293 if (ev->type == SDL_MOUSEBUTTONDOWN && ev->button.button == SDL_BUTTON_RIGHT) {
294 d->contextItem = NULL;
295 if (!isVisible_Widget(d->menu)) {
296 updateMouseHover_ListWidget(&d->list);
297 }
298 if (constHoverItem_ListWidget(&d->list) || isVisible_Widget(d->menu)) {
299 d->contextItem = hoverItem_ListWidget(&d->list);
300 /* Context is drawn in hover state. */
301 if (d->contextIndex != iInvalidPos) {
302 invalidateItem_ListWidget(&d->list, d->contextIndex);
303 }
304 d->contextIndex = hoverItemIndex_ListWidget(&d->list);
305 updateContextMenu_CertListWidget_(d);
306 /* TODO: Some callback-based mechanism would be nice for updating menus right
307 before they open? At least move these to `updateContextMenu_ */
308 const iGmIdentity *ident = constHoverIdentity_CertListWidget(d);
309 const iString * docUrl = url_DocumentWidget(document_App());
310 iForEach(ObjectList, i, children_Widget(d->menu)) {
311 if (isInstance_Object(i.object, &Class_LabelWidget)) {
312 iLabelWidget *menuItem = i.object;
313 const char * cmdItem = cstr_String(command_LabelWidget(menuItem));
314 if (equal_Command(cmdItem, "ident.use")) {
315 const iBool cmdUse = arg_Command(cmdItem) != 0;
316 const iBool cmdClear = argLabel_Command(cmdItem, "clear") != 0;
317 setFlags_Widget(
318 as_Widget(menuItem),
319 disabled_WidgetFlag,
320 (cmdClear && !isUsed_GmIdentity(ident)) ||
321 (!cmdClear && cmdUse && isUsedOn_GmIdentity(ident, docUrl)) ||
322 (!cmdClear && !cmdUse && !isUsedOn_GmIdentity(ident, docUrl)));
323 }
324 }
325 }
326 }
327 if (hoverItem_ListWidget(&d->list) || isVisible_Widget(d->menu)) {
328 processContextMenuEvent_Widget(d->menu, ev, {});
329 }
330 }
331 return ((iWidgetClass *) class_Widget(w)->super)->processEvent(w, ev);
332}
333
334static void draw_CertListWidget_(const iCertListWidget *d) {
335 const iWidget *w = constAs_Widget(d);
336 ((iWidgetClass *) class_Widget(w)->super)->draw(w);
337}
338
339static void draw_CertItem_(const iCertItem *d, iPaint *p, iRect itemRect,
340 const iListWidget *list) {
341 const iCertListWidget *certList = (const iCertListWidget *) list;
342 const iBool isMenuVisible = isVisible_Widget(certList->menu);
343 const iBool isDragging = constDragItem_ListWidget(list) == d;
344 const iBool isPressing = isMouseDown_ListWidget(list) && !isDragging;
345 const iBool isHover =
346 (!isMenuVisible &&
347 isHover_Widget(constAs_Widget(list)) &&
348 constHoverItem_ListWidget(list) == d) ||
349 (isMenuVisible && certList->contextItem == d) ||
350 isDragging;
351 const int itemHeight = height_Rect(itemRect);
352 const int iconColor = isHover ? (isPressing ? uiTextPressed_ColorId : uiIconHover_ColorId)
353 : uiIcon_ColorId;
354 const int altIconColor = isPressing ? uiTextPressed_ColorId : uiTextCaution_ColorId;
355 const int font = certList->itemFonts[d->isBold ? 1 : 0];
356 int bg = uiBackgroundSidebar_ColorId;
357 if (isHover) {
358 bg = isPressing ? uiBackgroundPressed_ColorId
359 : uiBackgroundFramelessHover_ColorId;
360 fillRect_Paint(p, itemRect, bg);
361 }
362 else if (d->listItem.isSelected) {
363 bg = uiBackgroundUnfocusedSelection_ColorId;
364 fillRect_Paint(p, itemRect, bg);
365 }
366// iInt2 pos = itemRect.pos;
367 const int fg = isHover ? (isPressing ? uiTextPressed_ColorId : uiTextFramelessHover_ColorId)
368 : uiTextStrong_ColorId;
369 const iBool isUsedOnDomain = (d->indent != 0);
370 iString icon;
371 initUnicodeN_String(&icon, &d->icon, 1);
372 iInt2 cPos = topLeft_Rect(itemRect);
373 const int indent = 1.4f * lineHeight_Text(font);
374 addv_I2(&cPos,
375 init_I2(3 * gap_UI,
376 (itemHeight - lineHeight_Text(uiLabel_FontId) * 2 - lineHeight_Text(font)) /
377 2));
378 const int metaFg = isHover ? permanent_ColorId | (isPressing ? uiTextPressed_ColorId
379 : uiTextFramelessHover_ColorId)
380 : uiTextDim_ColorId;
381 if (!d->listItem.isSelected && !isUsedOnDomain) {
382 drawOutline_Text(font, cPos, metaFg, none_ColorId, range_String(&icon));
383 }
384 drawRange_Text(font,
385 cPos,
386 d->listItem.isSelected ? iconColor
387 : isUsedOnDomain ? altIconColor
388 : uiBackgroundSidebar_ColorId,
389 range_String(&icon));
390 deinit_String(&icon);
391 drawRange_Text(d->listItem.isSelected ? certList->itemFonts[1] : font,
392 add_I2(cPos, init_I2(indent, 0)),
393 fg,
394 range_String(&d->label));
395 drawRange_Text(uiLabel_FontId,
396 add_I2(cPos, init_I2(indent, lineHeight_Text(font))),
397 metaFg,
398 range_String(&d->meta));
399}
400
401void init_CertListWidget(iCertListWidget *d) {
402 iWidget *w = as_Widget(d);
403 init_ListWidget(&d->list);
404 setId_Widget(w, "certlist");
405 setBackgroundColor_Widget(w, none_ColorId);
406 d->itemFonts[0] = uiContent_FontId;
407 d->itemFonts[1] = uiContentBold_FontId;
408#if defined (iPlatformMobile)
409 if (deviceType_App() == phone_AppDeviceType) {
410 d->itemFonts[0] = uiLabelBig_FontId;
411 d->itemFonts[1] = uiLabelBigBold_FontId;
412 }
413#endif
414 updateItemHeight_CertListWidget(d);
415 d->menu = NULL;
416 d->contextItem = NULL;
417 d->contextIndex = iInvalidPos;
418}
419
420void updateItemHeight_CertListWidget(iCertListWidget *d) {
421 setItemHeight_ListWidget(&d->list, 3.5f * lineHeight_Text(d->itemFonts[0]));
422}
423
424iBool updateItems_CertListWidget(iCertListWidget *d) {
425 clear_ListWidget(&d->list);
426 destroy_Widget(d->menu);
427 d->menu = NULL;
428 const iString *tabUrl = url_DocumentWidget(document_App());
429 const iRangecc tabHost = urlHost_String(tabUrl);
430 iBool haveItems = iFalse;
431 iConstForEach(PtrArray, i, identities_GmCerts(certs_App())) {
432 const iGmIdentity *ident = i.ptr;
433 iCertItem *item = new_CertItem();
434 item->id = (uint32_t) index_PtrArrayConstIterator(&i);
435 item->icon = 0x1f464; /* person */
436 set_String(&item->label, name_GmIdentity(ident));
437 iDate until;
438 validUntil_TlsCertificate(ident->cert, &until);
439 const iBool isActive = isUsedOn_GmIdentity(ident, tabUrl);
440 format_String(&item->meta,
441 "%s",
442 isActive ? cstr_Lang("ident.using")
443 : isUsed_GmIdentity(ident)
444 ? formatCStrs_Lang("ident.usedonurls.n", size_StringSet(ident->useUrls))
445 : cstr_Lang("ident.notused"));
446 const char *expiry =
447 ident->flags & temporary_GmIdentityFlag
448 ? cstr_Lang("ident.temporary")
449 : cstrCollect_String(format_Date(&until, cstr_Lang("ident.expiry")));
450 if (isEmpty_String(&ident->notes)) {
451 appendFormat_String(&item->meta, "\n%s", expiry);
452 }
453 else {
454 appendFormat_String(&item->meta,
455 " \u2014 %s\n%s%s",
456 expiry,
457 escape_Color(uiHeading_ColorId),
458 cstr_String(&ident->notes));
459 }
460 item->listItem.isSelected = isActive;
461 if (!isActive && isUsedOnDomain_GmIdentity(ident, tabHost)) {
462 item->indent = 1; /* will be highlighted */
463 }
464 addItem_ListWidget(&d->list, item);
465 haveItems = iTrue;
466 iRelease(item);
467 }
468 return haveItems;
469}
470
471void deinit_CertListWidget(iCertListWidget *d) {
472 iUnused(d);
473}
474
475const iGmIdentity *constHoverIdentity_CertListWidget(const iCertListWidget *d) {
476 const iCertItem *hoverItem = constHoverItem_ListWidget(&d->list);
477 if (hoverItem) {
478 return identity_GmCerts(certs_App(), hoverItem->id);
479 }
480 return NULL;
481}
482
483iGmIdentity *hoverIdentity_CertListWidget(const iCertListWidget *d) {
484 return iConstCast(iGmIdentity *, constHoverIdentity_CertListWidget(d));
485}
486
487iBeginDefineSubclass(CertListWidget, ListWidget)
488 .processEvent = (iAny *) processEvent_CertListWidget_,
489 .draw = (iAny *) draw_CertListWidget_,
490iEndDefineSubclass(CertListWidget)
diff --git a/src/ui/certlistwidget.h b/src/ui/certlistwidget.h
new file mode 100644
index 00000000..2e5f6247
--- /dev/null
+++ b/src/ui/certlistwidget.h
@@ -0,0 +1,40 @@
1/* Copyright 2021 Jaakko Keränen <jaakko.keranen@iki.fi>
2
3Redistribution and use in source and binary forms, with or without
4modification, are permitted provided that the following conditions are met:
5
61. Redistributions of source code must retain the above copyright notice, this
7 list of conditions and the following disclaimer.
82. Redistributions in binary form must reproduce the above copyright notice,
9 this list of conditions and the following disclaimer in the documentation
10 and/or other materials provided with the distribution.
11
12THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
13ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
14WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
15DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
16ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
17(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
18LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
19ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
20(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
21SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22
23#pragma once
24
25#include "listwidget.h"
26
27iDeclareType(CertListWidget)
28
29typedef iListWidgetClass iCertListWidgetClass;
30extern iCertListWidgetClass Class_CertListWidget;
31
32iDeclareObjectConstruction(CertListWidget)
33
34iDeclareType(GmIdentity)
35
36const iGmIdentity * constHoverIdentity_CertListWidget(const iCertListWidget *);
37iGmIdentity * hoverIdentity_CertListWidget (const iCertListWidget *);
38
39iBool updateItems_CertListWidget (iCertListWidget *); /* returns False is empty */
40void updateItemHeight_CertListWidget (iCertListWidget *);
diff --git a/src/ui/color.c b/src/ui/color.c
index 3ea98d8c..3c2f0339 100644
--- a/src/ui/color.c
+++ b/src/ui/color.c
@@ -90,8 +90,8 @@ void setThemePalette_Color(enum iColorTheme theme) {
90 const int accentLo = (prefs->accent == cyan_ColorAccent ? teal_ColorId : brown_ColorId); 90 const int accentLo = (prefs->accent == cyan_ColorAccent ? teal_ColorId : brown_ColorId);
91 const int altAccentHi = (prefs->accent == cyan_ColorAccent ? orange_ColorId : cyan_ColorId); 91 const int altAccentHi = (prefs->accent == cyan_ColorAccent ? orange_ColorId : cyan_ColorId);
92 const int altAccentLo = (prefs->accent == cyan_ColorAccent ? brown_ColorId : teal_ColorId); 92 const int altAccentLo = (prefs->accent == cyan_ColorAccent ? brown_ColorId : teal_ColorId);
93 const iColor accentMid = mix_Color(get_Color(accentHi), get_Color(accentLo), 0.5f); 93 //const iColor accentMid = mix_Color(get_Color(accentHi), get_Color(accentLo), 0.5f);
94 const iColor altAccentMid = mix_Color(get_Color(altAccentHi), get_Color(altAccentLo), 0.5f); 94 //const iColor altAccentMid = mix_Color(get_Color(altAccentHi), get_Color(altAccentLo), 0.5f);
95 switch (theme) { 95 switch (theme) {
96 case pureBlack_ColorTheme: { 96 case pureBlack_ColorTheme: {
97 copy_(uiBackground_ColorId, black_ColorId); 97 copy_(uiBackground_ColorId, black_ColorId);
@@ -832,7 +832,7 @@ void ansiColors_Color(iRangecc escapeSequence, int fgDefault, int bgDefault,
832 int rgb[3] = { 0, 0, 0 }; 832 int rgb[3] = { 0, 0, 0 };
833 iForIndices(i, rgb) { 833 iForIndices(i, rgb) {
834 if (ch >= escapeSequence.end) break; 834 if (ch >= escapeSequence.end) break;
835 rgb[i] = strtoul(ch + 1, &endPtr, 10); 835 rgb[i] = (int) strtoul(ch + 1, &endPtr, 10);
836 ch = endPtr; 836 ch = endPtr;
837 } 837 }
838 dst->r = iClamp(rgb[0], 0, 255); 838 dst->r = iClamp(rgb[0], 0, 255);
diff --git a/src/ui/color.h b/src/ui/color.h
index 0b3f8bed..24f9e713 100644
--- a/src/ui/color.h
+++ b/src/ui/color.h
@@ -136,7 +136,7 @@ enum iColorId {
136 tmBackgroundAltText_ColorId, /* derived from other theme colors */ 136 tmBackgroundAltText_ColorId, /* derived from other theme colors */
137 tmFrameAltText_ColorId, /* derived from other theme colors */ 137 tmFrameAltText_ColorId, /* derived from other theme colors */
138 tmBackgroundOpenLink_ColorId, /* derived from other theme colors */ 138 tmBackgroundOpenLink_ColorId, /* derived from other theme colors */
139 tmFrameOpenLink_ColorId, /* derived from other theme colors */ 139 tmLinkFeedEntryDate_ColorId, /* derived from other theme colors */
140 tmLinkCustomIconVisited_ColorId, /* derived from other theme colors */ 140 tmLinkCustomIconVisited_ColorId, /* derived from other theme colors */
141 tmBadLink_ColorId, 141 tmBadLink_ColorId,
142 142
diff --git a/src/ui/command.c b/src/ui/command.c
index 3ae0f0c9..a4868ca9 100644
--- a/src/ui/command.c
+++ b/src/ui/command.c
@@ -26,6 +26,34 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
26#include <the_Foundation/string.h> 26#include <the_Foundation/string.h>
27#include <ctype.h> 27#include <ctype.h>
28 28
29iDeclareType(Token)
30
31#define maxLen_Token 64
32
33struct Impl_Token {
34 char buf[64];
35 size_t size;
36};
37
38static void init_Token(iToken *d, const char *label) {
39 const size_t len = strlen(label);
40 iAssert(len < sizeof(d->buf) - 3);
41 d->buf[0] = ' ';
42 memcpy(d->buf + 1, label, len);
43 d->buf[1 + len] = ':';
44 d->buf[1 + len + 1] = 0;
45 d->size = len + 2;
46}
47
48static iRangecc find_Token(const iToken *d, const char *cmd) {
49 iRangecc range = iNullRange;
50 range.start = strstr(cmd, d->buf);
51 if (range.start) {
52 range.end = range.start + d->size;
53 }
54 return range;
55}
56
29iBool equal_Command(const char *cmdWithArgs, const char *cmd) { 57iBool equal_Command(const char *cmdWithArgs, const char *cmd) {
30 if (strchr(cmdWithArgs, ':')) { 58 if (strchr(cmdWithArgs, ':')) {
31 return startsWith_CStr(cmdWithArgs, cmd) && cmdWithArgs[strlen(cmd)] == ' '; 59 return startsWith_CStr(cmdWithArgs, cmd) && cmdWithArgs[strlen(cmd)] == ' ';
@@ -33,15 +61,12 @@ iBool equal_Command(const char *cmdWithArgs, const char *cmd) {
33 return equal_CStr(cmdWithArgs, cmd); 61 return equal_CStr(cmdWithArgs, cmd);
34} 62}
35 63
36static const iString *tokenString_(const char *label) {
37 return collectNewFormat_String(" %s:", label);
38}
39
40int argLabel_Command(const char *cmd, const char *label) { 64int argLabel_Command(const char *cmd, const char *label) {
41 const iString *tok = tokenString_(label); 65 iToken tok;
42 const char *ptr = strstr(cmd, cstr_String(tok)); 66 init_Token(&tok, label);
43 if (ptr) { 67 iRangecc ptr = find_Token(&tok, cmd);
44 return atoi(ptr + size_String(tok)); 68 if (ptr.start) {
69 return atoi(ptr.end);
45 } 70 }
46 return 0; 71 return 0;
47} 72}
@@ -51,19 +76,21 @@ int arg_Command(const char *cmd) {
51} 76}
52 77
53uint32_t argU32Label_Command(const char *cmd, const char *label) { 78uint32_t argU32Label_Command(const char *cmd, const char *label) {
54 const iString *tok = tokenString_(label); 79 iToken tok;
55 const char *ptr = strstr(cmd, cstr_String(tok)); 80 init_Token(&tok, label);
56 if (ptr) { 81 const iRangecc ptr = find_Token(&tok, cmd);
57 return strtoul(ptr + size_String(tok), NULL, 10); 82 if (ptr.start) {
83 return (uint32_t) strtoul(ptr.end, NULL, 10);
58 } 84 }
59 return 0; 85 return 0;
60} 86}
61 87
62float argfLabel_Command(const char *cmd, const char *label) { 88float argfLabel_Command(const char *cmd, const char *label) {
63 const iString *tok = tokenString_(label); 89 iToken tok;
64 const char *ptr = strstr(cmd, cstr_String(tok)); 90 init_Token(&tok, label);
65 if (ptr) { 91 const iRangecc ptr = find_Token(&tok, cmd);
66 return strtof(ptr + size_String(tok), NULL); 92 if (ptr.start) {
93 return strtof(ptr.end, NULL);
67 } 94 }
68 return 0.0f; 95 return 0.0f;
69} 96}
@@ -77,11 +104,12 @@ float argf_Command(const char *cmd) {
77} 104}
78 105
79void *pointerLabel_Command(const char *cmd, const char *label) { 106void *pointerLabel_Command(const char *cmd, const char *label) {
80 const iString *tok = tokenString_(label); 107 iToken tok;
81 const char *ptr = strstr(cmd, cstr_String(tok)); 108 init_Token(&tok, label);
82 if (ptr) { 109 const iRangecc ptr = find_Token(&tok, cmd);
110 if (ptr.start) {
83 void *val = NULL; 111 void *val = NULL;
84 sscanf(ptr + size_String(tok), "%p", &val); 112 sscanf(ptr.end, "%p", &val);
85 return val; 113 return val;
86 } 114 }
87 return NULL; 115 return NULL;
@@ -92,10 +120,11 @@ void *pointer_Command(const char *cmd) {
92} 120}
93 121
94const char *suffixPtr_Command(const char *cmd, const char *label) { 122const char *suffixPtr_Command(const char *cmd, const char *label) {
95 const iString *tok = tokenString_(label); 123 iToken tok;
96 const char *ptr = strstr(cmd, cstr_String(tok)); 124 init_Token(&tok, label);
97 if (ptr) { 125 const iRangecc ptr = find_Token(&tok, cmd);
98 return ptr + size_String(tok); 126 if (ptr.start) {
127 return ptr.end;
99 } 128 }
100 return NULL; 129 return NULL;
101} 130}
diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c
index 46af5fcd..fdb55232 100644
--- a/src/ui/documentwidget.c
+++ b/src/ui/documentwidget.c
@@ -20,8 +20,8 @@ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
20(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 20(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
21SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ 21SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22 22
23/* TODO: This file is a little (!) too large. DocumentWidget could be split into 23/* TODO: Move DocumentView into a source file of its own. Consider cleaning up the network
24 a couple of smaller objects. One for rendering the document, for instance. */ 24 request handling. */
25 25
26#include "documentwidget.h" 26#include "documentwidget.h"
27 27
@@ -41,6 +41,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
41#include "inputwidget.h" 41#include "inputwidget.h"
42#include "keys.h" 42#include "keys.h"
43#include "labelwidget.h" 43#include "labelwidget.h"
44#include "linkinfo.h"
44#include "media.h" 45#include "media.h"
45#include "paint.h" 46#include "paint.h"
46#include "periodic.h" 47#include "periodic.h"
@@ -55,6 +56,9 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
55#include "visbuf.h" 56#include "visbuf.h"
56#include "visited.h" 57#include "visited.h"
57 58
59#if defined (iPlatformAppleDesktop)
60# include "macos.h"
61#endif
58#if defined (iPlatformAppleMobile) 62#if defined (iPlatformAppleMobile)
59# include "ios.h" 63# include "ios.h"
60#endif 64#endif
@@ -161,10 +165,10 @@ enum iDrawBufsFlag {
161}; 165};
162 166
163struct Impl_DrawBufs { 167struct Impl_DrawBufs {
164 int flags; 168 int flags;
165 SDL_Texture * sideIconBuf; 169 SDL_Texture *sideIconBuf;
166 iTextBuf * timestampBuf; 170 iTextBuf *timestampBuf;
167 uint32_t lastRenderTime; 171 uint32_t lastRenderTime;
168}; 172};
169 173
170static void init_DrawBufs(iDrawBufs *d) { 174static void init_DrawBufs(iDrawBufs *d) {
@@ -198,16 +202,6 @@ static void visBufInvalidated_(iVisBuf *d, size_t index) {
198 202
199/*----------------------------------------------------------------------------------------------*/ 203/*----------------------------------------------------------------------------------------------*/
200 204
201static void animate_DocumentWidget_ (void *ticker);
202static void animateMedia_DocumentWidget_ (iDocumentWidget *d);
203static void updateSideIconBuf_DocumentWidget_ (const iDocumentWidget *d);
204static void prerender_DocumentWidget_ (iAny *);
205static void scrollBegan_DocumentWidget_ (iAnyObject *, int, uint32_t);
206
207static const int smoothDuration_DocumentWidget_(enum iScrollType type) {
208 return 600 /* milliseconds */ * scrollSpeedFactor_Prefs(prefs_App(), type);
209}
210
211enum iRequestState { 205enum iRequestState {
212 blank_RequestState, 206 blank_RequestState,
213 fetching_RequestState, 207 fetching_RequestState,
@@ -229,8 +223,14 @@ enum iDocumentWidgetFlag {
229 movingSelectMarkEnd_DocumentWidgetFlag = iBit(11), 223 movingSelectMarkEnd_DocumentWidgetFlag = iBit(11),
230 otherRootByDefault_DocumentWidgetFlag = iBit(12), /* links open to other root by default */ 224 otherRootByDefault_DocumentWidgetFlag = iBit(12), /* links open to other root by default */
231 urlChanged_DocumentWidgetFlag = iBit(13), 225 urlChanged_DocumentWidgetFlag = iBit(13),
232 openedFromSidebar_DocumentWidgetFlag = iBit(14), 226 drawDownloadCounter_DocumentWidgetFlag = iBit(14),
233 drawDownloadCounter_DocumentWidgetFlag = iBit(15), 227 fromCache_DocumentWidgetFlag = iBit(15), /* don't write anything to cache */
228 animationPlaceholder_DocumentWidgetFlag = iBit(16), /* avoid slow operations */
229 invalidationPending_DocumentWidgetFlag = iBit(17), /* invalidate as soon as convenient */
230 leftWheelSwipe_DocumentWidgetFlag = iBit(18), /* swipe state flags are used on desktop */
231 rightWheelSwipe_DocumentWidgetFlag = iBit(19),
232 eitherWheelSwipe_DocumentWidgetFlag = leftWheelSwipe_DocumentWidgetFlag |
233 rightWheelSwipe_DocumentWidgetFlag,
234}; 234};
235 235
236enum iDocumentLinkOrdinalMode { 236enum iDocumentLinkOrdinalMode {
@@ -238,10 +238,44 @@ enum iDocumentLinkOrdinalMode {
238 homeRow_DocumentLinkOrdinalMode, 238 homeRow_DocumentLinkOrdinalMode,
239}; 239};
240 240
241enum iWheelSwipeState {
242 none_WheelSwipeState,
243 direct_WheelSwipeState,
244};
245
246/* TODO: DocumentView is supposed to be useful on its own; move to a separate source file. */
247iDeclareType(DocumentView)
248
249struct Impl_DocumentView {
250 iDocumentWidget *owner; /* TODO: Convert to an abstract provider of metrics? */
251 iGmDocument * doc;
252 int pageMargin;
253 iSmoothScroll scrollY;
254 iAnim sideOpacity;
255 iAnim altTextOpacity;
256 iGmRunRange visibleRuns;
257 iPtrArray visibleLinks;
258 iPtrArray visiblePre;
259 iPtrArray visibleMedia; /* currently playing audio / ongoing downloads */
260 iPtrArray visibleWideRuns; /* scrollable blocks; TODO: merge into `visiblePre` */
261 const iGmRun * hoverPre; /* for clicking */
262 const iGmRun * hoverAltPre; /* for drawing alt text */
263 const iGmRun * hoverLink;
264 iArray wideRunOffsets;
265 iAnim animWideRunOffset;
266 uint16_t animWideRunId;
267 iGmRunRange animWideRunRange;
268 iDrawBufs * drawBufs; /* dynamic state for drawing */
269 iVisBuf * visBuf;
270 iVisBufMeta * visBufMeta;
271 iGmRunRange renderRuns;
272 iPtrSet * invalidRuns;
273};
274
241struct Impl_DocumentWidget { 275struct Impl_DocumentWidget {
242 iWidget widget; 276 iWidget widget;
243 int flags; /* internal behavior, see enum iDocumentWidgetFlag */ 277 int flags; /* internal behavior, see enum iDocumentWidgetFlag */
244 278
245 /* User interface: */ 279 /* User interface: */
246 enum iDocumentLinkOrdinalMode ordinalMode; 280 enum iDocumentLinkOrdinalMode ordinalMode;
247 size_t ordinalBase; 281 size_t ordinalBase;
@@ -251,19 +285,22 @@ struct Impl_DocumentWidget {
251 const iGmRun * grabbedPlayer; /* currently adjusting volume in a player */ 285 const iGmRun * grabbedPlayer; /* currently adjusting volume in a player */
252 float grabbedStartVolume; 286 float grabbedStartVolume;
253 int mediaTimer; 287 int mediaTimer;
254 const iGmRun * hoverPre; /* for clicking */
255 const iGmRun * hoverAltPre; /* for drawing alt text */
256 const iGmRun * hoverLink;
257 const iGmRun * contextLink; 288 const iGmRun * contextLink;
258 iClick click; 289 iClick click;
259 iInt2 contextPos; /* coordinates of latest right click */ 290 iInt2 contextPos; /* coordinates of latest right click */
260 int pinchZoomInitial; 291 int pinchZoomInitial;
261 int pinchZoomPosted; 292 int pinchZoomPosted;
293 float swipeSpeed; /* points/sec */
294 uint32_t lastSwipeTime;
295 int wheelSwipeDistance;
296 enum iWheelSwipeState wheelSwipeState;
262 iString pendingGotoHeading; 297 iString pendingGotoHeading;
263 298 iString linePrecedingLink;
299
264 /* Network request: */ 300 /* Network request: */
265 enum iRequestState state; 301 enum iRequestState state;
266 iGmRequest * request; 302 iGmRequest * request;
303 iGmLinkId requestLinkId; /* ID of the link that initiated the current request */
267 iAtomicInt isRequestUpdated; /* request has new content, need to parse it */ 304 iAtomicInt isRequestUpdated; /* request has new content, need to parse it */
268 int certFlags; 305 int certFlags;
269 iBlock * certFingerprint; 306 iBlock * certFingerprint;
@@ -271,7 +308,7 @@ struct Impl_DocumentWidget {
271 iString * certSubject; 308 iString * certSubject;
272 int redirectCount; 309 int redirectCount;
273 iObjectList * media; /* inline media requests */ 310 iObjectList * media; /* inline media requests */
274 311
275 /* Document: */ 312 /* Document: */
276 iPersistentDocumentState mod; 313 iPersistentDocumentState mod;
277 iString * titleUser; 314 iString * titleUser;
@@ -281,31 +318,14 @@ struct Impl_DocumentWidget {
281 iBlock sourceContent; /* original content as received, for saving; set on request finish */ 318 iBlock sourceContent; /* original content as received, for saving; set on request finish */
282 iTime sourceTime; 319 iTime sourceTime;
283 iGempub * sourceGempub; /* NULL unless the page is Gempub content */ 320 iGempub * sourceGempub; /* NULL unless the page is Gempub content */
284 iGmDocument * doc;
285 iBanner * banner; 321 iBanner * banner;
286
287 /* Rendering: */
288 int pageMargin;
289 float initNormScrollY; 322 float initNormScrollY;
290 iSmoothScroll scrollY; 323
291 iAnim sideOpacity; 324 /* Rendering: */
292 iAnim altTextOpacity; 325 iDocumentView view;
293 iGmRunRange visibleRuns; 326 iLinkInfo * linkInfo;
294 iPtrArray visibleLinks; 327
295 iPtrArray visiblePre; 328 /* Widget structure: */
296 iPtrArray visibleMedia; /* currently playing audio / ongoing downloads */
297 iPtrArray visibleWideRuns; /* scrollable blocks; TODO: merge into `visiblePre` */
298 iArray wideRunOffsets;
299 iAnim animWideRunOffset;
300 uint16_t animWideRunId;
301 iGmRunRange animWideRunRange;
302 iDrawBufs * drawBufs; /* dynamic state for drawing */
303 iVisBuf * visBuf;
304 iVisBufMeta * visBufMeta;
305 iGmRunRange renderRuns;
306 iPtrSet * invalidRuns;
307
308 /* Widget structure: */
309 iScrollWidget *scroll; 329 iScrollWidget *scroll;
310 iWidget * footerButtons; 330 iWidget * footerButtons;
311 iWidget * menu; 331 iWidget * menu;
@@ -317,46 +337,159 @@ struct Impl_DocumentWidget {
317 337
318iDefineObjectConstruction(DocumentWidget) 338iDefineObjectConstruction(DocumentWidget)
319 339
340/* Sorted by proximity to F and J. */
341static const int homeRowKeys_[] = {
342 'f', 'd', 's', 'a',
343 'j', 'k', 'l',
344 'r', 'e', 'w', 'q',
345 'u', 'i', 'o', 'p',
346 'v', 'c', 'x', 'z',
347 'm', 'n',
348 'g', 'h',
349 'b',
350 't', 'y',
351};
320static int docEnum_ = 0; 352static int docEnum_ = 0;
321 353
322void init_DocumentWidget(iDocumentWidget *d) { 354static void animate_DocumentWidget_ (void *ticker);
323 iWidget *w = as_Widget(d); 355static void animateMedia_DocumentWidget_ (iDocumentWidget *d);
324 init_Widget(w); 356static void updateSideIconBuf_DocumentWidget_ (const iDocumentWidget *d);
325 setId_Widget(w, format_CStr("document%03d", ++docEnum_)); 357static void prerender_DocumentWidget_ (iAny *);
326 setFlags_Widget(w, hover_WidgetFlag | noBackground_WidgetFlag, iTrue); 358static void scrollBegan_DocumentWidget_ (iAnyObject *, int, uint32_t);
327 if (deviceType_App() != desktop_AppDeviceType) { 359static void refreshWhileScrolling_DocumentWidget_ (iAny *);
328 setFlags_Widget(w, leftEdgeDraggable_WidgetFlag | rightEdgeDraggable_WidgetFlag | 360
329 horizontalOffset_WidgetFlag, iTrue); 361/* TODO: The following methods are called from DocumentView, which goes the wrong way. */
362
363static iRangecc selectMark_DocumentWidget_(const iDocumentWidget *d) {
364 /* Normalize so start < end. */
365 iRangecc norm = d->selectMark;
366 if (norm.start > norm.end) {
367 iSwap(const char *, norm.start, norm.end);
330 } 368 }
331 init_PersistentDocumentState(&d->mod); 369 return norm;
332 d->flags = 0; 370}
333 d->phoneToolbar = NULL; 371
334 d->footerButtons = NULL; 372static int phoneToolbarHeight_DocumentWidget_(const iDocumentWidget *d) {
335 iZap(d->certExpiry); 373 if (!d->phoneToolbar) {
336 d->certFingerprint = new_Block(0); 374 return 0;
337 d->certFlags = 0; 375 }
338 d->certSubject = new_String(); 376 const iWidget *w = constAs_Widget(d);
339 d->state = blank_RequestState; 377 return bottom_Rect(rect_Root(w->root)) - top_Rect(boundsWithoutVisualOffset_Widget(d->phoneToolbar));
340 d->titleUser = new_String(); 378}
341 d->request = NULL; 379
342 d->isRequestUpdated = iFalse; 380static int footerHeight_DocumentWidget_(const iDocumentWidget *d) {
343 d->media = new_ObjectList(); 381 int hgt = height_Widget(d->footerButtons);
382 if (isPortraitPhone_App()) {
383 hgt += phoneToolbarHeight_DocumentWidget_(d);
384 }
385 return hgt;
386}
387
388static iBool isHoverAllowed_DocumentWidget_(const iDocumentWidget *d) {
389 if (!isHover_Widget(d)) {
390 return iFalse;
391 }
392 if (!(d->state == ready_RequestState || d->state == receivedPartialResponse_RequestState)) {
393 return iFalse;
394 }
395 if (d->flags & (noHoverWhileScrolling_DocumentWidgetFlag |
396 drawDownloadCounter_DocumentWidgetFlag)) {
397 return iFalse;
398 }
399 if (d->flags & pinchZoom_DocumentWidgetFlag) {
400 return iFalse;
401 }
402 if (flags_Widget(constAs_Widget(d)) & touchDrag_WidgetFlag) {
403 return iFalse;
404 }
405 if (flags_Widget(constAs_Widget(d->scroll)) & pressed_WidgetFlag) {
406 return iFalse;
407 }
408 return iTrue;
409}
410
411static iMediaRequest *findMediaRequest_DocumentWidget_(const iDocumentWidget *d, iGmLinkId linkId) {
412 iConstForEach(ObjectList, i, d->media) {
413 const iMediaRequest *req = (const iMediaRequest *) i.object;
414 if (req->linkId == linkId) {
415 return iConstCast(iMediaRequest *, req);
416 }
417 }
418 return NULL;
419}
420
421static size_t linkOrdinalFromKey_DocumentWidget_(const iDocumentWidget *d, int key) {
422 size_t ord = iInvalidPos;
423 if (d->ordinalMode == numbersAndAlphabet_DocumentLinkOrdinalMode) {
424 if (key >= '1' && key <= '9') {
425 return key - '1';
426 }
427 if (key < 'a' || key > 'z') {
428 return iInvalidPos;
429 }
430 ord = key - 'a' + 9;
431#if defined (iPlatformApple)
432 /* Skip keys that would conflict with default system shortcuts: hide, minimize, quit, close. */
433 if (key == 'h' || key == 'm' || key == 'q' || key == 'w') {
434 return iInvalidPos;
435 }
436 if (key > 'h') ord--;
437 if (key > 'm') ord--;
438 if (key > 'q') ord--;
439 if (key > 'w') ord--;
440#endif
441 }
442 else {
443 iForIndices(i, homeRowKeys_) {
444 if (homeRowKeys_[i] == key) {
445 return i;
446 }
447 }
448 }
449 return ord;
450}
451
452static iChar linkOrdinalChar_DocumentWidget_(const iDocumentWidget *d, size_t ord) {
453 if (d->ordinalMode == numbersAndAlphabet_DocumentLinkOrdinalMode) {
454 if (ord < 9) {
455 return '1' + ord;
456 }
457#if defined (iPlatformApple)
458 if (ord < 9 + 22) {
459 int key = 'a' + ord - 9;
460 if (key >= 'h') key++;
461 if (key >= 'm') key++;
462 if (key >= 'q') key++;
463 if (key >= 'w') key++;
464 return 'A' + key - 'a';
465 }
466#else
467 if (ord < 9 + 26) {
468 return 'A' + ord - 9;
469 }
470#endif
471 }
472 else {
473 if (ord < iElemCount(homeRowKeys_)) {
474 return 'A' + homeRowKeys_[ord] - 'a';
475 }
476 }
477 return 0;
478}
479
480/*----------------------------------------------------------------------------------------------*/
481
482void init_DocumentView(iDocumentView *d) {
483 d->owner = NULL;
344 d->doc = new_GmDocument(); 484 d->doc = new_GmDocument();
345 d->banner = new_Banner(); 485 d->invalidRuns = new_PtrSet();
346 setOwner_Banner(d->banner, d); 486 d->drawBufs = new_DrawBufs();
347 d->redirectCount = 0; 487 d->pageMargin = 5;
348 d->ordinalBase = 0; 488 d->hoverPre = NULL;
349 d->initNormScrollY = 0; 489 d->hoverAltPre = NULL;
350 init_SmoothScroll(&d->scrollY, w, scrollBegan_DocumentWidget_); 490 d->hoverLink = NULL;
351 d->animWideRunId = 0; 491 d->animWideRunId = 0;
352 init_Anim(&d->animWideRunOffset, 0); 492 init_Anim(&d->animWideRunOffset, 0);
353 d->selectMark = iNullRange;
354 d->foundMark = iNullRange;
355 d->pageMargin = 5;
356 d->hoverPre = NULL;
357 d->hoverAltPre = NULL;
358 d->hoverLink = NULL;
359 d->contextLink = NULL;
360 iZap(d->renderRuns); 493 iZap(d->renderRuns);
361 iZap(d->visibleRuns); 494 iZap(d->visibleRuns);
362 d->visBuf = new_VisBuf(); { 495 d->visBuf = new_VisBuf(); {
@@ -367,212 +500,105 @@ void init_DocumentWidget(iDocumentWidget *d) {
367 d->visBuf->buffers[i].user = d->visBufMeta + i; 500 d->visBuf->buffers[i].user = d->visBufMeta + i;
368 } 501 }
369 } 502 }
370 d->invalidRuns = new_PtrSet();
371 init_Anim(&d->sideOpacity, 0); 503 init_Anim(&d->sideOpacity, 0);
372 init_Anim(&d->altTextOpacity, 0); 504 init_Anim(&d->altTextOpacity, 0);
373 d->sourceStatus = none_GmStatusCode;
374 init_String(&d->sourceHeader);
375 init_String(&d->sourceMime);
376 init_Block(&d->sourceContent, 0);
377 iZap(d->sourceTime);
378 d->sourceGempub = NULL;
379 init_PtrArray(&d->visibleLinks); 505 init_PtrArray(&d->visibleLinks);
380 init_PtrArray(&d->visiblePre); 506 init_PtrArray(&d->visiblePre);
381 init_PtrArray(&d->visibleWideRuns); 507 init_PtrArray(&d->visibleWideRuns);
382 init_Array(&d->wideRunOffsets, sizeof(int)); 508 init_Array(&d->wideRunOffsets, sizeof(int));
383 init_PtrArray(&d->visibleMedia); 509 init_PtrArray(&d->visibleMedia);
384 d->grabbedPlayer = NULL;
385 d->mediaTimer = 0;
386 init_String(&d->pendingGotoHeading);
387 init_Click(&d->click, d, SDL_BUTTON_LEFT);
388 addChild_Widget(w, iClob(d->scroll = new_ScrollWidget()));
389 d->menu = NULL; /* created when clicking */
390 d->playerMenu = NULL;
391 d->copyMenu = NULL;
392 d->drawBufs = new_DrawBufs();
393 d->translation = NULL;
394 addChildFlags_Widget(w,
395 iClob(new_IndicatorWidget()),
396 resizeToParentWidth_WidgetFlag | resizeToParentHeight_WidgetFlag);
397#if !defined (iPlatformAppleDesktop) /* in system menu */
398 addAction_Widget(w, reload_KeyShortcut, "navigate.reload");
399 addAction_Widget(w, closeTab_KeyShortcut, "tabs.close");
400 addAction_Widget(w, SDLK_d, KMOD_PRIMARY, "bookmark.add");
401 addAction_Widget(w, subscribeToPage_KeyModifier, "feeds.subscribe");
402#endif
403 addAction_Widget(w, navigateBack_KeyShortcut, "navigate.back");
404 addAction_Widget(w, navigateForward_KeyShortcut, "navigate.forward");
405 addAction_Widget(w, navigateParent_KeyShortcut, "navigate.parent");
406 addAction_Widget(w, navigateRoot_KeyShortcut, "navigate.root");
407} 510}
408 511
409void cancelAllRequests_DocumentWidget(iDocumentWidget *d) { 512void deinit_DocumentView(iDocumentView *d) {
410 iForEach(ObjectList, i, d->media) {
411 iMediaRequest *mr = i.object;
412 cancel_GmRequest(mr->req);
413 }
414 if (d->request) {
415 cancel_GmRequest(d->request);
416 }
417}
418
419void deinit_DocumentWidget(iDocumentWidget *d) {
420 cancelAllRequests_DocumentWidget(d);
421 pauseAllPlayers_Media(media_GmDocument(d->doc), iTrue);
422 removeTicker_App(animate_DocumentWidget_, d);
423 removeTicker_App(prerender_DocumentWidget_, d);
424 remove_Periodic(periodic_App(), d);
425 delete_Translation(d->translation);
426 delete_DrawBufs(d->drawBufs); 513 delete_DrawBufs(d->drawBufs);
427 delete_VisBuf(d->visBuf); 514 delete_VisBuf(d->visBuf);
428 free(d->visBufMeta); 515 free(d->visBufMeta);
429 delete_PtrSet(d->invalidRuns); 516 delete_PtrSet(d->invalidRuns);
430 iRelease(d->media);
431 iRelease(d->request);
432 delete_Gempub(d->sourceGempub);
433 deinit_String(&d->pendingGotoHeading);
434 deinit_Block(&d->sourceContent);
435 deinit_String(&d->sourceMime);
436 deinit_String(&d->sourceHeader);
437 delete_Banner(d->banner);
438 iRelease(d->doc);
439 if (d->mediaTimer) {
440 SDL_RemoveTimer(d->mediaTimer);
441 }
442 deinit_Array(&d->wideRunOffsets); 517 deinit_Array(&d->wideRunOffsets);
443 deinit_PtrArray(&d->visibleMedia); 518 deinit_PtrArray(&d->visibleMedia);
444 deinit_PtrArray(&d->visibleWideRuns); 519 deinit_PtrArray(&d->visibleWideRuns);
445 deinit_PtrArray(&d->visiblePre); 520 deinit_PtrArray(&d->visiblePre);
446 deinit_PtrArray(&d->visibleLinks); 521 deinit_PtrArray(&d->visibleLinks);
447 delete_Block(d->certFingerprint); 522 iReleasePtr(&d->doc);
448 delete_String(d->certSubject);
449 delete_String(d->titleUser);
450 deinit_PersistentDocumentState(&d->mod);
451}
452
453static iRangecc selectMark_DocumentWidget_(const iDocumentWidget *d) {
454 /* Normalize so start < end. */
455 iRangecc norm = d->selectMark;
456 if (norm.start > norm.end) {
457 iSwap(const char *, norm.start, norm.end);
458 }
459 return norm;
460} 523}
461 524
462static void enableActions_DocumentWidget_(iDocumentWidget *d, iBool enable) { 525static void setOwner_DocumentView_(iDocumentView *d, iDocumentWidget *doc) {
463 /* Actions are invisible child widgets of the DocumentWidget. */ 526 d->owner = doc;
464 iForEach(ObjectList, i, children_Widget(d)) { 527 init_SmoothScroll(&d->scrollY, as_Widget(doc), scrollBegan_DocumentWidget_);
465 if (isAction_Widget(i.object)) { 528 if (deviceType_App() != desktop_AppDeviceType) {
466 setFlags_Widget(i.object, disabled_WidgetFlag, !enable); 529 d->scrollY.flags |= pullDownAction_SmoothScrollFlag; /* pull to refresh */
467 }
468 }
469}
470
471static void setLinkNumberMode_DocumentWidget_(iDocumentWidget *d, iBool set) {
472 iChangeFlags(d->flags, showLinkNumbers_DocumentWidgetFlag, set);
473 /* Children have priority when handling events. */
474 enableActions_DocumentWidget_(d, !set);
475 if (d->menu) {
476 setFlags_Widget(d->menu, disabled_WidgetFlag, set);
477 } 530 }
478} 531}
479 532
480static void resetWideRuns_DocumentWidget_(iDocumentWidget *d) { 533static void resetWideRuns_DocumentView_(iDocumentView *d) {
481 clear_Array(&d->wideRunOffsets); 534 clear_Array(&d->wideRunOffsets);
482 d->animWideRunId = 0; 535 d->animWideRunId = 0;
483 init_Anim(&d->animWideRunOffset, 0); 536 init_Anim(&d->animWideRunOffset, 0);
484 iZap(d->animWideRunRange); 537 iZap(d->animWideRunRange);
485} 538}
486 539
487static void requestUpdated_DocumentWidget_(iAnyObject *obj) { 540static int documentWidth_DocumentView_(const iDocumentView *d) {
488 iDocumentWidget *d = obj; 541 const iWidget *w = constAs_Widget(d->owner);
489 const int wasUpdated = exchange_Atomic(&d->isRequestUpdated, iTrue);
490 if (!wasUpdated) {
491 postCommand_Widget(obj,
492 "document.request.updated doc:%p reqid:%u request:%p",
493 d,
494 id_GmRequest(d->request),
495 d->request);
496 }
497}
498
499static void requestFinished_DocumentWidget_(iAnyObject *obj) {
500 iDocumentWidget *d = obj;
501 postCommand_Widget(obj,
502 "document.request.finished doc:%p reqid:%u request:%p",
503 d,
504 id_GmRequest(d->request),
505 d->request);
506}
507
508static int documentWidth_DocumentWidget_(const iDocumentWidget *d) {
509 const iWidget *w = constAs_Widget(d);
510 const iRect bounds = bounds_Widget(w); 542 const iRect bounds = bounds_Widget(w);
511 const iPrefs * prefs = prefs_App(); 543 const iPrefs * prefs = prefs_App();
512 const int minWidth = 50 * gap_UI; /* lines must fit a word at least */ 544 const int minWidth = 50 * gap_UI; /* lines must fit a word at least */
513 const float adjust = iClamp((float) bounds.size.x / gap_UI / 11 - 12, 545 const float adjust = iClamp((float) bounds.size.x / gap_UI / 11 - 12,
514 -1.0f, 10.0f); /* adapt to width */ 546 -1.0f, 10.0f); /* adapt to width */
515 //printf("%f\n", adjust); fflush(stdout); 547 //printf("%f\n", adjust); fflush(stdout);
516 return iMini(iMax(minWidth, bounds.size.x - gap_UI * (d->pageMargin + adjust) * 2), 548 return iMini(iMax(minWidth, bounds.size.x - gap_UI * (d->pageMargin + adjust) * 2),
517 fontSize_UI * //emRatio_Text(paragraph_FontId) * /* dependent on avg. glyph width */ 549 fontSize_UI * //emRatio_Text(paragraph_FontId) * /* dependent on avg. glyph width */
518 prefs->lineWidth * prefs->zoomPercent / 100); 550 prefs->lineWidth * prefs->zoomPercent / 100);
519} 551}
520 552
521static int documentTopPad_DocumentWidget_(const iDocumentWidget *d) { 553static int documentTopPad_DocumentView_(const iDocumentView *d) {
522 /* Amount of space between banner and top of the document. */ 554 /* Amount of space between banner and top of the document. */
523 return isEmpty_Banner(d->banner) ? 0 : lineHeight_Text(paragraph_FontId); 555 return isEmpty_Banner(d->owner->banner) ? 0 : lineHeight_Text(paragraph_FontId);
524}
525
526static int documentTopMargin_DocumentWidget_(const iDocumentWidget *d) {
527 return (isEmpty_Banner(d->banner) ? d->pageMargin * gap_UI : height_Banner(d->banner)) +
528 documentTopPad_DocumentWidget_(d);
529} 556}
530 557
531static int pageHeight_DocumentWidget_(const iDocumentWidget *d) { 558static int documentTopMargin_DocumentView_(const iDocumentView *d) {
532 return height_Banner(d->banner) + documentTopPad_DocumentWidget_(d) + size_GmDocument(d->doc).y; 559 return (isEmpty_Banner(d->owner->banner) ? d->pageMargin * gap_UI : height_Banner(d->owner->banner)) +
560 documentTopPad_DocumentView_(d);
533} 561}
534 562
535static int footerButtonsHeight_DocumentWidget_(const iDocumentWidget *d) { 563static int pageHeight_DocumentView_(const iDocumentView *d) {
536 int height = height_Widget(d->footerButtons); 564 return height_Banner(d->owner->banner) + documentTopPad_DocumentView_(d) + size_GmDocument(d->doc).y;
537// if (height) {
538// height += 3 * gap_UI; /* padding */
539// }
540 return height;
541} 565}
542 566
543static iRect documentBounds_DocumentWidget_(const iDocumentWidget *d) { 567static iRect documentBounds_DocumentView_(const iDocumentView *d) {
544 const iRect bounds = bounds_Widget(constAs_Widget(d)); 568 const iRect bounds = bounds_Widget(constAs_Widget(d->owner));
545 const int margin = gap_UI * d->pageMargin; 569 const int margin = gap_UI * d->pageMargin;
546 iRect rect; 570 iRect rect;
547 rect.size.x = documentWidth_DocumentWidget_(d); 571 rect.size.x = documentWidth_DocumentView_(d);
548 rect.pos.x = mid_Rect(bounds).x - rect.size.x / 2; 572 rect.pos.x = mid_Rect(bounds).x - rect.size.x / 2;
549 rect.pos.y = top_Rect(bounds) + margin; 573 rect.pos.y = top_Rect(bounds) + margin;
550 rect.size.y = height_Rect(bounds) - margin; 574 rect.size.y = height_Rect(bounds) - margin;
551 iBool wasCentered = iFalse; 575 iBool wasCentered = iFalse;
552 if (d->flags & centerVertically_DocumentWidgetFlag) { 576 /* TODO: Further separation of View and Widget: configure header and footer heights
577 without involving the widget here. */
578 if (d->owner->flags & centerVertically_DocumentWidgetFlag) {
553 const int docSize = size_GmDocument(d->doc).y + 579 const int docSize = size_GmDocument(d->doc).y +
554 documentTopMargin_DocumentWidget_(d); 580 documentTopMargin_DocumentView_(d);
555 if (size_GmDocument(d->doc).y == 0) { 581 if (size_GmDocument(d->doc).y == 0) {
556 /* Document is empty; maybe just showing an error banner. */ 582 /* Document is empty; maybe just showing an error banner. */
557 rect.pos.y = top_Rect(bounds) + height_Rect(bounds) / 2 - 583 rect.pos.y = top_Rect(bounds) + height_Rect(bounds) / 2 -
558 documentTopPad_DocumentWidget_(d) - height_Banner(d->banner) / 2; 584 documentTopPad_DocumentView_(d) - height_Banner(d->owner->banner) / 2;
559 rect.size.y = 0; 585 rect.size.y = 0;
560 wasCentered = iTrue; 586 wasCentered = iTrue;
561 } 587 }
562 else if (docSize < rect.size.y - footerButtonsHeight_DocumentWidget_(d)) { 588 else if (docSize < rect.size.y - footerHeight_DocumentWidget_(d->owner)) {
563 /* TODO: Phone toolbar? */ 589 /* TODO: Phone toolbar? */
564 /* Center vertically when the document is short. */ 590 /* Center vertically when the document is short. */
565 const int relMidY = (height_Rect(bounds) - footerButtonsHeight_DocumentWidget_(d)) / 2; 591 const int relMidY = (height_Rect(bounds) - footerHeight_DocumentWidget_(d->owner)) / 2;
566 const int visHeight = size_GmDocument(d->doc).y; 592 const int visHeight = size_GmDocument(d->doc).y;
567 const int offset = -height_Banner(d->banner) - documentTopPad_DocumentWidget_(d); 593 const int offset = -height_Banner(d->owner->banner) - documentTopPad_DocumentView_(d);
568 rect.pos.y = top_Rect(bounds) + iMaxi(0, relMidY - visHeight / 2 + offset); 594 rect.pos.y = top_Rect(bounds) + iMaxi(0, relMidY - visHeight / 2 + offset);
569 rect.size.y = size_GmDocument(d->doc).y + documentTopMargin_DocumentWidget_(d); 595 rect.size.y = size_GmDocument(d->doc).y + documentTopMargin_DocumentView_(d);
570 wasCentered = iTrue; 596 wasCentered = iTrue;
571 } 597 }
572 } 598 }
573 if (!wasCentered) { 599 if (!wasCentered) {
574 /* The banner overtakes the top margin. */ 600 /* The banner overtakes the top margin. */
575 if (!isEmpty_Banner(d->banner)) { 601 if (!isEmpty_Banner(d->owner->banner)) {
576 rect.pos.y -= margin; 602 rect.pos.y -= margin;
577 } 603 }
578 else { 604 else {
@@ -582,38 +608,28 @@ static iRect documentBounds_DocumentWidget_(const iDocumentWidget *d) {
582 return rect; 608 return rect;
583} 609}
584 610
585static int viewPos_DocumentWidget_(const iDocumentWidget *d) { 611static int viewPos_DocumentView_(const iDocumentView *d) {
586 return height_Banner(d->banner) + documentTopPad_DocumentWidget_(d) - pos_SmoothScroll(&d->scrollY); 612 return height_Banner(d->owner->banner) + documentTopPad_DocumentView_(d) -
587} 613 pos_SmoothScroll(&d->scrollY);
588
589#if 0
590static iRect siteBannerRect_DocumentWidget_(const iDocumentWidget *d) {
591 const iGmRun *banner = siteBanner_GmDocument(d->doc);
592 if (!banner) {
593 return zero_Rect();
594 }
595 const iRect docBounds = documentBounds_DocumentWidget_(d);
596 const iInt2 origin = addY_I2(topLeft_Rect(docBounds), -pos_SmoothScroll(&d->scrollY));
597 return moved_Rect(banner->visBounds, origin);
598} 614}
599#endif
600 615
601static iInt2 documentPos_DocumentWidget_(const iDocumentWidget *d, iInt2 pos) { 616static iInt2 documentPos_DocumentView_(const iDocumentView *d, iInt2 pos) {
602 return addY_I2(sub_I2(pos, topLeft_Rect(documentBounds_DocumentWidget_(d))), 617 return addY_I2(sub_I2(pos, topLeft_Rect(documentBounds_DocumentView_(d))),
603 -viewPos_DocumentWidget_(d)); 618 -viewPos_DocumentView_(d));
604} 619}
605 620
606static iRangei visibleRange_DocumentWidget_(const iDocumentWidget *d) { 621static iRangei visibleRange_DocumentView_(const iDocumentView *d) {
607 int top = pos_SmoothScroll(&d->scrollY) - height_Banner(d->banner) - documentTopPad_DocumentWidget_(d); 622 int top = pos_SmoothScroll(&d->scrollY) - height_Banner(d->owner->banner) -
608 if (isEmpty_Banner(d->banner)) { 623 documentTopPad_DocumentView_(d);
624 if (isEmpty_Banner(d->owner->banner)) {
609 /* Top padding is not collapsed. */ 625 /* Top padding is not collapsed. */
610 top -= d->pageMargin * gap_UI; 626 top -= d->pageMargin * gap_UI;
611 } 627 }
612 return (iRangei){ top, top + height_Rect(bounds_Widget(constAs_Widget(d))) }; 628 return (iRangei){ top, top + height_Rect(bounds_Widget(constAs_Widget(d->owner))) };
613} 629}
614 630
615static void addVisible_DocumentWidget_(void *context, const iGmRun *run) { 631static void addVisible_DocumentView_(void *context, const iGmRun *run) {
616 iDocumentWidget *d = context; 632 iDocumentView *d = context;
617 if (~run->flags & decoration_GmRunFlag && !run->mediaId) { 633 if (~run->flags & decoration_GmRunFlag && !run->mediaId) {
618 if (!d->visibleRuns.start) { 634 if (!d->visibleRuns.start) {
619 d->visibleRuns.start = run; 635 d->visibleRuns.start = run;
@@ -636,7 +652,7 @@ static void addVisible_DocumentWidget_(void *context, const iGmRun *run) {
636 } 652 }
637} 653}
638 654
639static const iGmRun *lastVisibleLink_DocumentWidget_(const iDocumentWidget *d) { 655static const iGmRun *lastVisibleLink_DocumentView_(const iDocumentView *d) {
640 iReverseConstForEach(PtrArray, i, &d->visibleLinks) { 656 iReverseConstForEach(PtrArray, i, &d->visibleLinks) {
641 const iGmRun *run = i.ptr; 657 const iGmRun *run = i.ptr;
642 if (run->flags & decoration_GmRunFlag && run->linkId) { 658 if (run->flags & decoration_GmRunFlag && run->linkId) {
@@ -646,23 +662,24 @@ static const iGmRun *lastVisibleLink_DocumentWidget_(const iDocumentWidget *d) {
646 return NULL; 662 return NULL;
647} 663}
648 664
649static float normScrollPos_DocumentWidget_(const iDocumentWidget *d) { 665static float normScrollPos_DocumentView_(const iDocumentView *d) {
650 const int docSize = pageHeight_DocumentWidget_(d); // size_GmDocument(d->doc).y; 666 const int docSize = pageHeight_DocumentView_(d);
651 if (docSize) { 667 if (docSize) {
652 return pos_SmoothScroll(&d->scrollY) / (float) docSize; 668 float pos = pos_SmoothScroll(&d->scrollY) / (float) docSize;
669 return iMax(pos, 0.0f);
653 } 670 }
654 return 0; 671 return 0;
655} 672}
656 673
657static int scrollMax_DocumentWidget_(const iDocumentWidget *d) { 674static int scrollMax_DocumentView_(const iDocumentView *d) {
658 const iWidget *w = constAs_Widget(d); 675 const iWidget *w = constAs_Widget(d->owner);
659 int sm = pageHeight_DocumentWidget_(d) - height_Rect(bounds_Widget(w)) + 676 int sm = pageHeight_DocumentView_(d) +
660 (isEmpty_Banner(d->banner) ? 2 : 1) * d->pageMargin * gap_UI + /* top and bottom margins */ 677 (isEmpty_Banner(d->owner->banner) ? 2 : 1) * d->pageMargin * gap_UI + /* top and bottom margins */
661 iMax(height_Widget(d->phoneToolbar), height_Widget(d->footerButtons)); 678 footerHeight_DocumentWidget_(d->owner) - height_Rect(bounds_Widget(w));
662 return sm; 679 return sm;
663} 680}
664 681
665static void invalidateLink_DocumentWidget_(iDocumentWidget *d, iGmLinkId id) { 682static void invalidateLink_DocumentView_(iDocumentView *d, iGmLinkId id) {
666 /* A link has multiple runs associated with it. */ 683 /* A link has multiple runs associated with it. */
667 iConstForEach(PtrArray, i, &d->visibleLinks) { 684 iConstForEach(PtrArray, i, &d->visibleLinks) {
668 const iGmRun *run = i.ptr; 685 const iGmRun *run = i.ptr;
@@ -672,7 +689,7 @@ static void invalidateLink_DocumentWidget_(iDocumentWidget *d, iGmLinkId id) {
672 } 689 }
673} 690}
674 691
675static void invalidateVisibleLinks_DocumentWidget_(iDocumentWidget *d) { 692static void invalidateVisibleLinks_DocumentView_(iDocumentView *d) {
676 iConstForEach(PtrArray, i, &d->visibleLinks) { 693 iConstForEach(PtrArray, i, &d->visibleLinks) {
677 const iGmRun *run = i.ptr; 694 const iGmRun *run = i.ptr;
678 if (run->linkId) { 695 if (run->linkId) {
@@ -681,7 +698,7 @@ static void invalidateVisibleLinks_DocumentWidget_(iDocumentWidget *d) {
681 } 698 }
682} 699}
683 700
684static int runOffset_DocumentWidget_(const iDocumentWidget *d, const iGmRun *run) { 701static int runOffset_DocumentView_(const iDocumentView *d, const iGmRun *run) {
685 if (preId_GmRun(run) && run->flags & wide_GmRunFlag) { 702 if (preId_GmRun(run) && run->flags & wide_GmRunFlag) {
686 if (d->animWideRunId == preId_GmRun(run)) { 703 if (d->animWideRunId == preId_GmRun(run)) {
687 return -value_Anim(&d->animWideRunOffset); 704 return -value_Anim(&d->animWideRunOffset);
@@ -695,56 +712,24 @@ static int runOffset_DocumentWidget_(const iDocumentWidget *d, const iGmRun *run
695 return 0; 712 return 0;
696} 713}
697 714
698static void invalidateWideRunsWithNonzeroOffset_DocumentWidget_(iDocumentWidget *d) { 715static void invalidateWideRunsWithNonzeroOffset_DocumentView_(iDocumentView *d) {
699 iConstForEach(PtrArray, i, &d->visibleWideRuns) { 716 iConstForEach(PtrArray, i, &d->visibleWideRuns) {
700 const iGmRun *run = i.ptr; 717 const iGmRun *run = i.ptr;
701 if (runOffset_DocumentWidget_(d, run)) { 718 if (runOffset_DocumentView_(d, run)) {
702 insert_PtrSet(d->invalidRuns, run); 719 insert_PtrSet(d->invalidRuns, run);
703 } 720 }
704 } 721 }
705} 722}
706 723
707static void updateHover_DocumentWidget_(iDocumentWidget *d, iInt2 mouse); 724static void updateHover_DocumentView_(iDocumentView *d, iInt2 mouse) {
708 725 const iWidget *w = constAs_Widget(d->owner);
709static void animate_DocumentWidget_(void *ticker) { 726 const iRect docBounds = documentBounds_DocumentView_(d);
710 iDocumentWidget *d = ticker;
711 refresh_Widget(d);
712 if (!isFinished_Anim(&d->sideOpacity) || !isFinished_Anim(&d->altTextOpacity)) {
713 addTicker_App(animate_DocumentWidget_, d);
714 }
715}
716
717static iBool isHoverAllowed_DocumentWidget_(const iDocumentWidget *d) {
718 if (!isHover_Widget(d)) {
719 return iFalse;
720 }
721 if (!(d->state == ready_RequestState || d->state == receivedPartialResponse_RequestState)) {
722 return iFalse;
723 }
724 if (d->flags & noHoverWhileScrolling_DocumentWidgetFlag) {
725 return iFalse;
726 }
727 if (d->flags & pinchZoom_DocumentWidgetFlag) {
728 return iFalse;
729 }
730 if (flags_Widget(constAs_Widget(d)) & touchDrag_WidgetFlag) {
731 return iFalse;
732 }
733 if (flags_Widget(constAs_Widget(d->scroll)) & pressed_WidgetFlag) {
734 return iFalse;
735 }
736 return iTrue;
737}
738
739static void updateHover_DocumentWidget_(iDocumentWidget *d, iInt2 mouse) {
740 const iWidget *w = constAs_Widget(d);
741 const iRect docBounds = documentBounds_DocumentWidget_(d);
742 const iGmRun * oldHoverLink = d->hoverLink; 727 const iGmRun * oldHoverLink = d->hoverLink;
743 d->hoverPre = NULL; 728 d->hoverPre = NULL;
744 d->hoverLink = NULL; 729 d->hoverLink = NULL;
745 const iInt2 hoverPos = addY_I2(sub_I2(mouse, topLeft_Rect(docBounds)), 730 const iInt2 hoverPos = addY_I2(sub_I2(mouse, topLeft_Rect(docBounds)),
746 -viewPos_DocumentWidget_(d)); 731 -viewPos_DocumentView_(d));
747 if (isHoverAllowed_DocumentWidget_(d)) { 732 if (isHoverAllowed_DocumentWidget_(d->owner)) {
748 iConstForEach(PtrArray, i, &d->visibleLinks) { 733 iConstForEach(PtrArray, i, &d->visibleLinks) {
749 const iGmRun *run = i.ptr; 734 const iGmRun *run = i.ptr;
750 /* Click targets are slightly expanded so there are no gaps between links. */ 735 /* Click targets are slightly expanded so there are no gaps between links. */
@@ -756,15 +741,21 @@ static void updateHover_DocumentWidget_(iDocumentWidget *d, iInt2 mouse) {
756 } 741 }
757 if (d->hoverLink != oldHoverLink) { 742 if (d->hoverLink != oldHoverLink) {
758 if (oldHoverLink) { 743 if (oldHoverLink) {
759 invalidateLink_DocumentWidget_(d, oldHoverLink->linkId); 744 invalidateLink_DocumentView_(d, oldHoverLink->linkId);
760 } 745 }
761 if (d->hoverLink) { 746 if (d->hoverLink) {
762 invalidateLink_DocumentWidget_(d, d->hoverLink->linkId); 747 invalidateLink_DocumentView_(d, d->hoverLink->linkId);
748 }
749 if (update_LinkInfo(d->owner->linkInfo,
750 d->doc,
751 d->hoverLink ? d->hoverLink->linkId : 0,
752 width_Widget(w))) {
753 animate_DocumentWidget_(d->owner);
763 } 754 }
764 refresh_Widget(w); 755 refresh_Widget(w);
765 } 756 }
766 /* Hovering over preformatted blocks. */ 757 /* Hovering over preformatted blocks. */
767 if (isHoverAllowed_DocumentWidget_(d)) { 758 if (isHoverAllowed_DocumentWidget_(d->owner)) {
768 iConstForEach(PtrArray, j, &d->visiblePre) { 759 iConstForEach(PtrArray, j, &d->visiblePre) {
769 const iGmRun *run = j.ptr; 760 const iGmRun *run = j.ptr;
770 if (contains_Rect(run->bounds, hoverPos)) { 761 if (contains_Rect(run->bounds, hoverPos)) {
@@ -777,18 +768,18 @@ static void updateHover_DocumentWidget_(iDocumentWidget *d, iInt2 mouse) {
777 if (!d->hoverPre) { 768 if (!d->hoverPre) {
778 setValueSpeed_Anim(&d->altTextOpacity, 0.0f, 1.5f); 769 setValueSpeed_Anim(&d->altTextOpacity, 0.0f, 1.5f);
779 if (!isFinished_Anim(&d->altTextOpacity)) { 770 if (!isFinished_Anim(&d->altTextOpacity)) {
780 animate_DocumentWidget_(d); 771 animate_DocumentWidget_(d->owner);
781 } 772 }
782 } 773 }
783 else if (d->hoverPre && 774 else if (d->hoverPre &&
784 preHasAltText_GmDocument(d->doc, preId_GmRun(d->hoverPre)) && 775 preHasAltText_GmDocument(d->doc, preId_GmRun(d->hoverPre)) &&
785 ~d->flags & noHoverWhileScrolling_DocumentWidgetFlag) { 776 ~d->owner->flags & noHoverWhileScrolling_DocumentWidgetFlag) {
786 setValueSpeed_Anim(&d->altTextOpacity, 1.0f, 1.5f); 777 setValueSpeed_Anim(&d->altTextOpacity, 1.0f, 1.5f);
787 if (!isFinished_Anim(&d->altTextOpacity)) { 778 if (!isFinished_Anim(&d->altTextOpacity)) {
788 animate_DocumentWidget_(d); 779 animate_DocumentWidget_(d->owner);
789 } 780 }
790 } 781 }
791 if (isHover_Widget(w) && !contains_Widget(constAs_Widget(d->scroll), mouse)) { 782 if (isHover_Widget(w) && !contains_Widget(constAs_Widget(d->owner->scroll), mouse)) {
792 setCursor_Window(get_Window(), 783 setCursor_Window(get_Window(),
793 d->hoverLink || d->hoverPre ? SDL_SYSTEM_CURSOR_HAND 784 d->hoverLink || d->hoverPre ? SDL_SYSTEM_CURSOR_HAND
794 : SDL_SYSTEM_CURSOR_IBEAM); 785 : SDL_SYSTEM_CURSOR_IBEAM);
@@ -799,15 +790,1198 @@ static void updateHover_DocumentWidget_(iDocumentWidget *d, iInt2 mouse) {
799 } 790 }
800} 791}
801 792
802static void updateSideOpacity_DocumentWidget_(iDocumentWidget *d, iBool isAnimated) { 793static void updateSideOpacity_DocumentView_(iDocumentView *d, iBool isAnimated) {
803 float opacity = 0.0f; 794 float opacity = 0.0f;
804// const iGmRun *banner = siteBanner_GmDocument(d->doc); 795 if (!isEmpty_Banner(d->owner->banner) &&
805 if (!isEmpty_Banner(d->banner) && height_Banner(d->banner) < pos_SmoothScroll(&d->scrollY)) { 796 height_Banner(d->owner->banner) < pos_SmoothScroll(&d->scrollY)) {
806// if (banner && bottom_Rect(banner->visBounds) < pos_SmoothScroll(&d->scrollY)) {
807 opacity = 1.0f; 797 opacity = 1.0f;
808 } 798 }
809 setValue_Anim(&d->sideOpacity, opacity, isAnimated ? (opacity < 0.5f ? 100 : 200) : 0); 799 setValue_Anim(&d->sideOpacity, opacity, isAnimated ? (opacity < 0.5f ? 100 : 200) : 0);
810 animate_DocumentWidget_(d); 800 animate_DocumentWidget_(d->owner);
801}
802
803static iRangecc currentHeading_DocumentView_(const iDocumentView *d) {
804 iRangecc heading = iNullRange;
805 if (d->visibleRuns.start) {
806 iConstForEach(Array, i, headings_GmDocument(d->doc)) {
807 const iGmHeading *head = i.value;
808 if (head->level == 0) {
809 if (head->text.start <= d->visibleRuns.start->text.start) {
810 heading = head->text;
811 }
812 if (d->visibleRuns.end && head->text.start > d->visibleRuns.end->text.start) {
813 break;
814 }
815 }
816 }
817 }
818 return heading;
819}
820
821static int updateScrollMax_DocumentView_(iDocumentView *d) {
822 arrange_Widget(d->owner->footerButtons); /* scrollMax depends on footer height */
823 const int scrollMax = scrollMax_DocumentView_(d);
824 setMax_SmoothScroll(&d->scrollY, scrollMax);
825 return scrollMax;
826}
827
828static void updateVisible_DocumentView_(iDocumentView *d) {
829 /* TODO: The concerns of Widget and View are too tangled together here. */
830 iChangeFlags(d->owner->flags,
831 centerVertically_DocumentWidgetFlag,
832 prefs_App()->centerShortDocs || startsWithCase_String(d->owner->mod.url, "about:") ||
833 !isSuccess_GmStatusCode(d->owner->sourceStatus));
834 iScrollWidget *scrollBar = d->owner->scroll;
835 const iRangei visRange = visibleRange_DocumentView_(d);
836 // printf("visRange: %d...%d\n", visRange.start, visRange.end);
837 const iRect bounds = bounds_Widget(as_Widget(d->owner));
838 const int scrollMax = updateScrollMax_DocumentView_(d);
839 /* Reposition the footer buttons as appropriate. */
840 setRange_ScrollWidget(scrollBar, (iRangei){ 0, scrollMax });
841 const int docSize = pageHeight_DocumentView_(d) + footerHeight_DocumentWidget_(d->owner);
842 const float scrollPos = pos_SmoothScroll(&d->scrollY);
843 setThumb_ScrollWidget(scrollBar,
844 pos_SmoothScroll(&d->scrollY),
845 docSize > 0 ? height_Rect(bounds) * size_Range(&visRange) / docSize : 0);
846 if (d->owner->footerButtons) {
847 const iRect bounds = bounds_Widget(as_Widget(d->owner));
848 const iRect docBounds = documentBounds_DocumentView_(d);
849 const int hPad = (width_Rect(bounds) - iMin(120 * gap_UI, width_Rect(docBounds))) / 2;
850 const int vPad = 3 * gap_UI;
851 setPadding_Widget(d->owner->footerButtons, hPad, 0, hPad, vPad);
852 d->owner->footerButtons->rect.pos.y = height_Rect(bounds) -
853 footerHeight_DocumentWidget_(d->owner) +
854 (scrollMax > 0 ? scrollMax - scrollPos : 0);
855 }
856 clear_PtrArray(&d->visibleLinks);
857 clear_PtrArray(&d->visibleWideRuns);
858 clear_PtrArray(&d->visiblePre);
859 clear_PtrArray(&d->visibleMedia);
860 const iRangecc oldHeading = currentHeading_DocumentView_(d);
861 /* Scan for visible runs. */ {
862 iZap(d->visibleRuns);
863 render_GmDocument(d->doc, visRange, addVisible_DocumentView_, d);
864 }
865 const iRangecc newHeading = currentHeading_DocumentView_(d);
866 if (memcmp(&oldHeading, &newHeading, sizeof(oldHeading))) {
867 d->drawBufs->flags |= updateSideBuf_DrawBufsFlag;
868 }
869 updateHover_DocumentView_(d, mouseCoord_Window(get_Window(), 0));
870 updateSideOpacity_DocumentView_(d, iTrue);
871 animateMedia_DocumentWidget_(d->owner);
872 /* Remember scroll positions of recently visited pages. */ {
873 iRecentUrl *recent = mostRecentUrl_History(d->owner->mod.history);
874 if (recent && docSize && d->owner->state == ready_RequestState &&
875 equal_String(&recent->url, d->owner->mod.url)) {
876 recent->normScrollY = normScrollPos_DocumentView_(d);
877 }
878 }
879 /* After scrolling/resizing stops, begin pre-rendering the visbuf contents. */ {
880 removeTicker_App(prerender_DocumentWidget_, d->owner);
881 remove_Periodic(periodic_App(), d);
882 add_Periodic(periodic_App(), d->owner, "document.render");
883 }
884}
885
886static void swap_DocumentView_(iDocumentView *d, iDocumentView *swapBuffersWith) {
887 d->scrollY = swapBuffersWith->scrollY;
888 d->scrollY.widget = as_Widget(d->owner);
889 iSwap(iVisBuf *, d->visBuf, swapBuffersWith->visBuf);
890 iSwap(iVisBufMeta *, d->visBufMeta, swapBuffersWith->visBufMeta);
891 iSwap(iDrawBufs *, d->drawBufs, swapBuffersWith->drawBufs);
892 updateVisible_DocumentView_(d);
893 updateVisible_DocumentView_(swapBuffersWith);
894}
895
896static void updateTimestampBuf_DocumentView_(const iDocumentView *d) {
897 if (!isExposed_Window(get_Window())) {
898 return;
899 }
900 if (d->drawBufs->timestampBuf) {
901 delete_TextBuf(d->drawBufs->timestampBuf);
902 d->drawBufs->timestampBuf = NULL;
903 }
904 if (isValid_Time(&d->owner->sourceTime)) {
905 iString *fmt = timeFormatHourPreference_Lang("page.timestamp");
906 d->drawBufs->timestampBuf = newRange_TextBuf(
907 uiLabel_FontId,
908 white_ColorId,
909 range_String(collect_String(format_Time(&d->owner->sourceTime, cstr_String(fmt)))));
910 delete_String(fmt);
911 }
912 d->drawBufs->flags &= ~updateTimestampBuf_DrawBufsFlag;
913}
914
915static void invalidate_DocumentView_(iDocumentView *d) {
916 invalidate_VisBuf(d->visBuf);
917 clear_PtrSet(d->invalidRuns);
918}
919
920static void documentRunsInvalidated_DocumentView_(iDocumentView *d) {
921 d->hoverPre = NULL;
922 d->hoverAltPre = NULL;
923 d->hoverLink = NULL;
924 iZap(d->visibleRuns);
925 iZap(d->renderRuns);
926}
927
928static void resetScroll_DocumentView_(iDocumentView *d) {
929 reset_SmoothScroll(&d->scrollY);
930 init_Anim(&d->sideOpacity, 0);
931 init_Anim(&d->altTextOpacity, 0);
932 resetWideRuns_DocumentView_(d);
933}
934
935static void updateWidth_DocumentView_(iDocumentView *d) {
936 updateWidth_GmDocument(d->doc, documentWidth_DocumentView_(d), width_Widget(d->owner));
937}
938
939static void updateWidthAndRedoLayout_DocumentView_(iDocumentView *d) {
940 setWidth_GmDocument(d->doc, documentWidth_DocumentView_(d), width_Widget(d->owner));
941}
942
943static void clampScroll_DocumentView_(iDocumentView *d) {
944 move_SmoothScroll(&d->scrollY, 0);
945}
946
947static void immediateScroll_DocumentView_(iDocumentView *d, int offset) {
948 move_SmoothScroll(&d->scrollY, offset);
949}
950
951static void smoothScroll_DocumentView_(iDocumentView *d, int offset, int duration) {
952 moveSpan_SmoothScroll(&d->scrollY, offset, duration);
953}
954
955static void scrollTo_DocumentView_(iDocumentView *d, int documentY, iBool centered) {
956 if (!isEmpty_Banner(d->owner->banner)) {
957 documentY += height_Banner(d->owner->banner) + documentTopPad_DocumentView_(d);
958 }
959 else {
960 documentY += documentTopPad_DocumentView_(d) + d->pageMargin * gap_UI;
961 }
962 init_Anim(&d->scrollY.pos,
963 documentY - (centered ? documentBounds_DocumentView_(d).size.y / 2
964 : lineHeight_Text(paragraph_FontId)));
965 clampScroll_DocumentView_(d);
966}
967
968static void scrollToHeading_DocumentView_(iDocumentView *d, const char *heading) {
969 iConstForEach(Array, h, headings_GmDocument(d->doc)) {
970 const iGmHeading *head = h.value;
971 if (startsWithCase_Rangecc(head->text, heading)) {
972 postCommandf_Root(as_Widget(d->owner)->root, "document.goto loc:%p", head->text.start);
973 break;
974 }
975 }
976}
977
978static iBool scrollWideBlock_DocumentView_(iDocumentView *d, iInt2 mousePos, int delta,
979 int duration) {
980 if (delta == 0 || d->owner->flags & eitherWheelSwipe_DocumentWidgetFlag) {
981 return iFalse;
982 }
983 const iInt2 docPos = documentPos_DocumentView_(d, mousePos);
984 iConstForEach(PtrArray, i, &d->visibleWideRuns) {
985 const iGmRun *run = i.ptr;
986 if (docPos.y >= top_Rect(run->bounds) && docPos.y <= bottom_Rect(run->bounds)) {
987 /* We can scroll this run. First find out how much is allowed. */
988 const iGmRunRange range = findPreformattedRange_GmDocument(d->doc, run);
989 int maxWidth = 0;
990 for (const iGmRun *r = range.start; r != range.end; r++) {
991 maxWidth = iMax(maxWidth, width_Rect(r->visBounds));
992 }
993 const int maxOffset = maxWidth - documentWidth_DocumentView_(d) + d->pageMargin * gap_UI;
994 if (size_Array(&d->wideRunOffsets) <= preId_GmRun(run)) {
995 resize_Array(&d->wideRunOffsets, preId_GmRun(run) + 1);
996 }
997 int *offset = at_Array(&d->wideRunOffsets, preId_GmRun(run) - 1);
998 const int oldOffset = *offset;
999 *offset = iClamp(*offset + delta, 0, maxOffset);
1000 /* Make sure the whole block gets redraw. */
1001 if (oldOffset != *offset) {
1002 for (const iGmRun *r = range.start; r != range.end; r++) {
1003 insert_PtrSet(d->invalidRuns, r);
1004 }
1005 refresh_Widget(d->owner);
1006 d->owner->selectMark = iNullRange;
1007 d->owner->foundMark = iNullRange;
1008 }
1009 if (duration) {
1010 if (d->animWideRunId != preId_GmRun(run) || isFinished_Anim(&d->animWideRunOffset)) {
1011 d->animWideRunId = preId_GmRun(run);
1012 init_Anim(&d->animWideRunOffset, oldOffset);
1013 }
1014 setValueEased_Anim(&d->animWideRunOffset, *offset, duration);
1015 d->animWideRunRange = range;
1016 addTicker_App(refreshWhileScrolling_DocumentWidget_, d->owner);
1017 }
1018 else {
1019 d->animWideRunId = 0;
1020 init_Anim(&d->animWideRunOffset, 0);
1021 }
1022 return iTrue;
1023 }
1024 }
1025 return iFalse;
1026}
1027
1028static iRangecc sourceLoc_DocumentView_(const iDocumentView *d, iInt2 pos) {
1029 return findLoc_GmDocument(d->doc, documentPos_DocumentView_(d, pos));
1030}
1031
1032iDeclareType(MiddleRunParams)
1033
1034struct Impl_MiddleRunParams {
1035 int midY;
1036 const iGmRun *closest;
1037 int distance;
1038};
1039
1040static void find_MiddleRunParams_(void *params, const iGmRun *run) {
1041 iMiddleRunParams *d = params;
1042 if (isEmpty_Rect(run->bounds)) {
1043 return;
1044 }
1045 const int distance = iAbs(mid_Rect(run->bounds).y - d->midY);
1046 if (!d->closest || distance < d->distance) {
1047 d->closest = run;
1048 d->distance = distance;
1049 }
1050}
1051
1052static const iGmRun *middleRun_DocumentView_(const iDocumentView *d) {
1053 iRangei visRange = visibleRange_DocumentView_(d);
1054 iMiddleRunParams params = { (visRange.start + visRange.end) / 2, NULL, 0 };
1055 render_GmDocument(d->doc, visRange, find_MiddleRunParams_, &params);
1056 return params.closest;
1057}
1058
1059static void allocVisBuffer_DocumentView_(const iDocumentView *d) {
1060 const iWidget *w = constAs_Widget(d->owner);
1061 const iBool isVisible = isVisible_Widget(w);
1062 const iInt2 size = bounds_Widget(w).size;
1063 if (isVisible) {
1064 alloc_VisBuf(d->visBuf, size, 1);
1065 }
1066 else {
1067 dealloc_VisBuf(d->visBuf);
1068 }
1069}
1070
1071static size_t visibleLinkOrdinal_DocumentView_(const iDocumentView *d, iGmLinkId linkId) {
1072 size_t ord = 0;
1073 const iRangei visRange = visibleRange_DocumentView_(d);
1074 iConstForEach(PtrArray, i, &d->visibleLinks) {
1075 const iGmRun *run = i.ptr;
1076 if (top_Rect(run->visBounds) >= visRange.start + gap_UI * d->pageMargin * 4 / 5) {
1077 if (run->flags & decoration_GmRunFlag && run->linkId) {
1078 if (run->linkId == linkId) return ord;
1079 ord++;
1080 }
1081 }
1082 }
1083 return iInvalidPos;
1084}
1085
1086static void documentRunsInvalidated_DocumentWidget_(iDocumentWidget *d) {
1087 d->foundMark = iNullRange;
1088 d->selectMark = iNullRange;
1089 d->contextLink = NULL;
1090 documentRunsInvalidated_DocumentView_(&d->view);
1091}
1092
1093static iBool updateDocumentWidthRetainingScrollPosition_DocumentView_(iDocumentView *d,
1094 iBool keepCenter) {
1095 const int newWidth = documentWidth_DocumentView_(d);
1096 if (newWidth == size_GmDocument(d->doc).x && !keepCenter /* not a font change */) {
1097 return iFalse;
1098 }
1099 /* Font changes (i.e., zooming) will keep the view centered, otherwise keep the top
1100 of the visible area fixed. */
1101 const iGmRun *run = keepCenter ? middleRun_DocumentView_(d) : d->visibleRuns.start;
1102 const char * runLoc = (run ? run->text.start : NULL);
1103 int voffset = 0;
1104 if (!keepCenter && run) {
1105 /* Keep the first visible run visible at the same position. */
1106 /* TODO: First *fully* visible run? */
1107 voffset = visibleRange_DocumentView_(d).start - top_Rect(run->visBounds);
1108 }
1109 setWidth_GmDocument(d->doc, newWidth, width_Widget(d->owner));
1110 setWidth_Banner(d->owner->banner, newWidth);
1111 documentRunsInvalidated_DocumentWidget_(d->owner);
1112 if (runLoc && !keepCenter) {
1113 run = findRunAtLoc_GmDocument(d->doc, runLoc);
1114 if (run) {
1115 scrollTo_DocumentView_(
1116 d, top_Rect(run->visBounds) + lineHeight_Text(paragraph_FontId) + voffset, iFalse);
1117 }
1118 }
1119 else if (runLoc && keepCenter) {
1120 run = findRunAtLoc_GmDocument(d->doc, runLoc);
1121 if (run) {
1122 scrollTo_DocumentView_(d, mid_Rect(run->bounds).y, iTrue);
1123 }
1124 }
1125 return iTrue;
1126}
1127
1128static iRect runRect_DocumentView_(const iDocumentView *d, const iGmRun *run) {
1129 const iRect docBounds = documentBounds_DocumentView_(d);
1130 return moved_Rect(run->bounds, addY_I2(topLeft_Rect(docBounds), viewPos_DocumentView_(d)));
1131}
1132
1133iDeclareType(DrawContext)
1134
1135struct Impl_DrawContext {
1136 const iDocumentView *view;
1137 iRect widgetBounds;
1138 iRect docBounds;
1139 iRangei vis;
1140 iInt2 viewPos; /* document area origin */
1141 iPaint paint;
1142 iBool inSelectMark;
1143 iBool inFoundMark;
1144 iBool showLinkNumbers;
1145 iRect firstMarkRect;
1146 iRect lastMarkRect;
1147 iGmRunRange runsDrawn;
1148};
1149
1150static void fillRange_DrawContext_(iDrawContext *d, const iGmRun *run, enum iColorId color,
1151 iRangecc mark, iBool *isInside) {
1152 if (mark.start > mark.end) {
1153 /* Selection may be done in either direction. */
1154 iSwap(const char *, mark.start, mark.end);
1155 }
1156 if (*isInside || (contains_Range(&run->text, mark.start) ||
1157 contains_Range(&mark, run->text.start))) {
1158 int x = 0;
1159 if (!*isInside) {
1160 x = measureRange_Text(run->font,
1161 (iRangecc){ run->text.start, iMax(run->text.start, mark.start) })
1162 .advance.x;
1163 }
1164 int w = width_Rect(run->visBounds) - x;
1165 if (contains_Range(&run->text, mark.end) || mark.end < run->text.start) {
1166 iRangecc mk = !*isInside ? mark
1167 : (iRangecc){ run->text.start, iMax(run->text.start, mark.end) };
1168 mk.start = iMax(mk.start, run->text.start);
1169 w = measureRange_Text(run->font, mk).advance.x;
1170 *isInside = iFalse;
1171 }
1172 else {
1173 *isInside = iTrue; /* at least until the next run */
1174 }
1175 if (w > width_Rect(run->visBounds) - x) {
1176 w = width_Rect(run->visBounds) - x;
1177 }
1178 if (~run->flags & decoration_GmRunFlag) {
1179 const iInt2 visPos =
1180 add_I2(run->bounds.pos, addY_I2(d->viewPos, viewPos_DocumentView_(d->view)));
1181 const iRect rangeRect = { addX_I2(visPos, x), init_I2(w, height_Rect(run->bounds)) };
1182 if (rangeRect.size.x) {
1183 fillRect_Paint(&d->paint, rangeRect, color);
1184 /* Keep track of the first and last marked rects. */
1185 if (d->firstMarkRect.size.x == 0) {
1186 d->firstMarkRect = rangeRect;
1187 }
1188 d->lastMarkRect = rangeRect;
1189 }
1190 }
1191 }
1192 /* Link URLs are not part of the visible document, so they are ignored above. Handle
1193 these ranges as a special case. */
1194 if (run->linkId && run->flags & decoration_GmRunFlag) {
1195 const iRangecc url = linkUrlRange_GmDocument(d->view->doc, run->linkId);
1196 if (contains_Range(&url, mark.start) &&
1197 (contains_Range(&url, mark.end) || url.end == mark.end)) {
1198 fillRect_Paint(
1199 &d->paint,
1200 moved_Rect(run->visBounds, addY_I2(d->viewPos, viewPos_DocumentView_(d->view))),
1201 color);
1202 }
1203 }
1204}
1205
1206static void drawMark_DrawContext_(void *context, const iGmRun *run) {
1207 iDrawContext *d = context;
1208 if (!isMedia_GmRun(run)) {
1209 fillRange_DrawContext_(d, run, uiMatching_ColorId, d->view->owner->foundMark, &d->inFoundMark);
1210 fillRange_DrawContext_(d, run, uiMarked_ColorId, d->view->owner->selectMark, &d->inSelectMark);
1211 }
1212}
1213
1214static void drawRun_DrawContext_(void *context, const iGmRun *run) {
1215 iDrawContext *d = context;
1216 const iInt2 origin = d->viewPos;
1217 /* Keep track of the drawn visible runs. */ {
1218 if (!d->runsDrawn.start || run < d->runsDrawn.start) {
1219 d->runsDrawn.start = run;
1220 }
1221 if (!d->runsDrawn.end || run > d->runsDrawn.end) {
1222 d->runsDrawn.end = run;
1223 }
1224 }
1225 if (run->mediaType == image_MediaType) {
1226 SDL_Texture *tex = imageTexture_Media(media_GmDocument(d->view->doc), mediaId_GmRun(run));
1227 const iRect dst = moved_Rect(run->visBounds, origin);
1228 if (tex) {
1229 fillRect_Paint(&d->paint, dst, tmBackground_ColorId); /* in case the image has alpha */
1230 SDL_RenderCopy(d->paint.dst->render, tex, NULL,
1231 &(SDL_Rect){ dst.pos.x, dst.pos.y, dst.size.x, dst.size.y });
1232 }
1233 else {
1234 drawRect_Paint(&d->paint, dst, tmQuoteIcon_ColorId);
1235 drawCentered_Text(uiLabel_FontId,
1236 dst,
1237 iFalse,
1238 tmQuote_ColorId,
1239 explosion_Icon " Error Loading Image");
1240 }
1241 return;
1242 }
1243 else if (isMedia_GmRun(run)) {
1244 /* Media UIs are drawn afterwards as a dynamic overlay. */
1245 return;
1246 }
1247 enum iColorId fg = run->color;
1248 const iGmDocument *doc = d->view->doc;
1249 const int linkFlags = linkFlags_GmDocument(doc, run->linkId);
1250 /* Hover state of a link. */
1251 const iBool isPartOfHover = (run->linkId && d->view->hoverLink &&
1252 run->linkId == d->view->hoverLink->linkId);
1253 iBool isHover = (isPartOfHover && ~run->flags & decoration_GmRunFlag);
1254 /* Visible (scrolled) position of the run. */
1255 const iInt2 visPos = addX_I2(add_I2(run->visBounds.pos, origin),
1256 /* Preformatted runs can be scrolled. */
1257 runOffset_DocumentView_(d->view, run));
1258 const iRect visRect = { visPos, run->visBounds.size };
1259 /* Fill the background. */ {
1260#if 0
1261 iBool isInlineImageCaption = run->linkId && linkFlags & content_GmLinkFlag &&
1262 ~linkFlags & permanent_GmLinkFlag;
1263 if (run->flags & decoration_GmRunFlag && ~run->flags & startOfLine_GmRunFlag) {
1264 /* This is the metadata. */
1265 isInlineImageCaption = iFalse;
1266 }
1267#endif
1268 iBool isMobileHover = deviceType_App() != desktop_AppDeviceType &&
1269 (isPartOfHover || contains_PtrSet(d->view->invalidRuns, run)) &&
1270 (~run->flags & decoration_GmRunFlag || run->flags & startOfLine_GmRunFlag
1271 /* highlight link icon but not image captions */);
1272 /* While this is consistent, it's a bit excessive to indicate that an inlined image
1273 is open: the image itself is the indication. */
1274 const iBool isInlineImageCaption = iFalse;
1275 if (run->linkId && (linkFlags & isOpen_GmLinkFlag || isInlineImageCaption || isMobileHover)) {
1276 /* Open links get a highlighted background. */
1277 int bg = tmBackgroundOpenLink_ColorId;
1278 if (isMobileHover && !isPartOfHover) {
1279 bg = tmBackground_ColorId; /* hover ended and was invalidated */
1280 }
1281// const int frame = tmFrameOpenLink_ColorId;
1282 const int pad = gap_Text;
1283 iRect wideRect = { init_I2(origin.x - pad, visPos.y),
1284 init_I2(d->docBounds.size.x + 2 * pad,
1285 height_Rect(run->visBounds)) };
1286 adjustEdges_Rect(&wideRect,
1287 run->flags & startOfLine_GmRunFlag ? -pad * 3 / 4 : 0, 0,
1288 run->flags & endOfLine_GmRunFlag ? pad * 3 / 4 : 0, 0);
1289 /* The first line is composed of two runs that may be drawn in either order, so
1290 only draw half of the background. */
1291 if (run->flags & decoration_GmRunFlag) {
1292 wideRect.size.x = right_Rect(visRect) - left_Rect(wideRect);
1293 }
1294 else if (run->flags & startOfLine_GmRunFlag) {
1295 wideRect.size.x = right_Rect(wideRect) - left_Rect(visRect);
1296 wideRect.pos.x = left_Rect(visRect);
1297 }
1298 fillRect_Paint(&d->paint, wideRect, bg);
1299 }
1300 else {
1301 /* Normal background for other runs. There are cases when runs get drawn multiple times,
1302 e.g., at the buffer boundary, and there are slightly overlapping characters in
1303 monospace blocks. Clearing the background here ensures a cleaner visual appearance
1304 since only one glyph is visible at any given point. */
1305 fillRect_Paint(&d->paint, visRect, tmBackground_ColorId);
1306 }
1307 }
1308 if (run->linkId) {
1309 if (run->flags & decoration_GmRunFlag && run->flags & startOfLine_GmRunFlag) {
1310 /* Link icon. */
1311 if (linkFlags & content_GmLinkFlag) {
1312 fg = linkColor_GmDocument(doc, run->linkId, textHover_GmLinkPart);
1313 }
1314 }
1315 else if (~run->flags & decoration_GmRunFlag) {
1316 fg = linkColor_GmDocument(doc, run->linkId, isHover ? textHover_GmLinkPart : text_GmLinkPart);
1317 if (linkFlags & content_GmLinkFlag) {
1318 fg = linkColor_GmDocument(doc, run->linkId, textHover_GmLinkPart); /* link is inactive */
1319 }
1320 }
1321 }
1322 if (run->flags & altText_GmRunFlag) {
1323 const iInt2 margin = preRunMargin_GmDocument(doc, preId_GmRun(run));
1324 fillRect_Paint(&d->paint, (iRect){ visPos, run->visBounds.size }, tmBackgroundAltText_ColorId);
1325 drawRect_Paint(&d->paint, (iRect){ visPos, run->visBounds.size }, tmFrameAltText_ColorId);
1326 drawWrapRange_Text(run->font,
1327 add_I2(visPos, margin),
1328 run->visBounds.size.x - 2 * margin.x,
1329 run->color,
1330 run->text);
1331 }
1332 else {
1333 if (d->showLinkNumbers && run->linkId && run->flags & decoration_GmRunFlag) {
1334 const size_t ord = visibleLinkOrdinal_DocumentView_(d->view, run->linkId);
1335 if (ord >= d->view->owner->ordinalBase) {
1336 const iChar ordChar =
1337 linkOrdinalChar_DocumentWidget_(d->view->owner, ord - d->view->owner->ordinalBase);
1338 if (ordChar) {
1339 const char *circle = "\u25ef"; /* Large Circle */
1340 const int circleFont = FONT_ID(default_FontId, regular_FontStyle, contentRegular_FontSize);
1341 iRect nbArea = { init_I2(d->viewPos.x - gap_UI / 3, visPos.y),
1342 init_I2(3.95f * gap_Text, 1.0f * lineHeight_Text(circleFont)) };
1343 drawRange_Text(
1344 circleFont, topLeft_Rect(nbArea), tmQuote_ColorId, range_CStr(circle));
1345 iRect circleArea = visualBounds_Text(circleFont, range_CStr(circle));
1346 addv_I2(&circleArea.pos, topLeft_Rect(nbArea));
1347 drawCentered_Text(FONT_ID(default_FontId, regular_FontStyle, contentSmall_FontSize),
1348 circleArea,
1349 iTrue,
1350 tmQuote_ColorId,
1351 "%lc",
1352 (int) ordChar);
1353 goto runDrawn;
1354 }
1355 }
1356 }
1357 if (run->flags & quoteBorder_GmRunFlag) {
1358 drawVLine_Paint(&d->paint,
1359 addX_I2(visPos,
1360 !run->isRTL
1361 ? -gap_Text * 5 / 2
1362 : (width_Rect(run->visBounds) + gap_Text * 5 / 2)),
1363 height_Rect(run->visBounds),
1364 tmQuoteIcon_ColorId);
1365 }
1366 /* Base attributes. */ {
1367 int f, c;
1368 runBaseAttributes_GmDocument(doc, run, &f, &c);
1369 setBaseAttributes_Text(f, c);
1370 }
1371 /* Fancy date in Gemini feed links. */ {
1372 if (run->linkId && run->flags & startOfLine_GmRunFlag && ~run->flags & decoration_GmRunFlag) {
1373 static iRegExp *datePattern_;
1374 if (!datePattern_) {
1375 datePattern_ = new_RegExp("^[12][0-9][0-9][0-9]-[01][0-9]-[0-3][0-9]\\s", 0);
1376 }
1377 iRegExpMatch m;
1378 init_RegExpMatch(&m);
1379 if (matchRange_RegExp(datePattern_, run->text, &m)) {
1380 /* The date uses regular weight and a dimmed color. */
1381 iString styled;
1382 initRange_String(&styled, run->text);
1383 insertData_Block(&styled.chars, 10, "\x1b[0m", 4); /* restore */
1384 iBlock buf;
1385 init_Block(&buf, 0);
1386 appendCStr_Block(&buf, "\x1b[10m"); /* regular font weight */
1387 appendCStr_Block(&buf, escape_Color(isHover ? fg : tmLinkFeedEntryDate_ColorId));
1388 insertData_Block(&styled.chars, 0, constData_Block(&buf), size_Block(&buf));
1389 deinit_Block(&buf);
1390 const int oldAnsi = ansiFlags_Text();
1391 setAnsiFlags_Text(oldAnsi | allowFontStyle_AnsiFlag);
1392 setBaseAttributes_Text(run->font, fg);
1393 drawBoundRange_Text(run->font,
1394 visPos,
1395 (run->isRTL ? -1 : 1) * width_Rect(run->visBounds),
1396 fg,
1397 range_String(&styled));
1398 setAnsiFlags_Text(oldAnsi);
1399 deinit_String(&styled);
1400 goto runDrawn;
1401 }
1402 }
1403 }
1404 drawBoundRange_Text(run->font,
1405 visPos,
1406 (run->isRTL ? -1 : 1) * width_Rect(run->visBounds),
1407 fg,
1408 run->text);
1409 runDrawn:;
1410 setBaseAttributes_Text(-1, -1);
1411 }
1412 /* Presentation of links. */
1413 if (run->linkId && ~run->flags & decoration_GmRunFlag) {
1414 const int metaFont = paragraph_FontId;
1415 /* TODO: Show status of an ongoing media request. */
1416 const int flags = linkFlags;
1417 const iRect linkRect = moved_Rect(run->visBounds, origin);
1418 iMediaRequest *mr = NULL;
1419 /* Show metadata about inline content. */
1420 if (flags & content_GmLinkFlag && run->flags & endOfLine_GmRunFlag) {
1421 fg = linkColor_GmDocument(doc, run->linkId, textHover_GmLinkPart);
1422 iString text;
1423 init_String(&text);
1424 const iMediaId linkMedia = findMediaForLink_Media(constMedia_GmDocument(doc),
1425 run->linkId, none_MediaType);
1426 iAssert(linkMedia.type != none_MediaType);
1427 iGmMediaInfo info;
1428 info_Media(constMedia_GmDocument(doc), linkMedia, &info);
1429 switch (linkMedia.type) {
1430 case image_MediaType: {
1431 /* There's a separate decorative GmRun for the metadata. */
1432 break;
1433 }
1434 case audio_MediaType:
1435 format_String(&text, "%s", info.type);
1436 break;
1437 case download_MediaType:
1438 format_String(&text, "%s", info.type);
1439 break;
1440 default:
1441 break;
1442 }
1443 if (linkMedia.type != download_MediaType && /* can't cancel downloads currently */
1444 linkMedia.type != image_MediaType &&
1445 findMediaRequest_DocumentWidget_(d->view->owner, run->linkId)) {
1446 appendFormat_String(
1447 &text, " %s" close_Icon, isHover ? escape_Color(tmLinkText_ColorId) : "");
1448 }
1449 const iInt2 size = measureRange_Text(metaFont, range_String(&text)).bounds.size;
1450 if (size.x) {
1451 fillRect_Paint(
1452 &d->paint,
1453 (iRect){ add_I2(origin, addX_I2(topRight_Rect(run->bounds), -size.x - gap_UI)),
1454 addX_I2(size, 2 * gap_UI) },
1455 tmBackground_ColorId);
1456 drawAlign_Text(metaFont,
1457 add_I2(topRight_Rect(run->bounds), origin),
1458 fg,
1459 right_Alignment,
1460 "%s", cstr_String(&text));
1461 }
1462 deinit_String(&text);
1463 }
1464 else if (run->flags & endOfLine_GmRunFlag &&
1465 (mr = findMediaRequest_DocumentWidget_(d->view->owner, run->linkId)) != NULL) {
1466 if (!isFinished_GmRequest(mr->req)) {
1467 draw_Text(metaFont,
1468 topRight_Rect(linkRect),
1469 tmInlineContentMetadata_ColorId,
1470 translateCStr_Lang(" \u2014 ${doc.fetching}\u2026 (%.1f ${mb})"),
1471 (float) bodySize_GmRequest(mr->req) / 1.0e6f);
1472 }
1473 }
1474 }
1475 if (0) {
1476 drawRect_Paint(&d->paint, (iRect){ visPos, run->bounds.size }, green_ColorId);
1477 drawRect_Paint(&d->paint, (iRect){ visPos, run->visBounds.size }, red_ColorId);
1478 }
1479}
1480
1481static int drawSideRect_(iPaint *p, iRect rect) {
1482 int bg = tmBannerBackground_ColorId;
1483 int fg = tmBannerIcon_ColorId;
1484 if (equal_Color(get_Color(bg), get_Color(tmBackground_ColorId))) {
1485 bg = tmBannerIcon_ColorId;
1486 fg = tmBannerBackground_ColorId;
1487 }
1488 fillRect_Paint(p, rect, bg);
1489 return fg;
1490}
1491
1492static int sideElementAvailWidth_DocumentView_(const iDocumentView *d) {
1493 return left_Rect(documentBounds_DocumentView_(d)) -
1494 left_Rect(bounds_Widget(constAs_Widget(d->owner))) - 2 * d->pageMargin * gap_UI;
1495}
1496
1497static iBool isSideHeadingVisible_DocumentView_(const iDocumentView *d) {
1498 return sideElementAvailWidth_DocumentView_(d) >= lineHeight_Text(banner_FontId) * 4.5f;
1499}
1500
1501static void updateSideIconBuf_DocumentView_(const iDocumentView *d) {
1502 if (!isExposed_Window(get_Window())) {
1503 return;
1504 }
1505 iDrawBufs *dbuf = d->drawBufs;
1506 dbuf->flags &= ~updateSideBuf_DrawBufsFlag;
1507 if (dbuf->sideIconBuf) {
1508 SDL_DestroyTexture(dbuf->sideIconBuf);
1509 dbuf->sideIconBuf = NULL;
1510 }
1511 // const iGmRun *banner = siteBanner_GmDocument(d->doc);
1512 if (isEmpty_Banner(d->owner->banner)) {
1513 return;
1514 }
1515 const int margin = gap_UI * d->pageMargin;
1516 const int minBannerSize = lineHeight_Text(banner_FontId) * 2;
1517 const iChar icon = siteIcon_GmDocument(d->doc);
1518 const int avail = sideElementAvailWidth_DocumentView_(d) - margin;
1519 iBool isHeadingVisible = isSideHeadingVisible_DocumentView_(d);
1520 /* Determine the required size. */
1521 iInt2 bufSize = init1_I2(minBannerSize);
1522 const int sideHeadingFont = FONT_ID(documentHeading_FontId, regular_FontStyle, contentBig_FontSize);
1523 if (isHeadingVisible) {
1524 const iInt2 headingSize = measureWrapRange_Text(sideHeadingFont, avail,
1525 currentHeading_DocumentView_(d)).bounds.size;
1526 if (headingSize.x > 0) {
1527 bufSize.y += gap_Text + headingSize.y;
1528 bufSize.x = iMax(bufSize.x, headingSize.x);
1529 }
1530 else {
1531 isHeadingVisible = iFalse;
1532 }
1533 }
1534 SDL_Renderer *render = renderer_Window(get_Window());
1535 dbuf->sideIconBuf = SDL_CreateTexture(render,
1536 SDL_PIXELFORMAT_RGBA4444,
1537 SDL_TEXTUREACCESS_STATIC | SDL_TEXTUREACCESS_TARGET,
1538 bufSize.x, bufSize.y);
1539 iPaint p;
1540 init_Paint(&p);
1541 beginTarget_Paint(&p, dbuf->sideIconBuf);
1542 const iColor back = get_Color(tmBannerSideTitle_ColorId);
1543 SDL_SetRenderDrawColor(render, back.r, back.g, back.b, 0); /* better blending of the edge */
1544 SDL_RenderClear(render);
1545 const iRect iconRect = { zero_I2(), init1_I2(minBannerSize) };
1546 int fg = drawSideRect_(&p, iconRect);
1547 iString str;
1548 initUnicodeN_String(&str, &icon, 1);
1549 drawCentered_Text(banner_FontId, iconRect, iTrue, fg, "%s", cstr_String(&str));
1550 deinit_String(&str);
1551 if (isHeadingVisible) {
1552 iRangecc text = currentHeading_DocumentView_(d);
1553 iInt2 pos = addY_I2(bottomLeft_Rect(iconRect), gap_Text);
1554 const int font = sideHeadingFont;
1555 drawWrapRange_Text(font, pos, avail, tmBannerSideTitle_ColorId, text);
1556 }
1557 endTarget_Paint(&p);
1558 SDL_SetTextureBlendMode(dbuf->sideIconBuf, SDL_BLENDMODE_BLEND);
1559}
1560
1561static void drawSideElements_DocumentView_(const iDocumentView *d) {
1562 const iWidget *w = constAs_Widget(d->owner);
1563 const iRect bounds = bounds_Widget(w);
1564 const iRect docBounds = documentBounds_DocumentView_(d);
1565 const int margin = gap_UI * d->pageMargin;
1566 float opacity = value_Anim(&d->sideOpacity);
1567 const int avail = left_Rect(docBounds) - left_Rect(bounds) - 2 * margin;
1568 iDrawBufs * dbuf = d->drawBufs;
1569 iPaint p;
1570 init_Paint(&p);
1571 setClip_Paint(&p, boundsWithoutVisualOffset_Widget(w));
1572 /* Side icon and current heading. */
1573 if (prefs_App()->sideIcon && opacity > 0 && dbuf->sideIconBuf) {
1574 const iInt2 texSize = size_SDLTexture(dbuf->sideIconBuf);
1575 if (avail > texSize.x) {
1576 const int minBannerSize = lineHeight_Text(banner_FontId) * 2;
1577 iInt2 pos = addY_I2(add_I2(topLeft_Rect(bounds), init_I2(margin, 0)),
1578 height_Rect(bounds) / 2 - minBannerSize / 2 -
1579 (texSize.y > minBannerSize
1580 ? (gap_Text + lineHeight_Text(heading3_FontId)) / 2
1581 : 0));
1582 SDL_SetTextureAlphaMod(dbuf->sideIconBuf, 255 * opacity);
1583 SDL_RenderCopy(renderer_Window(get_Window()),
1584 dbuf->sideIconBuf, NULL,
1585 &(SDL_Rect){ pos.x, pos.y, texSize.x, texSize.y });
1586 }
1587 }
1588 /* Reception timestamp. */
1589 if (dbuf->timestampBuf && dbuf->timestampBuf->size.x <= avail) {
1590 draw_TextBuf(
1591 dbuf->timestampBuf,
1592 add_I2(
1593 bottomLeft_Rect(bounds),
1594 init_I2(margin,
1595 -margin + -dbuf->timestampBuf->size.y +
1596 iMax(0, d->scrollY.max - pos_SmoothScroll(&d->scrollY)))),
1597 tmQuoteIcon_ColorId);
1598 }
1599 unsetClip_Paint(&p);
1600}
1601
1602static void drawMedia_DocumentView_(const iDocumentView *d, iPaint *p) {
1603 iConstForEach(PtrArray, i, &d->visibleMedia) {
1604 const iGmRun * run = i.ptr;
1605 if (run->mediaType == audio_MediaType) {
1606 iPlayerUI ui;
1607 init_PlayerUI(&ui,
1608 audioPlayer_Media(media_GmDocument(d->doc), mediaId_GmRun(run)),
1609 runRect_DocumentView_(d, run));
1610 draw_PlayerUI(&ui, p);
1611 }
1612 else if (run->mediaType == download_MediaType) {
1613 iDownloadUI ui;
1614 init_DownloadUI(&ui, constMedia_GmDocument(d->doc), run->mediaId,
1615 runRect_DocumentView_(d, run));
1616 draw_DownloadUI(&ui, p);
1617 }
1618 }
1619}
1620
1621static void extend_GmRunRange_(iGmRunRange *runs) {
1622 if (runs->start) {
1623 runs->start--;
1624 runs->end++;
1625 }
1626}
1627
1628static iBool render_DocumentView_(const iDocumentView *d, iDrawContext *ctx, iBool prerenderExtra) {
1629 iBool didDraw = iFalse;
1630 const iRect bounds = bounds_Widget(constAs_Widget(d->owner));
1631 const iRect ctxWidgetBounds =
1632 init_Rect(0,
1633 0,
1634 width_Rect(bounds) - constAs_Widget(d->owner->scroll)->rect.size.x,
1635 height_Rect(bounds));
1636 const iRangei full = { 0, size_GmDocument(d->doc).y };
1637 const iRangei vis = ctx->vis;
1638 iVisBuf *visBuf = d->visBuf; /* will be updated now */
1639 d->drawBufs->lastRenderTime = SDL_GetTicks();
1640 /* Swap buffers around to have room available both before and after the visible region. */
1641 allocVisBuffer_DocumentView_(d);
1642 reposition_VisBuf(visBuf, vis);
1643 /* Redraw the invalid ranges. */
1644 if (~flags_Widget(constAs_Widget(d->owner)) & destroyPending_WidgetFlag) {
1645 iPaint *p = &ctx->paint;
1646 init_Paint(p);
1647 iForIndices(i, visBuf->buffers) {
1648 iVisBufTexture *buf = &visBuf->buffers[i];
1649 iVisBufMeta *meta = buf->user;
1650 const iRangei bufRange = intersect_Rangei(bufferRange_VisBuf(visBuf, i), full);
1651 const iRangei bufVisRange = intersect_Rangei(bufRange, vis);
1652 ctx->widgetBounds = moved_Rect(ctxWidgetBounds, init_I2(0, -buf->origin));
1653 ctx->viewPos = init_I2(left_Rect(ctx->docBounds) - left_Rect(bounds), -buf->origin);
1654 // printf(" buffer %zu: buf vis range %d...%d\n", i, bufVisRange.start, bufVisRange.end);
1655 if (!prerenderExtra && !isEmpty_Range(&bufVisRange)) {
1656 didDraw = iTrue;
1657 if (isEmpty_Rangei(buf->validRange)) {
1658 /* Fill the required currently visible range (vis). */
1659 const iRangei bufVisRange = intersect_Rangei(bufRange, vis);
1660 if (!isEmpty_Range(&bufVisRange)) {
1661 beginTarget_Paint(p, buf->texture);
1662 fillRect_Paint(p, (iRect){ zero_I2(), visBuf->texSize }, tmBackground_ColorId);
1663 iZap(ctx->runsDrawn);
1664 render_GmDocument(d->doc, bufVisRange, drawRun_DrawContext_, ctx);
1665 meta->runsDrawn = ctx->runsDrawn;
1666 extend_GmRunRange_(&meta->runsDrawn);
1667 buf->validRange = bufVisRange;
1668 // printf(" buffer %zu valid %d...%d\n", i, bufRange.start, bufRange.end);
1669 }
1670 }
1671 else {
1672 /* Progressively fill the required runs. */
1673 if (meta->runsDrawn.start) {
1674 beginTarget_Paint(p, buf->texture);
1675 meta->runsDrawn.start = renderProgressive_GmDocument(d->doc, meta->runsDrawn.start,
1676 -1, iInvalidSize,
1677 bufVisRange,
1678 drawRun_DrawContext_,
1679 ctx);
1680 buf->validRange.start = bufVisRange.start;
1681 }
1682 if (meta->runsDrawn.end) {
1683 beginTarget_Paint(p, buf->texture);
1684 meta->runsDrawn.end = renderProgressive_GmDocument(d->doc, meta->runsDrawn.end,
1685 +1, iInvalidSize,
1686 bufVisRange,
1687 drawRun_DrawContext_,
1688 ctx);
1689 buf->validRange.end = bufVisRange.end;
1690 }
1691 }
1692 }
1693 /* Progressively draw the rest of the buffer if it isn't fully valid. */
1694 if (prerenderExtra && !equal_Rangei(bufRange, buf->validRange)) {
1695 const iGmRun *next;
1696 // printf("%zu: prerenderExtra (start:%p end:%p)\n", i, meta->runsDrawn.start, meta->runsDrawn.end);
1697 if (meta->runsDrawn.start == NULL) {
1698 /* Haven't drawn anything yet in this buffer, so let's try seeding it. */
1699 const int rh = lineHeight_Text(paragraph_FontId);
1700 const int y = i >= iElemCount(visBuf->buffers) / 2 ? bufRange.start : (bufRange.end - rh);
1701 beginTarget_Paint(p, buf->texture);
1702 fillRect_Paint(p, (iRect){ zero_I2(), visBuf->texSize }, tmBackground_ColorId);
1703 buf->validRange = (iRangei){ y, y + rh };
1704 iZap(ctx->runsDrawn);
1705 render_GmDocument(d->doc, buf->validRange, drawRun_DrawContext_, ctx);
1706 meta->runsDrawn = ctx->runsDrawn;
1707 extend_GmRunRange_(&meta->runsDrawn);
1708 // printf("%zu: seeded, next %p:%p\n", i, meta->runsDrawn.start, meta->runsDrawn.end);
1709 didDraw = iTrue;
1710 }
1711 else {
1712 if (meta->runsDrawn.start) {
1713 const iRangei upper = intersect_Rangei(bufRange, (iRangei){ full.start, buf->validRange.start });
1714 if (upper.end > upper.start) {
1715 beginTarget_Paint(p, buf->texture);
1716 next = renderProgressive_GmDocument(d->doc, meta->runsDrawn.start,
1717 -1, 1, upper,
1718 drawRun_DrawContext_,
1719 ctx);
1720 if (next && meta->runsDrawn.start != next) {
1721 meta->runsDrawn.start = next;
1722 buf->validRange.start = bottom_Rect(next->visBounds);
1723 didDraw = iTrue;
1724 }
1725 else {
1726 buf->validRange.start = bufRange.start;
1727 }
1728 }
1729 }
1730 if (!didDraw && meta->runsDrawn.end) {
1731 const iRangei lower = intersect_Rangei(bufRange, (iRangei){ buf->validRange.end, full.end });
1732 if (lower.end > lower.start) {
1733 beginTarget_Paint(p, buf->texture);
1734 next = renderProgressive_GmDocument(d->doc, meta->runsDrawn.end,
1735 +1, 1, lower,
1736 drawRun_DrawContext_,
1737 ctx);
1738 if (next && meta->runsDrawn.end != next) {
1739 meta->runsDrawn.end = next;
1740 buf->validRange.end = top_Rect(next->visBounds);
1741 didDraw = iTrue;
1742 }
1743 else {
1744 buf->validRange.end = bufRange.end;
1745 }
1746 }
1747 }
1748 }
1749 }
1750 /* Draw any invalidated runs that fall within this buffer. */
1751 if (!prerenderExtra) {
1752 const iRangei bufRange = { buf->origin, buf->origin + visBuf->texSize.y };
1753 /* Clear full-width backgrounds first in case there are any dynamic elements. */ {
1754 iConstForEach(PtrSet, r, d->invalidRuns) {
1755 const iGmRun *run = *r.value;
1756 if (isOverlapping_Rangei(bufRange, ySpan_Rect(run->visBounds))) {
1757 beginTarget_Paint(p, buf->texture);
1758 fillRect_Paint(p,
1759 init_Rect(0,
1760 run->visBounds.pos.y - buf->origin,
1761 visBuf->texSize.x,
1762 run->visBounds.size.y),
1763 tmBackground_ColorId);
1764 }
1765 }
1766 }
1767 setAnsiFlags_Text(ansiEscapes_GmDocument(d->doc));
1768 iConstForEach(PtrSet, r, d->invalidRuns) {
1769 const iGmRun *run = *r.value;
1770 if (isOverlapping_Rangei(bufRange, ySpan_Rect(run->visBounds))) {
1771 beginTarget_Paint(p, buf->texture);
1772 drawRun_DrawContext_(ctx, run);
1773 }
1774 }
1775 setAnsiFlags_Text(allowAll_AnsiFlag);
1776 }
1777 endTarget_Paint(p);
1778 if (prerenderExtra && didDraw) {
1779 /* Just a run at a time. */
1780 break;
1781 }
1782 }
1783 if (!prerenderExtra) {
1784 clear_PtrSet(d->invalidRuns);
1785 }
1786 }
1787 return didDraw;
1788}
1789
1790static void draw_DocumentView_(const iDocumentView *d) {
1791 const iWidget *w = constAs_Widget(d->owner);
1792 const iRect bounds = bounds_Widget(w);
1793 const iRect boundsWithoutVisOff = boundsWithoutVisualOffset_Widget(w);
1794 const iRect clipBounds = intersect_Rect(bounds, boundsWithoutVisOff);
1795 /* Each document has its own palette, but the drawing routines rely on a global one.
1796 As we're now drawing a document, ensure that the right palette is in effect.
1797 Document theme colors can be used elsewhere, too, but first a document's palette
1798 must be made global. */
1799 makePaletteGlobal_GmDocument(d->doc);
1800 if (d->drawBufs->flags & updateTimestampBuf_DrawBufsFlag) {
1801 updateTimestampBuf_DocumentView_(d);
1802 }
1803 if (d->drawBufs->flags & updateSideBuf_DrawBufsFlag) {
1804 updateSideIconBuf_DocumentView_(d);
1805 }
1806 const iRect docBounds = documentBounds_DocumentView_(d);
1807 const iRangei vis = visibleRange_DocumentView_(d);
1808 iDrawContext ctx = {
1809 .view = d,
1810 .docBounds = docBounds,
1811 .vis = vis,
1812 .showLinkNumbers = (d->owner->flags & showLinkNumbers_DocumentWidgetFlag) != 0,
1813 };
1814 init_Paint(&ctx.paint);
1815 render_DocumentView_(d, &ctx, iFalse /* just the mandatory parts */);
1816 iBanner *banner = d->owner->banner;
1817 int yTop = docBounds.pos.y + viewPos_DocumentView_(d);
1818 const iBool isDocEmpty = size_GmDocument(d->doc).y == 0;
1819 const iBool isTouchSelecting = (flags_Widget(w) & touchDrag_WidgetFlag) != 0;
1820 if (!isDocEmpty || !isEmpty_Banner(banner)) {
1821 const int docBgColor = isDocEmpty ? tmBannerBackground_ColorId : tmBackground_ColorId;
1822 setClip_Paint(&ctx.paint, clipBounds);
1823 if (!isDocEmpty) {
1824 draw_VisBuf(d->visBuf, init_I2(bounds.pos.x, yTop), ySpan_Rect(bounds));
1825 }
1826 /* Text markers. */
1827 if (!isEmpty_Range(&d->owner->foundMark) || !isEmpty_Range(&d->owner->selectMark)) {
1828 SDL_Renderer *render = renderer_Window(get_Window());
1829 ctx.firstMarkRect = zero_Rect();
1830 ctx.lastMarkRect = zero_Rect();
1831 SDL_SetRenderDrawBlendMode(render,
1832 isDark_ColorTheme(colorTheme_App()) ? SDL_BLENDMODE_ADD
1833 : SDL_BLENDMODE_BLEND);
1834 ctx.viewPos = topLeft_Rect(docBounds);
1835 /* Marker starting outside the visible range? */
1836 if (d->visibleRuns.start) {
1837 if (!isEmpty_Range(&d->owner->selectMark) &&
1838 d->owner->selectMark.start < d->visibleRuns.start->text.start &&
1839 d->owner->selectMark.end > d->visibleRuns.start->text.start) {
1840 ctx.inSelectMark = iTrue;
1841 }
1842 if (isEmpty_Range(&d->owner->foundMark) &&
1843 d->owner->foundMark.start < d->visibleRuns.start->text.start &&
1844 d->owner->foundMark.end > d->visibleRuns.start->text.start) {
1845 ctx.inFoundMark = iTrue;
1846 }
1847 }
1848 render_GmDocument(d->doc, vis, drawMark_DrawContext_, &ctx);
1849 SDL_SetRenderDrawBlendMode(render, SDL_BLENDMODE_NONE);
1850 /* Selection range pins. */
1851 if (isTouchSelecting) {
1852 drawPin_Paint(&ctx.paint, ctx.firstMarkRect, 0, tmQuote_ColorId);
1853 drawPin_Paint(&ctx.paint, ctx.lastMarkRect, 1, tmQuote_ColorId);
1854 }
1855 }
1856 drawMedia_DocumentView_(d, &ctx.paint);
1857 /* Fill the top and bottom, in case the document is short. */
1858 if (yTop > top_Rect(bounds)) {
1859 fillRect_Paint(&ctx.paint,
1860 (iRect){ bounds.pos, init_I2(bounds.size.x, yTop - top_Rect(bounds)) },
1861 !isEmpty_Banner(banner) ? tmBannerBackground_ColorId
1862 : docBgColor);
1863 }
1864 /* Banner. */
1865 if (!isDocEmpty || numItems_Banner(banner) > 0) {
1866 /* Fill the part between the banner and the top of the document. */
1867 fillRect_Paint(&ctx.paint,
1868 (iRect){ init_I2(left_Rect(bounds),
1869 top_Rect(docBounds) + viewPos_DocumentView_(d) -
1870 documentTopPad_DocumentView_(d)),
1871 init_I2(bounds.size.x, documentTopPad_DocumentView_(d)) },
1872 docBgColor);
1873 setPos_Banner(banner, addY_I2(topLeft_Rect(docBounds),
1874 -pos_SmoothScroll(&d->scrollY)));
1875 draw_Banner(banner);
1876 }
1877 const int yBottom = yTop + size_GmDocument(d->doc).y;
1878 if (yBottom < bottom_Rect(bounds)) {
1879 fillRect_Paint(&ctx.paint,
1880 init_Rect(bounds.pos.x, yBottom, bounds.size.x, bottom_Rect(bounds) - yBottom),
1881 !isDocEmpty ? docBgColor : tmBannerBackground_ColorId);
1882 }
1883 unsetClip_Paint(&ctx.paint);
1884 drawSideElements_DocumentView_(d);
1885 /* Alt text. */
1886 const float altTextOpacity = value_Anim(&d->altTextOpacity) * 6 - 5;
1887 if (d->hoverAltPre && altTextOpacity > 0) {
1888 const iGmPreMeta *meta = preMeta_GmDocument(d->doc, preId_GmRun(d->hoverAltPre));
1889 if (meta->flags & topLeft_GmPreMetaFlag && ~meta->flags & decoration_GmRunFlag &&
1890 !isEmpty_Range(&meta->altText)) {
1891 const int margin = 3 * gap_UI / 2;
1892 const int altFont = uiLabel_FontId;
1893 const int wrap = docBounds.size.x - 2 * margin;
1894 iInt2 pos = addY_I2(add_I2(docBounds.pos, meta->pixelRect.pos),
1895 viewPos_DocumentView_(d));
1896 const iInt2 textSize = measureWrapRange_Text(altFont, wrap, meta->altText).bounds.size;
1897 pos.y -= textSize.y + gap_UI;
1898 pos.y = iMax(pos.y, top_Rect(bounds));
1899 const iRect altRect = { pos, init_I2(docBounds.size.x, textSize.y) };
1900 ctx.paint.alpha = altTextOpacity * 255;
1901 if (altTextOpacity < 1) {
1902 SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_BLEND);
1903 }
1904 fillRect_Paint(&ctx.paint, altRect, tmBackgroundAltText_ColorId);
1905 drawRect_Paint(&ctx.paint, altRect, tmFrameAltText_ColorId);
1906 setOpacity_Text(altTextOpacity);
1907 drawWrapRange_Text(altFont, addX_I2(pos, margin), wrap,
1908 tmQuote_ColorId, meta->altText);
1909 SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_NONE);
1910 setOpacity_Text(1.0f);
1911 }
1912 }
1913 /* Touch selection indicator. */
1914 if (isTouchSelecting) {
1915 iRect rect = { topLeft_Rect(bounds),
1916 init_I2(width_Rect(bounds), lineHeight_Text(uiLabelBold_FontId)) };
1917 fillRect_Paint(&ctx.paint, rect, uiTextAction_ColorId);
1918 const iRangecc mark = selectMark_DocumentWidget_(d->owner);
1919 drawCentered_Text(uiLabelBold_FontId,
1920 rect,
1921 iFalse,
1922 uiBackground_ColorId,
1923 "%zu bytes selected", /* TODO: i18n */
1924 size_Range(&mark));
1925 }
1926 }
1927}
1928
1929/*----------------------------------------------------------------------------------------------*/
1930
1931static void enableActions_DocumentWidget_(iDocumentWidget *d, iBool enable) {
1932 /* Actions are invisible child widgets of the DocumentWidget. */
1933 iForEach(ObjectList, i, children_Widget(d)) {
1934 if (isAction_Widget(i.object)) {
1935 setFlags_Widget(i.object, disabled_WidgetFlag, !enable);
1936 }
1937 }
1938}
1939
1940static void setLinkNumberMode_DocumentWidget_(iDocumentWidget *d, iBool set) {
1941 if (((d->flags & showLinkNumbers_DocumentWidgetFlag) != 0) != set) {
1942 iChangeFlags(d->flags, showLinkNumbers_DocumentWidgetFlag, set);
1943 /* Children have priority when handling events. */
1944 enableActions_DocumentWidget_(d, !set);
1945#if defined (iPlatformAppleDesktop)
1946 enableMenuItemsOnHomeRow_MacOS(!set);
1947#endif
1948 /* Ensure all keyboard events come here first. */
1949 setKeyboardGrab_Widget(set ? as_Widget(d) : NULL);
1950 if (d->menu) {
1951 setFlags_Widget(d->menu, disabled_WidgetFlag, set);
1952 }
1953 }
1954}
1955
1956static void requestUpdated_DocumentWidget_(iAnyObject *obj) {
1957 iDocumentWidget *d = obj;
1958 const int wasUpdated = exchange_Atomic(&d->isRequestUpdated, iTrue);
1959 if (!wasUpdated) {
1960 postCommand_Widget(obj,
1961 "document.request.updated doc:%p reqid:%u request:%p",
1962 d,
1963 id_GmRequest(d->request),
1964 d->request);
1965 }
1966}
1967
1968static void requestFinished_DocumentWidget_(iAnyObject *obj) {
1969 iDocumentWidget *d = obj;
1970 postCommand_Widget(obj,
1971 "document.request.finished doc:%p reqid:%u request:%p",
1972 d,
1973 id_GmRequest(d->request),
1974 d->request);
1975}
1976
1977static void animate_DocumentWidget_(void *ticker) {
1978 iDocumentWidget *d = ticker;
1979 iAssert(isInstance_Object(d, &Class_DocumentWidget));
1980 refresh_Widget(d);
1981 if (!isFinished_Anim(&d->view.sideOpacity) || !isFinished_Anim(&d->view.altTextOpacity) ||
1982 (d->linkInfo && !isFinished_Anim(&d->linkInfo->opacity))) {
1983 addTicker_App(animate_DocumentWidget_, d);
1984 }
811} 1985}
812 1986
813static uint32_t mediaUpdateInterval_DocumentWidget_(const iDocumentWidget *d) { 1987static uint32_t mediaUpdateInterval_DocumentWidget_(const iDocumentWidget *d) {
@@ -819,10 +1993,10 @@ static uint32_t mediaUpdateInterval_DocumentWidget_(const iDocumentWidget *d) {
819 } 1993 }
820 static const uint32_t invalidInterval_ = ~0u; 1994 static const uint32_t invalidInterval_ = ~0u;
821 uint32_t interval = invalidInterval_; 1995 uint32_t interval = invalidInterval_;
822 iConstForEach(PtrArray, i, &d->visibleMedia) { 1996 iConstForEach(PtrArray, i, &d->view.visibleMedia) {
823 const iGmRun *run = i.ptr; 1997 const iGmRun *run = i.ptr;
824 if (run->mediaType == audio_MediaType) { 1998 if (run->mediaType == audio_MediaType) {
825 iPlayer *plr = audioPlayer_Media(media_GmDocument(d->doc), mediaId_GmRun(run)); 1999 iPlayer *plr = audioPlayer_Media(media_GmDocument(d->view.doc), mediaId_GmRun(run));
826 if (flags_Player(plr) & adjustingVolume_PlayerFlag || 2000 if (flags_Player(plr) & adjustingVolume_PlayerFlag ||
827 (isStarted_Player(plr) && !isPaused_Player(plr))) { 2001 (isStarted_Player(plr) && !isPaused_Player(plr))) {
828 interval = iMin(interval, 1000 / 15); 2002 interval = iMin(interval, 1000 / 15);
@@ -845,10 +2019,10 @@ static uint32_t postMediaUpdate_DocumentWidget_(uint32_t interval, void *context
845static void updateMedia_DocumentWidget_(iDocumentWidget *d) { 2019static void updateMedia_DocumentWidget_(iDocumentWidget *d) {
846 if (document_App() == d) { 2020 if (document_App() == d) {
847 refresh_Widget(d); 2021 refresh_Widget(d);
848 iConstForEach(PtrArray, i, &d->visibleMedia) { 2022 iConstForEach(PtrArray, i, &d->view.visibleMedia) {
849 const iGmRun *run = i.ptr; 2023 const iGmRun *run = i.ptr;
850 if (run->mediaType == audio_MediaType) { 2024 if (run->mediaType == audio_MediaType) {
851 iPlayer *plr = audioPlayer_Media(media_GmDocument(d->doc), mediaId_GmRun(run)); 2025 iPlayer *plr = audioPlayer_Media(media_GmDocument(d->view.doc), mediaId_GmRun(run));
852 if (idleTimeMs_Player(plr) > 3000 && ~flags_Player(plr) & volumeGrabbed_PlayerFlag && 2026 if (idleTimeMs_Player(plr) > 3000 && ~flags_Player(plr) & volumeGrabbed_PlayerFlag &&
853 flags_Player(plr) & adjustingVolume_PlayerFlag) { 2027 flags_Player(plr) & adjustingVolume_PlayerFlag) {
854 setFlags_Player(plr, adjustingVolume_PlayerFlag, iFalse); 2028 setFlags_Player(plr, adjustingVolume_PlayerFlag, iFalse);
@@ -876,88 +2050,6 @@ static void animateMedia_DocumentWidget_(iDocumentWidget *d) {
876 } 2050 }
877} 2051}
878 2052
879static iRangecc currentHeading_DocumentWidget_(const iDocumentWidget *d) {
880 iRangecc heading = iNullRange;
881 if (d->visibleRuns.start) {
882 iConstForEach(Array, i, headings_GmDocument(d->doc)) {
883 const iGmHeading *head = i.value;
884 if (head->level == 0) {
885 if (head->text.start <= d->visibleRuns.start->text.start) {
886 heading = head->text;
887 }
888 if (d->visibleRuns.end && head->text.start > d->visibleRuns.end->text.start) {
889 break;
890 }
891 }
892 }
893 }
894 return heading;
895}
896
897static int updateScrollMax_DocumentWidget_(iDocumentWidget *d) {
898 arrange_Widget(d->footerButtons); /* scrollMax depends on footer height */
899 const int scrollMax = scrollMax_DocumentWidget_(d);
900 setMax_SmoothScroll(&d->scrollY, scrollMax);
901 return scrollMax;
902}
903
904static void updateVisible_DocumentWidget_(iDocumentWidget *d) {
905 iChangeFlags(d->flags,
906 centerVertically_DocumentWidgetFlag,
907 prefs_App()->centerShortDocs || startsWithCase_String(d->mod.url, "about:") ||
908 !isSuccess_GmStatusCode(d->sourceStatus));
909 const iRangei visRange = visibleRange_DocumentWidget_(d);
910// printf("visRange: %d...%d\n", visRange.start, visRange.end);
911 const iRect bounds = bounds_Widget(as_Widget(d));
912 const int scrollMax = updateScrollMax_DocumentWidget_(d);
913 /* Reposition the footer buttons as appropriate. */
914 /* TODO: You can just position `footerButtons` here completely without having to get
915 `Widget` involved with the offset in any way. */
916 setRange_ScrollWidget(d->scroll, (iRangei){ 0, scrollMax });
917 const int docSize = pageHeight_DocumentWidget_(d) + iMax(height_Widget(d->phoneToolbar),
918 height_Widget(d->footerButtons));
919 const float scrollPos = pos_SmoothScroll(&d->scrollY);
920 setThumb_ScrollWidget(d->scroll,
921 pos_SmoothScroll(&d->scrollY),
922 docSize > 0 ? height_Rect(bounds) * size_Range(&visRange) / docSize : 0);
923 if (d->footerButtons) {
924 const iRect bounds = bounds_Widget(as_Widget(d));
925 const iRect docBounds = documentBounds_DocumentWidget_(d);
926 const int hPad = (width_Rect(bounds) - iMin(120 * gap_UI, width_Rect(docBounds))) / 2;
927 const int vPad = 3 * gap_UI;
928 setPadding_Widget(d->footerButtons, hPad, 0, hPad, vPad);
929 d->footerButtons->rect.pos.y = height_Rect(bounds) - height_Widget(d->footerButtons) +
930 (scrollMax > 0 ? scrollMax - scrollPos : 0);
931 }
932 clear_PtrArray(&d->visibleLinks);
933 clear_PtrArray(&d->visibleWideRuns);
934 clear_PtrArray(&d->visiblePre);
935 clear_PtrArray(&d->visibleMedia);
936 const iRangecc oldHeading = currentHeading_DocumentWidget_(d);
937 /* Scan for visible runs. */ {
938 iZap(d->visibleRuns);
939 render_GmDocument(d->doc, visRange, addVisible_DocumentWidget_, d);
940 }
941 const iRangecc newHeading = currentHeading_DocumentWidget_(d);
942 if (memcmp(&oldHeading, &newHeading, sizeof(oldHeading))) {
943 d->drawBufs->flags |= updateSideBuf_DrawBufsFlag;
944 }
945 updateHover_DocumentWidget_(d, mouseCoord_Window(get_Window(), 0));
946 updateSideOpacity_DocumentWidget_(d, iTrue);
947 animateMedia_DocumentWidget_(d);
948 /* Remember scroll positions of recently visited pages. */ {
949 iRecentUrl *recent = mostRecentUrl_History(d->mod.history);
950 if (recent && docSize && d->state == ready_RequestState) {
951 recent->normScrollY = normScrollPos_DocumentWidget_(d);
952 }
953 }
954 /* After scrolling/resizing stops, begin pre-rendering the visbuf contents. */ {
955 removeTicker_App(prerender_DocumentWidget_, d);
956 remove_Periodic(periodic_App(), d);
957 add_Periodic(periodic_App(), d, "document.render");
958 }
959}
960
961static void updateWindowTitle_DocumentWidget_(const iDocumentWidget *d) { 2053static void updateWindowTitle_DocumentWidget_(const iDocumentWidget *d) {
962 iLabelWidget *tabButton = tabPageButton_Widget(findChild_Widget(root_Widget(constAs_Widget(d)), 2054 iLabelWidget *tabButton = tabPageButton_Widget(findChild_Widget(root_Widget(constAs_Widget(d)),
963 "doctabs"), d); 2055 "doctabs"), d);
@@ -966,8 +2058,8 @@ static void updateWindowTitle_DocumentWidget_(const iDocumentWidget *d) {
966 return; 2058 return;
967 } 2059 }
968 iStringArray *title = iClob(new_StringArray()); 2060 iStringArray *title = iClob(new_StringArray());
969 if (!isEmpty_String(title_GmDocument(d->doc))) { 2061 if (!isEmpty_String(title_GmDocument(d->view.doc))) {
970 pushBack_StringArray(title, title_GmDocument(d->doc)); 2062 pushBack_StringArray(title, title_GmDocument(d->view.doc));
971 } 2063 }
972 if (!isEmpty_String(d->titleUser)) { 2064 if (!isEmpty_String(d->titleUser)) {
973 pushBack_StringArray(title, d->titleUser); 2065 pushBack_StringArray(title, d->titleUser);
@@ -998,7 +2090,7 @@ static void updateWindowTitle_DocumentWidget_(const iDocumentWidget *d) {
998 setTitle_MainWindow(get_MainWindow(), text); 2090 setTitle_MainWindow(get_MainWindow(), text);
999 setWindow = iFalse; 2091 setWindow = iFalse;
1000 } 2092 }
1001 const iChar siteIcon = siteIcon_GmDocument(d->doc); 2093 const iChar siteIcon = siteIcon_GmDocument(d->view.doc);
1002 if (siteIcon) { 2094 if (siteIcon) {
1003 if (!isEmpty_String(text)) { 2095 if (!isEmpty_String(text)) {
1004 prependCStr_String(text, " " restore_ColorEscape); 2096 prependCStr_String(text, " " restore_ColorEscape);
@@ -1038,50 +2130,28 @@ static void updateWindowTitle_DocumentWidget_(const iDocumentWidget *d) {
1038 } 2130 }
1039} 2131}
1040 2132
1041static void updateTimestampBuf_DocumentWidget_(const iDocumentWidget *d) { 2133static void invalidate_DocumentWidget_(iDocumentWidget *d) {
1042 if (!isExposed_Window(get_Window())) { 2134 if (flags_Widget(as_Widget(d)) & destroyPending_WidgetFlag) {
1043 return; 2135 return;
1044 } 2136 }
1045 if (d->drawBufs->timestampBuf) { 2137 if (d->flags & invalidationPending_DocumentWidgetFlag) {
1046 delete_TextBuf(d->drawBufs->timestampBuf); 2138 return;
1047 d->drawBufs->timestampBuf = NULL;
1048 }
1049 if (isValid_Time(&d->sourceTime)) {
1050 iString *fmt = timeFormatHourPreference_Lang("page.timestamp");
1051 d->drawBufs->timestampBuf = newRange_TextBuf(
1052 uiLabel_FontId,
1053 white_ColorId,
1054 range_String(collect_String(format_Time(&d->sourceTime, cstr_String(fmt)))));
1055 delete_String(fmt);
1056 } 2139 }
1057 d->drawBufs->flags &= ~updateTimestampBuf_DrawBufsFlag; 2140 if (isAffectedByVisualOffset_Widget(as_Widget(d))) {
1058} 2141 d->flags |= invalidationPending_DocumentWidgetFlag;
1059
1060static void invalidate_DocumentWidget_(iDocumentWidget *d) {
1061 if (flags_Widget(as_Widget(d)) & destroyPending_WidgetFlag) {
1062 return; 2142 return;
1063 } 2143 }
1064 invalidate_VisBuf(d->visBuf); 2144 d->flags &= ~invalidationPending_DocumentWidgetFlag;
1065 clear_PtrSet(d->invalidRuns); 2145 invalidate_DocumentView_(&d->view);
2146// printf("[%p] '%s' invalidated\n", d, cstr_String(id_Widget(as_Widget(d))));
1066} 2147}
1067 2148
1068static iRangecc siteText_DocumentWidget_(const iDocumentWidget *d) { 2149static iRangecc siteText_DocumentWidget_(const iDocumentWidget *d) {
1069 return isEmpty_String(d->titleUser) ? urlHost_String(d->mod.url) 2150 return isEmpty_String(d->titleUser) ? urlHost_String(d->mod.url)
1070 : range_String(d->titleUser); 2151 : range_String(d->titleUser);
1071} 2152}
1072 2153
1073static void documentRunsInvalidated_DocumentWidget_(iDocumentWidget *d) { 2154static iBool isPinned_DocumentWidget_(const iDocumentWidget *d) {
1074 d->foundMark = iNullRange;
1075 d->selectMark = iNullRange;
1076 d->hoverPre = NULL;
1077 d->hoverAltPre = NULL;
1078 d->hoverLink = NULL;
1079 d->contextLink = NULL;
1080 iZap(d->visibleRuns);
1081 iZap(d->renderRuns);
1082}
1083
1084iBool isPinned_DocumentWidget_(const iDocumentWidget *d) {
1085 if (deviceType_App() == phone_AppDeviceType) { 2155 if (deviceType_App() == phone_AppDeviceType) {
1086 return iFalse; 2156 return iFalse;
1087 } 2157 }
@@ -1104,12 +2174,20 @@ static void showOrHidePinningIndicator_DocumentWidget_(iDocumentWidget *d) {
1104 isPinned_DocumentWidget_(d)); 2174 isPinned_DocumentWidget_(d));
1105} 2175}
1106 2176
2177static void updateBanner_DocumentWidget_(iDocumentWidget *d) {
2178 setSite_Banner(d->banner, siteText_DocumentWidget_(d), siteIcon_GmDocument(d->view.doc));
2179}
2180
1107static void documentWasChanged_DocumentWidget_(iDocumentWidget *d) { 2181static void documentWasChanged_DocumentWidget_(iDocumentWidget *d) {
1108 updateVisitedLinks_GmDocument(d->doc); 2182 iChangeFlags(d->flags, selecting_DocumentWidgetFlag, iFalse);
2183 setFlags_Widget(as_Widget(d), touchDrag_WidgetFlag, iFalse);
2184 d->requestLinkId = 0;
2185 updateVisitedLinks_GmDocument(d->view.doc);
1109 documentRunsInvalidated_DocumentWidget_(d); 2186 documentRunsInvalidated_DocumentWidget_(d);
1110 updateWindowTitle_DocumentWidget_(d); 2187 updateWindowTitle_DocumentWidget_(d);
1111 updateVisible_DocumentWidget_(d); 2188 updateBanner_DocumentWidget_(d);
1112 d->drawBufs->flags |= updateSideBuf_DrawBufsFlag; 2189 updateVisible_DocumentView_(&d->view);
2190 d->view.drawBufs->flags |= updateSideBuf_DrawBufsFlag;
1113 invalidate_DocumentWidget_(d); 2191 invalidate_DocumentWidget_(d);
1114 refresh_Widget(as_Widget(d)); 2192 refresh_Widget(as_Widget(d));
1115 /* Check for special bookmark tags. */ 2193 /* Check for special bookmark tags. */
@@ -1117,59 +2195,28 @@ static void documentWasChanged_DocumentWidget_(iDocumentWidget *d) {
1117 const uint16_t bmid = findUrl_Bookmarks(bookmarks_App(), d->mod.url); 2195 const uint16_t bmid = findUrl_Bookmarks(bookmarks_App(), d->mod.url);
1118 if (bmid) { 2196 if (bmid) {
1119 const iBookmark *bm = get_Bookmarks(bookmarks_App(), bmid); 2197 const iBookmark *bm = get_Bookmarks(bookmarks_App(), bmid);
1120 if (hasTag_Bookmark(bm, linkSplit_BookmarkTag)) { 2198 if (bm->flags & linkSplit_BookmarkFlag) {
1121 d->flags |= otherRootByDefault_DocumentWidgetFlag; 2199 d->flags |= otherRootByDefault_DocumentWidgetFlag;
1122 } 2200 }
1123 } 2201 }
1124 showOrHidePinningIndicator_DocumentWidget_(d); 2202 showOrHidePinningIndicator_DocumentWidget_(d);
1125 setCachedDocument_History(d->mod.history, 2203 if (~d->flags & fromCache_DocumentWidgetFlag) {
1126 d->doc, /* keeps a ref */ 2204 setCachedDocument_History(d->mod.history, d->view.doc /* keeps a ref */);
1127 (d->flags & openedFromSidebar_DocumentWidgetFlag) != 0); 2205 }
1128}
1129
1130void setSource_DocumentWidget(iDocumentWidget *d, const iString *source) {
1131 setUrl_GmDocument(d->doc, d->mod.url);
1132 const int docWidth = documentWidth_DocumentWidget_(d);
1133 setSource_GmDocument(d->doc,
1134 source,
1135 docWidth,
1136 width_Widget(d),
1137 isFinished_GmRequest(d->request) ? final_GmDocumentUpdate
1138 : partial_GmDocumentUpdate);
1139 setWidth_Banner(d->banner, docWidth);
1140 documentWasChanged_DocumentWidget_(d);
1141} 2206}
1142 2207
1143static void replaceDocument_DocumentWidget_(iDocumentWidget *d, iGmDocument *newDoc) { 2208static void replaceDocument_DocumentWidget_(iDocumentWidget *d, iGmDocument *newDoc) {
1144 pauseAllPlayers_Media(media_GmDocument(d->doc), iTrue); 2209 pauseAllPlayers_Media(media_GmDocument(d->view.doc), iTrue);
1145 iRelease(d->doc); 2210 iRelease(d->view.doc);
1146 d->doc = ref_Object(newDoc); 2211 d->view.doc = ref_Object(newDoc);
1147 documentWasChanged_DocumentWidget_(d); 2212 documentWasChanged_DocumentWidget_(d);
1148} 2213}
1149 2214
1150static void updateBanner_DocumentWidget_(iDocumentWidget *d) {
1151 /* TODO: Set width. */
1152 setSite_Banner(d->banner, siteText_DocumentWidget_(d), siteIcon_GmDocument(d->doc));
1153}
1154
1155static void updateTheme_DocumentWidget_(iDocumentWidget *d) { 2215static void updateTheme_DocumentWidget_(iDocumentWidget *d) {
1156 if (document_App() != d || category_GmStatusCode(d->sourceStatus) == categoryInput_GmStatusCode) { 2216 if (document_App() != d || category_GmStatusCode(d->sourceStatus) == categoryInput_GmStatusCode) {
1157 return; 2217 return;
1158 } 2218 }
1159 if (equalCase_Rangecc(urlScheme_String(d->mod.url), "file")) { 2219 d->view.drawBufs->flags |= updateTimestampBuf_DrawBufsFlag;
1160 iBlock empty;
1161 init_Block(&empty, 0);
1162 setThemeSeed_GmDocument(d->doc, &empty);
1163 deinit_Block(&empty);
1164 }
1165 else if (isEmpty_String(d->titleUser)) {
1166 setThemeSeed_GmDocument(d->doc,
1167 collect_Block(newRange_Block(urlHost_String(d->mod.url))));
1168 }
1169 else {
1170 setThemeSeed_GmDocument(d->doc, &d->titleUser->chars);
1171 }
1172 d->drawBufs->flags |= updateTimestampBuf_DrawBufsFlag;
1173 updateBanner_DocumentWidget_(d); 2220 updateBanner_DocumentWidget_(d);
1174} 2221}
1175 2222
@@ -1186,7 +2233,6 @@ static void makeFooterButtons_DocumentWidget_(iDocumentWidget *d, const iMenuIte
1186 resizeWidthOfChildren_WidgetFlag | arrangeHeight_WidgetFlag | 2233 resizeWidthOfChildren_WidgetFlag | arrangeHeight_WidgetFlag |
1187 fixedPosition_WidgetFlag | resizeToParentWidth_WidgetFlag, 2234 fixedPosition_WidgetFlag | resizeToParentWidth_WidgetFlag,
1188 iTrue); 2235 iTrue);
1189 //setBackgroundColor_Widget(d->footerButtons, tmBackground_ColorId);
1190 for (size_t i = 0; i < count; ++i) { 2236 for (size_t i = 0; i < count; ++i) {
1191 iLabelWidget *button = addChildFlags_Widget( 2237 iLabelWidget *button = addChildFlags_Widget(
1192 d->footerButtons, 2238 d->footerButtons,
@@ -1196,25 +2242,18 @@ static void makeFooterButtons_DocumentWidget_(iDocumentWidget *d, const iMenuIte
1196 setPadding1_Widget(as_Widget(button), gap_UI / 2); 2242 setPadding1_Widget(as_Widget(button), gap_UI / 2);
1197 checkIcon_LabelWidget(button); 2243 checkIcon_LabelWidget(button);
1198 setFont_LabelWidget(button, uiContent_FontId); 2244 setFont_LabelWidget(button, uiContent_FontId);
1199 } 2245 setBackgroundColor_Widget(as_Widget(button), uiBackgroundSidebar_ColorId);
1200 if (deviceType_App() == phone_AppDeviceType) {
1201 /* Footer buttons shouldn't be under the toolbar. */
1202 addChild_Widget(d->footerButtons, iClob(makePadding_Widget(height_Widget(d->phoneToolbar))));
1203 } 2246 }
1204 addChild_Widget(as_Widget(d), iClob(d->footerButtons)); 2247 addChild_Widget(as_Widget(d), iClob(d->footerButtons));
1205 arrange_Widget(d->footerButtons); 2248 arrange_Widget(d->footerButtons);
1206 arrange_Widget(w); 2249 arrange_Widget(w);
1207 updateVisible_DocumentWidget_(d); /* final placement for the buttons */ 2250 updateVisible_DocumentView_(&d->view); /* final placement for the buttons */
1208} 2251}
1209 2252
1210static void showErrorPage_DocumentWidget_(iDocumentWidget *d, enum iGmStatusCode code, 2253static void showErrorPage_DocumentWidget_(iDocumentWidget *d, enum iGmStatusCode code,
1211 const iString *meta) { 2254 const iString *meta) {
1212 /* TODO: No such thing as an "error page". It should be an empty page with an error banner. */ 2255 iString *src = collectNew_String();
1213 iString *src = collectNew_String();
1214 const iGmError *msg = get_GmError(code); 2256 const iGmError *msg = get_GmError(code);
1215 // appendChar_String(src, msg->icon ? msg->icon : 0x2327); /* X in a box */
1216 //appendFormat_String(src, " %s\n%s", msg->title, msg->info);
1217// iBool useBanner = iTrue;
1218 destroy_Widget(d->footerButtons); 2257 destroy_Widget(d->footerButtons);
1219 d->footerButtons = NULL; 2258 d->footerButtons = NULL;
1220 const iString *serverErrorMsg = NULL; 2259 const iString *serverErrorMsg = NULL;
@@ -1264,6 +2303,11 @@ static void showErrorPage_DocumentWidget_(iDocumentWidget *d, enum iGmStatusCode
1264 0, 2303 0,
1265 format_CStr("document.setmediatype mime:%s", mtype) }); 2304 format_CStr("document.setmediatype mime:%s", mtype) });
1266 } 2305 }
2306 pushBack_Array(&items,
2307 &(iMenuItem){ export_Icon " ${menu.open.external}",
2308 SDLK_RETURN,
2309 KMOD_PRIMARY,
2310 "document.save extview:1" });
1267 pushBack_Array( 2311 pushBack_Array(
1268 &items, 2312 &items,
1269 &(iMenuItem){ translateCStr_Lang(download_Icon " " saveToDownloads_Label), 2313 &(iMenuItem){ translateCStr_Lang(download_Icon " " saveToDownloads_Label),
@@ -1285,12 +2329,18 @@ static void showErrorPage_DocumentWidget_(iDocumentWidget *d, enum iGmStatusCode
1285 if (category_GmStatusCode(code) == categoryClientCertificate_GmStatus) { 2329 if (category_GmStatusCode(code) == categoryClientCertificate_GmStatus) {
1286 makeFooterButtons_DocumentWidget_( 2330 makeFooterButtons_DocumentWidget_(
1287 d, 2331 d,
1288 (iMenuItem[]){ { leftHalf_Icon " ${menu.show.identities}", '4', KMOD_PRIMARY, "sidebar.mode arg:3 show:1" }, 2332 (iMenuItem[]){
1289 { person_Icon " ${menu.identity.new}", newIdentity_KeyShortcut, "ident.new" } }, 2333 { leftHalf_Icon " ${menu.show.identities}",
2334 '4',
2335 KMOD_PRIMARY,
2336 deviceType_App() == desktop_AppDeviceType ? "sidebar.mode arg:3 show:1"
2337 : "preferences idents:1" },
2338 { person_Icon " ${menu.identity.new}", newIdentity_KeyShortcut, "ident.new" } },
1290 2); 2339 2);
1291 } 2340 }
1292 /* Make a new document for the error page.*/ 2341 /* Make a new document for the error page.*/
1293 iGmDocument *errorDoc = new_GmDocument(); 2342 iGmDocument *errorDoc = new_GmDocument();
2343 setWidth_GmDocument(errorDoc, documentWidth_DocumentView_(&d->view), width_Widget(d));
1294 setUrl_GmDocument(errorDoc, d->mod.url); 2344 setUrl_GmDocument(errorDoc, d->mod.url);
1295 setFormat_GmDocument(errorDoc, gemini_SourceFormat); 2345 setFormat_GmDocument(errorDoc, gemini_SourceFormat);
1296 replaceDocument_DocumentWidget_(d, errorDoc); 2346 replaceDocument_DocumentWidget_(d, errorDoc);
@@ -1300,10 +2350,7 @@ static void showErrorPage_DocumentWidget_(iDocumentWidget *d, enum iGmStatusCode
1300 d->state = ready_RequestState; 2350 d->state = ready_RequestState;
1301 setSource_DocumentWidget(d, src); 2351 setSource_DocumentWidget(d, src);
1302 updateTheme_DocumentWidget_(d); 2352 updateTheme_DocumentWidget_(d);
1303 reset_SmoothScroll(&d->scrollY); 2353 resetScroll_DocumentView_(&d->view);
1304 init_Anim(&d->sideOpacity, 0);
1305 init_Anim(&d->altTextOpacity, 0);
1306 resetWideRuns_DocumentWidget_(d);
1307} 2354}
1308 2355
1309static void updateFetchProgress_DocumentWidget_(iDocumentWidget *d) { 2356static void updateFetchProgress_DocumentWidget_(iDocumentWidget *d) {
@@ -1419,9 +2466,9 @@ static void postProcessRequestContent_DocumentWidget_(iDocumentWidget *d, iBool
1419 "document.save" } }, 2466 "document.save" } },
1420 2); 2467 2);
1421 } 2468 }
1422 if (preloadCoverImage_Gempub(d->sourceGempub, d->doc)) { 2469 if (preloadCoverImage_Gempub(d->sourceGempub, d->view.doc)) {
1423 redoLayout_GmDocument(d->doc); 2470 redoLayout_GmDocument(d->view.doc);
1424 updateVisible_DocumentWidget_(d); 2471 updateVisible_DocumentView_(&d->view);
1425 invalidate_DocumentWidget_(d); 2472 invalidate_DocumentWidget_(d);
1426 } 2473 }
1427 } 2474 }
@@ -1497,7 +2544,7 @@ static void postProcessRequestContent_DocumentWidget_(iDocumentWidget *d, iBool
1497 } 2544 }
1498 else { 2545 else {
1499 postCommandf_App( 2546 postCommandf_App(
1500 "open newtab:%d url:%s", otherRoot_OpenTabFlag, cstr_String(navStart)); 2547 "open splitmode:1 newtab:%d url:%s", otherRoot_OpenTabFlag, cstr_String(navStart));
1501 } 2548 }
1502 } 2549 }
1503 } 2550 }
@@ -1525,7 +2572,7 @@ static void updateDocument_DocumentWidget_(iDocumentWidget *d,
1525 } 2572 }
1526 clear_String(&d->sourceMime); 2573 clear_String(&d->sourceMime);
1527 d->sourceTime = response->when; 2574 d->sourceTime = response->when;
1528 d->drawBufs->flags |= updateTimestampBuf_DrawBufsFlag; 2575 d->view.drawBufs->flags |= updateTimestampBuf_DrawBufsFlag;
1529 initBlock_String(&str, &response->body); /* Note: Body may be megabytes in size. */ 2576 initBlock_String(&str, &response->body); /* Note: Body may be megabytes in size. */
1530 if (isSuccess_GmStatusCode(statusCode)) { 2577 if (isSuccess_GmStatusCode(statusCode)) {
1531 /* Check the MIME type. */ 2578 /* Check the MIME type. */
@@ -1565,7 +2612,7 @@ static void updateDocument_DocumentWidget_(iDocumentWidget *d,
1565 setRange_String(&d->sourceMime, param); 2612 setRange_String(&d->sourceMime, param);
1566 format_String(&str, "# TrueType Font\n"); 2613 format_String(&str, "# TrueType Font\n");
1567 iString *decUrl = collect_String(urlDecode_String(d->mod.url)); 2614 iString *decUrl = collect_String(urlDecode_String(d->mod.url));
1568 iRangecc name = baseName_Path(decUrl); 2615 iRangecc name = baseNameSep_Path(decUrl, "/");
1569 iBool isInstalled = iFalse; 2616 iBool isInstalled = iFalse;
1570 if (startsWith_String(collect_String(localFilePathFromUrl_String(d->mod.url)), 2617 if (startsWith_String(collect_String(localFilePathFromUrl_String(d->mod.url)),
1571 cstr_String(dataDir_App()))) { 2618 cstr_String(dataDir_App()))) {
@@ -1628,7 +2675,7 @@ static void updateDocument_DocumentWidget_(iDocumentWidget *d,
1628 appendFormat_String(&str, 2675 appendFormat_String(&str,
1629 cstr_Lang("doc.archive"), 2676 cstr_Lang("doc.archive"),
1630 cstr_Rangecc(baseName_Path(d->mod.url))); 2677 cstr_Rangecc(baseName_Path(d->mod.url)));
1631 appendCStr_String(&str, "\n"); 2678 appendCStr_String(&str, "\n");
1632 } 2679 }
1633 appendCStr_String(&str, "\n"); 2680 appendCStr_String(&str, "\n");
1634 iString *localPath = localFilePathFromUrl_String(d->mod.url); 2681 iString *localPath = localFilePathFromUrl_String(d->mod.url);
@@ -1669,16 +2716,16 @@ static void updateDocument_DocumentWidget_(iDocumentWidget *d,
1669 format_String(&str, "=> %s %s\n", 2716 format_String(&str, "=> %s %s\n",
1670 cstr_String(canonicalUrl_String(d->mod.url)), 2717 cstr_String(canonicalUrl_String(d->mod.url)),
1671 linkTitle); 2718 linkTitle);
1672 setData_Media(media_GmDocument(d->doc), 2719 setData_Media(media_GmDocument(d->view.doc),
1673 imgLinkId, 2720 imgLinkId,
1674 mimeStr, 2721 mimeStr,
1675 &response->body, 2722 &response->body,
1676 !isRequestFinished ? partialData_MediaFlag : 0); 2723 !isRequestFinished ? partialData_MediaFlag : 0);
1677 redoLayout_GmDocument(d->doc); 2724 redoLayout_GmDocument(d->view.doc);
1678 } 2725 }
1679 else if (isAudio && !isInitialUpdate) { 2726 else if (isAudio && !isInitialUpdate) {
1680 /* Update the audio content. */ 2727 /* Update the audio content. */
1681 setData_Media(media_GmDocument(d->doc), 2728 setData_Media(media_GmDocument(d->view.doc),
1682 imgLinkId, 2729 imgLinkId,
1683 mimeStr, 2730 mimeStr,
1684 &response->body, 2731 &response->body,
@@ -1708,11 +2755,12 @@ static void updateDocument_DocumentWidget_(iDocumentWidget *d,
1708 return; 2755 return;
1709 } 2756 }
1710 d->flags |= drawDownloadCounter_DocumentWidgetFlag; 2757 d->flags |= drawDownloadCounter_DocumentWidgetFlag;
1711 clear_PtrSet(d->invalidRuns); 2758 clear_PtrSet(d->view.invalidRuns);
2759 documentRunsInvalidated_DocumentWidget_(d);
1712 deinit_String(&str); 2760 deinit_String(&str);
1713 return; 2761 return;
1714 } 2762 }
1715 setFormat_GmDocument(d->doc, docFormat); 2763 setFormat_GmDocument(d->view.doc, docFormat);
1716 /* Convert the source to UTF-8 if needed. */ 2764 /* Convert the source to UTF-8 if needed. */
1717 if (!equalCase_Rangecc(charset, "utf-8")) { 2765 if (!equalCase_Rangecc(charset, "utf-8")) {
1718 set_String(&str, 2766 set_String(&str,
@@ -1721,7 +2769,7 @@ static void updateDocument_DocumentWidget_(iDocumentWidget *d,
1721 } 2769 }
1722 if (cachedDoc) { 2770 if (cachedDoc) {
1723 replaceDocument_DocumentWidget_(d, cachedDoc); 2771 replaceDocument_DocumentWidget_(d, cachedDoc);
1724 updateWidth_GmDocument(d->doc, documentWidth_DocumentWidget_(d), width_Widget(d)); 2772 updateWidth_DocumentView_(&d->view);
1725 } 2773 }
1726 else if (setSource) { 2774 else if (setSource) {
1727 setSource_DocumentWidget(d, &str); 2775 setSource_DocumentWidget(d, &str);
@@ -1731,6 +2779,7 @@ static void updateDocument_DocumentWidget_(iDocumentWidget *d,
1731} 2779}
1732 2780
1733static void fetch_DocumentWidget_(iDocumentWidget *d) { 2781static void fetch_DocumentWidget_(iDocumentWidget *d) {
2782 iAssert(~d->flags & animationPlaceholder_DocumentWidgetFlag);
1734 /* Forget the previous request. */ 2783 /* Forget the previous request. */
1735 if (d->request) { 2784 if (d->request) {
1736 iRelease(d->request); 2785 iRelease(d->request);
@@ -1740,8 +2789,6 @@ static void fetch_DocumentWidget_(iDocumentWidget *d) {
1740 "document.request.started doc:%p url:%s", 2789 "document.request.started doc:%p url:%s",
1741 d, 2790 d,
1742 cstr_String(d->mod.url)); 2791 cstr_String(d->mod.url));
1743 clear_ObjectList(d->media);
1744 d->certFlags = 0;
1745 setLinkNumberMode_DocumentWidget_(d, iFalse); 2792 setLinkNumberMode_DocumentWidget_(d, iFalse);
1746 d->flags &= ~drawDownloadCounter_DocumentWidgetFlag; 2793 d->flags &= ~drawDownloadCounter_DocumentWidgetFlag;
1747 d->state = fetching_RequestState; 2794 d->state = fetching_RequestState;
@@ -1774,7 +2821,7 @@ static void updateTrust_DocumentWidget_(iDocumentWidget *d, const iGmResponse *r
1774 } 2821 }
1775 else if (~d->certFlags & timeVerified_GmCertFlag) { 2822 else if (~d->certFlags & timeVerified_GmCertFlag) {
1776 updateTextCStr_LabelWidget(lock, isDarkMode ? orange_ColorEscape warning_Icon 2823 updateTextCStr_LabelWidget(lock, isDarkMode ? orange_ColorEscape warning_Icon
1777 : black_ColorEscape warning_Icon); 2824 : black_ColorEscape warning_Icon);
1778 } 2825 }
1779 else { 2826 else {
1780 updateTextCStr_LabelWidget(lock, green_ColorEscape closedLock_Icon); 2827 updateTextCStr_LabelWidget(lock, green_ColorEscape closedLock_Icon);
@@ -1793,17 +2840,24 @@ static void cacheRunGlyphs_(void *data, const iGmRun *run) {
1793} 2840}
1794 2841
1795static void cacheDocumentGlyphs_DocumentWidget_(const iDocumentWidget *d) { 2842static void cacheDocumentGlyphs_DocumentWidget_(const iDocumentWidget *d) {
1796 if (isFinishedLaunching_App() && isExposed_Window(get_Window())) { 2843 if (isFinishedLaunching_App() && isExposed_Window(get_Window()) &&
2844 ~d->flags & animationPlaceholder_DocumentWidgetFlag) {
1797 /* Just cache the top of the document, since this is what we usually need. */ 2845 /* Just cache the top of the document, since this is what we usually need. */
1798 int maxY = height_Widget(&d->widget) * 2; 2846 int maxY = height_Widget(&d->widget) * 2;
1799 if (maxY == 0) { 2847 if (maxY == 0) {
1800 maxY = size_GmDocument(d->doc).y; 2848 maxY = size_GmDocument(d->view.doc).y;
1801 } 2849 }
1802 render_GmDocument(d->doc, (iRangei){ 0, maxY }, cacheRunGlyphs_, NULL); 2850 render_GmDocument(d->view.doc, (iRangei){ 0, maxY }, cacheRunGlyphs_, NULL);
1803 } 2851 }
1804} 2852}
1805 2853
1806static void addBannerWarnings_DocumentWidget_(iDocumentWidget *d) { 2854static void addBannerWarnings_DocumentWidget_(iDocumentWidget *d) {
2855 updateBanner_DocumentWidget_(d);
2856 /* Warnings are not shown on internal pages. */
2857 if (equalCase_Rangecc(urlScheme_String(d->mod.url), "about")) {
2858 clear_Banner(d->banner);
2859 return;
2860 }
1807 /* Warnings related to certificates and trust. */ 2861 /* Warnings related to certificates and trust. */
1808 const int certFlags = d->certFlags; 2862 const int certFlags = d->certFlags;
1809 const int req = timeVerified_GmCertFlag | domainVerified_GmCertFlag | trusted_GmCertFlag; 2863 const int req = timeVerified_GmCertFlag | domainVerified_GmCertFlag | trusted_GmCertFlag;
@@ -1850,7 +2904,7 @@ static void addBannerWarnings_DocumentWidget_(iDocumentWidget *d) {
1850 value_SiteSpec(collectNewRange_String(urlRoot_String(d->mod.url)), 2904 value_SiteSpec(collectNewRange_String(urlRoot_String(d->mod.url)),
1851 dismissWarnings_SiteSpecKey) | 2905 dismissWarnings_SiteSpecKey) |
1852 (!prefs_App()->warnAboutMissingGlyphs ? missingGlyphs_GmDocumentWarning : 0); 2906 (!prefs_App()->warnAboutMissingGlyphs ? missingGlyphs_GmDocumentWarning : 0);
1853 const int warnings = warnings_GmDocument(d->doc) & ~dismissed; 2907 const int warnings = warnings_GmDocument(d->view.doc) & ~dismissed;
1854 if (warnings & missingGlyphs_GmDocumentWarning) { 2908 if (warnings & missingGlyphs_GmDocumentWarning) {
1855 add_Banner(d->banner, warning_BannerType, missingGlyphs_GmStatusCode, NULL, NULL); 2909 add_Banner(d->banner, warning_BannerType, missingGlyphs_GmStatusCode, NULL, NULL);
1856 /* TODO: List one or more of the missing characters and/or their Unicode blocks? */ 2910 /* TODO: List one or more of the missing characters and/or their Unicode blocks? */
@@ -1862,17 +2916,18 @@ static void addBannerWarnings_DocumentWidget_(iDocumentWidget *d) {
1862 2916
1863static void updateFromCachedResponse_DocumentWidget_(iDocumentWidget *d, float normScrollY, 2917static void updateFromCachedResponse_DocumentWidget_(iDocumentWidget *d, float normScrollY,
1864 const iGmResponse *resp, iGmDocument *cachedDoc) { 2918 const iGmResponse *resp, iGmDocument *cachedDoc) {
2919// iAssert(width_Widget(d) > 0); /* must be laid out by now */
1865 setLinkNumberMode_DocumentWidget_(d, iFalse); 2920 setLinkNumberMode_DocumentWidget_(d, iFalse);
1866 clear_ObjectList(d->media); 2921 clear_ObjectList(d->media);
1867 delete_Gempub(d->sourceGempub); 2922 delete_Gempub(d->sourceGempub);
1868 d->sourceGempub = NULL; 2923 d->sourceGempub = NULL;
1869 pauseAllPlayers_Media(media_GmDocument(d->doc), iTrue); 2924 pauseAllPlayers_Media(media_GmDocument(d->view.doc), iTrue);
1870 iRelease(d->doc);
1871 destroy_Widget(d->footerButtons); 2925 destroy_Widget(d->footerButtons);
1872 d->footerButtons = NULL; 2926 d->footerButtons = NULL;
1873 d->doc = new_GmDocument(); 2927 iRelease(d->view.doc);
1874 resetWideRuns_DocumentWidget_(d); 2928 d->view.doc = new_GmDocument();
1875 d->state = fetching_RequestState; 2929 d->state = fetching_RequestState;
2930 d->flags |= fromCache_DocumentWidgetFlag;
1876 /* Do the fetch. */ { 2931 /* Do the fetch. */ {
1877 d->initNormScrollY = normScrollY; 2932 d->initNormScrollY = normScrollY;
1878 /* Use the cached response data. */ 2933 /* Use the cached response data. */
@@ -1881,36 +2936,37 @@ static void updateFromCachedResponse_DocumentWidget_(iDocumentWidget *d, float n
1881 d->sourceStatus = success_GmStatusCode; 2936 d->sourceStatus = success_GmStatusCode;
1882 format_String(&d->sourceHeader, cstr_Lang("pageinfo.header.cached")); 2937 format_String(&d->sourceHeader, cstr_Lang("pageinfo.header.cached"));
1883 set_Block(&d->sourceContent, &resp->body); 2938 set_Block(&d->sourceContent, &resp->body);
2939 if (!cachedDoc) {
2940 updateWidthAndRedoLayout_DocumentView_(&d->view);
2941 }
1884 updateDocument_DocumentWidget_(d, resp, cachedDoc, iTrue); 2942 updateDocument_DocumentWidget_(d, resp, cachedDoc, iTrue);
1885// setCachedDocument_History(d->mod.history, d->doc,
1886// (d->flags & openedFromSidebar_DocumentWidgetFlag) != 0);
1887 clear_Banner(d->banner); 2943 clear_Banner(d->banner);
1888 updateBanner_DocumentWidget_(d); 2944 updateBanner_DocumentWidget_(d);
1889 addBannerWarnings_DocumentWidget_(d); 2945 addBannerWarnings_DocumentWidget_(d);
1890 } 2946 }
1891 d->state = ready_RequestState; 2947 d->state = ready_RequestState;
1892 postProcessRequestContent_DocumentWidget_(d, iTrue); 2948 postProcessRequestContent_DocumentWidget_(d, iTrue);
1893 init_Anim(&d->altTextOpacity, 0); 2949 resetScroll_DocumentView_(&d->view);
1894 reset_SmoothScroll(&d->scrollY); 2950 init_Anim(&d->view.scrollY.pos, d->initNormScrollY * pageHeight_DocumentView_(&d->view));
1895 init_Anim(&d->scrollY.pos, d->initNormScrollY * pageHeight_DocumentWidget_(d)); 2951 updateVisible_DocumentView_(&d->view);
1896 updateSideOpacity_DocumentWidget_(d, iFalse); 2952 moveSpan_SmoothScroll(&d->view.scrollY, 0, 0); /* clamp position to new max */
1897 updateVisible_DocumentWidget_(d); 2953 updateSideOpacity_DocumentView_(&d->view, iFalse);
1898 moveSpan_SmoothScroll(&d->scrollY, 0, 0); /* clamp position to new max */
1899 cacheDocumentGlyphs_DocumentWidget_(d); 2954 cacheDocumentGlyphs_DocumentWidget_(d);
1900 d->drawBufs->flags |= updateTimestampBuf_DrawBufsFlag | updateSideBuf_DrawBufsFlag; 2955 d->view.drawBufs->flags |= updateTimestampBuf_DrawBufsFlag | updateSideBuf_DrawBufsFlag;
1901 d->flags &= ~(urlChanged_DocumentWidgetFlag | drawDownloadCounter_DocumentWidgetFlag); 2956 d->flags &= ~(urlChanged_DocumentWidgetFlag | drawDownloadCounter_DocumentWidgetFlag);
1902 postCommandf_Root( 2957 postCommandf_Root(
1903 as_Widget(d)->root, "document.changed doc:%p url:%s", d, cstr_String(d->mod.url)); 2958 as_Widget(d)->root, "document.changed doc:%p url:%s", d, cstr_String(d->mod.url));
1904} 2959}
1905 2960
1906static iBool updateFromHistory_DocumentWidget_(iDocumentWidget *d) { 2961static iBool updateFromHistory_DocumentWidget_(iDocumentWidget *d) {
1907 const iRecentUrl *recent = findUrl_History(d->mod.history, d->mod.url); 2962 const iRecentUrl *recent = constMostRecentUrl_History(d->mod.history);
1908 if (recent && recent->cachedResponse) { 2963 if (recent && recent->cachedResponse && equalCase_String(&recent->url, d->mod.url)) {
1909 iChangeFlags(d->flags,
1910 openedFromSidebar_DocumentWidgetFlag,
1911 recent->flags.openedFromSidebar);
1912 updateFromCachedResponse_DocumentWidget_( 2964 updateFromCachedResponse_DocumentWidget_(
1913 d, recent->normScrollY, recent->cachedResponse, recent->cachedDoc); 2965 d, recent->normScrollY, recent->cachedResponse, recent->cachedDoc);
2966 if (!recent->cachedDoc) {
2967 /* We have a cached copy now. */
2968 setCachedDocument_History(d->mod.history, d->view.doc);
2969 }
1914 return iTrue; 2970 return iTrue;
1915 } 2971 }
1916 else if (!isEmpty_String(d->mod.url)) { 2972 else if (!isEmpty_String(d->mod.url)) {
@@ -1924,23 +2980,25 @@ static iBool updateFromHistory_DocumentWidget_(iDocumentWidget *d) {
1924} 2980}
1925 2981
1926static void refreshWhileScrolling_DocumentWidget_(iAny *ptr) { 2982static void refreshWhileScrolling_DocumentWidget_(iAny *ptr) {
2983 iAssert(isInstance_Object(ptr, &Class_DocumentWidget));
1927 iDocumentWidget *d = ptr; 2984 iDocumentWidget *d = ptr;
1928 updateVisible_DocumentWidget_(d); 2985 iDocumentView *view = &d->view;
2986 updateVisible_DocumentView_(view);
1929 refresh_Widget(d); 2987 refresh_Widget(d);
1930 if (d->animWideRunId) { 2988 if (view->animWideRunId) {
1931 for (const iGmRun *r = d->animWideRunRange.start; r != d->animWideRunRange.end; r++) { 2989 for (const iGmRun *r = view->animWideRunRange.start; r != view->animWideRunRange.end; r++) {
1932 insert_PtrSet(d->invalidRuns, r); 2990 insert_PtrSet(view->invalidRuns, r);
1933 } 2991 }
1934 } 2992 }
1935 if (isFinished_Anim(&d->animWideRunOffset)) { 2993 if (isFinished_Anim(&view->animWideRunOffset)) {
1936 d->animWideRunId = 0; 2994 view->animWideRunId = 0;
1937 } 2995 }
1938 if (!isFinished_SmoothScroll(&d->scrollY) || !isFinished_Anim(&d->animWideRunOffset)) { 2996 if (!isFinished_SmoothScroll(&view->scrollY) || !isFinished_Anim(&view->animWideRunOffset)) {
1939 addTicker_App(refreshWhileScrolling_DocumentWidget_, d); 2997 addTicker_App(refreshWhileScrolling_DocumentWidget_, d);
1940 } 2998 }
1941 if (isFinished_SmoothScroll(&d->scrollY)) { 2999 if (isFinished_SmoothScroll(&view->scrollY)) {
1942 iChangeFlags(d->flags, noHoverWhileScrolling_DocumentWidgetFlag, iFalse); 3000 iChangeFlags(d->flags, noHoverWhileScrolling_DocumentWidgetFlag, iFalse);
1943 updateHover_DocumentWidget_(d, mouseCoord_Window(get_Window(), 0)); 3001 updateHover_DocumentView_(view, mouseCoord_Window(get_Window(), 0));
1944 } 3002 }
1945} 3003}
1946 3004
@@ -1949,16 +3007,16 @@ static void scrollBegan_DocumentWidget_(iAnyObject *any, int offset, uint32_t du
1949 /* Get rid of link numbers when scrolling. */ 3007 /* Get rid of link numbers when scrolling. */
1950 if (offset && d->flags & showLinkNumbers_DocumentWidgetFlag) { 3008 if (offset && d->flags & showLinkNumbers_DocumentWidgetFlag) {
1951 setLinkNumberMode_DocumentWidget_(d, iFalse); 3009 setLinkNumberMode_DocumentWidget_(d, iFalse);
1952 invalidateVisibleLinks_DocumentWidget_(d); 3010 invalidateVisibleLinks_DocumentView_(&d->view);
1953 } 3011 }
1954 /* Show and hide toolbar on scroll. */ 3012 /* Show and hide toolbar on scroll. */
1955 if (deviceType_App() == phone_AppDeviceType) { 3013 if (deviceType_App() == phone_AppDeviceType) {
1956 const float normPos = normScrollPos_DocumentWidget_(d); 3014 const float normPos = normScrollPos_DocumentView_(&d->view);
1957 if (prefs_App()->hideToolbarOnScroll && iAbs(offset) > 5 && normPos >= 0) { 3015 if (prefs_App()->hideToolbarOnScroll && iAbs(offset) > 5 && normPos >= 0) {
1958 showToolbar_Root(as_Widget(d)->root, offset < 0); 3016 showToolbar_Root(as_Widget(d)->root, offset < 0);
1959 } 3017 }
1960 } 3018 }
1961 updateVisible_DocumentWidget_(d); 3019 updateVisible_DocumentView_(&d->view);
1962 refresh_Widget(as_Widget(d)); 3020 refresh_Widget(as_Widget(d));
1963 if (duration > 0) { 3021 if (duration > 0) {
1964 iChangeFlags(d->flags, noHoverWhileScrolling_DocumentWidgetFlag, iTrue); 3022 iChangeFlags(d->flags, noHoverWhileScrolling_DocumentWidgetFlag, iTrue);
@@ -1966,98 +3024,14 @@ static void scrollBegan_DocumentWidget_(iAnyObject *any, int offset, uint32_t du
1966 } 3024 }
1967} 3025}
1968 3026
1969static void clampScroll_DocumentWidget_(iDocumentWidget *d) {
1970 move_SmoothScroll(&d->scrollY, 0);
1971}
1972
1973static void immediateScroll_DocumentWidget_(iDocumentWidget *d, int offset) {
1974 move_SmoothScroll(&d->scrollY, offset);
1975}
1976
1977static void smoothScroll_DocumentWidget_(iDocumentWidget *d, int offset, int duration) {
1978 moveSpan_SmoothScroll(&d->scrollY, offset, duration);
1979}
1980
1981static void scrollTo_DocumentWidget_(iDocumentWidget *d, int documentY, iBool centered) {
1982 if (!isEmpty_Banner(d->banner)) {
1983 documentY += height_Banner(d->banner) + documentTopPad_DocumentWidget_(d);
1984 }
1985 else {
1986 documentY += documentTopPad_DocumentWidget_(d) + d->pageMargin * gap_UI;
1987 }
1988 init_Anim(&d->scrollY.pos,
1989 documentY - (centered ? documentBounds_DocumentWidget_(d).size.y / 2
1990 : lineHeight_Text(paragraph_FontId)));
1991 clampScroll_DocumentWidget_(d);
1992}
1993
1994static void scrollToHeading_DocumentWidget_(iDocumentWidget *d, const char *heading) {
1995 iConstForEach(Array, h, headings_GmDocument(d->doc)) {
1996 const iGmHeading *head = h.value;
1997 if (startsWithCase_Rangecc(head->text, heading)) {
1998 postCommandf_Root(as_Widget(d)->root, "document.goto loc:%p", head->text.start);
1999 break;
2000 }
2001 }
2002}
2003
2004static void scrollWideBlock_DocumentWidget_(iDocumentWidget *d, iInt2 mousePos, int delta,
2005 int duration) {
2006 if (delta == 0) {
2007 return;
2008 }
2009 const iInt2 docPos = documentPos_DocumentWidget_(d, mousePos);
2010 iConstForEach(PtrArray, i, &d->visibleWideRuns) {
2011 const iGmRun *run = i.ptr;
2012 if (docPos.y >= top_Rect(run->bounds) && docPos.y <= bottom_Rect(run->bounds)) {
2013 /* We can scroll this run. First find out how much is allowed. */
2014 const iGmRunRange range = findPreformattedRange_GmDocument(d->doc, run);
2015 int maxWidth = 0;
2016 for (const iGmRun *r = range.start; r != range.end; r++) {
2017 maxWidth = iMax(maxWidth, width_Rect(r->visBounds));
2018 }
2019 const int maxOffset = maxWidth - documentWidth_DocumentWidget_(d) + d->pageMargin * gap_UI;
2020 if (size_Array(&d->wideRunOffsets) <= preId_GmRun(run)) {
2021 resize_Array(&d->wideRunOffsets, preId_GmRun(run) + 1);
2022 }
2023 int *offset = at_Array(&d->wideRunOffsets, preId_GmRun(run) - 1);
2024 const int oldOffset = *offset;
2025 *offset = iClamp(*offset + delta, 0, maxOffset);
2026 /* Make sure the whole block gets redraw. */
2027 if (oldOffset != *offset) {
2028 for (const iGmRun *r = range.start; r != range.end; r++) {
2029 insert_PtrSet(d->invalidRuns, r);
2030 }
2031 refresh_Widget(d);
2032 d->selectMark = iNullRange;
2033 d->foundMark = iNullRange;
2034 }
2035 if (duration) {
2036 if (d->animWideRunId != preId_GmRun(run) || isFinished_Anim(&d->animWideRunOffset)) {
2037 d->animWideRunId = preId_GmRun(run);
2038 init_Anim(&d->animWideRunOffset, oldOffset);
2039 }
2040 setValueEased_Anim(&d->animWideRunOffset, *offset, duration);
2041 d->animWideRunRange = range;
2042 addTicker_App(refreshWhileScrolling_DocumentWidget_, d);
2043 }
2044 else {
2045 d->animWideRunId = 0;
2046 init_Anim(&d->animWideRunOffset, 0);
2047 }
2048 break;
2049 }
2050 }
2051}
2052
2053static void togglePreFold_DocumentWidget_(iDocumentWidget *d, uint16_t preId) { 3027static void togglePreFold_DocumentWidget_(iDocumentWidget *d, uint16_t preId) {
2054 d->hoverPre = NULL; 3028 d->view.hoverPre = NULL;
2055 d->hoverAltPre = NULL; 3029 d->view.hoverAltPre = NULL;
2056 d->selectMark = iNullRange; 3030 d->selectMark = iNullRange;
2057 foldPre_GmDocument(d->doc, preId); 3031 foldPre_GmDocument(d->view.doc, preId);
2058 redoLayout_GmDocument(d->doc); 3032 redoLayout_GmDocument(d->view.doc);
2059 clampScroll_DocumentWidget_(d); 3033 clampScroll_DocumentView_(&d->view);
2060 updateHover_DocumentWidget_(d, mouseCoord_Window(get_Window(), 0)); 3034 updateHover_DocumentView_(&d->view, mouseCoord_Window(get_Window(), 0));
2061 invalidate_DocumentWidget_(d); 3035 invalidate_DocumentWidget_(d);
2062 refresh_Widget(as_Widget(d)); 3036 refresh_Widget(as_Widget(d));
2063} 3037}
@@ -2071,7 +3045,15 @@ static iString *makeQueryUrl_DocumentWidget_(const iDocumentWidget *d,
2071 remove_Block(&url->chars, qPos, iInvalidSize); 3045 remove_Block(&url->chars, qPos, iInvalidSize);
2072 } 3046 }
2073 appendCStr_String(url, "?"); 3047 appendCStr_String(url, "?");
2074 append_String(url, collect_String(urlEncode_String(userEnteredText))); 3048 iString *cleaned = copy_String(userEnteredText);
3049 if (deviceType_App() != desktop_AppDeviceType) {
3050 trimEnd_String(cleaned); /* autocorrect may insert an extra space */
3051 if (isEmpty_String(cleaned)) {
3052 set_String(cleaned, userEnteredText); /* user wanted just spaces? */
3053 }
3054 }
3055 append_String(url, collect_String(urlEncode_String(cleaned)));
3056 delete_String(cleaned);
2075 return url; 3057 return url;
2076} 3058}
2077 3059
@@ -2107,6 +3089,14 @@ static const char *humanReadableStatusCode_(enum iGmStatusCode code) {
2107 return format_CStr("%d ", code); 3089 return format_CStr("%d ", code);
2108} 3090}
2109 3091
3092static void setUrl_DocumentWidget_(iDocumentWidget *d, const iString *url) {
3093 url = canonicalUrl_String(url);
3094 if (!equal_String(d->mod.url, url)) {
3095 d->flags |= urlChanged_DocumentWidgetFlag;
3096 set_String(d->mod.url, url);
3097 }
3098}
3099
2110static void checkResponse_DocumentWidget_(iDocumentWidget *d) { 3100static void checkResponse_DocumentWidget_(iDocumentWidget *d) {
2111 if (!d->request) { 3101 if (!d->request) {
2112 return; 3102 return;
@@ -2117,7 +3107,35 @@ static void checkResponse_DocumentWidget_(iDocumentWidget *d) {
2117 } 3107 }
2118 iGmResponse *resp = lockResponse_GmRequest(d->request); 3108 iGmResponse *resp = lockResponse_GmRequest(d->request);
2119 if (d->state == fetching_RequestState) { 3109 if (d->state == fetching_RequestState) {
3110 /* Under certain conditions, inline any image response into the current document. */
3111 if (d->requestLinkId &&
3112 isSuccess_GmStatusCode(d->sourceStatus) &&
3113 startsWithCase_String(&d->sourceMime, "text/gemini") &&
3114 isSuccess_GmStatusCode(statusCode) &&
3115 startsWithCase_String(&resp->meta, "image/")) {
3116 /* This request is turned into a new media request in the current document. */
3117 iDisconnect(GmRequest, d->request, updated, d, requestUpdated_DocumentWidget_);
3118 iDisconnect(GmRequest, d->request, finished, d, requestFinished_DocumentWidget_);
3119 iMediaRequest *mr = newReused_MediaRequest(d, d->requestLinkId, d->request);
3120 unlockResponse_GmRequest(d->request);
3121 d->request = NULL; /* ownership moved */
3122 postCommand_Widget(d, "document.request.cancelled doc:%p", d);
3123 pushBack_ObjectList(d->media, mr);
3124 iRelease(mr);
3125 /* Reset the fetch state, returning to the originating page. */
3126 d->state = ready_RequestState;
3127 if (equal_String(&mostRecentUrl_History(d->mod.history)->url, url_GmRequest(mr->req))) {
3128 undo_History(d->mod.history);
3129 }
3130 setUrl_DocumentWidget_(d, url_GmDocument(d->view.doc));
3131 updateFetchProgress_DocumentWidget_(d);
3132 postCommand_Widget(d, "media.updated link:%u request:%p", d->requestLinkId, mr);
3133 return;
3134 }
3135 /* Get ready for the incoming new document. */
2120 d->state = receivedPartialResponse_RequestState; 3136 d->state = receivedPartialResponse_RequestState;
3137 d->flags &= ~fromCache_DocumentWidgetFlag;
3138 clear_ObjectList(d->media);
2121 updateTrust_DocumentWidget_(d, resp); 3139 updateTrust_DocumentWidget_(d, resp);
2122 if (isSuccess_GmStatusCode(statusCode)) { 3140 if (isSuccess_GmStatusCode(statusCode)) {
2123 clear_Banner(d->banner); 3141 clear_Banner(d->banner);
@@ -2128,8 +3146,8 @@ static void checkResponse_DocumentWidget_(iDocumentWidget *d) {
2128 equalCase_Rangecc(urlScheme_String(d->mod.url), "gemini")) { 3146 equalCase_Rangecc(urlScheme_String(d->mod.url), "gemini")) {
2129 statusCode = tlsServerCertificateNotVerified_GmStatusCode; 3147 statusCode = tlsServerCertificateNotVerified_GmStatusCode;
2130 } 3148 }
2131 init_Anim(&d->sideOpacity, 0); 3149 init_Anim(&d->view.sideOpacity, 0);
2132 init_Anim(&d->altTextOpacity, 0); 3150 init_Anim(&d->view.altTextOpacity, 0);
2133 format_String(&d->sourceHeader, 3151 format_String(&d->sourceHeader,
2134 "%s%s", 3152 "%s%s",
2135 humanReadableStatusCode_(statusCode), 3153 humanReadableStatusCode_(statusCode),
@@ -2143,7 +3161,7 @@ static void checkResponse_DocumentWidget_(iDocumentWidget *d) {
2143 it is only displayed as an input dialog. */ 3161 it is only displayed as an input dialog. */
2144 visitUrl_Visited(visited_App(), d->mod.url, transient_VisitedUrlFlag); 3162 visitUrl_Visited(visited_App(), d->mod.url, transient_VisitedUrlFlag);
2145 iUrl parts; 3163 iUrl parts;
2146 init_Url(&parts, d->mod.url); 3164 init_Url(&parts, d->mod.url);
2147 iWidget *dlg = makeValueInput_Widget( 3165 iWidget *dlg = makeValueInput_Widget(
2148 as_Widget(d), 3166 as_Widget(d),
2149 NULL, 3167 NULL,
@@ -2169,19 +3187,41 @@ static void checkResponse_DocumentWidget_(iDocumentWidget *d) {
2169 NULL); 3187 NULL);
2170 insertChildAfter_Widget(buttons, iClob(lineBreak), 0); 3188 insertChildAfter_Widget(buttons, iClob(lineBreak), 0);
2171 } 3189 }
2172 else { 3190 if (lineBreak) {
2173 lineBreak = new_LabelWidget("${dlg.input.linebreak}", "text.insert arg:10"); 3191 setFlags_Widget(as_Widget(lineBreak), frameless_WidgetFlag, iTrue);
3192 setTextColor_LabelWidget(lineBreak, uiTextDim_ColorId);
2174 } 3193 }
2175 setFlags_Widget(as_Widget(lineBreak), frameless_WidgetFlag, iTrue);
2176 setTextColor_LabelWidget(lineBreak, uiTextDim_ColorId);
2177 } 3194 }
2178 setId_Widget(addChildPosFlags_Widget(buttons, 3195 iWidget *counter = (iWidget *) new_LabelWidget("", NULL);
2179 iClob(new_LabelWidget("", NULL)), 3196 setId_Widget(counter, "valueinput.counter");
2180 front_WidgetAddPos, frameless_WidgetFlag), 3197 setFlags_Widget(counter, frameless_WidgetFlag | resizeToParentHeight_WidgetFlag, iTrue);
2181 "valueinput.counter"); 3198 if (deviceType_App() == desktop_AppDeviceType) {
3199 addChildPos_Widget(buttons, iClob(counter), front_WidgetAddPos);
3200 }
3201 else {
3202 insertChildAfter_Widget(buttons, iClob(counter), 1);
3203 }
2182 if (lineBreak && deviceType_App() != desktop_AppDeviceType) { 3204 if (lineBreak && deviceType_App() != desktop_AppDeviceType) {
2183 addChildPos_Widget(buttons, iClob(lineBreak), front_WidgetAddPos); 3205 addChildPos_Widget(buttons, iClob(lineBreak), front_WidgetAddPos);
2184 } 3206 }
3207 /* Menu for additional actions, past entries. */ {
3208 iMenuItem items[] = { { "${menu.input.precedingline}",
3209 SDLK_v,
3210 KMOD_PRIMARY | KMOD_SHIFT,
3211 format_CStr("!valueinput.set ptr:%p text:%s",
3212 buttons,
3213 cstr_String(&d->linePrecedingLink)) } };
3214 iLabelWidget *menu = makeMenuButton_LabelWidget(midEllipsis_Icon, items, 1);
3215 if (deviceType_App() == desktop_AppDeviceType) {
3216 addChildPos_Widget(buttons, iClob(menu), front_WidgetAddPos);
3217 }
3218 else {
3219 insertChildAfterFlags_Widget(buttons, iClob(menu), 0,
3220 frameless_WidgetFlag | noBackground_WidgetFlag);
3221 setFont_LabelWidget(menu, font_LabelWidget((iLabelWidget *) lastChild_Widget(buttons)));
3222 setTextColor_LabelWidget(menu, uiTextAction_ColorId);
3223 }
3224 }
2185 setValidator_InputWidget(findChild_Widget(dlg, "input"), inputQueryValidator_, d); 3225 setValidator_InputWidget(findChild_Widget(dlg, "input"), inputQueryValidator_, d);
2186 setSensitiveContent_InputWidget(findChild_Widget(dlg, "input"), 3226 setSensitiveContent_InputWidget(findChild_Widget(dlg, "input"),
2187 statusCode == sensitiveInput_GmStatusCode); 3227 statusCode == sensitiveInput_GmStatusCode);
@@ -2196,16 +3236,17 @@ static void checkResponse_DocumentWidget_(iDocumentWidget *d) {
2196 case categorySuccess_GmStatusCode: 3236 case categorySuccess_GmStatusCode:
2197 if (d->flags & urlChanged_DocumentWidgetFlag) { 3237 if (d->flags & urlChanged_DocumentWidgetFlag) {
2198 /* Keep scroll position when reloading the same page. */ 3238 /* Keep scroll position when reloading the same page. */
2199 reset_SmoothScroll(&d->scrollY); 3239 resetScroll_DocumentView_(&d->view);
2200 } 3240 }
2201 pauseAllPlayers_Media(media_GmDocument(d->doc), iTrue); 3241 d->view.scrollY.pullActionTriggered = 0;
2202 iRelease(d->doc); /* new content incoming */ 3242 pauseAllPlayers_Media(media_GmDocument(d->view.doc), iTrue);
2203 d->doc = new_GmDocument(); 3243 iReleasePtr(&d->view.doc); /* new content incoming */
2204 delete_Gempub(d->sourceGempub); 3244 delete_Gempub(d->sourceGempub);
2205 d->sourceGempub = NULL; 3245 d->sourceGempub = NULL;
2206 destroy_Widget(d->footerButtons); 3246 destroy_Widget(d->footerButtons);
2207 d->footerButtons = NULL; 3247 d->footerButtons = NULL;
2208 resetWideRuns_DocumentWidget_(d); 3248 d->view.doc = new_GmDocument();
3249 resetWideRuns_DocumentView_(&d->view);
2209 updateDocument_DocumentWidget_(d, resp, NULL, iTrue); 3250 updateDocument_DocumentWidget_(d, resp, NULL, iTrue);
2210 break; 3251 break;
2211 case categoryRedirect_GmStatusCode: 3252 case categoryRedirect_GmStatusCode:
@@ -2260,6 +3301,7 @@ static void checkResponse_DocumentWidget_(iDocumentWidget *d) {
2260 } 3301 }
2261 } 3302 }
2262 else if (d->state == receivedPartialResponse_RequestState) { 3303 else if (d->state == receivedPartialResponse_RequestState) {
3304 d->flags &= ~fromCache_DocumentWidgetFlag;
2263 switch (category_GmStatusCode(statusCode)) { 3305 switch (category_GmStatusCode(statusCode)) {
2264 case categorySuccess_GmStatusCode: 3306 case categorySuccess_GmStatusCode:
2265 /* More content available. */ 3307 /* More content available. */
@@ -2272,37 +3314,6 @@ static void checkResponse_DocumentWidget_(iDocumentWidget *d) {
2272 unlockResponse_GmRequest(d->request); 3314 unlockResponse_GmRequest(d->request);
2273} 3315}
2274 3316
2275static iRangecc sourceLoc_DocumentWidget_(const iDocumentWidget *d, iInt2 pos) {
2276 return findLoc_GmDocument(d->doc, documentPos_DocumentWidget_(d, pos));
2277}
2278
2279iDeclareType(MiddleRunParams)
2280
2281struct Impl_MiddleRunParams {
2282 int midY;
2283 const iGmRun *closest;
2284 int distance;
2285};
2286
2287static void find_MiddleRunParams_(void *params, const iGmRun *run) {
2288 iMiddleRunParams *d = params;
2289 if (isEmpty_Rect(run->bounds)) {
2290 return;
2291 }
2292 const int distance = iAbs(mid_Rect(run->bounds).y - d->midY);
2293 if (!d->closest || distance < d->distance) {
2294 d->closest = run;
2295 d->distance = distance;
2296 }
2297}
2298
2299static const iGmRun *middleRun_DocumentWidget_(const iDocumentWidget *d) {
2300 iRangei visRange = visibleRange_DocumentWidget_(d);
2301 iMiddleRunParams params = { (visRange.start + visRange.end) / 2, NULL, 0 };
2302 render_GmDocument(d->doc, visRange, find_MiddleRunParams_, &params);
2303 return params.closest;
2304}
2305
2306static void removeMediaRequest_DocumentWidget_(iDocumentWidget *d, iGmLinkId linkId) { 3317static void removeMediaRequest_DocumentWidget_(iDocumentWidget *d, iGmLinkId linkId) {
2307 iForEach(ObjectList, i, d->media) { 3318 iForEach(ObjectList, i, d->media) {
2308 iMediaRequest *req = (iMediaRequest *) i.object; 3319 iMediaRequest *req = (iMediaRequest *) i.object;
@@ -2313,19 +3324,9 @@ static void removeMediaRequest_DocumentWidget_(iDocumentWidget *d, iGmLinkId lin
2313 } 3324 }
2314} 3325}
2315 3326
2316static iMediaRequest *findMediaRequest_DocumentWidget_(const iDocumentWidget *d, iGmLinkId linkId) {
2317 iConstForEach(ObjectList, i, d->media) {
2318 const iMediaRequest *req = (const iMediaRequest *) i.object;
2319 if (req->linkId == linkId) {
2320 return iConstCast(iMediaRequest *, req);
2321 }
2322 }
2323 return NULL;
2324}
2325
2326static iBool requestMedia_DocumentWidget_(iDocumentWidget *d, iGmLinkId linkId, iBool enableFilters) { 3327static iBool requestMedia_DocumentWidget_(iDocumentWidget *d, iGmLinkId linkId, iBool enableFilters) {
2327 if (!findMediaRequest_DocumentWidget_(d, linkId)) { 3328 if (!findMediaRequest_DocumentWidget_(d, linkId)) {
2328 const iString *mediaUrl = absoluteUrl_String(d->mod.url, linkUrl_GmDocument(d->doc, linkId)); 3329 const iString *mediaUrl = absoluteUrl_String(d->mod.url, linkUrl_GmDocument(d->view.doc, linkId));
2329 pushBack_ObjectList(d->media, iClob(new_MediaRequest(d, linkId, mediaUrl, enableFilters))); 3330 pushBack_ObjectList(d->media, iClob(new_MediaRequest(d, linkId, mediaUrl, enableFilters)));
2330 invalidate_DocumentWidget_(d); 3331 invalidate_DocumentWidget_(d);
2331 return iTrue; 3332 return iTrue;
@@ -2334,7 +3335,7 @@ static iBool requestMedia_DocumentWidget_(iDocumentWidget *d, iGmLinkId linkId,
2334} 3335}
2335 3336
2336static iBool isDownloadRequest_DocumentWidget(const iDocumentWidget *d, const iMediaRequest *req) { 3337static iBool isDownloadRequest_DocumentWidget(const iDocumentWidget *d, const iMediaRequest *req) {
2337 return findMediaForLink_Media(constMedia_GmDocument(d->doc), req->linkId, download_MediaType).type != 0; 3338 return findMediaForLink_Media(constMedia_GmDocument(d->view.doc), req->linkId, download_MediaType).type != 0;
2338} 3339}
2339 3340
2340static iBool handleMediaCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) { 3341static iBool handleMediaCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) {
@@ -2358,21 +3359,21 @@ static iBool handleMediaCommand_DocumentWidget_(iDocumentWidget *d, const char *
2358 if (isDownloadRequest_DocumentWidget(d, req) || 3359 if (isDownloadRequest_DocumentWidget(d, req) ||
2359 startsWith_String(&resp->meta, "audio/")) { 3360 startsWith_String(&resp->meta, "audio/")) {
2360 /* TODO: Use a helper? This is same as below except for the partialData flag. */ 3361 /* TODO: Use a helper? This is same as below except for the partialData flag. */
2361 if (setData_Media(media_GmDocument(d->doc), 3362 if (setData_Media(media_GmDocument(d->view.doc),
2362 req->linkId, 3363 req->linkId,
2363 &resp->meta, 3364 &resp->meta,
2364 &resp->body, 3365 &resp->body,
2365 partialData_MediaFlag | allowHide_MediaFlag)) { 3366 partialData_MediaFlag | allowHide_MediaFlag)) {
2366 redoLayout_GmDocument(d->doc); 3367 redoLayout_GmDocument(d->view.doc);
2367 } 3368 }
2368 updateVisible_DocumentWidget_(d); 3369 updateVisible_DocumentView_(&d->view);
2369 invalidate_DocumentWidget_(d); 3370 invalidate_DocumentWidget_(d);
2370 refresh_Widget(as_Widget(d)); 3371 refresh_Widget(as_Widget(d));
2371 } 3372 }
2372 unlockResponse_GmRequest(req->req); 3373 unlockResponse_GmRequest(req->req);
2373 } 3374 }
2374 /* Update the link's progress. */ 3375 /* Update the link's progress. */
2375 invalidateLink_DocumentWidget_(d, req->linkId); 3376 invalidateLink_DocumentView_(&d->view, req->linkId);
2376 refresh_Widget(d); 3377 refresh_Widget(d);
2377 return iTrue; 3378 return iTrue;
2378 } 3379 }
@@ -2383,14 +3384,14 @@ static iBool handleMediaCommand_DocumentWidget_(iDocumentWidget *d, const char *
2383 if (isDownloadRequest_DocumentWidget(d, req) || 3384 if (isDownloadRequest_DocumentWidget(d, req) ||
2384 startsWith_String(meta_GmRequest(req->req), "image/") || 3385 startsWith_String(meta_GmRequest(req->req), "image/") ||
2385 startsWith_String(meta_GmRequest(req->req), "audio/")) { 3386 startsWith_String(meta_GmRequest(req->req), "audio/")) {
2386 setData_Media(media_GmDocument(d->doc), 3387 setData_Media(media_GmDocument(d->view.doc),
2387 req->linkId, 3388 req->linkId,
2388 meta_GmRequest(req->req), 3389 meta_GmRequest(req->req),
2389 body_GmRequest(req->req), 3390 body_GmRequest(req->req),
2390 allowHide_MediaFlag); 3391 allowHide_MediaFlag);
2391 redoLayout_GmDocument(d->doc); 3392 redoLayout_GmDocument(d->view.doc);
2392 iZap(d->visibleRuns); /* pointers invalidated */ 3393 iZap(d->view.visibleRuns); /* pointers invalidated */
2393 updateVisible_DocumentWidget_(d); 3394 updateVisible_DocumentView_(&d->view);
2394 invalidate_DocumentWidget_(d); 3395 invalidate_DocumentWidget_(d);
2395 refresh_Widget(as_Widget(d)); 3396 refresh_Widget(as_Widget(d));
2396 } 3397 }
@@ -2405,25 +3406,13 @@ static iBool handleMediaCommand_DocumentWidget_(iDocumentWidget *d, const char *
2405 return iFalse; 3406 return iFalse;
2406} 3407}
2407 3408
2408static void allocVisBuffer_DocumentWidget_(const iDocumentWidget *d) {
2409 const iWidget *w = constAs_Widget(d);
2410 const iBool isVisible = isVisible_Widget(w);
2411 const iInt2 size = bounds_Widget(w).size;
2412 if (isVisible) {
2413 alloc_VisBuf(d->visBuf, size, 1);
2414 }
2415 else {
2416 dealloc_VisBuf(d->visBuf);
2417 }
2418}
2419
2420static iBool fetchNextUnfetchedImage_DocumentWidget_(iDocumentWidget *d) { 3409static iBool fetchNextUnfetchedImage_DocumentWidget_(iDocumentWidget *d) {
2421 iConstForEach(PtrArray, i, &d->visibleLinks) { 3410 iConstForEach(PtrArray, i, &d->view.visibleLinks) {
2422 const iGmRun *run = i.ptr; 3411 const iGmRun *run = i.ptr;
2423 if (run->linkId && run->mediaType == none_MediaType && 3412 if (run->linkId && run->mediaType == none_MediaType &&
2424 ~run->flags & decoration_GmRunFlag) { 3413 ~run->flags & decoration_GmRunFlag) {
2425 const int linkFlags = linkFlags_GmDocument(d->doc, run->linkId); 3414 const int linkFlags = linkFlags_GmDocument(d->view.doc, run->linkId);
2426 if (isMediaLink_GmDocument(d->doc, run->linkId) && 3415 if (isMediaLink_GmDocument(d->view.doc, run->linkId) &&
2427 linkFlags & imageFileExtension_GmLinkFlag && 3416 linkFlags & imageFileExtension_GmLinkFlag &&
2428 ~linkFlags & content_GmLinkFlag && ~linkFlags & permanent_GmLinkFlag ) { 3417 ~linkFlags & content_GmLinkFlag && ~linkFlags & permanent_GmLinkFlag ) {
2429 if (requestMedia_DocumentWidget_(d, run->linkId, iTrue)) { 3418 if (requestMedia_DocumentWidget_(d, run->linkId, iTrue)) {
@@ -2435,9 +3424,8 @@ static iBool fetchNextUnfetchedImage_DocumentWidget_(iDocumentWidget *d) {
2435 return iFalse; 3424 return iFalse;
2436} 3425}
2437 3426
2438static const iString *saveToDownloads_(const iString *url, const iString *mime, const iBlock *content, 3427static iBool saveToFile_(const iString *savePath, const iBlock *content, iBool showDialog) {
2439 iBool showDialog) { 3428 iBool ok = iFalse;
2440 const iString *savePath = downloadPathForUrl_App(url, mime);
2441 /* Write the file. */ { 3429 /* Write the file. */ {
2442 iFile *f = new_File(savePath); 3430 iFile *f = new_File(savePath);
2443 if (open_File(f, writeOnly_FileMode)) { 3431 if (open_File(f, writeOnly_FileMode)) {
@@ -2449,21 +3437,21 @@ static const iString *saveToDownloads_(const iString *url, const iString *mime,
2449 exportDownloadedFile_iOS(savePath); 3437 exportDownloadedFile_iOS(savePath);
2450#else 3438#else
2451 if (showDialog) { 3439 if (showDialog) {
2452 const iMenuItem items[2] = { 3440 const iMenuItem items[2] = {
2453 { "${dlg.save.opendownload}", 0, 0, 3441 { "${dlg.save.opendownload}", 0, 0,
2454 format_CStr("!open url:%s", cstrCollect_String(makeFileUrl_String(savePath))) }, 3442 format_CStr("!open url:%s", cstrCollect_String(makeFileUrl_String(savePath))) },
2455 { "${dlg.message.ok}", 0, 0, "message.ok" }, 3443 { "${dlg.message.ok}", 0, 0, "message.ok" },
2456 }; 3444 };
2457 makeMessage_Widget(uiHeading_ColorEscape "${heading.save}", 3445 makeMessage_Widget(uiHeading_ColorEscape "${heading.save}",
2458 format_CStr("%s\n${dlg.save.size} %.3f %s", 3446 format_CStr("%s\n${dlg.save.size} %.3f %s",
2459 cstr_String(path_File(f)), 3447 cstr_String(path_File(f)),
2460 isMega ? size / 1.0e6f : (size / 1.0e3f), 3448 isMega ? size / 1.0e6f : (size / 1.0e3f),
2461 isMega ? "${mb}" : "${kb}"), 3449 isMega ? "${mb}" : "${kb}"),
2462 items, 3450 items,
2463 iElemCount(items)); 3451 iElemCount(items));
2464 } 3452 }
2465#endif 3453#endif
2466 return savePath; 3454 ok = iTrue;
2467 } 3455 }
2468 else { 3456 else {
2469 makeSimpleMessage_Widget(uiTextCaution_ColorEscape "${heading.save.error}", 3457 makeSimpleMessage_Widget(uiTextCaution_ColorEscape "${heading.save.error}",
@@ -2471,7 +3459,16 @@ static const iString *saveToDownloads_(const iString *url, const iString *mime,
2471 } 3459 }
2472 iRelease(f); 3460 iRelease(f);
2473 } 3461 }
2474 return collectNew_String(); 3462 return ok;
3463}
3464
3465static const iString *saveToDownloads_(const iString *url, const iString *mime, const iBlock *content,
3466 iBool showDialog) {
3467 const iString *savePath = downloadPathForUrl_App(url, mime);
3468 if (!saveToFile_(savePath, content, showDialog)) {
3469 return collectNew_String();
3470 }
3471 return savePath;
2475} 3472}
2476 3473
2477static void addAllLinks_(void *context, const iGmRun *run) { 3474static void addAllLinks_(void *context, const iGmRun *run) {
@@ -2481,71 +3478,6 @@ static void addAllLinks_(void *context, const iGmRun *run) {
2481 } 3478 }
2482} 3479}
2483 3480
2484static size_t visibleLinkOrdinal_DocumentWidget_(const iDocumentWidget *d, iGmLinkId linkId) {
2485 size_t ord = 0;
2486 const iRangei visRange = visibleRange_DocumentWidget_(d);
2487 iConstForEach(PtrArray, i, &d->visibleLinks) {
2488 const iGmRun *run = i.ptr;
2489 if (top_Rect(run->visBounds) >= visRange.start + gap_UI * d->pageMargin * 4 / 5) {
2490 if (run->flags & decoration_GmRunFlag && run->linkId) {
2491 if (run->linkId == linkId) return ord;
2492 ord++;
2493 }
2494 }
2495 }
2496 return iInvalidPos;
2497}
2498
2499/* Sorted by proximity to F and J. */
2500static const int homeRowKeys_[] = {
2501 'f', 'd', 's', 'a',
2502 'j', 'k', 'l',
2503 'r', 'e', 'w', 'q',
2504 'u', 'i', 'o', 'p',
2505 'v', 'c', 'x', 'z',
2506 'm', 'n',
2507 'g', 'h',
2508 'b',
2509 't', 'y',
2510};
2511
2512static iBool updateDocumentWidthRetainingScrollPosition_DocumentWidget_(iDocumentWidget *d,
2513 iBool keepCenter) {
2514 const int newWidth = documentWidth_DocumentWidget_(d);
2515 if (newWidth == size_GmDocument(d->doc).x && !keepCenter /* not a font change */) {
2516 return iFalse;
2517 }
2518 /* Font changes (i.e., zooming) will keep the view centered, otherwise keep the top
2519 of the visible area fixed. */
2520 const iGmRun *run = keepCenter ? middleRun_DocumentWidget_(d) : d->visibleRuns.start;
2521 const char * runLoc = (run ? run->text.start : NULL);
2522 int voffset = 0;
2523 if (!keepCenter && run) {
2524 /* Keep the first visible run visible at the same position. */
2525 /* TODO: First *fully* visible run? */
2526 voffset = visibleRange_DocumentWidget_(d).start - top_Rect(run->visBounds);
2527 }
2528 setWidth_GmDocument(d->doc, newWidth, width_Widget(d));
2529 setWidth_Banner(d->banner, newWidth);
2530 documentRunsInvalidated_DocumentWidget_(d);
2531 if (runLoc && !keepCenter) {
2532 run = findRunAtLoc_GmDocument(d->doc, runLoc);
2533 if (run) {
2534 scrollTo_DocumentWidget_(d,
2535 top_Rect(run->visBounds) +
2536 lineHeight_Text(paragraph_FontId) + voffset,
2537 iFalse);
2538 }
2539 }
2540 else if (runLoc && keepCenter) {
2541 run = findRunAtLoc_GmDocument(d->doc, runLoc);
2542 if (run) {
2543 scrollTo_DocumentWidget_(d, mid_Rect(run->bounds).y, iTrue);
2544 }
2545 }
2546 return iTrue;
2547}
2548
2549static iBool handlePinch_DocumentWidget_(iDocumentWidget *d, const char *cmd) { 3481static iBool handlePinch_DocumentWidget_(iDocumentWidget *d, const char *cmd) {
2550 if (equal_Command(cmd, "pinch.began")) { 3482 if (equal_Command(cmd, "pinch.began")) {
2551 d->pinchZoomInitial = d->pinchZoomPosted = prefs_App()->zoomPercent; 3483 d->pinchZoomInitial = d->pinchZoomPosted = prefs_App()->zoomPercent;
@@ -2577,15 +3509,12 @@ static void swap_DocumentWidget_(iDocumentWidget *d, iGmDocument *doc,
2577 iDocumentWidget *swapBuffersWith) { 3509 iDocumentWidget *swapBuffersWith) {
2578 if (doc) { 3510 if (doc) {
2579 iAssert(isInstance_Object(doc, &Class_GmDocument)); 3511 iAssert(isInstance_Object(doc, &Class_GmDocument));
2580 iGmDocument *copy = ref_Object(doc); 3512 replaceDocument_DocumentWidget_(d, doc);
2581 iRelease(d->doc); 3513 iSwap(iBanner *, d->banner, swapBuffersWith->banner);
2582 d->doc = copy; 3514 setOwner_Banner(d->banner, d);
2583 d->scrollY = swapBuffersWith->scrollY; 3515 setOwner_Banner(swapBuffersWith->banner, swapBuffersWith);
2584 updateVisible_DocumentWidget_(d); 3516 swap_DocumentView_(&d->view, &swapBuffersWith->view);
2585 iSwap(iVisBuf *, d->visBuf, swapBuffersWith->visBuf); 3517// invalidate_DocumentWidget_(swapBuffersWith);
2586 iSwap(iVisBufMeta *, d->visBufMeta, swapBuffersWith->visBufMeta);
2587 iSwap(iDrawBufs *, d->drawBufs, swapBuffersWith->drawBufs);
2588 invalidate_DocumentWidget_(swapBuffersWith);
2589 } 3518 }
2590} 3519}
2591 3520
@@ -2593,11 +3522,49 @@ static iWidget *swipeParent_DocumentWidget_(iDocumentWidget *d) {
2593 return findChild_Widget(as_Widget(d)->root->widget, "doctabs"); 3522 return findChild_Widget(as_Widget(d)->root->widget, "doctabs");
2594} 3523}
2595 3524
3525static void setupSwipeOverlay_DocumentWidget_(iDocumentWidget *d, iWidget *overlay) {
3526 iWidget *w = as_Widget(d);
3527 iWidget *swipeParent = swipeParent_DocumentWidget_(d);
3528 /* The target takes the old document and jumps on top. */
3529 overlay->rect.pos = windowToInner_Widget(swipeParent, innerToWindow_Widget(w, zero_I2()));
3530 /* Note: `innerToWindow_Widget` does not apply visual offset. */
3531 overlay->rect.size = w->rect.size;
3532 setFlags_Widget(overlay, fixedPosition_WidgetFlag | fixedSize_WidgetFlag, iTrue);
3533// swap_DocumentWidget_(target, d->doc, d);
3534 setFlags_Widget(as_Widget(d), refChildrenOffset_WidgetFlag, iTrue);
3535 as_Widget(d)->offsetRef = swipeParent;
3536 /* `overlay` animates off the screen to the right. */
3537 const int fromPos = value_Anim(&w->visualOffset);
3538 const int toPos = width_Widget(overlay);
3539 setVisualOffset_Widget(overlay, fromPos, 0, 0);
3540 /* Bigger screen, faster swipes. */
3541 if (deviceType_App() == desktop_AppDeviceType) {
3542 setVisualOffset_Widget(overlay, toPos, 250, easeOut_AnimFlag | softer_AnimFlag);
3543 }
3544 else {
3545 const float devFactor = (deviceType_App() == phone_AppDeviceType ? 1.0f : 2.0f);
3546 float swipe = iClamp(d->swipeSpeed, devFactor * 400, devFactor * 1000) * gap_UI;
3547 uint32_t span = ((toPos - fromPos) / swipe) * 1000;
3548 // printf("from:%d to:%d swipe:%f span:%u\n", fromPos, toPos, d->swipeSpeed, span);
3549 setVisualOffset_Widget(overlay, toPos, span, deviceType_App() == tablet_AppDeviceType ?
3550 easeOut_AnimFlag : 0);
3551 }
3552 setVisualOffset_Widget(w, 0, 0, 0);
3553}
3554
2596static iBool handleSwipe_DocumentWidget_(iDocumentWidget *d, const char *cmd) { 3555static iBool handleSwipe_DocumentWidget_(iDocumentWidget *d, const char *cmd) {
3556 /* TODO: Cleanup
3557 If DocumentWidget is refactored to split the document presentation from state
3558 and request management (a new DocumentView class), plain views could be used for this
3559 animation without having to mess with the complete state of the DocumentWidget. That
3560 seems like a less error-prone approach -- the current implementation will likely break
3561 down (again) if anything is changed in the document internals.
3562 */
2597 iWidget *w = as_Widget(d); 3563 iWidget *w = as_Widget(d);
2598 /* Swipe animations are rather complex and utilize both cached GmDocument content 3564 /* The swipe animation is implemented in a rather complex way. It utilizes both cached
2599 and temporary DocumentWidgets. Depending on the swipe direction, this DocumentWidget 3565 GmDocument content and temporary underlay/overlay DocumentWidgets. Depending on the
2600 may wait until the finger is released to actually perform the navigation action. */ 3566 swipe direction, the DocumentWidget `d` may wait until the finger is released to actually
3567 perform the navigation action. */
2601 if (equal_Command(cmd, "edgeswipe.moved")) { 3568 if (equal_Command(cmd, "edgeswipe.moved")) {
2602 //printf("[%p] responds to edgeswipe.moved\n", d); 3569 //printf("[%p] responds to edgeswipe.moved\n", d);
2603 as_Widget(d)->offsetRef = NULL; 3570 as_Widget(d)->offsetRef = NULL;
@@ -2608,32 +3575,40 @@ static iBool handleSwipe_DocumentWidget_(iDocumentWidget *d, const char *cmd) {
2608 return iTrue; 3575 return iTrue;
2609 } 3576 }
2610 iWidget *swipeParent = swipeParent_DocumentWidget_(d); 3577 iWidget *swipeParent = swipeParent_DocumentWidget_(d);
2611 /* The temporary "swipeIn" will display the previous page until the finger is lifted. */ 3578 if (findChild_Widget(swipeParent, "swipeout")) {
3579 return iTrue; /* too fast, previous animation hasn't finished */
3580 }
3581 /* The temporary "swipein" will display the previous page until the finger is lifted. */
2612 iDocumentWidget *swipeIn = findChild_Widget(swipeParent, "swipein"); 3582 iDocumentWidget *swipeIn = findChild_Widget(swipeParent, "swipein");
2613 if (!swipeIn) { 3583 if (!swipeIn) {
2614 const iBool sidebarSwipe = (isPortraitPhone_App() &&
2615 d->flags & openedFromSidebar_DocumentWidgetFlag &&
2616 !isVisible_Widget(findWidget_App("sidebar")));
2617 swipeIn = new_DocumentWidget(); 3584 swipeIn = new_DocumentWidget();
3585 swipeIn->flags |= animationPlaceholder_DocumentWidgetFlag;
2618 setId_Widget(as_Widget(swipeIn), "swipein"); 3586 setId_Widget(as_Widget(swipeIn), "swipein");
2619 setFlags_Widget(as_Widget(swipeIn), 3587 setFlags_Widget(as_Widget(swipeIn),
2620 disabled_WidgetFlag | refChildrenOffset_WidgetFlag | 3588 disabled_WidgetFlag | refChildrenOffset_WidgetFlag |
2621 fixedPosition_WidgetFlag | fixedSize_WidgetFlag, iTrue); 3589 fixedPosition_WidgetFlag | fixedSize_WidgetFlag, iTrue);
3590 setFlags_Widget(findChild_Widget(as_Widget(swipeIn), "scroll"), hidden_WidgetFlag, iTrue);
2622 swipeIn->widget.rect.pos = windowToInner_Widget(swipeParent, localToWindow_Widget(w, w->rect.pos)); 3591 swipeIn->widget.rect.pos = windowToInner_Widget(swipeParent, localToWindow_Widget(w, w->rect.pos));
2623 swipeIn->widget.rect.size = d->widget.rect.size; 3592 swipeIn->widget.rect.size = d->widget.rect.size;
2624 swipeIn->widget.offsetRef = parent_Widget(w); 3593 swipeIn->widget.offsetRef = parent_Widget(w);
2625 if (!sidebarSwipe) { 3594 /* Use a cached document for the layer underneath. */ {
2626 iRecentUrl *recent = new_RecentUrl(); 3595 lock_History(d->mod.history);
2627 preceding_History(d->mod.history, recent); 3596 iRecentUrl *recent = precedingLocked_History(d->mod.history);
2628 if (recent->cachedDoc) { 3597 if (recent && recent->cachedResponse) {
2629 iChangeRef(swipeIn->doc, recent->cachedDoc); 3598 setUrl_DocumentWidget_(swipeIn, &recent->url);
2630 updateScrollMax_DocumentWidget_(d); 3599 updateFromCachedResponse_DocumentWidget_(swipeIn,
2631 setValue_Anim(&swipeIn->scrollY.pos, 3600 recent->normScrollY,
2632 pageHeight_DocumentWidget_(d) * recent->normScrollY, 0); 3601 recent->cachedResponse,
2633 updateVisible_DocumentWidget_(swipeIn); 3602 recent->cachedDoc);
2634 swipeIn->drawBufs->flags |= updateTimestampBuf_DrawBufsFlag | updateSideBuf_DrawBufsFlag; 3603 parseUser_DocumentWidget_(swipeIn);
3604 updateBanner_DocumentWidget_(swipeIn);
2635 } 3605 }
2636 delete_RecentUrl(recent); 3606 else {
3607 setUrlAndSource_DocumentWidget(swipeIn, &recent->url,
3608 collectNewCStr_String("text/gemini"),
3609 collect_Block(new_Block(0)));
3610 }
3611 unlock_History(d->mod.history);
2637 } 3612 }
2638 addChildPos_Widget(swipeParent, iClob(swipeIn), front_WidgetAddPos); 3613 addChildPos_Widget(swipeParent, iClob(swipeIn), front_WidgetAddPos);
2639 } 3614 }
@@ -2641,24 +3616,36 @@ static iBool handleSwipe_DocumentWidget_(iDocumentWidget *d, const char *cmd) {
2641 if (side == 2) { /* right edge */ 3616 if (side == 2) { /* right edge */
2642 if (offset < -get_Window()->pixelRatio * 10) { 3617 if (offset < -get_Window()->pixelRatio * 10) {
2643 int animSpan = 10; 3618 int animSpan = 10;
2644 if (!atLatest_History(d->mod.history) && 3619 if (!atNewest_History(d->mod.history) && ~flags_Widget(w) & dragged_WidgetFlag) {
2645 ~flags_Widget(w) & dragged_WidgetFlag) { 3620 iWidget *swipeParent = swipeParent_DocumentWidget_(d);
3621 if (findChild_Widget(swipeParent, "swipeout")) {
3622 return iTrue; /* too fast, previous animation hasn't finished */
3623 }
3624 /* Setup the drag. `d` will be moving with the finger. */
2646 animSpan = 0; 3625 animSpan = 0;
2647 postCommand_Widget(d, "navigate.forward"); 3626 postCommand_Widget(d, "navigate.forward");
2648 setFlags_Widget(w, dragged_WidgetFlag, iTrue); 3627 setFlags_Widget(w, dragged_WidgetFlag, iTrue);
2649 /* Set up the swipe dummy. */ 3628 /* Set up the swipe dummy. */
2650 iWidget *swipeParent = swipeParent_DocumentWidget_(d);
2651 iDocumentWidget *target = new_DocumentWidget(); 3629 iDocumentWidget *target = new_DocumentWidget();
3630 target->flags |= animationPlaceholder_DocumentWidgetFlag;
2652 setId_Widget(as_Widget(target), "swipeout"); 3631 setId_Widget(as_Widget(target), "swipeout");
2653 /* The target takes the old document and jumps on top. */ 3632 /* "swipeout" takes `d`'s document and goes underneath. */
2654 target->widget.rect.pos = windowToInner_Widget(swipeParent, localToWindow_Widget(w, w->rect.pos)); 3633 target->widget.rect.pos = windowToInner_Widget(swipeParent, localToWindow_Widget(w, w->rect.pos));
2655 target->widget.rect.size = d->widget.rect.size; 3634 target->widget.rect.size = d->widget.rect.size;
2656 setFlags_Widget(as_Widget(target), fixedPosition_WidgetFlag | fixedSize_WidgetFlag, iTrue); 3635 setFlags_Widget(as_Widget(target), fixedPosition_WidgetFlag | fixedSize_WidgetFlag, iTrue);
2657 swap_DocumentWidget_(target, d->doc, d); 3636 swap_DocumentWidget_(target, d->view.doc, d);
2658 addChildPos_Widget(swipeParent, iClob(target), front_WidgetAddPos); 3637 addChildPos_Widget(swipeParent, iClob(target), front_WidgetAddPos);
2659 setFlags_Widget(as_Widget(target), refChildrenOffset_WidgetFlag, iTrue); 3638 setFlags_Widget(as_Widget(target), refChildrenOffset_WidgetFlag, iTrue);
2660 as_Widget(target)->offsetRef = parent_Widget(w); 3639 as_Widget(target)->offsetRef = parent_Widget(w);
2661 destroy_Widget(as_Widget(target)); /* will be actually deleted after animation finishes */ 3640 /* Mark it for deletion after animation finishes. */
3641 destroy_Widget(as_Widget(target));
3642 /* The `d` document will now navigate forward and be replaced with a cached
3643 copy. However, if a cached response isn't available, we'll need to show a
3644 blank page. */
3645 setUrlAndSource_DocumentWidget(d,
3646 collectNewCStr_String("about:blank"),
3647 collectNewCStr_String("text/gemini"),
3648 collect_Block(new_Block(0)));
2662 } 3649 }
2663 if (flags_Widget(w) & dragged_WidgetFlag) { 3650 if (flags_Widget(w) & dragged_WidgetFlag) {
2664 setVisualOffset_Widget(w, width_Widget(w) + 3651 setVisualOffset_Widget(w, width_Widget(w) +
@@ -2674,41 +3661,68 @@ static iBool handleSwipe_DocumentWidget_(iDocumentWidget *d, const char *cmd) {
2674 } 3661 }
2675 if (equal_Command(cmd, "edgeswipe.ended") && argLabel_Command(cmd, "side") == 2) { 3662 if (equal_Command(cmd, "edgeswipe.ended") && argLabel_Command(cmd, "side") == 2) {
2676 if (argLabel_Command(cmd, "abort") && flags_Widget(w) & dragged_WidgetFlag) { 3663 if (argLabel_Command(cmd, "abort") && flags_Widget(w) & dragged_WidgetFlag) {
3664 setFlags_Widget(w, dragged_WidgetFlag, iFalse);
2677 postCommand_Widget(d, "navigate.back"); 3665 postCommand_Widget(d, "navigate.back");
3666 /* We must now undo the swap that was done when the drag started. */
3667 /* TODO: Currently not animated! What exactly is the appropriate thing to do here? */
3668 iWidget *swipeParent = swipeParent_DocumentWidget_(d);
3669 iDocumentWidget *swipeOut = findChild_Widget(swipeParent, "swipeout");
3670 swap_DocumentWidget_(d, swipeOut->view.doc, swipeOut);
3671// const int visOff = visualOffsetByReference_Widget(w);
3672 w->offsetRef = NULL;
3673// setVisualOffset_Widget(w, visOff, 0, 0);
3674// setVisualOffset_Widget(w, 0, 150, 0);
3675 setVisualOffset_Widget(w, 0, 0, 0);
3676 /* Make it an overlay instead. */
3677// removeChild_Widget(swipeParent, swipeOut);
3678// addChildPos_Widget(swipeParent, iClob(swipeOut), back_WidgetAddPos);
3679// setupSwipeOverlay_DocumentWidget_(d, as_Widget(swipeOut));
3680 return iTrue;
2678 } 3681 }
3682 iAssert(~d->flags & animationPlaceholder_DocumentWidgetFlag);
2679 setFlags_Widget(w, dragged_WidgetFlag, iFalse); 3683 setFlags_Widget(w, dragged_WidgetFlag, iFalse);
2680 setVisualOffset_Widget(w, 0, 100, 0); 3684 setVisualOffset_Widget(w, 0, 250, easeOut_AnimFlag | softer_AnimFlag);
2681 return iTrue; 3685 return iTrue;
2682 } 3686 }
2683 if (equal_Command(cmd, "edgeswipe.ended") && argLabel_Command(cmd, "side") == 1) { 3687 if (equal_Command(cmd, "edgeswipe.ended") && argLabel_Command(cmd, "side") == 1) {
2684 iWidget *swipeParent = swipeParent_DocumentWidget_(d); 3688 iWidget *swipeParent = swipeParent_DocumentWidget_(d);
2685 iWidget *swipeIn = findChild_Widget(swipeParent, "swipein"); 3689 iDocumentWidget *swipeIn = findChild_Widget(swipeParent, "swipein");
3690 d->swipeSpeed = argLabel_Command(cmd, "speed") / gap_UI;
3691 /* "swipe.back" will soon follow. The `d` document will do the actual back navigation,
3692 switching immediately to a cached page. However, if one is not available, we'll need
3693 to show a blank page for a while. */
2686 if (swipeIn) { 3694 if (swipeIn) {
2687 swipeIn->offsetRef = NULL; 3695 if (!argLabel_Command(cmd, "abort")) {
2688 destroy_Widget(swipeIn); 3696 iWidget *swipeParent = swipeParent_DocumentWidget_(d);
3697 /* What was being shown in the `d` document is now being swapped to
3698 the outgoing page animation. */
3699 iDocumentWidget *target = new_DocumentWidget();
3700 target->flags |= animationPlaceholder_DocumentWidgetFlag;
3701 addChildPos_Widget(swipeParent, iClob(target), back_WidgetAddPos);
3702 setId_Widget(as_Widget(target), "swipeout");
3703 setFlags_Widget(as_Widget(target), disabled_WidgetFlag, iTrue);
3704 swap_DocumentWidget_(target, d->view.doc, d);
3705 setUrlAndSource_DocumentWidget(d,
3706 swipeIn->mod.url,
3707 collectNewCStr_String("text/gemini"),
3708 collect_Block(new_Block(0)));
3709 as_Widget(swipeIn)->offsetRef = NULL;
3710 }
3711 destroy_Widget(as_Widget(swipeIn));
2689 } 3712 }
2690 } 3713 }
2691 if (equal_Command(cmd, "swipe.back")) { 3714 if (equal_Command(cmd, "swipe.back")) {
3715 iWidget *swipeParent = swipeParent_DocumentWidget_(d);
3716 iDocumentWidget *target = findChild_Widget(swipeParent, "swipeout");
2692 if (atOldest_History(d->mod.history)) { 3717 if (atOldest_History(d->mod.history)) {
2693 setVisualOffset_Widget(w, 0, 100, 0); 3718 setVisualOffset_Widget(w, 0, 100, 0);
3719 if (target) {
3720 destroy_Widget(as_Widget(target)); /* didn't need it after all */
3721 }
2694 return iTrue; 3722 return iTrue;
2695 } 3723 }
2696 iWidget *swipeParent = swipeParent_DocumentWidget_(d); 3724 setupSwipeOverlay_DocumentWidget_(d, as_Widget(target));
2697 iDocumentWidget *target = new_DocumentWidget();
2698 setId_Widget(as_Widget(target), "swipeout");
2699 /* The target takes the old document and jumps on top. */
2700 target->widget.rect.pos = windowToInner_Widget(swipeParent, innerToWindow_Widget(w, zero_I2()));
2701 /* Note: `innerToWindow_Widget` does not apply visual offset. */
2702 target->widget.rect.size = w->rect.size;
2703 setFlags_Widget(as_Widget(target), fixedPosition_WidgetFlag | fixedSize_WidgetFlag, iTrue);
2704 swap_DocumentWidget_(target, d->doc, d);
2705 addChildPos_Widget(swipeParent, iClob(target), back_WidgetAddPos);
2706 setFlags_Widget(as_Widget(d), refChildrenOffset_WidgetFlag, iTrue);
2707 as_Widget(d)->offsetRef = swipeParent;
2708 setVisualOffset_Widget(as_Widget(target), value_Anim(&w->visualOffset), 0, 0);
2709 setVisualOffset_Widget(as_Widget(target), width_Widget(target), 150, 0);
2710 destroy_Widget(as_Widget(target)); /* will be actually deleted after animation finishes */ 3725 destroy_Widget(as_Widget(target)); /* will be actually deleted after animation finishes */
2711 setVisualOffset_Widget(w, 0, 0, 0);
2712 postCommand_Widget(d, "navigate.back"); 3726 postCommand_Widget(d, "navigate.back");
2713 return iTrue; 3727 return iTrue;
2714 } 3728 }
@@ -2733,27 +3747,34 @@ static iBool cancelRequest_DocumentWidget_(iDocumentWidget *d, iBool postBack) {
2733 return iFalse; 3747 return iFalse;
2734} 3748}
2735 3749
3750static const int smoothDuration_DocumentWidget_(enum iScrollType type) {
3751 return 600 /* milliseconds */ * scrollSpeedFactor_Prefs(prefs_App(), type);
3752}
3753
2736static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) { 3754static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) {
2737 iWidget *w = as_Widget(d); 3755 iWidget *w = as_Widget(d);
2738 if (equal_Command(cmd, "document.openurls.changed")) { 3756 if (equal_Command(cmd, "document.openurls.changed")) {
3757 if (d->flags & animationPlaceholder_DocumentWidgetFlag) {
3758 return iFalse;
3759 }
2739 /* When any tab changes its document URL, update the open link indicators. */ 3760 /* When any tab changes its document URL, update the open link indicators. */
2740 if (updateOpenURLs_GmDocument(d->doc)) { 3761 if (updateOpenURLs_GmDocument(d->view.doc)) {
2741 invalidate_DocumentWidget_(d); 3762 invalidate_DocumentWidget_(d);
2742 refresh_Widget(d); 3763 refresh_Widget(d);
2743 } 3764 }
2744 return iFalse; 3765 return iFalse;
2745 } 3766 }
2746 if (equal_Command(cmd, "visited.changed")) { 3767 if (equal_Command(cmd, "visited.changed")) {
2747 updateVisitedLinks_GmDocument(d->doc); 3768 updateVisitedLinks_GmDocument(d->view.doc);
2748 invalidateVisibleLinks_DocumentWidget_(d); 3769 invalidateVisibleLinks_DocumentView_(&d->view);
2749 return iFalse; 3770 return iFalse;
2750 } 3771 }
2751 if (equal_Command(cmd, "document.render")) /* `Periodic` makes direct dispatch to here */ { 3772 if (equal_Command(cmd, "document.render")) /* `Periodic` makes direct dispatch to here */ {
2752// printf("%u: document.render\n", SDL_GetTicks()); 3773// printf("%u: document.render\n", SDL_GetTicks());
2753 if (SDL_GetTicks() - d->drawBufs->lastRenderTime > 150) { 3774 if (SDL_GetTicks() - d->view.drawBufs->lastRenderTime > 150) {
2754 remove_Periodic(periodic_App(), d); 3775 remove_Periodic(periodic_App(), d);
2755 /* Scrolling has stopped, begin filling up the buffer. */ 3776 /* Scrolling has stopped, begin filling up the buffer. */
2756 if (d->visBuf->buffers[0].texture) { 3777 if (d->view.visBuf->buffers[0].texture) {
2757 addTicker_App(prerender_DocumentWidget_, d); 3778 addTicker_App(prerender_DocumentWidget_, d);
2758 } 3779 }
2759 } 3780 }
@@ -2762,17 +3783,17 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
2762 else if (equal_Command(cmd, "window.resized") || equal_Command(cmd, "font.changed") || 3783 else if (equal_Command(cmd, "window.resized") || equal_Command(cmd, "font.changed") ||
2763 equal_Command(cmd, "keyroot.changed")) { 3784 equal_Command(cmd, "keyroot.changed")) {
2764 if (equal_Command(cmd, "font.changed")) { 3785 if (equal_Command(cmd, "font.changed")) {
2765 invalidateCachedLayout_History(d->mod.history); 3786 invalidateCachedLayout_History(d->mod.history);
2766 } 3787 }
2767 /* Alt/Option key may be involved in window size changes. */ 3788 /* Alt/Option key may be involved in window size changes. */
2768 setLinkNumberMode_DocumentWidget_(d, iFalse); 3789 setLinkNumberMode_DocumentWidget_(d, iFalse);
2769 d->phoneToolbar = findWidget_App("toolbar"); 3790 d->phoneToolbar = findWidget_App("toolbar");
2770 const iBool keepCenter = equal_Command(cmd, "font.changed"); 3791 const iBool keepCenter = equal_Command(cmd, "font.changed");
2771 updateDocumentWidthRetainingScrollPosition_DocumentWidget_(d, keepCenter); 3792 updateDocumentWidthRetainingScrollPosition_DocumentView_(&d->view, keepCenter);
2772 d->drawBufs->flags |= updateSideBuf_DrawBufsFlag; 3793 d->view.drawBufs->flags |= updateSideBuf_DrawBufsFlag;
2773 updateVisible_DocumentWidget_(d); 3794 updateVisible_DocumentView_(&d->view);
2774 invalidate_DocumentWidget_(d); 3795 invalidate_DocumentWidget_(d);
2775 dealloc_VisBuf(d->visBuf); 3796 dealloc_VisBuf(d->view.visBuf);
2776 updateWindowTitle_DocumentWidget_(d); 3797 updateWindowTitle_DocumentWidget_(d);
2777 showOrHidePinningIndicator_DocumentWidget_(d); 3798 showOrHidePinningIndicator_DocumentWidget_(d);
2778 refresh_Widget(w); 3799 refresh_Widget(w);
@@ -2780,7 +3801,7 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
2780 else if (equal_Command(cmd, "window.focus.lost")) { 3801 else if (equal_Command(cmd, "window.focus.lost")) {
2781 if (d->flags & showLinkNumbers_DocumentWidgetFlag) { 3802 if (d->flags & showLinkNumbers_DocumentWidgetFlag) {
2782 setLinkNumberMode_DocumentWidget_(d, iFalse); 3803 setLinkNumberMode_DocumentWidget_(d, iFalse);
2783 invalidateVisibleLinks_DocumentWidget_(d); 3804 invalidateVisibleLinks_DocumentView_(&d->view);
2784 refresh_Widget(w); 3805 refresh_Widget(w);
2785 } 3806 }
2786 return iFalse; 3807 return iFalse;
@@ -2788,18 +3809,21 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
2788 else if (equal_Command(cmd, "window.mouse.exited")) { 3809 else if (equal_Command(cmd, "window.mouse.exited")) {
2789 return iFalse; 3810 return iFalse;
2790 } 3811 }
2791 else if (equal_Command(cmd, "theme.changed") && document_App() == d) { 3812 else if (equal_Command(cmd, "theme.changed")) {
2792// invalidateTheme_History(d->mod.history); /* cached colors */ 3813 invalidatePalette_GmDocument(d->view.doc);
2793 updateTheme_DocumentWidget_(d); 3814 invalidateTheme_History(d->mod.history); /* forget cached color palettes */
2794 updateVisible_DocumentWidget_(d); 3815 if (document_App() == d) {
2795 updateTrust_DocumentWidget_(d, NULL); 3816 updateTheme_DocumentWidget_(d);
2796 d->drawBufs->flags |= updateSideBuf_DrawBufsFlag; 3817 updateVisible_DocumentView_(&d->view);
2797 invalidate_DocumentWidget_(d); 3818 updateTrust_DocumentWidget_(d, NULL);
2798 refresh_Widget(w); 3819 d->view.drawBufs->flags |= updateSideBuf_DrawBufsFlag;
3820 invalidate_DocumentWidget_(d);
3821 refresh_Widget(w);
3822 }
2799 } 3823 }
2800 else if (equal_Command(cmd, "document.layout.changed") && document_Root(get_Root()) == d) { 3824 else if (equal_Command(cmd, "document.layout.changed") && document_Root(get_Root()) == d) {
2801 if (argLabel_Command(cmd, "redo")) { 3825 if (argLabel_Command(cmd, "redo")) {
2802 redoLayout_GmDocument(d->doc); 3826 redoLayout_GmDocument(d->view.doc);
2803 } 3827 }
2804 updateSize_DocumentWidget(d); 3828 updateSize_DocumentWidget(d);
2805 } 3829 }
@@ -2822,11 +3846,11 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
2822 updateFetchProgress_DocumentWidget_(d); 3846 updateFetchProgress_DocumentWidget_(d);
2823 updateHover_Window(window_Widget(w)); 3847 updateHover_Window(window_Widget(w));
2824 } 3848 }
2825 init_Anim(&d->sideOpacity, 0); 3849 init_Anim(&d->view.sideOpacity, 0);
2826 init_Anim(&d->altTextOpacity, 0); 3850 init_Anim(&d->view.altTextOpacity, 0);
2827 updateSideOpacity_DocumentWidget_(d, iFalse); 3851 updateSideOpacity_DocumentView_(&d->view, iFalse);
2828 updateWindowTitle_DocumentWidget_(d); 3852 updateWindowTitle_DocumentWidget_(d);
2829 allocVisBuffer_DocumentWidget_(d); 3853 allocVisBuffer_DocumentView_(&d->view);
2830 animateMedia_DocumentWidget_(d); 3854 animateMedia_DocumentWidget_(d);
2831 remove_Periodic(periodic_App(), d); 3855 remove_Periodic(periodic_App(), d);
2832 removeTicker_App(prerender_DocumentWidget_, d); 3856 removeTicker_App(prerender_DocumentWidget_, d);
@@ -2850,8 +3874,8 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
2850 selectWords_DocumentWidgetFlag; /* finger-based selection is imprecise */ 3874 selectWords_DocumentWidgetFlag; /* finger-based selection is imprecise */
2851 d->flags &= ~selectLines_DocumentWidgetFlag; 3875 d->flags &= ~selectLines_DocumentWidgetFlag;
2852 setFadeEnabled_ScrollWidget(d->scroll, iFalse); 3876 setFadeEnabled_ScrollWidget(d->scroll, iFalse);
2853 d->selectMark = sourceLoc_DocumentWidget_(d, d->contextPos); 3877 d->selectMark = sourceLoc_DocumentView_(&d->view, d->contextPos);
2854 extendRange_Rangecc(&d->selectMark, range_String(source_GmDocument(d->doc)), 3878 extendRange_Rangecc(&d->selectMark, range_String(source_GmDocument(d->view.doc)),
2855 word_RangeExtension | bothStartAndEnd_RangeExtension); 3879 word_RangeExtension | bothStartAndEnd_RangeExtension);
2856 d->initialSelectMark = d->selectMark; 3880 d->initialSelectMark = d->selectMark;
2857 } 3881 }
@@ -2865,7 +3889,7 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
2865 timeVerified_GmCertFlag); 3889 timeVerified_GmCertFlag);
2866 const iBool canTrust = ~d->certFlags & trusted_GmCertFlag && 3890 const iBool canTrust = ~d->certFlags & trusted_GmCertFlag &&
2867 ((d->certFlags & requiredForTrust) == requiredForTrust); 3891 ((d->certFlags & requiredForTrust) == requiredForTrust);
2868 const iRecentUrl *recent = findUrl_History(d->mod.history, d->mod.url); 3892 const iRecentUrl *recent = constMostRecentUrl_History(d->mod.history);
2869 const iString *meta = &d->sourceMime; 3893 const iString *meta = &d->sourceMime;
2870 if (recent && recent->cachedResponse) { 3894 if (recent && recent->cachedResponse) {
2871 meta = &recent->cachedResponse->meta; 3895 meta = &recent->cachedResponse->meta;
@@ -2945,7 +3969,7 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
2945// setFixedSize_Widget(sizer, init_I2(gap_UI * 65, 1)); 3969// setFixedSize_Widget(sizer, init_I2(gap_UI * 65, 1));
2946// addChildFlags_Widget(dlg, iClob(sizer), frameless_WidgetFlag); 3970// addChildFlags_Widget(dlg, iClob(sizer), frameless_WidgetFlag);
2947// setFlags_Widget(dlg, centerHorizontal_WidgetFlag, iFalse); 3971// setFlags_Widget(dlg, centerHorizontal_WidgetFlag, iFalse);
2948 if (deviceType_App() != phone_AppDeviceType) { 3972 if (deviceType_App() == desktop_AppDeviceType) {
2949 const iWidget *lockButton = findWidget_Root("navbar.lock"); 3973 const iWidget *lockButton = findWidget_Root("navbar.lock");
2950 setPos_Widget(dlg, windowToLocal_Widget(dlg, bottomLeft_Rect(bounds_Widget(lockButton)))); 3974 setPos_Widget(dlg, windowToLocal_Widget(dlg, bottomLeft_Rect(bounds_Widget(lockButton))));
2951 } 3975 }
@@ -2994,9 +4018,16 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
2994 } 4018 }
2995 else { 4019 else {
2996 /* Full document. */ 4020 /* Full document. */
2997 copied = copy_String(source_GmDocument(d->doc)); 4021 copied = copy_String(source_GmDocument(d->view.doc));
4022 }
4023 if (argLabel_Command(cmd, "share")) {
4024#if defined (iPlatformAppleMobile)
4025 openTextActivityView_iOS(copied);
4026#endif
4027 }
4028 else {
4029 SDL_SetClipboardText(cstr_String(copied));
2998 } 4030 }
2999 SDL_SetClipboardText(cstr_String(copied));
3000 delete_String(copied); 4031 delete_String(copied);
3001 if (flags_Widget(w) & touchDrag_WidgetFlag) { 4032 if (flags_Widget(w) & touchDrag_WidgetFlag) {
3002 postCommand_Widget(w, "document.select arg:0"); 4033 postCommand_Widget(w, "document.select arg:0");
@@ -3006,7 +4037,7 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
3006 else if (equal_Command(cmd, "document.copylink") && document_App() == d) { 4037 else if (equal_Command(cmd, "document.copylink") && document_App() == d) {
3007 if (d->contextLink) { 4038 if (d->contextLink) {
3008 SDL_SetClipboardText(cstr_String(canonicalUrl_String(absoluteUrl_String( 4039 SDL_SetClipboardText(cstr_String(canonicalUrl_String(absoluteUrl_String(
3009 d->mod.url, linkUrl_GmDocument(d->doc, d->contextLink->linkId))))); 4040 d->mod.url, linkUrl_GmDocument(d->view.doc, d->contextLink->linkId)))));
3010 } 4041 }
3011 else { 4042 else {
3012 SDL_SetClipboardText(cstr_String(canonicalUrl_String(d->mod.url))); 4043 SDL_SetClipboardText(cstr_String(canonicalUrl_String(d->mod.url)));
@@ -3016,13 +4047,13 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
3016 else if (equalWidget_Command(cmd, w, "document.downloadlink")) { 4047 else if (equalWidget_Command(cmd, w, "document.downloadlink")) {
3017 if (d->contextLink) { 4048 if (d->contextLink) {
3018 const iGmLinkId linkId = d->contextLink->linkId; 4049 const iGmLinkId linkId = d->contextLink->linkId;
3019 setUrl_Media(media_GmDocument(d->doc), 4050 setUrl_Media(media_GmDocument(d->view.doc),
3020 linkId, 4051 linkId,
3021 download_MediaType, 4052 download_MediaType,
3022 linkUrl_GmDocument(d->doc, linkId)); 4053 linkUrl_GmDocument(d->view.doc, linkId));
3023 requestMedia_DocumentWidget_(d, linkId, iFalse /* no filters */); 4054 requestMedia_DocumentWidget_(d, linkId, iFalse /* no filters */);
3024 redoLayout_GmDocument(d->doc); /* inline downloader becomes visible */ 4055 redoLayout_GmDocument(d->view.doc); /* inline downloader becomes visible */
3025 updateVisible_DocumentWidget_(d); 4056 updateVisible_DocumentView_(&d->view);
3026 invalidate_DocumentWidget_(d); 4057 invalidate_DocumentWidget_(d);
3027 refresh_Widget(w); 4058 refresh_Widget(w);
3028 } 4059 }
@@ -3055,6 +4086,7 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
3055 } 4086 }
3056 else if (equalWidget_Command(cmd, w, "document.request.finished") && 4087 else if (equalWidget_Command(cmd, w, "document.request.finished") &&
3057 id_GmRequest(d->request) == argU32Label_Command(cmd, "reqid")) { 4088 id_GmRequest(d->request) == argU32Label_Command(cmd, "reqid")) {
4089 d->flags &= ~fromCache_DocumentWidgetFlag;
3058 set_Block(&d->sourceContent, body_GmRequest(d->request)); 4090 set_Block(&d->sourceContent, body_GmRequest(d->request));
3059 if (!isSuccess_GmStatusCode(status_GmRequest(d->request))) { 4091 if (!isSuccess_GmStatusCode(status_GmRequest(d->request))) {
3060 /* TODO: Why is this here? Can it be removed? */ 4092 /* TODO: Why is this here? Can it be removed? */
@@ -3066,7 +4098,8 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
3066 updateFetchProgress_DocumentWidget_(d); 4098 updateFetchProgress_DocumentWidget_(d);
3067 checkResponse_DocumentWidget_(d); 4099 checkResponse_DocumentWidget_(d);
3068 if (category_GmStatusCode(status_GmRequest(d->request)) == categorySuccess_GmStatusCode) { 4100 if (category_GmStatusCode(status_GmRequest(d->request)) == categorySuccess_GmStatusCode) {
3069 init_Anim(&d->scrollY.pos, d->initNormScrollY * pageHeight_DocumentWidget_(d)); /* TODO: unless user already scrolled! */ 4101 init_Anim(&d->view.scrollY.pos, d->initNormScrollY * pageHeight_DocumentView_(&d->view));
4102 /* TODO: unless user already scrolled! */
3070 } 4103 }
3071 addBannerWarnings_DocumentWidget_(d); 4104 addBannerWarnings_DocumentWidget_(d);
3072 iChangeFlags(d->flags, 4105 iChangeFlags(d->flags,
@@ -3076,6 +4109,8 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
3076 postProcessRequestContent_DocumentWidget_(d, iFalse); 4109 postProcessRequestContent_DocumentWidget_(d, iFalse);
3077 /* The response may be cached. */ 4110 /* The response may be cached. */
3078 if (d->request) { 4111 if (d->request) {
4112 iAssert(~d->flags & animationPlaceholder_DocumentWidgetFlag);
4113 iAssert(~d->flags & fromCache_DocumentWidgetFlag);
3079 if (!equal_Rangecc(urlScheme_String(d->mod.url), "about") && 4114 if (!equal_Rangecc(urlScheme_String(d->mod.url), "about") &&
3080 (startsWithCase_String(meta_GmRequest(d->request), "text/") || 4115 (startsWithCase_String(meta_GmRequest(d->request), "text/") ||
3081 !cmp_String(&d->sourceMime, mimeType_Gempub))) { 4116 !cmp_String(&d->sourceMime, mimeType_Gempub))) {
@@ -3084,8 +4119,8 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
3084 } 4119 }
3085 } 4120 }
3086 iReleasePtr(&d->request); 4121 iReleasePtr(&d->request);
3087 updateVisible_DocumentWidget_(d); 4122 updateVisible_DocumentView_(&d->view);
3088 d->drawBufs->flags |= updateSideBuf_DrawBufsFlag; 4123 d->view.drawBufs->flags |= updateSideBuf_DrawBufsFlag;
3089 postCommandf_Root(w->root, 4124 postCommandf_Root(w->root,
3090 "document.changed doc:%p status:%d url:%s", 4125 "document.changed doc:%p status:%d url:%s",
3091 d, 4126 d,
@@ -3093,7 +4128,7 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
3093 cstr_String(d->mod.url)); 4128 cstr_String(d->mod.url));
3094 /* Check for a pending goto. */ 4129 /* Check for a pending goto. */
3095 if (!isEmpty_String(&d->pendingGotoHeading)) { 4130 if (!isEmpty_String(&d->pendingGotoHeading)) {
3096 scrollToHeading_DocumentWidget_(d, cstr_String(&d->pendingGotoHeading)); 4131 scrollToHeading_DocumentView_(&d->view, cstr_String(&d->pendingGotoHeading));
3097 clear_String(&d->pendingGotoHeading); 4132 clear_String(&d->pendingGotoHeading);
3098 } 4133 }
3099 cacheDocumentGlyphs_DocumentWidget_(d); 4134 cacheDocumentGlyphs_DocumentWidget_(d);
@@ -3102,6 +4137,11 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
3102 else if (equal_Command(cmd, "document.translate") && d == document_App()) { 4137 else if (equal_Command(cmd, "document.translate") && d == document_App()) {
3103 if (!d->translation) { 4138 if (!d->translation) {
3104 d->translation = new_Translation(d); 4139 d->translation = new_Translation(d);
4140 if (isUsingPanelLayout_Mobile()) {
4141 const iRect safe = safeRect_Root(w->root);
4142 d->translation->dlg->rect.pos = windowToLocal_Widget(w, zero_I2());
4143 d->translation->dlg->rect.size = safe.size;
4144 }
3105 } 4145 }
3106 return iTrue; 4146 return iTrue;
3107 } 4147 }
@@ -3117,14 +4157,19 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
3117 if (findChild_Widget(root_Widget(w), "upload")) { 4157 if (findChild_Widget(root_Widget(w), "upload")) {
3118 return iTrue; /* already open */ 4158 return iTrue; /* already open */
3119 } 4159 }
3120 if (equalCase_Rangecc(urlScheme_String(d->mod.url), "gemini") || 4160 const iBool isGemini = equalCase_Rangecc(urlScheme_String(d->mod.url), "gemini");
3121 equalCase_Rangecc(urlScheme_String(d->mod.url), "titan")) { 4161 if (isGemini || equalCase_Rangecc(urlScheme_String(d->mod.url), "titan")) {
3122 iUploadWidget *upload = new_UploadWidget(); 4162 iUploadWidget *upload = new_UploadWidget();
3123 setUrl_UploadWidget(upload, d->mod.url); 4163 setUrl_UploadWidget(upload, d->mod.url);
3124 setResponseViewer_UploadWidget(upload, d); 4164 setResponseViewer_UploadWidget(upload, d);
3125 addChild_Widget(get_Root()->widget, iClob(upload)); 4165 addChild_Widget(get_Root()->widget, iClob(upload));
3126// finalizeSheet_Mobile(as_Widget(upload));
3127 setupSheetTransition_Mobile(as_Widget(upload), iTrue); 4166 setupSheetTransition_Mobile(as_Widget(upload), iTrue);
4167 if (argLabel_Command(cmd, "copy") && isUtf8_Rangecc(range_Block(&d->sourceContent))) {
4168 iString text;
4169 initBlock_String(&text, &d->sourceContent);
4170 setText_UploadWidget(upload, &text);
4171 deinit_String(&text);
4172 }
3128 postRefresh_App(); 4173 postRefresh_App();
3129 } 4174 }
3130 return iTrue; 4175 return iTrue;
@@ -3135,7 +4180,7 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
3135 else if (equal_Command(cmd, "media.player.started")) { 4180 else if (equal_Command(cmd, "media.player.started")) {
3136 /* When one media player starts, pause the others that may be playing. */ 4181 /* When one media player starts, pause the others that may be playing. */
3137 const iPlayer *startedPlr = pointerLabel_Command(cmd, "player"); 4182 const iPlayer *startedPlr = pointerLabel_Command(cmd, "player");
3138 const iMedia * media = media_GmDocument(d->doc); 4183 const iMedia * media = media_GmDocument(d->view.doc);
3139 const size_t num = numAudio_Media(media); 4184 const size_t num = numAudio_Media(media);
3140 for (size_t id = 1; id <= num; id++) { 4185 for (size_t id = 1; id <= num; id++) {
3141 iPlayer *plr = audioPlayer_Media(media, (iMediaId){ audio_MediaType, id }); 4186 iPlayer *plr = audioPlayer_Media(media, (iMediaId){ audio_MediaType, id });
@@ -3167,22 +4212,37 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
3167 "${dlg.save.incomplete}"); 4212 "${dlg.save.incomplete}");
3168 } 4213 }
3169 else if (!isEmpty_Block(&d->sourceContent)) { 4214 else if (!isEmpty_Block(&d->sourceContent)) {
3170 const iBool doOpen = argLabel_Command(cmd, "open"); 4215 if (argLabel_Command(cmd, "extview")) {
3171 const iString *savePath = saveToDownloads_(d->mod.url, &d->sourceMime, 4216 if (equalCase_Rangecc(urlScheme_String(d->mod.url), "file")) {
3172 &d->sourceContent, !doOpen); 4217 /* Already a file so just open it directly. */
3173 if (!isEmpty_String(savePath) && doOpen) { 4218 postCommandf_Root(w->root, "!open default:1 url:%s", cstr_String(d->mod.url));
3174 postCommandf_Root( 4219 }
3175 w->root, "!open url:%s", cstrCollect_String(makeFileUrl_String(savePath))); 4220 else {
4221 const iString *tmpPath = temporaryPathForUrl_App(d->mod.url, &d->sourceMime);
4222 if (saveToFile_(tmpPath, &d->sourceContent, iFalse)) {
4223 postCommandf_Root(w->root, "!open default:1 url:%s",
4224 cstrCollect_String(makeFileUrl_String(tmpPath)));
4225 }
4226 }
4227 }
4228 else {
4229 const iBool doOpen = argLabel_Command(cmd, "open");
4230 const iString *savePath = saveToDownloads_(d->mod.url, &d->sourceMime,
4231 &d->sourceContent, !doOpen);
4232 if (!isEmpty_String(savePath) && doOpen) {
4233 postCommandf_Root(
4234 w->root, "!open url:%s", cstrCollect_String(makeFileUrl_String(savePath)));
4235 }
3176 } 4236 }
3177 } 4237 }
3178 return iTrue; 4238 return iTrue;
3179 } 4239 }
3180 else if (equal_Command(cmd, "document.reload") && document_Command(cmd) == d) { 4240 else if (equal_Command(cmd, "document.reload") && document_Command(cmd) == d) {
3181 d->initNormScrollY = normScrollPos_DocumentWidget_(d); 4241 d->initNormScrollY = normScrollPos_DocumentView_(&d->view);
3182 if (equalCase_Rangecc(urlScheme_String(d->mod.url), "titan")) { 4242 if (equalCase_Rangecc(urlScheme_String(d->mod.url), "titan")) {
3183 /* Reopen so the Upload dialog gets shown. */ 4243 /* Reopen so the Upload dialog gets shown. */
3184 postCommandf_App("open url:%s", cstr_String(d->mod.url)); 4244 postCommandf_App("open url:%s", cstr_String(d->mod.url));
3185 return iTrue; 4245 return iTrue;
3186 } 4246 }
3187 fetch_DocumentWidget_(d); 4247 fetch_DocumentWidget_(d);
3188 return iTrue; 4248 return iTrue;
@@ -3195,13 +4255,13 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
3195 if (d->flags & showLinkNumbers_DocumentWidgetFlag && 4255 if (d->flags & showLinkNumbers_DocumentWidgetFlag &&
3196 d->ordinalMode == homeRow_DocumentLinkOrdinalMode) { 4256 d->ordinalMode == homeRow_DocumentLinkOrdinalMode) {
3197 const size_t numKeys = iElemCount(homeRowKeys_); 4257 const size_t numKeys = iElemCount(homeRowKeys_);
3198 const iGmRun *last = lastVisibleLink_DocumentWidget_(d); 4258 const iGmRun *last = lastVisibleLink_DocumentView_(&d->view);
3199 if (!last) { 4259 if (!last) {
3200 d->ordinalBase = 0; 4260 d->ordinalBase = 0;
3201 } 4261 }
3202 else { 4262 else {
3203 d->ordinalBase += numKeys; 4263 d->ordinalBase += numKeys;
3204 if (visibleLinkOrdinal_DocumentWidget_(d, last->linkId) < d->ordinalBase) { 4264 if (visibleLinkOrdinal_DocumentView_(&d->view, last->linkId) < d->ordinalBase) {
3205 d->ordinalBase = 0; 4265 d->ordinalBase = 0;
3206 } 4266 }
3207 } 4267 }
@@ -3221,23 +4281,11 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
3221 iChangeFlags(d->flags, newTabViaHomeKeys_DocumentWidgetFlag, 4281 iChangeFlags(d->flags, newTabViaHomeKeys_DocumentWidgetFlag,
3222 argLabel_Command(cmd, "newtab") != 0); 4282 argLabel_Command(cmd, "newtab") != 0);
3223 } 4283 }
3224 invalidateVisibleLinks_DocumentWidget_(d); 4284 invalidateVisibleLinks_DocumentView_(&d->view);
3225 refresh_Widget(d); 4285 refresh_Widget(d);
3226 return iTrue; 4286 return iTrue;
3227 } 4287 }
3228 else if (equal_Command(cmd, "navigate.back") && document_App() == d) { 4288 else if (equal_Command(cmd, "navigate.back") && document_App() == d) {
3229 if (isPortraitPhone_App()) {
3230 if (d->flags & openedFromSidebar_DocumentWidgetFlag &&
3231 !isVisible_Widget(findWidget_App("sidebar"))) {
3232 postCommand_App("sidebar.toggle");
3233 showToolbar_Root(get_Root(), iTrue);
3234#if defined (iPlatformAppleMobile)
3235 playHapticEffect_iOS(gentleTap_HapticEffect);
3236#endif
3237 return iTrue;
3238 }
3239 d->flags &= ~openedFromSidebar_DocumentWidgetFlag;
3240 }
3241 if (d->request) { 4289 if (d->request) {
3242 postCommandf_Root(w->root, 4290 postCommandf_Root(w->root,
3243 "document.request.cancelled doc:%p url:%s", d, cstr_String(d->mod.url)); 4291 "document.request.cancelled doc:%p url:%s", d, cstr_String(d->mod.url));
@@ -3274,8 +4322,8 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
3274 return iTrue; 4322 return iTrue;
3275 } 4323 }
3276 else if (equalWidget_Command(cmd, w, "scroll.moved")) { 4324 else if (equalWidget_Command(cmd, w, "scroll.moved")) {
3277 init_Anim(&d->scrollY.pos, arg_Command(cmd)); 4325 init_Anim(&d->view.scrollY.pos, arg_Command(cmd));
3278 updateVisible_DocumentWidget_(d); 4326 updateVisible_DocumentView_(&d->view);
3279 return iTrue; 4327 return iTrue;
3280 } 4328 }
3281 else if (equal_Command(cmd, "scroll.page") && document_App() == d) { 4329 else if (equal_Command(cmd, "scroll.page") && document_App() == d) {
@@ -3286,25 +4334,26 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
3286 return iTrue; 4334 return iTrue;
3287 } 4335 }
3288 const float amount = argLabel_Command(cmd, "full") != 0 ? 1.0f : 0.5f; 4336 const float amount = argLabel_Command(cmd, "full") != 0 ? 1.0f : 0.5f;
3289 smoothScroll_DocumentWidget_(d, 4337 smoothScroll_DocumentView_(&d->view,
3290 dir * amount * height_Rect(documentBounds_DocumentWidget_(d)), 4338 dir * amount *
3291 smoothDuration_DocumentWidget_(keyboard_ScrollType)); 4339 height_Rect(documentBounds_DocumentView_(&d->view)),
4340 smoothDuration_DocumentWidget_(keyboard_ScrollType));
3292 return iTrue; 4341 return iTrue;
3293 } 4342 }
3294 else if (equal_Command(cmd, "scroll.top") && document_App() == d) { 4343 else if (equal_Command(cmd, "scroll.top") && document_App() == d) {
3295 init_Anim(&d->scrollY.pos, 0); 4344 init_Anim(&d->view.scrollY.pos, 0);
3296 invalidate_VisBuf(d->visBuf); 4345 invalidate_VisBuf(d->view.visBuf);
3297 clampScroll_DocumentWidget_(d); 4346 clampScroll_DocumentView_(&d->view);
3298 updateVisible_DocumentWidget_(d); 4347 updateVisible_DocumentView_(&d->view);
3299 refresh_Widget(w); 4348 refresh_Widget(w);
3300 return iTrue; 4349 return iTrue;
3301 } 4350 }
3302 else if (equal_Command(cmd, "scroll.bottom") && document_App() == d) { 4351 else if (equal_Command(cmd, "scroll.bottom") && document_App() == d) {
3303 updateScrollMax_DocumentWidget_(d); /* scrollY.max might not be fully updated */ 4352 updateScrollMax_DocumentView_(&d->view); /* scrollY.max might not be fully updated */
3304 init_Anim(&d->scrollY.pos, d->scrollY.max); 4353 init_Anim(&d->view.scrollY.pos, d->view.scrollY.max);
3305 invalidate_VisBuf(d->visBuf); 4354 invalidate_VisBuf(d->view.visBuf);
3306 clampScroll_DocumentWidget_(d); 4355 clampScroll_DocumentView_(&d->view);
3307 updateVisible_DocumentWidget_(d); 4356 updateVisible_DocumentView_(&d->view);
3308 refresh_Widget(w); 4357 refresh_Widget(w);
3309 return iTrue; 4358 return iTrue;
3310 } 4359 }
@@ -3315,9 +4364,9 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
3315 fetchNextUnfetchedImage_DocumentWidget_(d)) { 4364 fetchNextUnfetchedImage_DocumentWidget_(d)) {
3316 return iTrue; 4365 return iTrue;
3317 } 4366 }
3318 smoothScroll_DocumentWidget_(d, 4367 smoothScroll_DocumentView_(&d->view,
3319 3 * lineHeight_Text(paragraph_FontId) * dir, 4368 3 * lineHeight_Text(paragraph_FontId) * dir,
3320 smoothDuration_DocumentWidget_(keyboard_ScrollType)); 4369 smoothDuration_DocumentWidget_(keyboard_ScrollType));
3321 return iTrue; 4370 return iTrue;
3322 } 4371 }
3323 else if (equal_Command(cmd, "document.goto") && document_App() == d) { 4372 else if (equal_Command(cmd, "document.goto") && document_App() == d) {
@@ -3328,13 +4377,13 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
3328 setCStr_String(&d->pendingGotoHeading, heading); 4377 setCStr_String(&d->pendingGotoHeading, heading);
3329 return iTrue; 4378 return iTrue;
3330 } 4379 }
3331 scrollToHeading_DocumentWidget_(d, heading); 4380 scrollToHeading_DocumentView_(&d->view, heading);
3332 return iTrue; 4381 return iTrue;
3333 } 4382 }
3334 const char *loc = pointerLabel_Command(cmd, "loc"); 4383 const char *loc = pointerLabel_Command(cmd, "loc");
3335 const iGmRun *run = findRunAtLoc_GmDocument(d->doc, loc); 4384 const iGmRun *run = findRunAtLoc_GmDocument(d->view.doc, loc);
3336 if (run) { 4385 if (run) {
3337 scrollTo_DocumentWidget_(d, run->visBounds.pos.y, iFalse); 4386 scrollTo_DocumentView_(&d->view, run->visBounds.pos.y, iFalse);
3338 } 4387 }
3339 return iTrue; 4388 return iTrue;
3340 } 4389 }
@@ -3349,24 +4398,24 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
3349 } 4398 }
3350 else { 4399 else {
3351 const iBool wrap = d->foundMark.start != NULL; 4400 const iBool wrap = d->foundMark.start != NULL;
3352 d->foundMark = finder(d->doc, text_InputWidget(find), dir > 0 ? d->foundMark.end 4401 d->foundMark = finder(d->view.doc, text_InputWidget(find), dir > 0 ? d->foundMark.end
3353 : d->foundMark.start); 4402 : d->foundMark.start);
3354 if (!d->foundMark.start && wrap) { 4403 if (!d->foundMark.start && wrap) {
3355 /* Wrap around. */ 4404 /* Wrap around. */
3356 d->foundMark = finder(d->doc, text_InputWidget(find), NULL); 4405 d->foundMark = finder(d->view.doc, text_InputWidget(find), NULL);
3357 } 4406 }
3358 if (d->foundMark.start) { 4407 if (d->foundMark.start) {
3359 const iGmRun *found; 4408 const iGmRun *found;
3360 if ((found = findRunAtLoc_GmDocument(d->doc, d->foundMark.start)) != NULL) { 4409 if ((found = findRunAtLoc_GmDocument(d->view.doc, d->foundMark.start)) != NULL) {
3361 scrollTo_DocumentWidget_(d, mid_Rect(found->bounds).y, iTrue); 4410 scrollTo_DocumentView_(&d->view, mid_Rect(found->bounds).y, iTrue);
3362 } 4411 }
3363 } 4412 }
3364 } 4413 }
3365 if (flags_Widget(w) & touchDrag_WidgetFlag) { 4414 if (flags_Widget(w) & touchDrag_WidgetFlag) {
3366 postCommand_Root(w->root, "document.select arg:0"); /* we can't handle both at the same time */ 4415 postCommand_Root(w->root, "document.select arg:0"); /* we can't handle both at the same time */
3367 } 4416 }
3368 invalidateWideRunsWithNonzeroOffset_DocumentWidget_(d); /* markers don't support offsets */ 4417 invalidateWideRunsWithNonzeroOffset_DocumentView_(&d->view); /* markers don't support offsets */
3369 resetWideRuns_DocumentWidget_(d); 4418 resetWideRuns_DocumentView_(&d->view);
3370 refresh_Widget(w); 4419 refresh_Widget(w);
3371 return iTrue; 4420 return iTrue;
3372 } 4421 }
@@ -3379,16 +4428,16 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
3379 } 4428 }
3380 else if (equal_Command(cmd, "bookmark.links") && document_App() == d) { 4429 else if (equal_Command(cmd, "bookmark.links") && document_App() == d) {
3381 iPtrArray *links = collectNew_PtrArray(); 4430 iPtrArray *links = collectNew_PtrArray();
3382 render_GmDocument(d->doc, (iRangei){ 0, size_GmDocument(d->doc).y }, addAllLinks_, links); 4431 render_GmDocument(d->view.doc, (iRangei){ 0, size_GmDocument(d->view.doc).y }, addAllLinks_, links);
3383 /* Find links that aren't already bookmarked. */ 4432 /* Find links that aren't already bookmarked. */
3384 iForEach(PtrArray, i, links) { 4433 iForEach(PtrArray, i, links) {
3385 const iGmRun *run = i.ptr; 4434 const iGmRun *run = i.ptr;
3386 uint32_t bmid; 4435 uint32_t bmid;
3387 if ((bmid = findUrl_Bookmarks(bookmarks_App(), 4436 if ((bmid = findUrl_Bookmarks(bookmarks_App(),
3388 linkUrl_GmDocument(d->doc, run->linkId))) != 0) { 4437 linkUrl_GmDocument(d->view.doc, run->linkId))) != 0) {
3389 const iBookmark *bm = get_Bookmarks(bookmarks_App(), bmid); 4438 const iBookmark *bm = get_Bookmarks(bookmarks_App(), bmid);
3390 /* We can import local copies of remote bookmarks. */ 4439 /* We can import local copies of remote bookmarks. */
3391 if (!hasTag_Bookmark(bm, remote_BookmarkTag)) { 4440 if (~bm->flags & remote_BookmarkFlag) {
3392 remove_PtrArrayIterator(&i); 4441 remove_PtrArrayIterator(&i);
3393 } 4442 }
3394 } 4443 }
@@ -3412,7 +4461,7 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
3412 iConstForEach(PtrArray, j, links) { 4461 iConstForEach(PtrArray, j, links) {
3413 const iGmRun *run = j.ptr; 4462 const iGmRun *run = j.ptr;
3414 add_Bookmarks(bookmarks_App(), 4463 add_Bookmarks(bookmarks_App(),
3415 linkUrl_GmDocument(d->doc, run->linkId), 4464 linkUrl_GmDocument(d->view.doc, run->linkId),
3416 collect_String(newRange_String(run->text)), 4465 collect_String(newRange_String(run->text)),
3417 NULL, 4466 NULL,
3418 0x1f588 /* pin */); 4467 0x1f588 /* pin */);
@@ -3427,7 +4476,7 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
3427 return iTrue; 4476 return iTrue;
3428 } 4477 }
3429 else if (equalWidget_Command(cmd, w, "menu.closed")) { 4478 else if (equalWidget_Command(cmd, w, "menu.closed")) {
3430 updateHover_DocumentWidget_(d, mouseCoord_Window(get_Window(), 0)); 4479 updateHover_DocumentView_(&d->view, mouseCoord_Window(get_Window(), 0));
3431 } 4480 }
3432 else if (equal_Command(cmd, "document.autoreload")) { 4481 else if (equal_Command(cmd, "document.autoreload")) {
3433 if (d->mod.reloadInterval) { 4482 if (d->mod.reloadInterval) {
@@ -3485,7 +4534,7 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
3485 if (argLabel_Command(cmd, "ttf")) { 4534 if (argLabel_Command(cmd, "ttf")) {
3486 iAssert(!cmp_String(&d->sourceMime, "font/ttf")); 4535 iAssert(!cmp_String(&d->sourceMime, "font/ttf"));
3487 installFontFile_Fonts(collect_String(suffix_Command(cmd, "name")), &d->sourceContent); 4536 installFontFile_Fonts(collect_String(suffix_Command(cmd, "name")), &d->sourceContent);
3488 postCommand_App("open url:about:fonts"); 4537 postCommand_App("open url:about:fonts");
3489 } 4538 }
3490 else { 4539 else {
3491 const iString *id = idFromUrl_FontPack(d->mod.url); 4540 const iString *id = idFromUrl_FontPack(d->mod.url);
@@ -3497,14 +4546,9 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
3497 return iFalse; 4546 return iFalse;
3498} 4547}
3499 4548
3500static iRect runRect_DocumentWidget_(const iDocumentWidget *d, const iGmRun *run) {
3501 const iRect docBounds = documentBounds_DocumentWidget_(d);
3502 return moved_Rect(run->bounds, addY_I2(topLeft_Rect(docBounds), viewPos_DocumentWidget_(d)));
3503}
3504
3505static void setGrabbedPlayer_DocumentWidget_(iDocumentWidget *d, const iGmRun *run) { 4549static void setGrabbedPlayer_DocumentWidget_(iDocumentWidget *d, const iGmRun *run) {
3506 if (run && run->mediaType == audio_MediaType) { 4550 if (run && run->mediaType == audio_MediaType) {
3507 iPlayer *plr = audioPlayer_Media(media_GmDocument(d->doc), mediaId_GmRun(run)); 4551 iPlayer *plr = audioPlayer_Media(media_GmDocument(d->view.doc), mediaId_GmRun(run));
3508 setFlags_Player(plr, volumeGrabbed_PlayerFlag, iTrue); 4552 setFlags_Player(plr, volumeGrabbed_PlayerFlag, iTrue);
3509 d->grabbedStartVolume = volume_Player(plr); 4553 d->grabbedStartVolume = volume_Player(plr);
3510 d->grabbedPlayer = run; 4554 d->grabbedPlayer = run;
@@ -3512,7 +4556,7 @@ static void setGrabbedPlayer_DocumentWidget_(iDocumentWidget *d, const iGmRun *r
3512 } 4556 }
3513 else if (d->grabbedPlayer) { 4557 else if (d->grabbedPlayer) {
3514 setFlags_Player( 4558 setFlags_Player(
3515 audioPlayer_Media(media_GmDocument(d->doc), mediaId_GmRun(d->grabbedPlayer)), 4559 audioPlayer_Media(media_GmDocument(d->view.doc), mediaId_GmRun(d->grabbedPlayer)),
3516 volumeGrabbed_PlayerFlag, 4560 volumeGrabbed_PlayerFlag,
3517 iFalse); 4561 iFalse);
3518 d->grabbedPlayer = NULL; 4562 d->grabbedPlayer = NULL;
@@ -3528,24 +4572,33 @@ static iBool processMediaEvents_DocumentWidget_(iDocumentWidget *d, const SDL_Ev
3528 ev->type != SDL_MOUSEMOTION) { 4572 ev->type != SDL_MOUSEMOTION) {
3529 return iFalse; 4573 return iFalse;
3530 } 4574 }
3531 if (ev->type == SDL_MOUSEBUTTONDOWN || ev->type == SDL_MOUSEBUTTONUP) {
3532 if (ev->button.button != SDL_BUTTON_LEFT) {
3533 return iFalse;
3534 }
3535 }
3536 if (d->grabbedPlayer) { 4575 if (d->grabbedPlayer) {
3537 /* Updated in the drag. */ 4576 /* Updated in the drag. */
3538 return iFalse; 4577 return iFalse;
3539 } 4578 }
3540 const iInt2 mouse = init_I2(ev->button.x, ev->button.y); 4579 const iInt2 mouse = init_I2(ev->button.x, ev->button.y);
3541 iConstForEach(PtrArray, i, &d->visibleMedia) { 4580 iConstForEach(PtrArray, i, &d->view.visibleMedia) {
3542 const iGmRun *run = i.ptr; 4581 const iGmRun *run = i.ptr;
4582 if (run->mediaType == download_MediaType) {
4583 iDownloadUI ui;
4584 init_DownloadUI(&ui, media_GmDocument(d->view.doc), mediaId_GmRun(run).id,
4585 runRect_DocumentView_(&d->view, run));
4586 if (processEvent_DownloadUI(&ui, ev)) {
4587 return iTrue;
4588 }
4589 continue;
4590 }
3543 if (run->mediaType != audio_MediaType) { 4591 if (run->mediaType != audio_MediaType) {
3544 continue; 4592 continue;
3545 } 4593 }
4594 if (ev->type == SDL_MOUSEBUTTONDOWN || ev->type == SDL_MOUSEBUTTONUP) {
4595 if (ev->button.button != SDL_BUTTON_LEFT) {
4596 return iFalse;
4597 }
4598 }
3546 /* TODO: move this to mediaui.c */ 4599 /* TODO: move this to mediaui.c */
3547 const iRect rect = runRect_DocumentWidget_(d, run); 4600 const iRect rect = runRect_DocumentView_(&d->view, run);
3548 iPlayer * plr = audioPlayer_Media(media_GmDocument(d->doc), mediaId_GmRun(run)); 4601 iPlayer * plr = audioPlayer_Media(media_GmDocument(d->view.doc), mediaId_GmRun(run));
3549 if (contains_Rect(rect, mouse)) { 4602 if (contains_Rect(rect, mouse)) {
3550 iPlayerUI ui; 4603 iPlayerUI ui;
3551 init_PlayerUI(&ui, plr, rect); 4604 init_PlayerUI(&ui, plr, rect);
@@ -3610,83 +4663,140 @@ static iBool processMediaEvents_DocumentWidget_(iDocumentWidget *d, const SDL_Ev
3610 return iFalse; 4663 return iFalse;
3611} 4664}
3612 4665
3613static size_t linkOrdinalFromKey_DocumentWidget_(const iDocumentWidget *d, int key) { 4666static void beginMarkingSelection_DocumentWidget_(iDocumentWidget *d, iInt2 pos) {
3614 size_t ord = iInvalidPos; 4667 setFocus_Widget(NULL); /* TODO: Focus this document? */
3615 if (d->ordinalMode == numbersAndAlphabet_DocumentLinkOrdinalMode) { 4668 invalidateWideRunsWithNonzeroOffset_DocumentView_(&d->view);
3616 if (key >= '1' && key <= '9') { 4669 resetWideRuns_DocumentView_(&d->view); /* Selections don't support horizontal scrolling. */
3617 return key - '1'; 4670 iChangeFlags(d->flags, selecting_DocumentWidgetFlag, iTrue);
3618 } 4671 d->initialSelectMark = d->selectMark = sourceLoc_DocumentView_(&d->view, pos);
3619 if (key < 'a' || key > 'z') { 4672 refresh_Widget(as_Widget(d));
3620 return iInvalidPos; 4673}
3621 } 4674
3622 ord = key - 'a' + 9; 4675static void interactingWithLink_DocumentWidget_(iDocumentWidget *d, iGmLinkId id) {
3623#if defined (iPlatformApple) 4676 iRangecc loc = linkUrlRange_GmDocument(d->view.doc, id);
3624 /* Skip keys that would conflict with default system shortcuts: hide, minimize, quit, close. */ 4677 if (!loc.start) {
3625 if (key == 'h' || key == 'm' || key == 'q' || key == 'w') { 4678 clear_String(&d->linePrecedingLink);
3626 return iInvalidPos; 4679 return;
3627 }
3628 if (key > 'h') ord--;
3629 if (key > 'm') ord--;
3630 if (key > 'q') ord--;
3631 if (key > 'w') ord--;
3632#endif
3633 } 4680 }
3634 else { 4681 d->requestLinkId = id;
3635 iForIndices(i, homeRowKeys_) { 4682 const char *start = range_String(source_GmDocument(d->view.doc)).start;
3636 if (homeRowKeys_[i] == key) { 4683 /* Find the preceding line. This is offered as a prefill option for a possible input query. */
3637 return i; 4684 while (loc.start > start && *loc.start != '\n') {
3638 } 4685 loc.start--;
3639 }
3640 } 4686 }
3641 return ord; 4687 loc.end = loc.start; /* End of the preceding line. */
4688 if (loc.start > start) {
4689 loc.start--;
4690 }
4691 while (loc.start > start && *loc.start != '\n') {
4692 loc.start--;
4693 }
4694 if (*loc.start == '\n') {
4695 loc.start++; /* Start of the preceding line. */
4696 }
4697 setRange_String(&d->linePrecedingLink, loc);
3642} 4698}
3643 4699
3644static iChar linkOrdinalChar_DocumentWidget_(const iDocumentWidget *d, size_t ord) { 4700iLocalDef int wheelSwipeSide_DocumentWidget_(const iDocumentWidget *d) {
3645 if (d->ordinalMode == numbersAndAlphabet_DocumentLinkOrdinalMode) { 4701 return (d->flags & rightWheelSwipe_DocumentWidgetFlag ? 2
3646 if (ord < 9) { 4702 : d->flags & leftWheelSwipe_DocumentWidgetFlag ? 1
3647 return '1' + ord; 4703 : 0);
3648 } 4704}
3649#if defined (iPlatformApple) 4705
3650 if (ord < 9 + 22) { 4706static void finishWheelSwipe_DocumentWidget_(iDocumentWidget *d) {
3651 int key = 'a' + ord - 9; 4707 if (d->flags & eitherWheelSwipe_DocumentWidgetFlag &&
3652 if (key >= 'h') key++; 4708 d->wheelSwipeState == direct_WheelSwipeState) {
3653 if (key >= 'm') key++; 4709 const int side = wheelSwipeSide_DocumentWidget_(d);
3654 if (key >= 'q') key++; 4710 int abort = ((side == 1 && d->swipeSpeed < 0) || (side == 2 && d->swipeSpeed > 0));
3655 if (key >= 'w') key++; 4711 if (iAbs(d->wheelSwipeDistance) < width_Widget(d) / 4 && iAbs(d->swipeSpeed) < 4 * gap_UI) {
3656 return 'A' + key - 'a'; 4712 abort = 1;
3657 }
3658#else
3659 if (ord < 9 + 26) {
3660 return 'A' + ord - 9;
3661 }
3662#endif
3663 }
3664 else {
3665 if (ord < iElemCount(homeRowKeys_)) {
3666 return 'A' + homeRowKeys_[ord] - 'a';
3667 } 4713 }
4714 postCommand_Widget(d, "edgeswipe.ended side:%d abort:%d", side, abort);
4715 d->flags &= ~eitherWheelSwipe_DocumentWidgetFlag;
3668 } 4716 }
3669 return 0;
3670} 4717}
3671 4718
3672static void beginMarkingSelection_DocumentWidget_(iDocumentWidget *d, iInt2 pos) { 4719static iBool handleWheelSwipe_DocumentWidget_(iDocumentWidget *d, const SDL_MouseWheelEvent *ev) {
3673 setFocus_Widget(NULL); /* TODO: Focus this document? */ 4720 iWidget *w = as_Widget(d);
3674 invalidateWideRunsWithNonzeroOffset_DocumentWidget_(d); 4721 if (deviceType_App() != desktop_AppDeviceType) {
3675 resetWideRuns_DocumentWidget_(d); /* Selections don't support horizontal scrolling. */ 4722 return iFalse;
3676 iChangeFlags(d->flags, selecting_DocumentWidgetFlag, iTrue); 4723 }
3677 d->initialSelectMark = d->selectMark = sourceLoc_DocumentWidget_(d, pos); 4724 if (~flags_Widget(w) & horizontalOffset_WidgetFlag) {
3678 refresh_Widget(as_Widget(d)); 4725 return iFalse;
4726 }
4727 iAssert(~d->flags & animationPlaceholder_DocumentWidgetFlag);
4728// printf("STATE:%d wheel x:%d inert:%d end:%d\n", d->wheelSwipeState,
4729// ev->x, isInertia_MouseWheelEvent(ev),
4730// isScrollFinished_MouseWheelEvent(ev));
4731// fflush(stdout);
4732 switch (d->wheelSwipeState) {
4733 case none_WheelSwipeState:
4734 /* A new swipe starts. */
4735 if (!isInertia_MouseWheelEvent(ev) && !isScrollFinished_MouseWheelEvent(ev)) {
4736 int side = ev->x > 0 ? 1 : 2;
4737 d->wheelSwipeDistance = ev->x * 2;
4738 d->flags &= ~eitherWheelSwipe_DocumentWidgetFlag;
4739 d->flags |= (side == 1 ? leftWheelSwipe_DocumentWidgetFlag
4740 : rightWheelSwipe_DocumentWidgetFlag);
4741 // printf("swipe starts at %d, side %d\n", d->wheelSwipeDistance, side);
4742 d->wheelSwipeState = direct_WheelSwipeState;
4743 d->swipeSpeed = 0;
4744 postCommand_Widget(d, "edgeswipe.moved arg:%d side:%d", d->wheelSwipeDistance, side);
4745 return iTrue;
4746 }
4747 break;
4748 case direct_WheelSwipeState:
4749 if (isInertia_MouseWheelEvent(ev) || isScrollFinished_MouseWheelEvent(ev)) {
4750 finishWheelSwipe_DocumentWidget_(d);
4751 d->wheelSwipeState = none_WheelSwipeState;
4752 }
4753 else {
4754 int step = ev->x * 2;
4755 d->wheelSwipeDistance += step;
4756 /* Remember the maximum speed. */
4757 if (d->swipeSpeed < 0 && step < 0) {
4758 d->swipeSpeed = iMin(d->swipeSpeed, step);
4759 }
4760 else if (d->swipeSpeed > 0 && step > 0) {
4761 d->swipeSpeed = iMax(d->swipeSpeed, step);
4762 }
4763 else {
4764 d->swipeSpeed = step;
4765 }
4766 switch (wheelSwipeSide_DocumentWidget_(d)) {
4767 case 1:
4768 d->wheelSwipeDistance = iMax(0, d->wheelSwipeDistance);
4769 d->wheelSwipeDistance = iMin(width_Widget(d), d->wheelSwipeDistance);
4770 break;
4771 case 2:
4772 d->wheelSwipeDistance = iMin(0, d->wheelSwipeDistance);
4773 d->wheelSwipeDistance = iMax(-width_Widget(d), d->wheelSwipeDistance);
4774 break;
4775 }
4776 /* TODO: calculate speed, rememeber direction */
4777 //printf("swipe moved to %d, side %d\n", d->wheelSwipeDistance, side);
4778 postCommand_Widget(d, "edgeswipe.moved arg:%d side:%d", d->wheelSwipeDistance,
4779 wheelSwipeSide_DocumentWidget_(d));
4780 }
4781 return iTrue;
4782 }
4783 return iFalse;
3679} 4784}
3680 4785
3681static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *ev) { 4786static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *ev) {
3682 iWidget *w = as_Widget(d); 4787 iWidget *w = as_Widget(d);
4788 iDocumentView *view = &d->view;
3683 if (isMetricsChange_UserEvent(ev)) { 4789 if (isMetricsChange_UserEvent(ev)) {
3684 updateSize_DocumentWidget(d); 4790 updateSize_DocumentWidget(d);
3685 } 4791 }
3686 else if (processEvent_SmoothScroll(&d->scrollY, ev)) { 4792 else if (processEvent_SmoothScroll(&d->view.scrollY, ev)) {
3687 return iTrue; 4793 return iTrue;
3688 } 4794 }
3689 else if (ev->type == SDL_USEREVENT && ev->user.code == command_UserEventCode) { 4795 else if (ev->type == SDL_USEREVENT && ev->user.code == command_UserEventCode) {
4796 if (isCommand_Widget(w, ev, "pullaction")) {
4797 postCommand_Widget(w, "navigate.reload");
4798 return iTrue;
4799 }
3690 if (!handleCommand_DocumentWidget_(d, command_UserEvent(ev))) { 4800 if (!handleCommand_DocumentWidget_(d, command_UserEvent(ev))) {
3691 /* Base class commands. */ 4801 /* Base class commands. */
3692 return processEvent_Widget(w, ev); 4802 return processEvent_Widget(w, ev);
@@ -3698,27 +4808,28 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
3698 if ((d->flags & showLinkNumbers_DocumentWidgetFlag) && 4808 if ((d->flags & showLinkNumbers_DocumentWidgetFlag) &&
3699 ((key >= '1' && key <= '9') || (key >= 'a' && key <= 'z'))) { 4809 ((key >= '1' && key <= '9') || (key >= 'a' && key <= 'z'))) {
3700 const size_t ord = linkOrdinalFromKey_DocumentWidget_(d, key) + d->ordinalBase; 4810 const size_t ord = linkOrdinalFromKey_DocumentWidget_(d, key) + d->ordinalBase;
3701 iConstForEach(PtrArray, i, &d->visibleLinks) { 4811 iConstForEach(PtrArray, i, &d->view.visibleLinks) {
3702 if (ord == iInvalidPos) break; 4812 if (ord == iInvalidPos) break;
3703 const iGmRun *run = i.ptr; 4813 const iGmRun *run = i.ptr;
3704 if (run->flags & decoration_GmRunFlag && 4814 if (run->flags & decoration_GmRunFlag &&
3705 visibleLinkOrdinal_DocumentWidget_(d, run->linkId) == ord) { 4815 visibleLinkOrdinal_DocumentView_(view, run->linkId) == ord) {
3706 if (d->flags & setHoverViaKeys_DocumentWidgetFlag) { 4816 if (d->flags & setHoverViaKeys_DocumentWidgetFlag) {
3707 d->hoverLink = run; 4817 view->hoverLink = run;
3708 } 4818 }
3709 else { 4819 else {
3710 postCommandf_Root(w->root, 4820 postCommandf_Root(
3711 "open newtab:%d url:%s", 4821 w->root,
3712 (isPinned_DocumentWidget_(d) ? otherRoot_OpenTabFlag : 0) ^ 4822 "open newtab:%d url:%s",
3713 (d->ordinalMode == 4823 (isPinned_DocumentWidget_(d) ? otherRoot_OpenTabFlag : 0) ^
3714 numbersAndAlphabet_DocumentLinkOrdinalMode 4824 (d->ordinalMode == numbersAndAlphabet_DocumentLinkOrdinalMode
3715 ? openTabMode_Sym(modState_Keys()) 4825 ? openTabMode_Sym(modState_Keys())
3716 : (d->flags & newTabViaHomeKeys_DocumentWidgetFlag ? 1 : 0)), 4826 : (d->flags & newTabViaHomeKeys_DocumentWidgetFlag ? 1 : 0)),
3717 cstr_String(absoluteUrl_String( 4827 cstr_String(absoluteUrl_String(
3718 d->mod.url, linkUrl_GmDocument(d->doc, run->linkId)))); 4828 d->mod.url, linkUrl_GmDocument(view->doc, run->linkId))));
4829 interactingWithLink_DocumentWidget_(d, run->linkId);
3719 } 4830 }
3720 setLinkNumberMode_DocumentWidget_(d, iFalse); 4831 setLinkNumberMode_DocumentWidget_(d, iFalse);
3721 invalidateVisibleLinks_DocumentWidget_(d); 4832 invalidateVisibleLinks_DocumentView_(view);
3722 refresh_Widget(d); 4833 refresh_Widget(d);
3723 return iTrue; 4834 return iTrue;
3724 } 4835 }
@@ -3728,7 +4839,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
3728 case SDLK_ESCAPE: 4839 case SDLK_ESCAPE:
3729 if (d->flags & showLinkNumbers_DocumentWidgetFlag && document_App() == d) { 4840 if (d->flags & showLinkNumbers_DocumentWidgetFlag && document_App() == d) {
3730 setLinkNumberMode_DocumentWidget_(d, iFalse); 4841 setLinkNumberMode_DocumentWidget_(d, iFalse);
3731 invalidateVisibleLinks_DocumentWidget_(d); 4842 invalidateVisibleLinks_DocumentView_(view);
3732 refresh_Widget(d); 4843 refresh_Widget(d);
3733 return iTrue; 4844 return iTrue;
3734 } 4845 }
@@ -3740,7 +4851,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
3740 for (size_t i = 0; i < 64; ++i) { 4851 for (size_t i = 0; i < 64; ++i) {
3741 setByte_Block(seed, i, iRandom(0, 256)); 4852 setByte_Block(seed, i, iRandom(0, 256));
3742 } 4853 }
3743 setThemeSeed_GmDocument(d->doc, seed); 4854 setThemeSeed_GmDocument(view->doc, seed);
3744 delete_Block(seed); 4855 delete_Block(seed);
3745 invalidate_DocumentWidget_(d); 4856 invalidate_DocumentWidget_(d);
3746 refresh_Widget(w); 4857 refresh_Widget(w);
@@ -3770,13 +4881,24 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
3770#endif 4881#endif
3771 } 4882 }
3772 } 4883 }
4884#if defined (iPlatformAppleDesktop)
4885 else if (ev->type == SDL_MOUSEWHEEL &&
4886 ev->wheel.y == 0 &&
4887 d->wheelSwipeState == direct_WheelSwipeState &&
4888 handleWheelSwipe_DocumentWidget_(d, &ev->wheel)) {
4889 return iTrue;
4890 }
4891#endif
3773 else if (ev->type == SDL_MOUSEWHEEL && isHover_Widget(w)) { 4892 else if (ev->type == SDL_MOUSEWHEEL && isHover_Widget(w)) {
3774 const iInt2 mouseCoord = coord_MouseWheelEvent(&ev->wheel); 4893 const iInt2 mouseCoord = coord_MouseWheelEvent(&ev->wheel);
3775 if (isPerPixel_MouseWheelEvent(&ev->wheel)) { 4894 if (isPerPixel_MouseWheelEvent(&ev->wheel)) {
3776 const iInt2 wheel = init_I2(ev->wheel.x, ev->wheel.y); 4895 const iInt2 wheel = init_I2(ev->wheel.x, ev->wheel.y);
3777 stop_Anim(&d->scrollY.pos); 4896 stop_Anim(&d->view.scrollY.pos);
3778 immediateScroll_DocumentWidget_(d, -wheel.y); 4897 immediateScroll_DocumentView_(view, -wheel.y);
3779 scrollWideBlock_DocumentWidget_(d, mouseCoord, -wheel.x, 0); 4898 if (!scrollWideBlock_DocumentView_(view, mouseCoord, -wheel.x, 0) &&
4899 wheel.x) {
4900 handleWheelSwipe_DocumentWidget_(d, &ev->wheel);
4901 }
3780 } 4902 }
3781 else { 4903 else {
3782 /* Traditional mouse wheel. */ 4904 /* Traditional mouse wheel. */
@@ -3785,16 +4907,11 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
3785 postCommandf_App("zoom.delta arg:%d", amount > 0 ? 10 : -10); 4907 postCommandf_App("zoom.delta arg:%d", amount > 0 ? 10 : -10);
3786 return iTrue; 4908 return iTrue;
3787 } 4909 }
3788 smoothScroll_DocumentWidget_( 4910 smoothScroll_DocumentView_(view,
3789 d, 4911 -3 * amount * lineHeight_Text(paragraph_FontId),
3790 -3 * amount * lineHeight_Text(paragraph_FontId), 4912 smoothDuration_DocumentWidget_(mouse_ScrollType));
3791 smoothDuration_DocumentWidget_(mouse_ScrollType)); 4913 scrollWideBlock_DocumentView_(
3792 /* accelerated speed for repeated wheelings */ 4914 view, mouseCoord, -3 * ev->wheel.x * lineHeight_Text(paragraph_FontId), 167);
3793// * (!isFinished_SmoothScroll(&d->scrollY) && pos_Anim(&d->scrollY.pos) < 0.25f
3794// ? 0.5f
3795// : 1.0f));
3796 scrollWideBlock_DocumentWidget_(
3797 d, mouseCoord, -3 * ev->wheel.x * lineHeight_Text(paragraph_FontId), 167);
3798 } 4915 }
3799 iChangeFlags(d->flags, noHoverWhileScrolling_DocumentWidgetFlag, iTrue); 4916 iChangeFlags(d->flags, noHoverWhileScrolling_DocumentWidgetFlag, iTrue);
3800 return iTrue; 4917 return iTrue;
@@ -3813,16 +4930,19 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
3813 } 4930 }
3814#endif 4931#endif
3815 else { 4932 else {
3816 if (value_Anim(&d->altTextOpacity) < 0.833f) { 4933 if (value_Anim(&view->altTextOpacity) < 0.833f) {
3817 setValue_Anim(&d->altTextOpacity, 0, 0); /* keep it hidden while moving */ 4934 setValue_Anim(&view->altTextOpacity, 0, 0); /* keep it hidden while moving */
3818 } 4935 }
3819 updateHover_DocumentWidget_(d, mpos); 4936 updateHover_DocumentView_(view, mpos);
3820 } 4937 }
3821 } 4938 }
3822 if (ev->type == SDL_USEREVENT && ev->user.code == widgetTapBegins_UserEventCode) { 4939 if (ev->type == SDL_USEREVENT && ev->user.code == widgetTapBegins_UserEventCode) {
3823 iChangeFlags(d->flags, noHoverWhileScrolling_DocumentWidgetFlag, iFalse); 4940 iChangeFlags(d->flags, noHoverWhileScrolling_DocumentWidgetFlag, iFalse);
3824 return iTrue; 4941 return iTrue;
3825 } 4942 }
4943 if (processMediaEvents_DocumentWidget_(d, ev)) {
4944 return iTrue;
4945 }
3826 if (ev->type == SDL_MOUSEBUTTONDOWN) { 4946 if (ev->type == SDL_MOUSEBUTTONDOWN) {
3827 if (ev->button.button == SDL_BUTTON_X1) { 4947 if (ev->button.button == SDL_BUTTON_X1) {
3828 postCommand_Root(w->root, "navigate.back"); 4948 postCommand_Root(w->root, "navigate.back");
@@ -3832,17 +4952,18 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
3832 postCommand_Root(w->root, "navigate.forward"); 4952 postCommand_Root(w->root, "navigate.forward");
3833 return iTrue; 4953 return iTrue;
3834 } 4954 }
3835 if (ev->button.button == SDL_BUTTON_MIDDLE && d->hoverLink) { 4955 if (ev->button.button == SDL_BUTTON_MIDDLE && view->hoverLink) {
4956 interactingWithLink_DocumentWidget_(d, view->hoverLink->linkId);
3836 postCommandf_Root(w->root, "open newtab:%d url:%s", 4957 postCommandf_Root(w->root, "open newtab:%d url:%s",
3837 (isPinned_DocumentWidget_(d) ? otherRoot_OpenTabFlag : 0) | 4958 (isPinned_DocumentWidget_(d) ? otherRoot_OpenTabFlag : 0) |
3838 (modState_Keys() & KMOD_SHIFT ? new_OpenTabFlag : newBackground_OpenTabFlag), 4959 (modState_Keys() & KMOD_SHIFT ? new_OpenTabFlag : newBackground_OpenTabFlag),
3839 cstr_String(linkUrl_GmDocument(d->doc, d->hoverLink->linkId))); 4960 cstr_String(linkUrl_GmDocument(view->doc, view->hoverLink->linkId)));
3840 return iTrue; 4961 return iTrue;
3841 } 4962 }
3842 if (ev->button.button == SDL_BUTTON_RIGHT && 4963 if (ev->button.button == SDL_BUTTON_RIGHT &&
3843 contains_Widget(w, init_I2(ev->button.x, ev->button.y))) { 4964 contains_Widget(w, init_I2(ev->button.x, ev->button.y))) {
3844 if (!isVisible_Widget(d->menu)) { 4965 if (!isVisible_Widget(d->menu)) {
3845 d->contextLink = d->hoverLink; 4966 d->contextLink = view->hoverLink;
3846 d->contextPos = init_I2(ev->button.x, ev->button.y); 4967 d->contextPos = init_I2(ev->button.x, ev->button.y);
3847 if (d->menu) { 4968 if (d->menu) {
3848 destroy_Widget(d->menu); 4969 destroy_Widget(d->menu);
@@ -3853,15 +4974,18 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
3853 init_Array(&items, sizeof(iMenuItem)); 4974 init_Array(&items, sizeof(iMenuItem));
3854 if (d->contextLink) { 4975 if (d->contextLink) {
3855 /* Context menu for a link. */ 4976 /* Context menu for a link. */
3856 const iString *linkUrl = linkUrl_GmDocument(d->doc, d->contextLink->linkId); 4977 interactingWithLink_DocumentWidget_(d, d->contextLink->linkId); /* perhaps will be triggered */
4978 const iString *linkUrl = linkUrl_GmDocument(view->doc, d->contextLink->linkId);
3857// const int linkFlags = linkFlags_GmDocument(d->doc, d->contextLink->linkId); 4979// const int linkFlags = linkFlags_GmDocument(d->doc, d->contextLink->linkId);
3858 const iRangecc scheme = urlScheme_String(linkUrl); 4980 const iRangecc scheme = urlScheme_String(linkUrl);
3859 const iBool isGemini = equalCase_Rangecc(scheme, "gemini"); 4981 const iBool isGemini = equalCase_Rangecc(scheme, "gemini");
3860 iBool isNative = iFalse; 4982 iBool isNative = iFalse;
3861 if (deviceType_App() != desktop_AppDeviceType) { 4983 if (deviceType_App() != desktop_AppDeviceType) {
3862 /* Show the link as the first, non-interactive item. */ 4984 /* Show the link as the first, non-interactive item. */
4985 iString *infoText = collectNew_String();
4986 infoText_LinkInfo(d->view.doc, d->contextLink->linkId, infoText);
3863 pushBack_Array(&items, &(iMenuItem){ 4987 pushBack_Array(&items, &(iMenuItem){
3864 format_CStr("```%s", cstr_String(linkUrl)), 4988 format_CStr("```%s", cstr_String(infoText)),
3865 0, 0, NULL }); 4989 0, 0, NULL });
3866 } 4990 }
3867 if (willUseProxy_App(scheme) || isGemini || 4991 if (willUseProxy_App(scheme) || isGemini ||
@@ -3872,27 +4996,59 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
3872 /* Regular links that we can open. */ 4996 /* Regular links that we can open. */
3873 pushBackN_Array( 4997 pushBackN_Array(
3874 &items, 4998 &items,
3875 (iMenuItem[]){ 4999 (iMenuItem[]){ { openTab_Icon " ${link.newtab}",
3876 { openTab_Icon " ${link.newtab}", 5000 0,
3877 0, 5001 0,
3878 0, 5002 format_CStr("!open newtab:1 origin:%s url:%s",
3879 format_CStr("!open newtab:1 url:%s", cstr_String(linkUrl)) }, 5003 cstr_String(id_Widget(w)),
3880 { openTabBg_Icon " ${link.newtab.background}", 5004 cstr_String(linkUrl)) },
3881 0, 5005 { openTabBg_Icon " ${link.newtab.background}",
3882 0, 5006 0,
3883 format_CStr("!open newtab:2 url:%s", cstr_String(linkUrl)) }, 5007 0,
3884 { "${link.side}", 5008 format_CStr("!open newtab:2 origin:%s url:%s",
3885 0, 5009 cstr_String(id_Widget(w)),
3886 0, 5010 cstr_String(linkUrl)) },
3887 format_CStr("!open newtab:4 url:%s", cstr_String(linkUrl)) }, 5011 { "${link.side}",
3888 { "${link.side.newtab}", 5012 0,
3889 0, 5013 0,
3890 0, 5014 format_CStr("!open newtab:4 origin:%s url:%s",
3891 format_CStr("!open newtab:5 url:%s", cstr_String(linkUrl)) } }, 5015 cstr_String(id_Widget(w)),
5016 cstr_String(linkUrl)) },
5017 { "${link.side.newtab}",
5018 0,
5019 0,
5020 format_CStr("!open newtab:5 origin:%s url:%s",
5021 cstr_String(id_Widget(w)),
5022 cstr_String(linkUrl)) } },
3892 4); 5023 4);
3893 if (deviceType_App() == phone_AppDeviceType) { 5024 if (deviceType_App() == phone_AppDeviceType) {
3894 removeN_Array(&items, size_Array(&items) - 2, iInvalidSize); 5025 removeN_Array(&items, size_Array(&items) - 2, iInvalidSize);
3895 } 5026 }
5027 if (equalCase_Rangecc(scheme, "file")) {
5028 pushBack_Array(&items, &(iMenuItem){ "---" });
5029 pushBack_Array(&items,
5030 &(iMenuItem){ export_Icon " ${menu.open.external}",
5031 0,
5032 0,
5033 format_CStr("!open default:1 url:%s",
5034 cstr_String(linkUrl)) });
5035#if defined (iPlatformAppleDesktop)
5036 pushBack_Array(&items,
5037 &(iMenuItem){ "${menu.reveal.macos}",
5038 0,
5039 0,
5040 format_CStr("!reveal url:%s",
5041 cstr_String(linkUrl)) });
5042#endif
5043#if defined (iPlatformLinux)
5044 pushBack_Array(&items,
5045 &(iMenuItem){ "${menu.reveal.filemgr}",
5046 0,
5047 0,
5048 format_CStr("!reveal url:%s",
5049 cstr_String(linkUrl)) });
5050#endif
5051 }
3896 } 5052 }
3897 else if (!willUseProxy_App(scheme)) { 5053 else if (!willUseProxy_App(scheme)) {
3898 pushBack_Array( 5054 pushBack_Array(
@@ -3910,11 +5066,13 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
3910 { isGemini ? "${link.noproxy}" : openExt_Icon " ${link.browser}", 5066 { isGemini ? "${link.noproxy}" : openExt_Icon " ${link.browser}",
3911 0, 5067 0,
3912 0, 5068 0,
3913 format_CStr("!open noproxy:1 url:%s", cstr_String(linkUrl)) } }, 5069 format_CStr("!open origin:%s noproxy:1 url:%s",
5070 cstr_String(id_Widget(w)),
5071 cstr_String(linkUrl)) } },
3914 2); 5072 2);
3915 } 5073 }
3916 iString *linkLabel = collectNewRange_String( 5074 iString *linkLabel = collectNewRange_String(
3917 linkLabel_GmDocument(d->doc, d->contextLink->linkId)); 5075 linkLabel_GmDocument(view->doc, d->contextLink->linkId));
3918 urlEncodeSpaces_String(linkLabel); 5076 urlEncodeSpaces_String(linkLabel);
3919 pushBackN_Array(&items, 5077 pushBackN_Array(&items,
3920 (iMenuItem[]){ { "---" }, 5078 (iMenuItem[]){ { "---" },
@@ -3927,7 +5085,8 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
3927 cstr_String(linkUrl)) }, 5085 cstr_String(linkUrl)) },
3928 }, 5086 },
3929 3); 5087 3);
3930 if (isNative && d->contextLink->mediaType != download_MediaType) { 5088 if (isNative && d->contextLink->mediaType != download_MediaType &&
5089 !equalCase_Rangecc(scheme, "file")) {
3931 pushBackN_Array(&items, (iMenuItem[]){ 5090 pushBackN_Array(&items, (iMenuItem[]){
3932 { "---" }, 5091 { "---" },
3933 { download_Icon " ${link.download}", 0, 0, "document.downloadlink" }, 5092 { download_Icon " ${link.download}", 0, 0, "document.downloadlink" },
@@ -3947,6 +5106,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
3947 } 5106 }
3948 if (equalCase_Rangecc(scheme, "file")) { 5107 if (equalCase_Rangecc(scheme, "file")) {
3949 /* Local files may be deleted. */ 5108 /* Local files may be deleted. */
5109 pushBack_Array(&items, &(iMenuItem){ "---" });
3950 pushBack_Array( 5110 pushBack_Array(
3951 &items, 5111 &items,
3952 &(iMenuItem){ delete_Icon " " uiTextCaution_ColorEscape 5112 &(iMenuItem){ delete_Icon " " uiTextCaution_ColorEscape
@@ -3982,9 +5142,10 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
3982 { book_Icon " ${menu.page.import}", 0, 0, "bookmark.links confirm:1" }, 5142 { book_Icon " ${menu.page.import}", 0, 0, "bookmark.links confirm:1" },
3983 { globe_Icon " ${menu.page.translate}", 0, 0, "document.translate" }, 5143 { globe_Icon " ${menu.page.translate}", 0, 0, "document.translate" },
3984 { upload_Icon " ${menu.page.upload}", 0, 0, "document.upload" }, 5144 { upload_Icon " ${menu.page.upload}", 0, 0, "document.upload" },
5145 { "${menu.page.upload.edit}", 0, 0, "document.upload copy:1" },
3985 { "---" }, 5146 { "---" },
3986 { "${menu.page.copyurl}", 0, 0, "document.copylink" } }, 5147 { "${menu.page.copyurl}", 0, 0, "document.copylink" } },
3987 16); 5148 17);
3988 if (isEmpty_Range(&d->selectMark)) { 5149 if (isEmpty_Range(&d->selectMark)) {
3989 pushBackN_Array( 5150 pushBackN_Array(
3990 &items, 5151 &items,
@@ -4020,9 +5181,6 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
4020 processContextMenuEvent_Widget(d->menu, ev, {}); 5181 processContextMenuEvent_Widget(d->menu, ev, {});
4021 } 5182 }
4022 } 5183 }
4023 if (processMediaEvents_DocumentWidget_(d, ev)) {
4024 return iTrue;
4025 }
4026 if (processEvent_Banner(d->banner, ev)) { 5184 if (processEvent_Banner(d->banner, ev)) {
4027 return iTrue; 5185 return iTrue;
4028 } 5186 }
@@ -4035,7 +5193,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
4035 /* Enable hover state now that scrolling has surely finished. */ 5193 /* Enable hover state now that scrolling has surely finished. */
4036 if (d->flags & noHoverWhileScrolling_DocumentWidgetFlag) { 5194 if (d->flags & noHoverWhileScrolling_DocumentWidgetFlag) {
4037 d->flags &= ~noHoverWhileScrolling_DocumentWidgetFlag; 5195 d->flags &= ~noHoverWhileScrolling_DocumentWidgetFlag;
4038 updateHover_DocumentWidget_(d, mouseCoord_Window(get_Window(), ev->button.which)); 5196 updateHover_DocumentView_(view, mouseCoord_Window(get_Window(), ev->button.which));
4039 } 5197 }
4040 if (~flags_Widget(w) & touchDrag_WidgetFlag) { 5198 if (~flags_Widget(w) & touchDrag_WidgetFlag) {
4041 iChangeFlags(d->flags, selecting_DocumentWidgetFlag, iFalse); 5199 iChangeFlags(d->flags, selecting_DocumentWidgetFlag, iFalse);
@@ -4046,7 +5204,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
4046 beginMarkingSelection_DocumentWidget_(d, d->click.startPos); 5204 beginMarkingSelection_DocumentWidget_(d, d->click.startPos);
4047 extendRange_Rangecc( 5205 extendRange_Rangecc(
4048 &d->selectMark, 5206 &d->selectMark,
4049 range_String(source_GmDocument(d->doc)), 5207 range_String(source_GmDocument(view->doc)),
4050 bothStartAndEnd_RangeExtension | 5208 bothStartAndEnd_RangeExtension |
4051 (d->click.count == 2 ? word_RangeExtension : line_RangeExtension)); 5209 (d->click.count == 2 ? word_RangeExtension : line_RangeExtension));
4052 d->initialSelectMark = d->selectMark; 5210 d->initialSelectMark = d->selectMark;
@@ -4060,24 +5218,24 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
4060 case drag_ClickResult: { 5218 case drag_ClickResult: {
4061 if (d->grabbedPlayer) { 5219 if (d->grabbedPlayer) {
4062 iPlayer *plr = 5220 iPlayer *plr =
4063 audioPlayer_Media(media_GmDocument(d->doc), mediaId_GmRun(d->grabbedPlayer)); 5221 audioPlayer_Media(media_GmDocument(view->doc), mediaId_GmRun(d->grabbedPlayer));
4064 iPlayerUI ui; 5222 iPlayerUI ui;
4065 init_PlayerUI(&ui, plr, runRect_DocumentWidget_(d, d->grabbedPlayer)); 5223 init_PlayerUI(&ui, plr, runRect_DocumentView_(view, d->grabbedPlayer));
4066 float off = (float) delta_Click(&d->click).x / (float) width_Rect(ui.volumeSlider); 5224 float off = (float) delta_Click(&d->click).x / (float) width_Rect(ui.volumeSlider);
4067 setVolume_Player(plr, d->grabbedStartVolume + off); 5225 setVolume_Player(plr, d->grabbedStartVolume + off);
4068 refresh_Widget(w); 5226 refresh_Widget(w);
4069 return iTrue; 5227 return iTrue;
4070 } 5228 }
4071 /* Fold/unfold a preformatted block. */ 5229 /* Fold/unfold a preformatted block. */
4072 if (~d->flags & selecting_DocumentWidgetFlag && d->hoverPre && 5230 if (~d->flags & selecting_DocumentWidgetFlag && view->hoverPre &&
4073 preIsFolded_GmDocument(d->doc, preId_GmRun(d->hoverPre))) { 5231 preIsFolded_GmDocument(view->doc, preId_GmRun(view->hoverPre))) {
4074 return iTrue; 5232 return iTrue;
4075 } 5233 }
4076 /* Begin selecting a range of text. */ 5234 /* Begin selecting a range of text. */
4077 if (~d->flags & selecting_DocumentWidgetFlag) { 5235 if (~d->flags & selecting_DocumentWidgetFlag) {
4078 beginMarkingSelection_DocumentWidget_(d, d->click.startPos); 5236 beginMarkingSelection_DocumentWidget_(d, d->click.startPos);
4079 } 5237 }
4080 iRangecc loc = sourceLoc_DocumentWidget_(d, pos_Click(&d->click)); 5238 iRangecc loc = sourceLoc_DocumentView_(view, pos_Click(&d->click));
4081 if (d->selectMark.start == NULL) { 5239 if (d->selectMark.start == NULL) {
4082 d->selectMark = loc; 5240 d->selectMark = loc;
4083 } 5241 }
@@ -4088,7 +5246,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
4088 movingSelectMarkEnd_DocumentWidgetFlag))) { 5246 movingSelectMarkEnd_DocumentWidgetFlag))) {
4089 const iRangecc mark = selectMark_DocumentWidget_(d); 5247 const iRangecc mark = selectMark_DocumentWidget_(d);
4090 const char * midMark = mark.start + size_Range(&mark) / 2; 5248 const char * midMark = mark.start + size_Range(&mark) / 2;
4091 const iRangecc loc = sourceLoc_DocumentWidget_(d, pos_Click(&d->click)); 5249 const iRangecc loc = sourceLoc_DocumentView_(view, pos_Click(&d->click));
4092 const iBool isCloserToStart = d->selectMark.start > d->selectMark.end ? 5250 const iBool isCloserToStart = d->selectMark.start > d->selectMark.end ?
4093 (loc.start > midMark) : (loc.start < midMark); 5251 (loc.start > midMark) : (loc.start < midMark);
4094 iChangeFlags(d->flags, movingSelectMarkStart_DocumentWidgetFlag, isCloserToStart); 5252 iChangeFlags(d->flags, movingSelectMarkStart_DocumentWidgetFlag, isCloserToStart);
@@ -4118,7 +5276,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
4118 if (d->flags & (selectWords_DocumentWidgetFlag | selectLines_DocumentWidgetFlag)) { 5276 if (d->flags & (selectWords_DocumentWidgetFlag | selectLines_DocumentWidgetFlag)) {
4119 extendRange_Rangecc( 5277 extendRange_Rangecc(
4120 &d->selectMark, 5278 &d->selectMark,
4121 range_String(source_GmDocument(d->doc)), 5279 range_String(source_GmDocument(view->doc)),
4122 (d->flags & movingSelectMarkStart_DocumentWidgetFlag ? moveStart_RangeExtension 5280 (d->flags & movingSelectMarkStart_DocumentWidgetFlag ? moveStart_RangeExtension
4123 : moveEnd_RangeExtension) | 5281 : moveEnd_RangeExtension) |
4124 (d->flags & selectWords_DocumentWidgetFlag ? word_RangeExtension 5282 (d->flags & selectWords_DocumentWidgetFlag ? word_RangeExtension
@@ -4156,7 +5314,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
4156 setFocus_Widget(NULL); 5314 setFocus_Widget(NULL);
4157 /* Tap in tap selection mode. */ 5315 /* Tap in tap selection mode. */
4158 if (flags_Widget(w) & touchDrag_WidgetFlag) { 5316 if (flags_Widget(w) & touchDrag_WidgetFlag) {
4159 const iRangecc tapLoc = sourceLoc_DocumentWidget_(d, pos_Click(&d->click)); 5317 const iRangecc tapLoc = sourceLoc_DocumentView_(view, pos_Click(&d->click));
4160 /* Tapping on the selection will show a menu. */ 5318 /* Tapping on the selection will show a menu. */
4161 const iRangecc mark = selectMark_DocumentWidget_(d); 5319 const iRangecc mark = selectMark_DocumentWidget_(d);
4162 if (tapLoc.start >= mark.start && tapLoc.end <= mark.end) { 5320 if (tapLoc.start >= mark.start && tapLoc.end <= mark.end) {
@@ -4165,11 +5323,15 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
4165 destroy_Widget(d->copyMenu); 5323 destroy_Widget(d->copyMenu);
4166 d->copyMenu = NULL; 5324 d->copyMenu = NULL;
4167 } 5325 }
4168 d->copyMenu = makeMenu_Widget(w, (iMenuItem[]){ 5326 const iMenuItem items[] = {
4169 { clipCopy_Icon " ${menu.copy}", 0, 0, "copy" }, 5327 { clipCopy_Icon " ${menu.copy}", 0, 0, "copy" },
5328#if defined (iPlatformAppleMobile)
5329 { export_Icon " ${menu.share}", 0, 0, "copy share:1" },
5330#endif
4170 { "---" }, 5331 { "---" },
4171 { close_Icon " ${menu.select.clear}", 0, 0, "document.select arg:0" }, 5332 { close_Icon " ${menu.select.clear}", 0, 0, "document.select arg:0" },
4172 }, 3); 5333 };
5334 d->copyMenu = makeMenu_Widget(w, items, iElemCount(items));
4173 setFlags_Widget(d->copyMenu, noFadeBackground_WidgetFlag, iTrue); 5335 setFlags_Widget(d->copyMenu, noFadeBackground_WidgetFlag, iTrue);
4174 openMenu_Widget(d->copyMenu, pos_Click(&d->click)); 5336 openMenu_Widget(d->copyMenu, pos_Click(&d->click));
4175 return iTrue; 5337 return iTrue;
@@ -4180,18 +5342,18 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
4180 return iTrue; 5342 return iTrue;
4181 } 5343 }
4182 } 5344 }
4183 if (d->hoverPre) { 5345 if (view->hoverPre) {
4184 togglePreFold_DocumentWidget_(d, preId_GmRun(d->hoverPre)); 5346 togglePreFold_DocumentWidget_(d, preId_GmRun(view->hoverPre));
4185 return iTrue; 5347 return iTrue;
4186 } 5348 }
4187 if (d->hoverLink) { 5349 if (view->hoverLink) {
4188 /* TODO: Move this to a method. */ 5350 /* TODO: Move this to a method. */
4189 const iGmLinkId linkId = d->hoverLink->linkId; 5351 const iGmLinkId linkId = view->hoverLink->linkId;
4190 const iMediaId linkMedia = mediaId_GmRun(d->hoverLink); 5352 const iMediaId linkMedia = mediaId_GmRun(view->hoverLink);
4191 const int linkFlags = linkFlags_GmDocument(d->doc, linkId); 5353 const int linkFlags = linkFlags_GmDocument(view->doc, linkId);
4192 iAssert(linkId); 5354 iAssert(linkId);
4193 /* Media links are opened inline by default. */ 5355 /* Media links are opened inline by default. */
4194 if (isMediaLink_GmDocument(d->doc, linkId)) { 5356 if (isMediaLink_GmDocument(view->doc, linkId)) {
4195 if (linkFlags & content_GmLinkFlag && linkFlags & permanent_GmLinkFlag) { 5357 if (linkFlags & content_GmLinkFlag && linkFlags & permanent_GmLinkFlag) {
4196 /* We have the content and it cannot be dismissed, so nothing 5358 /* We have the content and it cannot be dismissed, so nothing
4197 further to do. */ 5359 further to do. */
@@ -4200,7 +5362,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
4200 if (!requestMedia_DocumentWidget_(d, linkId, iTrue)) { 5362 if (!requestMedia_DocumentWidget_(d, linkId, iTrue)) {
4201 if (linkFlags & content_GmLinkFlag) { 5363 if (linkFlags & content_GmLinkFlag) {
4202 /* Dismiss shown content on click. */ 5364 /* Dismiss shown content on click. */
4203 setData_Media(media_GmDocument(d->doc), 5365 setData_Media(media_GmDocument(view->doc),
4204 linkId, 5366 linkId,
4205 NULL, 5367 NULL,
4206 NULL, 5368 NULL,
@@ -4214,10 +5376,10 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
4214 be redone. */ 5376 be redone. */
4215 } 5377 }
4216 } 5378 }
4217 redoLayout_GmDocument(d->doc); 5379 redoLayout_GmDocument(view->doc);
4218 d->hoverLink = NULL; 5380 view->hoverLink = NULL;
4219 clampScroll_DocumentWidget_(d); 5381 clampScroll_DocumentView_(view);
4220 updateVisible_DocumentWidget_(d); 5382 updateVisible_DocumentView_(view);
4221 invalidate_DocumentWidget_(d); 5383 invalidate_DocumentWidget_(d);
4222 refresh_Widget(w); 5384 refresh_Widget(w);
4223 return iTrue; 5385 return iTrue;
@@ -4226,13 +5388,13 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
4226 /* Show the existing content again if we have it. */ 5388 /* Show the existing content again if we have it. */
4227 iMediaRequest *req = findMediaRequest_DocumentWidget_(d, linkId); 5389 iMediaRequest *req = findMediaRequest_DocumentWidget_(d, linkId);
4228 if (req) { 5390 if (req) {
4229 setData_Media(media_GmDocument(d->doc), 5391 setData_Media(media_GmDocument(view->doc),
4230 linkId, 5392 linkId,
4231 meta_GmRequest(req->req), 5393 meta_GmRequest(req->req),
4232 body_GmRequest(req->req), 5394 body_GmRequest(req->req),
4233 allowHide_MediaFlag); 5395 allowHide_MediaFlag);
4234 redoLayout_GmDocument(d->doc); 5396 redoLayout_GmDocument(view->doc);
4235 updateVisible_DocumentWidget_(d); 5397 updateVisible_DocumentView_(view);
4236 invalidate_DocumentWidget_(d); 5398 invalidate_DocumentWidget_(d);
4237 refresh_Widget(w); 5399 refresh_Widget(w);
4238 return iTrue; 5400 return iTrue;
@@ -4252,14 +5414,15 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
4252 if (isPinned_DocumentWidget_(d)) { 5414 if (isPinned_DocumentWidget_(d)) {
4253 tabMode ^= otherRoot_OpenTabFlag; 5415 tabMode ^= otherRoot_OpenTabFlag;
4254 } 5416 }
5417 interactingWithLink_DocumentWidget_(d, linkId);
4255 postCommandf_Root(w->root, "open newtab:%d url:%s", 5418 postCommandf_Root(w->root, "open newtab:%d url:%s",
4256 tabMode, 5419 tabMode,
4257 cstr_String(absoluteUrl_String( 5420 cstr_String(absoluteUrl_String(
4258 d->mod.url, linkUrl_GmDocument(d->doc, linkId)))); 5421 d->mod.url, linkUrl_GmDocument(view->doc, linkId))));
4259 } 5422 }
4260 else { 5423 else {
4261 const iString *url = absoluteUrl_String( 5424 const iString *url = absoluteUrl_String(
4262 d->mod.url, linkUrl_GmDocument(d->doc, linkId)); 5425 d->mod.url, linkUrl_GmDocument(view->doc, linkId));
4263 makeQuestion_Widget( 5426 makeQuestion_Widget(
4264 uiTextCaution_ColorEscape "${heading.openlink}", 5427 uiTextCaution_ColorEscape "${heading.openlink}",
4265 format_CStr( 5428 format_CStr(
@@ -4292,705 +5455,18 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
4292 return processEvent_Widget(w, ev); 5455 return processEvent_Widget(w, ev);
4293} 5456}
4294 5457
4295iDeclareType(DrawContext) 5458static void checkPendingInvalidation_DocumentWidget_(const iDocumentWidget *d) {
4296 5459 if (d->flags & invalidationPending_DocumentWidgetFlag &&
4297struct Impl_DrawContext { 5460 !isAffectedByVisualOffset_Widget(constAs_Widget(d))) {
4298 const iDocumentWidget *widget; 5461 // printf("%p visoff: %d\n", d, left_Rect(bounds_Widget(w)) - left_Rect(boundsWithoutVisualOffset_Widget(w)));
4299 iRect widgetBounds; 5462 iDocumentWidget *m = (iDocumentWidget *) d; /* Hrrm, not const... */
4300 iRect docBounds; 5463 m->flags &= ~invalidationPending_DocumentWidgetFlag;
4301 iRangei vis; 5464 invalidate_DocumentWidget_(m);
4302 iInt2 viewPos; /* document area origin */
4303 iPaint paint;
4304 iBool inSelectMark;
4305 iBool inFoundMark;
4306 iBool showLinkNumbers;
4307 iRect firstMarkRect;
4308 iRect lastMarkRect;
4309 iGmRunRange runsDrawn;
4310};
4311
4312static void fillRange_DrawContext_(iDrawContext *d, const iGmRun *run, enum iColorId color,
4313 iRangecc mark, iBool *isInside) {
4314 if (mark.start > mark.end) {
4315 /* Selection may be done in either direction. */
4316 iSwap(const char *, mark.start, mark.end);
4317 }
4318 if (*isInside || (contains_Range(&run->text, mark.start) ||
4319 contains_Range(&mark, run->text.start))) {
4320 int x = 0;
4321 if (!*isInside) {
4322 x = measureRange_Text(run->font,
4323 (iRangecc){ run->text.start, iMax(run->text.start, mark.start) })
4324 .advance.x;
4325 }
4326 int w = width_Rect(run->visBounds) - x;
4327 if (contains_Range(&run->text, mark.end) || mark.end < run->text.start) {
4328 iRangecc mk = !*isInside ? mark
4329 : (iRangecc){ run->text.start, iMax(run->text.start, mark.end) };
4330 mk.start = iMax(mk.start, run->text.start);
4331 w = measureRange_Text(run->font, mk).advance.x;
4332 *isInside = iFalse;
4333 }
4334 else {
4335 *isInside = iTrue; /* at least until the next run */
4336 }
4337 if (w > width_Rect(run->visBounds) - x) {
4338 w = width_Rect(run->visBounds) - x;
4339 }
4340 if (~run->flags & decoration_GmRunFlag) {
4341 const iInt2 visPos =
4342 add_I2(run->bounds.pos, addY_I2(d->viewPos, viewPos_DocumentWidget_(d->widget)));
4343 const iRect rangeRect = { addX_I2(visPos, x), init_I2(w, height_Rect(run->bounds)) };
4344 if (rangeRect.size.x) {
4345 fillRect_Paint(&d->paint, rangeRect, color);
4346 /* Keep track of the first and last marked rects. */
4347 if (d->firstMarkRect.size.x == 0) {
4348 d->firstMarkRect = rangeRect;
4349 }
4350 d->lastMarkRect = rangeRect;
4351 }
4352 }
4353 }
4354 /* Link URLs are not part of the visible document, so they are ignored above. Handle
4355 these ranges as a special case. */
4356 if (run->linkId && run->flags & decoration_GmRunFlag) {
4357 const iRangecc url = linkUrlRange_GmDocument(d->widget->doc, run->linkId);
4358 if (contains_Range(&url, mark.start) &&
4359 (contains_Range(&url, mark.end) || url.end == mark.end)) {
4360 fillRect_Paint(
4361 &d->paint,
4362 moved_Rect(run->visBounds, addY_I2(d->viewPos, viewPos_DocumentWidget_(d->widget))),
4363 color);
4364 }
4365 }
4366}
4367
4368static void drawMark_DrawContext_(void *context, const iGmRun *run) {
4369 iDrawContext *d = context;
4370 if (!isMedia_GmRun(run)) {
4371 fillRange_DrawContext_(d, run, uiMatching_ColorId, d->widget->foundMark, &d->inFoundMark);
4372 fillRange_DrawContext_(d, run, uiMarked_ColorId, d->widget->selectMark, &d->inSelectMark);
4373 }
4374}
4375
4376static void drawRun_DrawContext_(void *context, const iGmRun *run) {
4377 iDrawContext *d = context;
4378 const iInt2 origin = d->viewPos;
4379 /* Keep track of the drawn visible runs. */ {
4380 if (!d->runsDrawn.start || run < d->runsDrawn.start) {
4381 d->runsDrawn.start = run;
4382 }
4383 if (!d->runsDrawn.end || run > d->runsDrawn.end) {
4384 d->runsDrawn.end = run;
4385 }
4386 }
4387 if (run->mediaType == image_MediaType) {
4388 SDL_Texture *tex = imageTexture_Media(media_GmDocument(d->widget->doc), mediaId_GmRun(run));
4389 const iRect dst = moved_Rect(run->visBounds, origin);
4390 if (tex) {
4391 fillRect_Paint(&d->paint, dst, tmBackground_ColorId); /* in case the image has alpha */
4392 SDL_RenderCopy(d->paint.dst->render, tex, NULL,
4393 &(SDL_Rect){ dst.pos.x, dst.pos.y, dst.size.x, dst.size.y });
4394 }
4395 else {
4396 drawRect_Paint(&d->paint, dst, tmQuoteIcon_ColorId);
4397 drawCentered_Text(uiLabel_FontId,
4398 dst,
4399 iFalse,
4400 tmQuote_ColorId,
4401 explosion_Icon " Error Loading Image");
4402 }
4403 return;
4404 }
4405 else if (isMedia_GmRun(run)) {
4406 /* Media UIs are drawn afterwards as a dynamic overlay. */
4407 return;
4408 }
4409 enum iColorId fg = run->color;
4410 const iGmDocument *doc = d->widget->doc;
4411 const int linkFlags = linkFlags_GmDocument(doc, run->linkId);
4412 /* Hover state of a link. */
4413 iBool isHover =
4414 (run->linkId && d->widget->hoverLink && run->linkId == d->widget->hoverLink->linkId &&
4415 ~run->flags & decoration_GmRunFlag);
4416 /* Visible (scrolled) position of the run. */
4417 const iInt2 visPos = addX_I2(add_I2(run->visBounds.pos, origin),
4418 /* Preformatted runs can be scrolled. */
4419 runOffset_DocumentWidget_(d->widget, run));
4420 const iRect visRect = { visPos, run->visBounds.size };
4421#if 0
4422 if (run->flags & footer_GmRunFlag) {
4423 iRect footerBack =
4424 (iRect){ visPos, init_I2(width_Rect(d->widgetBounds), run->visBounds.size.y) };
4425 footerBack.pos.x = left_Rect(d->widgetBounds);
4426 fillRect_Paint(&d->paint, footerBack, tmBackground_ColorId);
4427 return;
4428 }
4429#endif
4430 /* Fill the background. */ {
4431 if (run->linkId && linkFlags & isOpen_GmLinkFlag && ~linkFlags & content_GmLinkFlag) {
4432 /* Open links get a highlighted background. */
4433 int bg = tmBackgroundOpenLink_ColorId;
4434 const int frame = tmFrameOpenLink_ColorId;
4435 iRect wideRect = { init_I2(left_Rect(d->widgetBounds), visPos.y),
4436 init_I2(width_Rect(d->widgetBounds) +
4437 width_Widget(d->widget->scroll),
4438 height_Rect(run->visBounds)) };
4439 /* The first line is composed of two runs that may be drawn in either order, so
4440 only draw half of the background. */
4441 if (run->flags & decoration_GmRunFlag) {
4442 wideRect.size.x = right_Rect(visRect) - left_Rect(wideRect);
4443 }
4444 else if (run->flags & startOfLine_GmRunFlag) {
4445 wideRect.size.x = right_Rect(wideRect) - left_Rect(visRect);
4446 wideRect.pos.x = left_Rect(visRect);
4447 }
4448 fillRect_Paint(&d->paint, wideRect, bg);
4449 if (run->flags & (startOfLine_GmRunFlag | decoration_GmRunFlag)) {
4450 drawHLine_Paint(&d->paint, topLeft_Rect(wideRect), width_Rect(wideRect), frame);
4451 }
4452 /* TODO: The decoration is not marked as endOfLine, so it lacks the bottom line. */
4453// if (run->flags & endOfLine_GmRunFlag) {
4454// drawHLine_Paint(
4455// &d->paint, addY_I2(bottomLeft_Rect(wideRect), -1), width_Rect(wideRect), frame);
4456// }
4457 }
4458 else {
4459 /* Normal background for other runs. There are cases when runs get drawn multiple times,
4460 e.g., at the buffer boundary, and there are slightly overlapping characters in
4461 monospace blocks. Clearing the background here ensures a cleaner visual appearance
4462 since only one glyph is visible at any given point. */
4463 fillRect_Paint(&d->paint, (iRect){ visPos, run->visBounds.size }, tmBackground_ColorId);
4464 }
4465 }
4466 if (run->linkId && ~run->flags & decoration_GmRunFlag) {
4467 fg = linkColor_GmDocument(doc, run->linkId, isHover ? textHover_GmLinkPart : text_GmLinkPart);
4468 if (linkFlags & content_GmLinkFlag) {
4469 fg = linkColor_GmDocument(doc, run->linkId, textHover_GmLinkPart); /* link is inactive */
4470 }
4471 }
4472 if (run->flags & altText_GmRunFlag) {
4473 const iInt2 margin = preRunMargin_GmDocument(doc, preId_GmRun(run));
4474 fillRect_Paint(&d->paint, (iRect){ visPos, run->visBounds.size }, tmBackgroundAltText_ColorId);
4475 drawRect_Paint(&d->paint, (iRect){ visPos, run->visBounds.size }, tmFrameAltText_ColorId);
4476 drawWrapRange_Text(run->font,
4477 add_I2(visPos, margin),
4478 run->visBounds.size.x - 2 * margin.x,
4479 run->color,
4480 run->text);
4481 }
4482#if 0
4483 else if (run->flags & siteBanner_GmRunFlag) {
4484 /* Banner background. */
4485 iRect bannerBack = initCorners_Rect(topLeft_Rect(d->widgetBounds),
4486 init_I2(right_Rect(bounds_Widget(constAs_Widget(d->widget))),
4487 visPos.y + height_Rect(run->visBounds)));
4488 fillRect_Paint(&d->paint, bannerBack, tmBannerBackground_ColorId);
4489 drawBannerRun_DrawContext_(d, run, visPos);
4490 }
4491#endif
4492 else {
4493 if (d->showLinkNumbers && run->linkId && run->flags & decoration_GmRunFlag) {
4494 const size_t ord = visibleLinkOrdinal_DocumentWidget_(d->widget, run->linkId);
4495 if (ord >= d->widget->ordinalBase) {
4496 const iChar ordChar =
4497 linkOrdinalChar_DocumentWidget_(d->widget, ord - d->widget->ordinalBase);
4498 if (ordChar) {
4499 const char *circle = "\u25ef"; /* Large Circle */
4500 const int circleFont = FONT_ID(default_FontId, regular_FontStyle, contentRegular_FontSize);
4501 iRect nbArea = { init_I2(d->viewPos.x - gap_UI / 3, visPos.y),
4502 init_I2(3.95f * gap_Text, 1.0f * lineHeight_Text(circleFont)) };
4503 drawRange_Text(
4504 circleFont, topLeft_Rect(nbArea), tmQuote_ColorId, range_CStr(circle));
4505 iRect circleArea = visualBounds_Text(circleFont, range_CStr(circle));
4506 addv_I2(&circleArea.pos, topLeft_Rect(nbArea));
4507 drawCentered_Text(FONT_ID(default_FontId, regular_FontStyle, contentSmall_FontSize),
4508 circleArea,
4509 iTrue,
4510 tmQuote_ColorId,
4511 "%lc",
4512 (int) ordChar);
4513 goto runDrawn;
4514 }
4515 }
4516 }
4517 if (run->flags & quoteBorder_GmRunFlag) {
4518 drawVLine_Paint(&d->paint,
4519 addX_I2(visPos,
4520 !run->isRTL
4521 ? -gap_Text * 5 / 2
4522 : (width_Rect(run->visBounds) + gap_Text * 5 / 2)),
4523 height_Rect(run->visBounds),
4524 tmQuoteIcon_ColorId);
4525 }
4526 /* Base attributes. */ {
4527 int f, c;
4528 runBaseAttributes_GmDocument(doc, run, &f, &c);
4529 setBaseAttributes_Text(f, c);
4530 }
4531 drawBoundRange_Text(run->font,
4532 visPos,
4533 (run->isRTL ? -1 : 1) * width_Rect(run->visBounds),
4534 fg,
4535 run->text);
4536 setBaseAttributes_Text(-1, -1);
4537 runDrawn:;
4538 }
4539 /* Presentation of links. */
4540 if (run->linkId && ~run->flags & decoration_GmRunFlag) {
4541 const int metaFont = paragraph_FontId;
4542 /* TODO: Show status of an ongoing media request. */
4543 const int flags = linkFlags;
4544 const iRect linkRect = moved_Rect(run->visBounds, origin);
4545 iMediaRequest *mr = NULL;
4546 /* Show metadata about inline content. */
4547 if (flags & content_GmLinkFlag && run->flags & endOfLine_GmRunFlag) {
4548 fg = linkColor_GmDocument(doc, run->linkId, textHover_GmLinkPart);
4549 iString text;
4550 init_String(&text);
4551 const iMediaId linkMedia = findMediaForLink_Media(constMedia_GmDocument(doc),
4552 run->linkId, none_MediaType);
4553 iAssert(linkMedia.type != none_MediaType);
4554 iGmMediaInfo info;
4555 info_Media(constMedia_GmDocument(doc), linkMedia, &info);
4556 switch (linkMedia.type) {
4557 case image_MediaType: {
4558 iAssert(!isEmpty_Rect(run->bounds));
4559 const iInt2 imgSize = imageSize_Media(constMedia_GmDocument(doc), linkMedia);
4560 format_String(&text, "%s \u2014 %d x %d \u2014 %.1f%s",
4561 info.type, imgSize.x, imgSize.y, info.numBytes / 1.0e6f,
4562 cstr_Lang("mb"));
4563 break;
4564 }
4565 case audio_MediaType:
4566 format_String(&text, "%s", info.type);
4567 break;
4568 case download_MediaType:
4569 format_String(&text, "%s", info.type);
4570 break;
4571 default:
4572 break;
4573 }
4574 if (linkMedia.type != download_MediaType && /* can't cancel downloads currently */
4575 findMediaRequest_DocumentWidget_(d->widget, run->linkId)) {
4576 appendFormat_String(
4577 &text, " %s" close_Icon, isHover ? escape_Color(tmLinkText_ColorId) : "");
4578 }
4579 const iInt2 size = measureRange_Text(metaFont, range_String(&text)).bounds.size;
4580 fillRect_Paint(
4581 &d->paint,
4582 (iRect){ add_I2(origin, addX_I2(topRight_Rect(run->bounds), -size.x - gap_UI)),
4583 addX_I2(size, 2 * gap_UI) },
4584 tmBackground_ColorId);
4585 drawAlign_Text(metaFont,
4586 add_I2(topRight_Rect(run->bounds), origin),
4587 fg,
4588 right_Alignment,
4589 "%s", cstr_String(&text));
4590 deinit_String(&text);
4591 }
4592 else if (run->flags & endOfLine_GmRunFlag &&
4593 (mr = findMediaRequest_DocumentWidget_(d->widget, run->linkId)) != NULL) {
4594 if (!isFinished_GmRequest(mr->req)) {
4595 draw_Text(metaFont,
4596 topRight_Rect(linkRect),
4597 tmInlineContentMetadata_ColorId,
4598 translateCStr_Lang(" \u2014 ${doc.fetching}\u2026 (%.1f ${mb})"),
4599 (float) bodySize_GmRequest(mr->req) / 1.0e6f);
4600 }
4601 }
4602 else if (isHover) {
4603 const iGmLinkId linkId = d->widget->hoverLink->linkId;
4604 const iString * url = linkUrl_GmDocument(doc, linkId);
4605 const int flags = linkFlags;
4606 iUrl parts;
4607 init_Url(&parts, url);
4608 fg = linkColor_GmDocument(doc, linkId, textHover_GmLinkPart);
4609 const enum iGmLinkScheme scheme = scheme_GmLinkFlag(flags);
4610 const iBool showHost = (flags & humanReadable_GmLinkFlag &&
4611 (!isEmpty_Range(&parts.host) ||
4612 scheme == mailto_GmLinkScheme));
4613 const iBool showImage = (flags & imageFileExtension_GmLinkFlag) != 0;
4614 const iBool showAudio = (flags & audioFileExtension_GmLinkFlag) != 0;
4615 iString str;
4616 init_String(&str);
4617 /* Show scheme and host. */
4618 if (run->flags & endOfLine_GmRunFlag &&
4619 (flags & (imageFileExtension_GmLinkFlag | audioFileExtension_GmLinkFlag) ||
4620 showHost)) {
4621 format_String(
4622 &str,
4623 "%s%s%s%s%s",
4624 showHost ? "" : "",
4625 showHost
4626 ? (scheme == mailto_GmLinkScheme ? cstr_String(url)
4627 : scheme != gemini_GmLinkScheme ? format_CStr("%s://%s",
4628 cstr_Rangecc(parts.scheme),
4629 cstr_Rangecc(parts.host))
4630 : cstr_Rangecc(parts.host))
4631 : "",
4632 showHost && (showImage || showAudio) ? " \u2014" : "",
4633 showImage || showAudio
4634 ? escape_Color(fg)
4635 : escape_Color(linkColor_GmDocument(doc, run->linkId, domain_GmLinkPart)),
4636 showImage || showAudio
4637 ? format_CStr(showImage ? " %s " photo_Icon : " %s \U0001f3b5",
4638 cstr_Lang(showImage ? "link.hint.image" : "link.hint.audio"))
4639 : "");
4640 }
4641 if (run->flags & endOfLine_GmRunFlag && flags & visited_GmLinkFlag) {
4642 iDate date;
4643 init_Date(&date, linkTime_GmDocument(doc, run->linkId));
4644 appendCStr_String(&str, " \u2014 ");
4645 appendCStr_String(
4646 &str, escape_Color(linkColor_GmDocument(doc, run->linkId, visited_GmLinkPart)));
4647 iString *dateStr = format_Date(&date, "%b %d");
4648 append_String(&str, dateStr);
4649 delete_String(dateStr);
4650 }
4651 if (!isEmpty_String(&str)) {
4652 if (run->isRTL) {
4653 appendCStr_String(&str, " \u2014 ");
4654 }
4655 else {
4656 prependCStr_String(&str, " \u2014 ");
4657 }
4658 const iInt2 textSize = measure_Text(metaFont, cstr_String(&str)).bounds.size;
4659 int tx = topRight_Rect(linkRect).x;
4660 const char *msg = cstr_String(&str);
4661 if (run->isRTL) {
4662 tx = topLeft_Rect(linkRect).x - textSize.x;
4663 }
4664 if (tx + textSize.x > right_Rect(d->widgetBounds)) {
4665 tx = right_Rect(d->widgetBounds) - textSize.x;
4666 fillRect_Paint(&d->paint, (iRect){ init_I2(tx, top_Rect(linkRect)), textSize },
4667 uiBackground_ColorId);
4668 msg += 4; /* skip the space and dash */
4669 tx += measure_Text(metaFont, " \u2014").advance.x / 2;
4670 }
4671 drawAlign_Text(metaFont,
4672 init_I2(tx, top_Rect(linkRect)),
4673 linkColor_GmDocument(doc, run->linkId, domain_GmLinkPart),
4674 left_Alignment,
4675 "%s",
4676 msg);
4677 deinit_String(&str);
4678 }
4679 }
4680 }
4681 if (0) {
4682 drawRect_Paint(&d->paint, (iRect){ visPos, run->bounds.size }, green_ColorId);
4683 drawRect_Paint(&d->paint, (iRect){ visPos, run->visBounds.size }, red_ColorId);
4684 }
4685}
4686
4687static int drawSideRect_(iPaint *p, iRect rect) {
4688 int bg = tmBannerBackground_ColorId;
4689 int fg = tmBannerIcon_ColorId;
4690 if (equal_Color(get_Color(bg), get_Color(tmBackground_ColorId))) {
4691 bg = tmBannerIcon_ColorId;
4692 fg = tmBannerBackground_ColorId;
4693 }
4694 fillRect_Paint(p, rect, bg);
4695 return fg;
4696}
4697
4698static int sideElementAvailWidth_DocumentWidget_(const iDocumentWidget *d) {
4699 return left_Rect(documentBounds_DocumentWidget_(d)) -
4700 left_Rect(bounds_Widget(constAs_Widget(d))) - 2 * d->pageMargin * gap_UI;
4701}
4702
4703static iBool isSideHeadingVisible_DocumentWidget_(const iDocumentWidget *d) {
4704 return sideElementAvailWidth_DocumentWidget_(d) >= lineHeight_Text(banner_FontId) * 4.5f;
4705}
4706
4707static void updateSideIconBuf_DocumentWidget_(const iDocumentWidget *d) {
4708 if (!isExposed_Window(get_Window())) {
4709 return;
4710 }
4711 iDrawBufs *dbuf = d->drawBufs;
4712 dbuf->flags &= ~updateSideBuf_DrawBufsFlag;
4713 if (dbuf->sideIconBuf) {
4714 SDL_DestroyTexture(dbuf->sideIconBuf);
4715 dbuf->sideIconBuf = NULL;
4716 }
4717// const iGmRun *banner = siteBanner_GmDocument(d->doc);
4718 if (isEmpty_Banner(d->banner)) {
4719 return;
4720 }
4721 const int margin = gap_UI * d->pageMargin;
4722 const int minBannerSize = lineHeight_Text(banner_FontId) * 2;
4723 const iChar icon = siteIcon_GmDocument(d->doc);
4724 const int avail = sideElementAvailWidth_DocumentWidget_(d) - margin;
4725 iBool isHeadingVisible = isSideHeadingVisible_DocumentWidget_(d);
4726 /* Determine the required size. */
4727 iInt2 bufSize = init1_I2(minBannerSize);
4728 const int sideHeadingFont = FONT_ID(documentHeading_FontId, regular_FontStyle, contentBig_FontSize);
4729 if (isHeadingVisible) {
4730 const iInt2 headingSize = measureWrapRange_Text(sideHeadingFont, avail,
4731 currentHeading_DocumentWidget_(d)).bounds.size;
4732 if (headingSize.x > 0) {
4733 bufSize.y += gap_Text + headingSize.y;
4734 bufSize.x = iMax(bufSize.x, headingSize.x);
4735 }
4736 else {
4737 isHeadingVisible = iFalse;
4738 }
4739 }
4740 SDL_Renderer *render = renderer_Window(get_Window());
4741 dbuf->sideIconBuf = SDL_CreateTexture(render,
4742 SDL_PIXELFORMAT_RGBA4444,
4743 SDL_TEXTUREACCESS_STATIC | SDL_TEXTUREACCESS_TARGET,
4744 bufSize.x, bufSize.y);
4745 iPaint p;
4746 init_Paint(&p);
4747 beginTarget_Paint(&p, dbuf->sideIconBuf);
4748 const iColor back = get_Color(tmBannerSideTitle_ColorId);
4749 SDL_SetRenderDrawColor(render, back.r, back.g, back.b, 0); /* better blending of the edge */
4750 SDL_RenderClear(render);
4751 const iRect iconRect = { zero_I2(), init1_I2(minBannerSize) };
4752 int fg = drawSideRect_(&p, iconRect);
4753 iString str;
4754 initUnicodeN_String(&str, &icon, 1);
4755 drawCentered_Text(banner_FontId, iconRect, iTrue, fg, "%s", cstr_String(&str));
4756 deinit_String(&str);
4757 if (isHeadingVisible) {
4758 iRangecc text = currentHeading_DocumentWidget_(d);
4759 iInt2 pos = addY_I2(bottomLeft_Rect(iconRect), gap_Text);
4760 const int font = sideHeadingFont;
4761 drawWrapRange_Text(font, pos, avail, tmBannerSideTitle_ColorId, text);
4762 }
4763 endTarget_Paint(&p);
4764 SDL_SetTextureBlendMode(dbuf->sideIconBuf, SDL_BLENDMODE_BLEND);
4765}
4766
4767static void drawSideElements_DocumentWidget_(const iDocumentWidget *d) {
4768 const iWidget *w = constAs_Widget(d);
4769 const iRect bounds = bounds_Widget(w);
4770 const iRect docBounds = documentBounds_DocumentWidget_(d);
4771 const int margin = gap_UI * d->pageMargin;
4772 float opacity = value_Anim(&d->sideOpacity);
4773 const int avail = left_Rect(docBounds) - left_Rect(bounds) - 2 * margin;
4774 iDrawBufs * dbuf = d->drawBufs;
4775 iPaint p;
4776 init_Paint(&p);
4777 setClip_Paint(&p, boundsWithoutVisualOffset_Widget(w));
4778 /* Side icon and current heading. */
4779 if (prefs_App()->sideIcon && opacity > 0 && dbuf->sideIconBuf) {
4780 const iInt2 texSize = size_SDLTexture(dbuf->sideIconBuf);
4781 if (avail > texSize.x) {
4782 const int minBannerSize = lineHeight_Text(banner_FontId) * 2;
4783 iInt2 pos = addY_I2(add_I2(topLeft_Rect(bounds), init_I2(margin, 0)),
4784 height_Rect(bounds) / 2 - minBannerSize / 2 -
4785 (texSize.y > minBannerSize
4786 ? (gap_Text + lineHeight_Text(heading3_FontId)) / 2
4787 : 0));
4788 SDL_SetTextureAlphaMod(dbuf->sideIconBuf, 255 * opacity);
4789 SDL_RenderCopy(renderer_Window(get_Window()),
4790 dbuf->sideIconBuf, NULL,
4791 &(SDL_Rect){ pos.x, pos.y, texSize.x, texSize.y });
4792 }
4793 }
4794 /* Reception timestamp. */
4795 if (dbuf->timestampBuf && dbuf->timestampBuf->size.x <= avail) {
4796 draw_TextBuf(
4797 dbuf->timestampBuf,
4798 add_I2(
4799 bottomLeft_Rect(bounds),
4800 init_I2(margin,
4801 -margin + -dbuf->timestampBuf->size.y +
4802 iMax(0, d->scrollY.max - pos_SmoothScroll(&d->scrollY)))),
4803 tmQuoteIcon_ColorId);
4804 }
4805 unsetClip_Paint(&p);
4806}
4807
4808static void drawMedia_DocumentWidget_(const iDocumentWidget *d, iPaint *p) {
4809 iConstForEach(PtrArray, i, &d->visibleMedia) {
4810 const iGmRun * run = i.ptr;
4811 if (run->mediaType == audio_MediaType) {
4812 iPlayerUI ui;
4813 init_PlayerUI(&ui,
4814 audioPlayer_Media(media_GmDocument(d->doc), mediaId_GmRun(run)),
4815 runRect_DocumentWidget_(d, run));
4816 draw_PlayerUI(&ui, p);
4817 }
4818 else if (run->mediaType == download_MediaType) {
4819 iDownloadUI ui;
4820 init_DownloadUI(&ui, constMedia_GmDocument(d->doc), run->mediaId,
4821 runRect_DocumentWidget_(d, run));
4822 draw_DownloadUI(&ui, p);
4823 }
4824 }
4825}
4826
4827static void extend_GmRunRange_(iGmRunRange *runs) {
4828 if (runs->start) {
4829 runs->start--;
4830 runs->end++;
4831 } 5465 }
4832} 5466}
4833 5467
4834static iBool render_DocumentWidget_(const iDocumentWidget *d, iDrawContext *ctx, iBool prerenderExtra) {
4835 iBool didDraw = iFalse;
4836 const iRect bounds = bounds_Widget(constAs_Widget(d));
4837 const iRect ctxWidgetBounds = init_Rect(
4838 0, 0, width_Rect(bounds) - constAs_Widget(d->scroll)->rect.size.x, height_Rect(bounds));
4839 const iRangei full = { 0, size_GmDocument(d->doc).y };
4840 const iRangei vis = ctx->vis;
4841 iVisBuf *visBuf = d->visBuf; /* will be updated now */
4842 d->drawBufs->lastRenderTime = SDL_GetTicks();
4843 /* Swap buffers around to have room available both before and after the visible region. */
4844 allocVisBuffer_DocumentWidget_(d);
4845 reposition_VisBuf(visBuf, vis);
4846 /* Redraw the invalid ranges. */
4847 if (~flags_Widget(constAs_Widget(d)) & destroyPending_WidgetFlag) {
4848 iPaint *p = &ctx->paint;
4849 init_Paint(p);
4850 iForIndices(i, visBuf->buffers) {
4851 iVisBufTexture *buf = &visBuf->buffers[i];
4852 iVisBufMeta *meta = buf->user;
4853 const iRangei bufRange = intersect_Rangei(bufferRange_VisBuf(visBuf, i), full);
4854 const iRangei bufVisRange = intersect_Rangei(bufRange, vis);
4855 ctx->widgetBounds = moved_Rect(ctxWidgetBounds, init_I2(0, -buf->origin));
4856 ctx->viewPos = init_I2(left_Rect(ctx->docBounds) - left_Rect(bounds), -buf->origin);
4857// printf(" buffer %zu: buf vis range %d...%d\n", i, bufVisRange.start, bufVisRange.end);
4858 if (!prerenderExtra && !isEmpty_Range(&bufVisRange)) {
4859 didDraw = iTrue;
4860 if (isEmpty_Rangei(buf->validRange)) {
4861 /* Fill the required currently visible range (vis). */
4862 const iRangei bufVisRange = intersect_Rangei(bufRange, vis);
4863 if (!isEmpty_Range(&bufVisRange)) {
4864 beginTarget_Paint(p, buf->texture);
4865 fillRect_Paint(p, (iRect){ zero_I2(), visBuf->texSize }, tmBackground_ColorId);
4866 iZap(ctx->runsDrawn);
4867 render_GmDocument(d->doc, bufVisRange, drawRun_DrawContext_, ctx);
4868 meta->runsDrawn = ctx->runsDrawn;
4869 extend_GmRunRange_(&meta->runsDrawn);
4870 buf->validRange = bufVisRange;
4871 // printf(" buffer %zu valid %d...%d\n", i, bufRange.start, bufRange.end);
4872 }
4873 }
4874 else {
4875 /* Progressively fill the required runs. */
4876 if (meta->runsDrawn.start) {
4877 beginTarget_Paint(p, buf->texture);
4878 meta->runsDrawn.start = renderProgressive_GmDocument(d->doc, meta->runsDrawn.start,
4879 -1, iInvalidSize,
4880 bufVisRange,
4881 drawRun_DrawContext_,
4882 ctx);
4883 buf->validRange.start = bufVisRange.start;
4884 }
4885 if (meta->runsDrawn.end) {
4886 beginTarget_Paint(p, buf->texture);
4887 meta->runsDrawn.end = renderProgressive_GmDocument(d->doc, meta->runsDrawn.end,
4888 +1, iInvalidSize,
4889 bufVisRange,
4890 drawRun_DrawContext_,
4891 ctx);
4892 buf->validRange.end = bufVisRange.end;
4893 }
4894 }
4895 }
4896 /* Progressively draw the rest of the buffer if it isn't fully valid. */
4897 if (prerenderExtra && !equal_Rangei(bufRange, buf->validRange)) {
4898 const iGmRun *next;
4899// printf("%zu: prerenderExtra (start:%p end:%p)\n", i, meta->runsDrawn.start, meta->runsDrawn.end);
4900 if (meta->runsDrawn.start == NULL) {
4901 /* Haven't drawn anything yet in this buffer, so let's try seeding it. */
4902 const int rh = lineHeight_Text(paragraph_FontId);
4903 const int y = i >= iElemCount(visBuf->buffers) / 2 ? bufRange.start : (bufRange.end - rh);
4904 beginTarget_Paint(p, buf->texture);
4905 fillRect_Paint(p, (iRect){ zero_I2(), visBuf->texSize }, tmBackground_ColorId);
4906 buf->validRange = (iRangei){ y, y + rh };
4907 iZap(ctx->runsDrawn);
4908 render_GmDocument(d->doc, buf->validRange, drawRun_DrawContext_, ctx);
4909 meta->runsDrawn = ctx->runsDrawn;
4910 extend_GmRunRange_(&meta->runsDrawn);
4911// printf("%zu: seeded, next %p:%p\n", i, meta->runsDrawn.start, meta->runsDrawn.end);
4912 didDraw = iTrue;
4913 }
4914 else {
4915 if (meta->runsDrawn.start) {
4916 const iRangei upper = intersect_Rangei(bufRange, (iRangei){ full.start, buf->validRange.start });
4917 if (upper.end > upper.start) {
4918 beginTarget_Paint(p, buf->texture);
4919 next = renderProgressive_GmDocument(d->doc, meta->runsDrawn.start,
4920 -1, 1, upper,
4921 drawRun_DrawContext_,
4922 ctx);
4923 if (next && meta->runsDrawn.start != next) {
4924 meta->runsDrawn.start = next;
4925 buf->validRange.start = bottom_Rect(next->visBounds);
4926 didDraw = iTrue;
4927 }
4928 else {
4929 buf->validRange.start = bufRange.start;
4930 }
4931 }
4932 }
4933 if (!didDraw && meta->runsDrawn.end) {
4934 const iRangei lower = intersect_Rangei(bufRange, (iRangei){ buf->validRange.end, full.end });
4935 if (lower.end > lower.start) {
4936 beginTarget_Paint(p, buf->texture);
4937 next = renderProgressive_GmDocument(d->doc, meta->runsDrawn.end,
4938 +1, 1, lower,
4939 drawRun_DrawContext_,
4940 ctx);
4941 if (next && meta->runsDrawn.end != next) {
4942 meta->runsDrawn.end = next;
4943 buf->validRange.end = top_Rect(next->visBounds);
4944 didDraw = iTrue;
4945 }
4946 else {
4947 buf->validRange.end = bufRange.end;
4948 }
4949 }
4950 }
4951 }
4952 }
4953 /* Draw any invalidated runs that fall within this buffer. */
4954 if (!prerenderExtra) {
4955 const iRangei bufRange = { buf->origin, buf->origin + visBuf->texSize.y };
4956 /* Clear full-width backgrounds first in case there are any dynamic elements. */ {
4957 iConstForEach(PtrSet, r, d->invalidRuns) {
4958 const iGmRun *run = *r.value;
4959 if (isOverlapping_Rangei(bufRange, ySpan_Rect(run->visBounds))) {
4960 beginTarget_Paint(p, buf->texture);
4961 fillRect_Paint(p,
4962 init_Rect(0,
4963 run->visBounds.pos.y - buf->origin,
4964 visBuf->texSize.x,
4965 run->visBounds.size.y),
4966 tmBackground_ColorId);
4967 }
4968 }
4969 }
4970 setAnsiFlags_Text(ansiEscapes_GmDocument(d->doc));
4971 iConstForEach(PtrSet, r, d->invalidRuns) {
4972 const iGmRun *run = *r.value;
4973 if (isOverlapping_Rangei(bufRange, ySpan_Rect(run->visBounds))) {
4974 beginTarget_Paint(p, buf->texture);
4975 drawRun_DrawContext_(ctx, run);
4976 }
4977 }
4978 setAnsiFlags_Text(allowAll_AnsiFlag);
4979 }
4980 endTarget_Paint(p);
4981 if (prerenderExtra && didDraw) {
4982 /* Just a run at a time. */
4983 break;
4984 }
4985 }
4986 if (!prerenderExtra) {
4987 clear_PtrSet(d->invalidRuns);
4988 }
4989 }
4990 return didDraw;
4991}
4992
4993static void prerender_DocumentWidget_(iAny *context) { 5468static void prerender_DocumentWidget_(iAny *context) {
5469 iAssert(isInstance_Object(context, &Class_DocumentWidget));
4994 if (current_Root() == NULL) { 5470 if (current_Root() == NULL) {
4995 /* The widget has probably been removed from the widget tree, pending destruction. 5471 /* The widget has probably been removed from the widget tree, pending destruction.
4996 Tickers are not cancelled until the widget is actually destroyed. */ 5472 Tickers are not cancelled until the widget is actually destroyed. */
@@ -4998,15 +5474,16 @@ static void prerender_DocumentWidget_(iAny *context) {
4998 } 5474 }
4999 const iDocumentWidget *d = context; 5475 const iDocumentWidget *d = context;
5000 iDrawContext ctx = { 5476 iDrawContext ctx = {
5001 .widget = d, 5477 .view = &d->view,
5002 .docBounds = documentBounds_DocumentWidget_(d), 5478 .docBounds = documentBounds_DocumentView_(&d->view),
5003 .vis = visibleRange_DocumentWidget_(d), 5479 .vis = visibleRange_DocumentView_(&d->view),
5004 .showLinkNumbers = (d->flags & showLinkNumbers_DocumentWidgetFlag) != 0 5480 .showLinkNumbers = (d->flags & showLinkNumbers_DocumentWidgetFlag) != 0
5005 }; 5481 };
5006// printf("%u prerendering\n", SDL_GetTicks()); 5482 // printf("%u prerendering\n", SDL_GetTicks());
5007 if (d->visBuf->buffers[0].texture) { 5483 if (d->view.visBuf->buffers[0].texture) {
5008 if (render_DocumentWidget_(d, &ctx, iTrue /* just fill up progressively */)) { 5484 makePaletteGlobal_GmDocument(d->view.doc);
5009 /* Something was drawn, should check if there is still more to do. */ 5485 if (render_DocumentView_(&d->view, &ctx, iTrue /* just fill up progressively */)) {
5486 /* Something was drawn, should check later if there is still more to do. */
5010 addTicker_App(prerender_DocumentWidget_, context); 5487 addTicker_App(prerender_DocumentWidget_, context);
5011 } 5488 }
5012 } 5489 }
@@ -5020,108 +5497,54 @@ static void draw_DocumentWidget_(const iDocumentWidget *d) {
5020 if (width_Rect(bounds) <= 0) { 5497 if (width_Rect(bounds) <= 0) {
5021 return; 5498 return;
5022 } 5499 }
5023 /* TODO: Come up with a better palette caching system. 5500 checkPendingInvalidation_DocumentWidget_(d);
5024 It should be able to recompute cached colors in `History` when the theme has changed. 5501 draw_DocumentView_(&d->view);
5025 Cache the theme seed in `GmDocument`? */ 5502 iPaint p;
5026// makePaletteGlobal_GmDocument(d->doc); 5503 init_Paint(&p);
5027 if (d->drawBufs->flags & updateTimestampBuf_DrawBufsFlag) {
5028 updateTimestampBuf_DocumentWidget_(d);
5029 }
5030 if (d->drawBufs->flags & updateSideBuf_DrawBufsFlag) {
5031 updateSideIconBuf_DocumentWidget_(d);
5032 }
5033 const iRect docBounds = documentBounds_DocumentWidget_(d);
5034 const iRangei vis = visibleRange_DocumentWidget_(d);
5035 iDrawContext ctx = {
5036 .widget = d,
5037 .docBounds = docBounds,
5038 .vis = vis,
5039 .showLinkNumbers = (d->flags & showLinkNumbers_DocumentWidgetFlag) != 0,
5040 };
5041 init_Paint(&ctx.paint);
5042 render_DocumentWidget_(d, &ctx, iFalse /* just the mandatory parts */);
5043 int yTop = docBounds.pos.y + viewPos_DocumentWidget_(d);
5044 const iBool isDocEmpty = size_GmDocument(d->doc).y == 0;
5045 const iBool isTouchSelecting = (flags_Widget(w) & touchDrag_WidgetFlag) != 0;
5046 if (!isDocEmpty || !isEmpty_Banner(d->banner)) {
5047 const int docBgColor = isDocEmpty ? tmBannerBackground_ColorId : tmBackground_ColorId;
5048 setClip_Paint(&ctx.paint, clipBounds);
5049 if (!isDocEmpty) {
5050 draw_VisBuf(d->visBuf, init_I2(bounds.pos.x, yTop), ySpan_Rect(bounds));
5051 }
5052 /* Text markers. */
5053 if (!isEmpty_Range(&d->foundMark) || !isEmpty_Range(&d->selectMark)) {
5054 SDL_Renderer *render = renderer_Window(get_Window());
5055 ctx.firstMarkRect = zero_Rect();
5056 ctx.lastMarkRect = zero_Rect();
5057 SDL_SetRenderDrawBlendMode(render,
5058 isDark_ColorTheme(colorTheme_App()) ? SDL_BLENDMODE_ADD
5059 : SDL_BLENDMODE_BLEND);
5060 ctx.viewPos = topLeft_Rect(docBounds);
5061 /* Marker starting outside the visible range? */
5062 if (d->visibleRuns.start) {
5063 if (!isEmpty_Range(&d->selectMark) &&
5064 d->selectMark.start < d->visibleRuns.start->text.start &&
5065 d->selectMark.end > d->visibleRuns.start->text.start) {
5066 ctx.inSelectMark = iTrue;
5067 }
5068 if (isEmpty_Range(&d->foundMark) &&
5069 d->foundMark.start < d->visibleRuns.start->text.start &&
5070 d->foundMark.end > d->visibleRuns.start->text.start) {
5071 ctx.inFoundMark = iTrue;
5072 }
5073 }
5074 render_GmDocument(d->doc, vis, drawMark_DrawContext_, &ctx);
5075 SDL_SetRenderDrawBlendMode(render, SDL_BLENDMODE_NONE);
5076 /* Selection range pins. */
5077 if (isTouchSelecting) {
5078 drawPin_Paint(&ctx.paint, ctx.firstMarkRect, 0, tmQuote_ColorId);
5079 drawPin_Paint(&ctx.paint, ctx.lastMarkRect, 1, tmQuote_ColorId);
5080 }
5081 }
5082 drawMedia_DocumentWidget_(d, &ctx.paint);
5083 /* Fill the top and bottom, in case the document is short. */
5084 if (yTop > top_Rect(bounds)) {
5085 fillRect_Paint(&ctx.paint,
5086 (iRect){ bounds.pos, init_I2(bounds.size.x, yTop - top_Rect(bounds)) },
5087 !isEmpty_Banner(d->banner) ? tmBannerBackground_ColorId
5088 : docBgColor);
5089 }
5090 /* Banner. */
5091 if (!isDocEmpty || numItems_Banner(d->banner) > 0) {
5092 /* Fill the part between the banner and the top of the document. */
5093 fillRect_Paint(&ctx.paint,
5094 (iRect){ init_I2(left_Rect(bounds),
5095 top_Rect(docBounds) + viewPos_DocumentWidget_(d) -
5096 documentTopPad_DocumentWidget_(d)),
5097 init_I2(bounds.size.x, documentTopPad_DocumentWidget_(d)) },
5098 docBgColor);
5099 setPos_Banner(d->banner, addY_I2(topLeft_Rect(docBounds),
5100 -pos_SmoothScroll(&d->scrollY)));
5101 draw_Banner(d->banner);
5102 }
5103 const int yBottom = yTop + size_GmDocument(d->doc).y;
5104 if (yBottom < bottom_Rect(bounds)) {
5105 fillRect_Paint(&ctx.paint,
5106 init_Rect(bounds.pos.x, yBottom, bounds.size.x, bottom_Rect(bounds) - yBottom),
5107 !isDocEmpty ? docBgColor : tmBannerBackground_ColorId);
5108 }
5109 unsetClip_Paint(&ctx.paint);
5110 drawSideElements_DocumentWidget_(d);
5111 if (prefs_App()->hoverLink && d->hoverLink) {
5112 const int font = uiLabel_FontId;
5113 const iRangecc linkUrl = range_String(linkUrl_GmDocument(d->doc, d->hoverLink->linkId));
5114 const iInt2 size = measureRange_Text(font, linkUrl).bounds.size;
5115 const iRect linkRect = { addY_I2(bottomLeft_Rect(bounds), -size.y),
5116 addX_I2(size, 2 * gap_UI) };
5117 fillRect_Paint(&ctx.paint, linkRect, tmBackground_ColorId);
5118 drawRange_Text(font, addX_I2(topLeft_Rect(linkRect), gap_UI), tmParagraph_ColorId, linkUrl);
5119 }
5120 }
5121 if (colorTheme_App() == pureWhite_ColorTheme) { 5504 if (colorTheme_App() == pureWhite_ColorTheme) {
5122 drawHLine_Paint(&ctx.paint, topLeft_Rect(bounds), width_Rect(bounds), uiSeparator_ColorId); 5505 drawHLine_Paint(&p, topLeft_Rect(bounds), width_Rect(bounds), uiSeparator_ColorId);
5123 } 5506 }
5507 /* Pull action indicator. */
5508 if (deviceType_App() != desktop_AppDeviceType) {
5509 float pullPos = pullActionPos_SmoothScroll(&d->view.scrollY);
5510 /* Account for the part where the indicator isn't yet visible. */
5511 pullPos = (pullPos - 0.2f) / 0.8f;
5512 iRect indRect = initCentered_Rect(init_I2(mid_Rect(bounds).x,
5513 top_Rect(bounds) - 5 * gap_UI -
5514 pos_SmoothScroll(&d->view.scrollY)),
5515 init_I2(20 * gap_UI, 2 * gap_UI));
5516 setClip_Paint(&p, clipBounds);
5517 int color = pullPos < 1.0f ? tmBannerItemFrame_ColorId : tmBannerItemText_ColorId;
5518 drawRect_Paint(&p, indRect, color);
5519 if (pullPos > 0) {
5520 shrink_Rect(&indRect, divi_I2(gap2_UI, 2));
5521 indRect.size.x *= pullPos;
5522 fillRect_Paint(&p, indRect, color);
5523 }
5524 unsetClip_Paint(&p);
5525 }
5526 /* Scroll bar. */
5124 drawChildren_Widget(w); 5527 drawChildren_Widget(w);
5528 /* Information about the hovered link. */
5529 if (deviceType_App() == desktop_AppDeviceType && prefs_App()->hoverLink && d->linkInfo) {
5530 const int pad = 0; /*gap_UI;*/
5531 update_LinkInfo(d->linkInfo,
5532 d->view.doc,
5533 d->view.hoverLink ? d->view.hoverLink->linkId : 0,
5534 width_Rect(bounds) - 2 * pad);
5535 const iInt2 infoSize = size_LinkInfo(d->linkInfo);
5536 iInt2 infoPos = add_I2(bottomLeft_Rect(bounds), init_I2(pad, -infoSize.y - pad));
5537 if (d->view.hoverLink) {
5538 const iRect runRect = runRect_DocumentView_(&d->view, d->view.hoverLink);
5539 d->linkInfo->isAltPos =
5540 (bottom_Rect(runRect) >= infoPos.y - lineHeight_Text(paragraph_FontId));
5541 }
5542 if (d->linkInfo->isAltPos) {
5543 infoPos.y = top_Rect(bounds) + pad;
5544 }
5545 draw_LinkInfo(d->linkInfo, infoPos);
5546 }
5547 /* Full-sized download indicator. */
5125 if (d->flags & drawDownloadCounter_DocumentWidgetFlag && isRequestOngoing_DocumentWidget(d)) { 5548 if (d->flags & drawDownloadCounter_DocumentWidgetFlag && isRequestOngoing_DocumentWidget(d)) {
5126 const int font = uiLabelLarge_FontId; 5549 const int font = uiLabelLarge_FontId;
5127 const iInt2 sevenSegWidth = measureRange_Text(font, range_CStr("\U0001fbf0")).bounds.size; 5550 const iInt2 sevenSegWidth = measureRange_Text(font, range_CStr("\U0001fbf0")).bounds.size;
@@ -5131,62 +5554,26 @@ static void draw_DocumentWidget_(const iDocumentWidget *d) {
5131 tmQuote_ColorId, tmQuoteIcon_ColorId, 5554 tmQuote_ColorId, tmQuoteIcon_ColorId,
5132 bodySize_GmRequest(d->request)); 5555 bodySize_GmRequest(d->request));
5133 } 5556 }
5134 /* Alt text. */
5135 const float altTextOpacity = value_Anim(&d->altTextOpacity) * 6 - 5;
5136 if (d->hoverAltPre && altTextOpacity > 0) {
5137 const iGmPreMeta *meta = preMeta_GmDocument(d->doc, preId_GmRun(d->hoverAltPre));
5138 if (meta->flags & topLeft_GmPreMetaFlag && ~meta->flags & decoration_GmRunFlag &&
5139 !isEmpty_Range(&meta->altText)) {
5140 const int margin = 3 * gap_UI / 2;
5141 const int altFont = uiLabel_FontId;
5142 const int wrap = docBounds.size.x - 2 * margin;
5143 iInt2 pos = addY_I2(add_I2(docBounds.pos, meta->pixelRect.pos),
5144 viewPos_DocumentWidget_(d));
5145 const iInt2 textSize = measureWrapRange_Text(altFont, wrap, meta->altText).bounds.size;
5146 pos.y -= textSize.y + gap_UI;
5147 pos.y = iMax(pos.y, top_Rect(bounds));
5148 const iRect altRect = { pos, init_I2(docBounds.size.x, textSize.y) };
5149 ctx.paint.alpha = altTextOpacity * 255;
5150 if (altTextOpacity < 1) {
5151 SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_BLEND);
5152 }
5153 fillRect_Paint(&ctx.paint, altRect, tmBackgroundAltText_ColorId);
5154 drawRect_Paint(&ctx.paint, altRect, tmFrameAltText_ColorId);
5155 setOpacity_Text(altTextOpacity);
5156 drawWrapRange_Text(altFont, addX_I2(pos, margin), wrap,
5157 tmQuote_ColorId, meta->altText);
5158 SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_NONE);
5159 setOpacity_Text(1.0f);
5160 }
5161 }
5162 /* Pinch zoom indicator. */ 5557 /* Pinch zoom indicator. */
5163 if (d->flags & pinchZoom_DocumentWidgetFlag) { 5558 if (d->flags & pinchZoom_DocumentWidgetFlag) {
5164 const int font = uiLabelLargeBold_FontId; 5559 const int font = uiLabelLargeBold_FontId;
5165 const int height = lineHeight_Text(font) * 2; 5560 const int height = lineHeight_Text(font) * 2;
5166 const iInt2 size = init_I2(height * 2, height); 5561 const iInt2 size = init_I2(height * 2, height);
5167 const iRect rect = { sub_I2(mid_Rect(bounds), divi_I2(size, 2)), size }; 5562 const iRect rect = { sub_I2(mid_Rect(bounds), divi_I2(size, 2)), size };
5168 fillRect_Paint(&ctx.paint, rect, d->pinchZoomPosted == 100 ? uiTextCaution_ColorId : uiTextAction_ColorId); 5563 fillRect_Paint(&p, rect, d->pinchZoomPosted == 100 ? uiTextCaution_ColorId : uiTextAction_ColorId);
5169 drawCentered_Text(font, bounds, iFalse, uiBackground_ColorId, "%d %%", 5564 drawCentered_Text(font, bounds, iFalse, uiBackground_ColorId, "%d %%",
5170 d->pinchZoomPosted); 5565 d->pinchZoomPosted);
5171 } 5566 }
5172 /* Touch selection indicator. */ 5567 /* Dimming during swipe animation. */
5173 if (isTouchSelecting) {
5174 iRect rect = { topLeft_Rect(bounds),
5175 init_I2(width_Rect(bounds), lineHeight_Text(uiLabelBold_FontId)) };
5176 fillRect_Paint(&ctx.paint, rect, uiTextAction_ColorId);
5177 const iRangecc mark = selectMark_DocumentWidget_(d);
5178 drawCentered_Text(uiLabelBold_FontId, rect, iFalse, uiBackground_ColorId, "%zu bytes selected",
5179 size_Range(&mark));
5180 }
5181 if (w->offsetRef) { 5568 if (w->offsetRef) {
5182 const int offX = visualOffsetByReference_Widget(w); 5569 const int offX = visualOffsetByReference_Widget(w);
5183 if (offX) { 5570 if (offX) {
5184 setClip_Paint(&ctx.paint, clipBounds); 5571 setClip_Paint(&p, clipBounds);
5185 SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_BLEND); 5572 SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_BLEND);
5186 ctx.paint.alpha = iAbs(offX) / (float) get_Window()->size.x * 300; 5573 p.alpha = iAbs(offX) / (float) get_Window()->size.x * 300;
5187 fillRect_Paint(&ctx.paint, bounds, backgroundFadeColor_Widget()); 5574 fillRect_Paint(&p, bounds, backgroundFadeColor_Widget());
5188 SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_NONE); 5575 SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_NONE);
5189 unsetClip_Paint(&ctx.paint); 5576 unsetClip_Paint(&p);
5190 } 5577 }
5191 else { 5578 else {
5192 /* TODO: Should have a better place to do this; drawing is supposed to be immutable. */ 5579 /* TODO: Should have a better place to do this; drawing is supposed to be immutable. */
@@ -5195,11 +5582,139 @@ static void draw_DocumentWidget_(const iDocumentWidget *d) {
5195 mut->flags &= ~refChildrenOffset_WidgetFlag; 5582 mut->flags &= ~refChildrenOffset_WidgetFlag;
5196 } 5583 }
5197 } 5584 }
5198// drawRect_Paint(&ctx.paint, docBounds, red_ColorId); 5585// drawRect_Paint(&p, docBounds, red_ColorId);
5586 if (deviceType_App() == phone_AppDeviceType) {
5587 /* The phone toolbar uses the palette of the active tab, but there may be other
5588 documents drawn before the toolbar, causing the colors to be incorrect. */
5589 makePaletteGlobal_GmDocument(document_App()->view.doc);
5590 }
5199} 5591}
5200 5592
5201/*----------------------------------------------------------------------------------------------*/ 5593/*----------------------------------------------------------------------------------------------*/
5202 5594
5595void init_DocumentWidget(iDocumentWidget *d) {
5596 iWidget *w = as_Widget(d);
5597 init_Widget(w);
5598 setId_Widget(w, format_CStr("document%03d", ++docEnum_));
5599 setFlags_Widget(w, hover_WidgetFlag | noBackground_WidgetFlag, iTrue);
5600#if defined (iPlatformAppleDesktop)
5601 iBool enableSwipeNavigation = iTrue; /* swipes on the trackpad */
5602#else
5603 iBool enableSwipeNavigation = (deviceType_App() != desktop_AppDeviceType);
5604#endif
5605 if (enableSwipeNavigation) {
5606 setFlags_Widget(w, leftEdgeDraggable_WidgetFlag | rightEdgeDraggable_WidgetFlag |
5607 horizontalOffset_WidgetFlag, iTrue);
5608 }
5609 init_PersistentDocumentState(&d->mod);
5610 d->flags = 0;
5611 d->phoneToolbar = findWidget_App("toolbar");
5612 d->footerButtons = NULL;
5613 iZap(d->certExpiry);
5614 d->certFingerprint = new_Block(0);
5615 d->certFlags = 0;
5616 d->certSubject = new_String();
5617 d->state = blank_RequestState;
5618 d->titleUser = new_String();
5619 d->request = NULL;
5620 d->requestLinkId = 0;
5621 d->isRequestUpdated = iFalse;
5622 d->media = new_ObjectList();
5623 d->banner = new_Banner();
5624 setOwner_Banner(d->banner, d);
5625 d->redirectCount = 0;
5626 d->ordinalBase = 0;
5627 d->wheelSwipeState = none_WheelSwipeState;
5628 d->selectMark = iNullRange;
5629 d->foundMark = iNullRange;
5630 d->contextLink = NULL;
5631 d->sourceStatus = none_GmStatusCode;
5632 init_String(&d->sourceHeader);
5633 init_String(&d->sourceMime);
5634 init_Block(&d->sourceContent, 0);
5635 iZap(d->sourceTime);
5636 d->sourceGempub = NULL;
5637 d->initNormScrollY = 0;
5638 d->grabbedPlayer = NULL;
5639 d->mediaTimer = 0;
5640 init_String(&d->pendingGotoHeading);
5641 init_String(&d->linePrecedingLink);
5642 init_Click(&d->click, d, SDL_BUTTON_LEFT);
5643 d->linkInfo = (deviceType_App() == desktop_AppDeviceType ? new_LinkInfo() : NULL);
5644 init_DocumentView(&d->view);
5645 setOwner_DocumentView_(&d->view, d);
5646 addChild_Widget(w, iClob(d->scroll = new_ScrollWidget()));
5647 d->menu = NULL; /* created when clicking */
5648 d->playerMenu = NULL;
5649 d->copyMenu = NULL;
5650 d->translation = NULL;
5651 addChildFlags_Widget(w,
5652 iClob(new_IndicatorWidget()),
5653 resizeToParentWidth_WidgetFlag | resizeToParentHeight_WidgetFlag);
5654#if !defined (iPlatformAppleDesktop) /* in system menu */
5655 addAction_Widget(w, reload_KeyShortcut, "navigate.reload");
5656 addAction_Widget(w, closeTab_KeyShortcut, "tabs.close");
5657 addAction_Widget(w, SDLK_d, KMOD_PRIMARY, "bookmark.add");
5658 addAction_Widget(w, subscribeToPage_KeyModifier, "feeds.subscribe");
5659#endif
5660 addAction_Widget(w, navigateBack_KeyShortcut, "navigate.back");
5661 addAction_Widget(w, navigateForward_KeyShortcut, "navigate.forward");
5662 addAction_Widget(w, navigateParent_KeyShortcut, "navigate.parent");
5663 addAction_Widget(w, navigateRoot_KeyShortcut, "navigate.root");
5664}
5665
5666void cancelAllRequests_DocumentWidget(iDocumentWidget *d) {
5667 iForEach(ObjectList, i, d->media) {
5668 iMediaRequest *mr = i.object;
5669 cancel_GmRequest(mr->req);
5670 }
5671 if (d->request) {
5672 cancel_GmRequest(d->request);
5673 }
5674}
5675
5676void deinit_DocumentWidget(iDocumentWidget *d) {
5677 // printf("\n* * * * * * * *\nDEINIT DOCUMENT: %s\n* * * * * * * *\n\n",
5678 // cstr_String(&d->widget.id)); fflush(stdout);
5679 cancelAllRequests_DocumentWidget(d);
5680 pauseAllPlayers_Media(media_GmDocument(d->view.doc), iTrue);
5681 removeTicker_App(animate_DocumentWidget_, d);
5682 removeTicker_App(prerender_DocumentWidget_, d);
5683 remove_Periodic(periodic_App(), d);
5684 delete_Translation(d->translation);
5685 deinit_DocumentView(&d->view);
5686 delete_LinkInfo(d->linkInfo);
5687 iRelease(d->media);
5688 iRelease(d->request);
5689 delete_Gempub(d->sourceGempub);
5690 deinit_String(&d->linePrecedingLink);
5691 deinit_String(&d->pendingGotoHeading);
5692 deinit_Block(&d->sourceContent);
5693 deinit_String(&d->sourceMime);
5694 deinit_String(&d->sourceHeader);
5695 delete_Banner(d->banner);
5696 if (d->mediaTimer) {
5697 SDL_RemoveTimer(d->mediaTimer);
5698 }
5699 delete_Block(d->certFingerprint);
5700 delete_String(d->certSubject);
5701 delete_String(d->titleUser);
5702 deinit_PersistentDocumentState(&d->mod);
5703}
5704
5705void setSource_DocumentWidget(iDocumentWidget *d, const iString *source) {
5706 setUrl_GmDocument(d->view.doc, d->mod.url);
5707 const int docWidth = documentWidth_DocumentView_(&d->view);
5708 setSource_GmDocument(d->view.doc,
5709 source,
5710 docWidth,
5711 width_Widget(d),
5712 isFinished_GmRequest(d->request) ? final_GmDocumentUpdate
5713 : partial_GmDocumentUpdate);
5714 setWidth_Banner(d->banner, docWidth);
5715 documentWasChanged_DocumentWidget_(d);
5716}
5717
5203iHistory *history_DocumentWidget(iDocumentWidget *d) { 5718iHistory *history_DocumentWidget(iDocumentWidget *d) {
5204 return d->mod.history; 5719 return d->mod.history;
5205} 5720}
@@ -5209,7 +5724,7 @@ const iString *url_DocumentWidget(const iDocumentWidget *d) {
5209} 5724}
5210 5725
5211const iGmDocument *document_DocumentWidget(const iDocumentWidget *d) { 5726const iGmDocument *document_DocumentWidget(const iDocumentWidget *d) {
5212 return d->doc; 5727 return d->view.doc;
5213} 5728}
5214 5729
5215const iBlock *sourceContent_DocumentWidget(const iDocumentWidget *d) { 5730const iBlock *sourceContent_DocumentWidget(const iDocumentWidget *d) {
@@ -5217,20 +5732,20 @@ const iBlock *sourceContent_DocumentWidget(const iDocumentWidget *d) {
5217} 5732}
5218 5733
5219int documentWidth_DocumentWidget(const iDocumentWidget *d) { 5734int documentWidth_DocumentWidget(const iDocumentWidget *d) {
5220 return documentWidth_DocumentWidget_(d); 5735 return documentWidth_DocumentView_(&d->view);
5221} 5736}
5222 5737
5223const iString *feedTitle_DocumentWidget(const iDocumentWidget *d) { 5738const iString *feedTitle_DocumentWidget(const iDocumentWidget *d) {
5224 if (!isEmpty_String(title_GmDocument(d->doc))) { 5739 if (!isEmpty_String(title_GmDocument(d->view.doc))) {
5225 return title_GmDocument(d->doc); 5740 return title_GmDocument(d->view.doc);
5226 } 5741 }
5227 return bookmarkTitle_DocumentWidget(d); 5742 return bookmarkTitle_DocumentWidget(d);
5228} 5743}
5229 5744
5230const iString *bookmarkTitle_DocumentWidget(const iDocumentWidget *d) { 5745const iString *bookmarkTitle_DocumentWidget(const iDocumentWidget *d) {
5231 iStringArray *title = iClob(new_StringArray()); 5746 iStringArray *title = iClob(new_StringArray());
5232 if (!isEmpty_String(title_GmDocument(d->doc))) { 5747 if (!isEmpty_String(title_GmDocument(d->view.doc))) {
5233 pushBack_StringArray(title, title_GmDocument(d->doc)); 5748 pushBack_StringArray(title, title_GmDocument(d->view.doc));
5234 } 5749 }
5235 if (!isEmpty_String(d->titleUser)) { 5750 if (!isEmpty_String(d->titleUser)) {
5236 pushBack_StringArray(title, d->titleUser); 5751 pushBack_StringArray(title, d->titleUser);
@@ -5258,30 +5773,19 @@ void deserializeState_DocumentWidget(iDocumentWidget *d, iStream *ins) {
5258 updateFromHistory_DocumentWidget_(d); 5773 updateFromHistory_DocumentWidget_(d);
5259} 5774}
5260 5775
5261static void setUrl_DocumentWidget_(iDocumentWidget *d, const iString *url) {
5262 url = canonicalUrl_String(url);
5263 if (!equal_String(d->mod.url, url)) {
5264 d->flags |= urlChanged_DocumentWidgetFlag;
5265 set_String(d->mod.url, url);
5266 }
5267}
5268
5269void setUrlFlags_DocumentWidget(iDocumentWidget *d, const iString *url, int setUrlFlags) { 5776void setUrlFlags_DocumentWidget(iDocumentWidget *d, const iString *url, int setUrlFlags) {
5270 iChangeFlags(d->flags, openedFromSidebar_DocumentWidgetFlag, 5777 const iBool allowCache = (setUrlFlags & useCachedContentIfAvailable_DocumentWidgetSetUrlFlag) != 0;
5271 (setUrlFlags & openedFromSidebar_DocumentWidgetSetUrlFlag) != 0);
5272 const iBool isFromCache = (setUrlFlags & useCachedContentIfAvailable_DocumentWidgetSetUrlFlag) != 0;
5273 setLinkNumberMode_DocumentWidget_(d, iFalse); 5778 setLinkNumberMode_DocumentWidget_(d, iFalse);
5274 setUrl_DocumentWidget_(d, urlFragmentStripped_String(url)); 5779 setUrl_DocumentWidget_(d, urlFragmentStripped_String(url));
5275 /* See if there a username in the URL. */ 5780 /* See if there a username in the URL. */
5276 parseUser_DocumentWidget_(d); 5781 parseUser_DocumentWidget_(d);
5277 if (!isFromCache || !updateFromHistory_DocumentWidget_(d)) { 5782 if (!allowCache || !updateFromHistory_DocumentWidget_(d)) {
5278 fetch_DocumentWidget_(d); 5783 fetch_DocumentWidget_(d);
5279 } 5784 }
5280} 5785}
5281 5786
5282void setUrlAndSource_DocumentWidget(iDocumentWidget *d, const iString *url, const iString *mime, 5787void setUrlAndSource_DocumentWidget(iDocumentWidget *d, const iString *url, const iString *mime,
5283 const iBlock *source) { 5788 const iBlock *source) {
5284 d->flags &= ~openedFromSidebar_DocumentWidgetFlag;
5285 setLinkNumberMode_DocumentWidget_(d, iFalse); 5789 setLinkNumberMode_DocumentWidget_(d, iFalse);
5286 setUrl_DocumentWidget_(d, url); 5790 setUrl_DocumentWidget_(d, url);
5287 parseUser_DocumentWidget_(d); 5791 parseUser_DocumentWidget_(d);
@@ -5291,18 +5795,26 @@ void setUrlAndSource_DocumentWidget(iDocumentWidget *d, const iString *url, cons
5291 set_String(&resp->meta, mime); 5795 set_String(&resp->meta, mime);
5292 set_Block(&resp->body, source); 5796 set_Block(&resp->body, source);
5293 updateFromCachedResponse_DocumentWidget_(d, 0, resp, NULL); 5797 updateFromCachedResponse_DocumentWidget_(d, 0, resp, NULL);
5798 updateBanner_DocumentWidget_(d);
5294 delete_GmResponse(resp); 5799 delete_GmResponse(resp);
5295} 5800}
5296 5801
5297iDocumentWidget *duplicate_DocumentWidget(const iDocumentWidget *orig) { 5802iDocumentWidget *duplicate_DocumentWidget(const iDocumentWidget *orig) {
5298 iDocumentWidget *d = new_DocumentWidget(); 5803 iDocumentWidget *d = new_DocumentWidget();
5299 delete_History(d->mod.history); 5804 delete_History(d->mod.history);
5300 d->initNormScrollY = normScrollPos_DocumentWidget_(d); 5805 d->initNormScrollY = normScrollPos_DocumentView_(&d->view);
5301 d->mod.history = copy_History(orig->mod.history); 5806 d->mod.history = copy_History(orig->mod.history);
5302 setUrlFlags_DocumentWidget(d, orig->mod.url, useCachedContentIfAvailable_DocumentWidgetSetUrlFlag); 5807 setUrlFlags_DocumentWidget(d, orig->mod.url, useCachedContentIfAvailable_DocumentWidgetSetUrlFlag);
5303 return d; 5808 return d;
5304} 5809}
5305 5810
5811void setOrigin_DocumentWidget(iDocumentWidget *d, const iDocumentWidget *other) {
5812 if (d != other) {
5813 /* TODO: Could remember the other's ID? */
5814 set_String(&d->linePrecedingLink, &other->linePrecedingLink);
5815 }
5816}
5817
5306void setUrl_DocumentWidget(iDocumentWidget *d, const iString *url) { 5818void setUrl_DocumentWidget(iDocumentWidget *d, const iString *url) {
5307 setUrlFlags_DocumentWidget(d, url, 0); 5819 setUrlFlags_DocumentWidget(d, url, 0);
5308} 5820}
@@ -5315,11 +5827,6 @@ void setRedirectCount_DocumentWidget(iDocumentWidget *d, int count) {
5315 d->redirectCount = count; 5827 d->redirectCount = count;
5316} 5828}
5317 5829
5318void setOpenedFromSidebar_DocumentWidget(iDocumentWidget *d, iBool fromSidebar) {
5319 iChangeFlags(d->flags, openedFromSidebar_DocumentWidgetFlag, fromSidebar);
5320// setCachedDocument_History(d->mod.history, d->doc, fromSidebar);
5321}
5322
5323iBool isRequestOngoing_DocumentWidget(const iDocumentWidget *d) { 5830iBool isRequestOngoing_DocumentWidget(const iDocumentWidget *d) {
5324 return d->request != NULL; 5831 return d->request != NULL;
5325} 5832}
@@ -5327,7 +5834,7 @@ iBool isRequestOngoing_DocumentWidget(const iDocumentWidget *d) {
5327void takeRequest_DocumentWidget(iDocumentWidget *d, iGmRequest *finishedRequest) { 5834void takeRequest_DocumentWidget(iDocumentWidget *d, iGmRequest *finishedRequest) {
5328 cancelRequest_DocumentWidget_(d, iFalse /* don't post anything */); 5835 cancelRequest_DocumentWidget_(d, iFalse /* don't post anything */);
5329 const iString *url = url_GmRequest(finishedRequest); 5836 const iString *url = url_GmRequest(finishedRequest);
5330 5837
5331 add_History(d->mod.history, url); 5838 add_History(d->mod.history, url);
5332 setUrl_DocumentWidget_(d, url); 5839 setUrl_DocumentWidget_(d, url);
5333 d->state = fetching_RequestState; 5840 d->state = fetching_RequestState;
@@ -5341,27 +5848,17 @@ void takeRequest_DocumentWidget(iDocumentWidget *d, iGmRequest *finishedRequest)
5341} 5848}
5342 5849
5343void updateSize_DocumentWidget(iDocumentWidget *d) { 5850void updateSize_DocumentWidget(iDocumentWidget *d) {
5344 updateDocumentWidthRetainingScrollPosition_DocumentWidget_(d, iFalse); 5851 iDocumentView *view = &d->view;
5345 resetWideRuns_DocumentWidget_(d); 5852 updateDocumentWidthRetainingScrollPosition_DocumentView_(view, iFalse);
5346 d->drawBufs->flags |= updateSideBuf_DrawBufsFlag; 5853 resetWideRuns_DocumentView_(view);
5347 updateVisible_DocumentWidget_(d); 5854 view->drawBufs->flags |= updateSideBuf_DrawBufsFlag;
5348 setWidth_Banner(d->banner, documentWidth_DocumentWidget(d)); 5855 updateVisible_DocumentView_(view);
5856 setWidth_Banner(d->banner, documentWidth_DocumentView_(view));
5349 invalidate_DocumentWidget_(d); 5857 invalidate_DocumentWidget_(d);
5350 arrange_Widget(d->footerButtons); 5858 arrange_Widget(d->footerButtons);
5351} 5859}
5352 5860
5353#if 0
5354static void sizeChanged_DocumentWidget_(iDocumentWidget *d) {
5355 if (current_Root()) {
5356 /* TODO: This gets called more than once during a single arrange.
5357 It could be done via some sort of callback instead. */
5358 updateVisible_DocumentWidget_(d);
5359 }
5360}
5361#endif
5362
5363iBeginDefineSubclass(DocumentWidget, Widget) 5861iBeginDefineSubclass(DocumentWidget, Widget)
5364 .processEvent = (iAny *) processEvent_DocumentWidget_, 5862 .processEvent = (iAny *) processEvent_DocumentWidget_,
5365 .draw = (iAny *) draw_DocumentWidget_, 5863 .draw = (iAny *) draw_DocumentWidget_,
5366// .sizeChanged = (iAny *) sizeChanged_DocumentWidget_,
5367iEndDefineSubclass(DocumentWidget) 5864iEndDefineSubclass(DocumentWidget)
diff --git a/src/ui/documentwidget.h b/src/ui/documentwidget.h
index 2df3392b..1bee8351 100644
--- a/src/ui/documentwidget.h
+++ b/src/ui/documentwidget.h
@@ -48,21 +48,17 @@ const iString * bookmarkTitle_DocumentWidget (const iDocumentWidget *);
48const iString * feedTitle_DocumentWidget (const iDocumentWidget *); 48const iString * feedTitle_DocumentWidget (const iDocumentWidget *);
49int documentWidth_DocumentWidget (const iDocumentWidget *); 49int documentWidth_DocumentWidget (const iDocumentWidget *);
50 50
51//iBool findCachedContent_DocumentWidget(const iDocumentWidget *, const iString *url,
52// iString *mime_out, iBlock *data_out);
53
54enum iDocumentWidgetSetUrlFlags { 51enum iDocumentWidgetSetUrlFlags {
55 useCachedContentIfAvailable_DocumentWidgetSetUrlFlag = iBit(1), 52 useCachedContentIfAvailable_DocumentWidgetSetUrlFlag = iBit(1),
56 openedFromSidebar_DocumentWidgetSetUrlFlag = iBit(2),
57}; 53};
58 54
55void setOrigin_DocumentWidget (iDocumentWidget *, const iDocumentWidget *other);
59void setUrl_DocumentWidget (iDocumentWidget *, const iString *url); 56void setUrl_DocumentWidget (iDocumentWidget *, const iString *url);
60void setUrlFlags_DocumentWidget (iDocumentWidget *, const iString *url, int setUrlFlags); 57void setUrlFlags_DocumentWidget (iDocumentWidget *, const iString *url, int setUrlFlags);
61void setUrlAndSource_DocumentWidget (iDocumentWidget *, const iString *url, const iString *mime, const iBlock *source); 58void setUrlAndSource_DocumentWidget (iDocumentWidget *, const iString *url, const iString *mime, const iBlock *source);
62void setInitialScroll_DocumentWidget (iDocumentWidget *, float normScrollY); /* set after content received */ 59void setInitialScroll_DocumentWidget (iDocumentWidget *, float normScrollY); /* set after content received */
63void setRedirectCount_DocumentWidget (iDocumentWidget *, int count); 60void setRedirectCount_DocumentWidget (iDocumentWidget *, int count);
64void setSource_DocumentWidget (iDocumentWidget *, const iString *sourceText); 61void setSource_DocumentWidget (iDocumentWidget *, const iString *sourceText);
65void setOpenedFromSidebar_DocumentWidget(iDocumentWidget *, iBool fromSidebar);
66 62
67void takeRequest_DocumentWidget (iDocumentWidget *, iGmRequest *finishedRequest); /* ownership given */ 63void takeRequest_DocumentWidget (iDocumentWidget *, iGmRequest *finishedRequest); /* ownership given */
68 64
diff --git a/src/ui/inputwidget.c b/src/ui/inputwidget.c
index a1fb8cb5..9261da0c 100644
--- a/src/ui/inputwidget.c
+++ b/src/ui/inputwidget.c
@@ -20,6 +20,10 @@ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
20(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 20(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
21SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ 21SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22 22
23/* InputWidget supports both fully custom and system-provided text editing.
24 The primary source of complexity is the handling of wrapped text content
25 in the custom text editor. */
26
23#include "inputwidget.h" 27#include "inputwidget.h"
24#include "command.h" 28#include "command.h"
25#include "paint.h" 29#include "paint.h"
@@ -40,10 +44,23 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
40# include "macos.h" 44# include "macos.h"
41#endif 45#endif
42 46
47#if defined (iPlatformAppleMobile)
48# include "ios.h"
49# define LAGRANGE_USE_SYSTEM_TEXT_INPUT 1 /* System-provided UI control almost handles everything. */
50#else
51# define LAGRANGE_USE_SYSTEM_TEXT_INPUT 0
52iDeclareType(SystemTextInput)
53#endif
54
43static const int refreshInterval_InputWidget_ = 512; 55static const int refreshInterval_InputWidget_ = 512;
44static const size_t maxUndo_InputWidget_ = 64; 56static const size_t maxUndo_InputWidget_ = 64;
45static const int unlimitedWidth_InputWidget_ = 1000000; /* TODO: WrapText disables some functionality if maxWidth==0 */ 57static const int unlimitedWidth_InputWidget_ = 1000000; /* TODO: WrapText disables some functionality if maxWidth==0 */
46 58
59static const iChar sensitiveChar_ = 0x25cf; /* black circle */
60static const char * sensitive_ = "\u25cf";
61
62#define minWidth_InputWidget_ (3 * gap_UI)
63
47static void enableEditorKeysInMenus_(iBool enable) { 64static void enableEditorKeysInMenus_(iBool enable) {
48#if defined (iPlatformAppleDesktop) 65#if defined (iPlatformAppleDesktop)
49 enableMenuItemsByKey_MacOS(SDLK_LEFT, KMOD_PRIMARY, enable); 66 enableMenuItemsByKey_MacOS(SDLK_LEFT, KMOD_PRIMARY, enable);
@@ -57,7 +74,10 @@ static void enableEditorKeysInMenus_(iBool enable) {
57#endif 74#endif
58} 75}
59 76
77static void updateMetrics_InputWidget_(iInputWidget *);
78
60/*----------------------------------------------------------------------------------------------*/ 79/*----------------------------------------------------------------------------------------------*/
80#if !LAGRANGE_USE_SYSTEM_TEXT_INPUT
61 81
62iDeclareType(InputLine) 82iDeclareType(InputLine)
63 83
@@ -178,6 +198,8 @@ static void deinit_InputUndo_(iInputUndo *d) {
178 deinit_String(&d->text); 198 deinit_String(&d->text);
179} 199}
180 200
201#endif /* USE_SYSTEM_TEXT_INPUT */
202
181enum iInputWidgetFlag { 203enum iInputWidgetFlag {
182 isSensitive_InputWidgetFlag = iBit(1), 204 isSensitive_InputWidgetFlag = iBit(1),
183 isUrl_InputWidgetFlag = iBit(2), /* affected by decoding preference */ 205 isUrl_InputWidgetFlag = iBit(2), /* affected by decoding preference */
@@ -203,41 +225,54 @@ enum iInputWidgetFlag {
203struct Impl_InputWidget { 225struct Impl_InputWidget {
204 iWidget widget; 226 iWidget widget;
205 enum iInputMode mode; 227 enum iInputMode mode;
228 int font;
206 int inFlags; 229 int inFlags;
207 size_t maxLen; /* characters */ 230 size_t maxLen; /* characters */
208 iArray lines; /* iInputLine[] */
209 iString oldText; /* for restoring if edits cancelled */
210 int lastUpdateWidth;
211 iString srcHint; 231 iString srcHint;
212 iString hint; 232 iString hint;
213 int leftPadding; 233 int leftPadding; /* additional padding between frame and content */
214 int rightPadding; 234 int rightPadding;
235 int minWrapLines, maxWrapLines; /* min/max number of visible lines allowed */
236 iRangei visWrapLines; /* which wrap lines are current visible */
237 iClick click;
238 int wheelAccum;
239 iTextBuf * buffered; /* pre-rendered static text */
240 iInputWidgetValidatorFunc validator;
241 void * validatorContext;
242 iString * backupPath;
243 int backupTimer;
244 iString oldText; /* for restoring if edits cancelled */
245 int lastUpdateWidth;
246 uint32_t lastOverflowScrollTime; /* scrolling to show focused widget */
247 iSystemTextInput *sysCtrl;
248#if LAGRANGE_USE_SYSTEM_TEXT_INPUT
249 iString text;
250#else
251 iArray lines; /* iInputLine[] */
215 iInt2 cursor; /* cursor position: x = byte offset, y = line index */ 252 iInt2 cursor; /* cursor position: x = byte offset, y = line index */
216 iInt2 prevCursor; /* previous cursor position */ 253 iInt2 prevCursor; /* previous cursor position */
217 iRangei visWrapLines; /* which wrap lines are current visible */
218 int minWrapLines, maxWrapLines; /* min/max number of visible lines allowed */
219 iRanges mark; /* TODO: would likely simplify things to use two Int2's for marking; no conversions needed */ 254 iRanges mark; /* TODO: would likely simplify things to use two Int2's for marking; no conversions needed */
220 iRanges initialMark; 255 iRanges initialMark;
221 iArray undoStack; 256 iArray undoStack;
222 int font;
223 iClick click;
224 uint32_t tapStartTime; 257 uint32_t tapStartTime;
225 uint32_t lastTapTime; 258 uint32_t lastTapTime;
226 iInt2 lastTapPos; 259 iInt2 lastTapPos;
227 int tapCount; 260 int tapCount;
228 int wheelAccum;
229 int cursorVis; 261 int cursorVis;
230 uint32_t timer; 262 uint32_t timer;
231 iTextBuf * buffered; /* pre-rendered static text */ 263#endif
232 iInputWidgetValidatorFunc validator;
233 void * validatorContext;
234 iString * backupPath;
235 int backupTimer;
236}; 264};
237 265
238iDefineObjectConstructionArgs(InputWidget, (size_t maxLen), maxLen) 266iDefineObjectConstructionArgs(InputWidget, (size_t maxLen), maxLen)
239 267
240static void updateMetrics_InputWidget_(iInputWidget *); 268static int extraPaddingHeight_InputWidget_(const iInputWidget *d) {
269 if ((isPortraitPhone_App() || deviceType_App() == tablet_AppDeviceType) &&
270 !cmp_String(id_Widget(&d->widget), "url")) {
271 /* Make the tap target more generous. */
272 return 2.5f * gap_UI;
273 }
274 return 1.25f * gap_UI;
275}
241 276
242static void restoreBackup_InputWidget_(iInputWidget *d) { 277static void restoreBackup_InputWidget_(iInputWidget *d) {
243 if (!d->backupPath) return; 278 if (!d->backupPath) return;
@@ -252,17 +287,21 @@ static void saveBackup_InputWidget_(iInputWidget *d) {
252 if (!d->backupPath) return; 287 if (!d->backupPath) return;
253 iFile *f = new_File(d->backupPath); 288 iFile *f = new_File(d->backupPath);
254 if (open_File(f, writeOnly_FileMode | text_FileMode)) { 289 if (open_File(f, writeOnly_FileMode | text_FileMode)) {
290#if LAGRANGE_USE_SYSTEM_TEXT_INPUT
291 write_File(f, utf8_String(&d->text));
292#else
255 iConstForEach(Array, i, &d->lines) { 293 iConstForEach(Array, i, &d->lines) {
256 const iInputLine *line = i.value; 294 const iInputLine *line = i.value;
257 write_File(f, utf8_String(&line->text)); 295 write_File(f, utf8_String(&line->text));
258 } 296 }
259 d->inFlags &= ~needBackup_InputWidgetFlag; 297# if !defined (NDEBUG)
260#if !defined (NDEBUG)
261 iConstForEach(Array, j, &d->lines) { 298 iConstForEach(Array, j, &d->lines) {
262 iAssert(endsWith_String(&((const iInputLine *) j.value)->text, "\n") || 299 iAssert(endsWith_String(&((const iInputLine *) j.value)->text, "\n") ||
263 index_ArrayConstIterator(&j) == size_Array(&d->lines) - 1); 300 index_ArrayConstIterator(&j) == size_Array(&d->lines) - 1);
264 } 301 }
302# endif
265#endif 303#endif
304 d->inFlags &= ~needBackup_InputWidgetFlag;
266 } 305 }
267 iRelease(f); 306 iRelease(f);
268} 307}
@@ -311,6 +350,12 @@ void setBackupFileName_InputWidget(iInputWidget *d, const char *fileName) {
311 restoreBackup_InputWidget_(d); 350 restoreBackup_InputWidget_(d);
312} 351}
313 352
353iLocalDef iInt2 padding_(void) {
354 return init_I2(gap_UI / 2, gap_UI / 2);
355}
356
357#if !LAGRANGE_USE_SYSTEM_TEXT_INPUT
358
314static void clearUndo_InputWidget_(iInputWidget *d) { 359static void clearUndo_InputWidget_(iInputWidget *d) {
315 iForEach(Array, i, &d->undoStack) { 360 iForEach(Array, i, &d->undoStack) {
316 deinit_InputUndo_(i.value); 361 deinit_InputUndo_(i.value);
@@ -318,11 +363,12 @@ static void clearUndo_InputWidget_(iInputWidget *d) {
318 clear_Array(&d->undoStack); 363 clear_Array(&d->undoStack);
319} 364}
320 365
321iLocalDef iInt2 padding_(void) { 366static const iInputLine *line_InputWidget_(const iInputWidget *d, size_t index) {
322 return init_I2(gap_UI / 2, gap_UI / 2); 367 iAssert(!isEmpty_Array(&d->lines));
368 return constAt_Array(&d->lines, index);
323} 369}
324 370
325#define extraPaddingHeight_ (1.25f * gap_UI) 371#endif /* !LAGRANGE_USE_SYSTEM_TEXT_INPUT */
326 372
327static iRect contentBounds_InputWidget_(const iInputWidget *d) { 373static iRect contentBounds_InputWidget_(const iInputWidget *d) {
328 const iWidget *w = constAs_Widget(d); 374 const iWidget *w = constAs_Widget(d);
@@ -332,11 +378,39 @@ static iRect contentBounds_InputWidget_(const iInputWidget *d) {
332 shrink_Rect(&bounds, init_I2(gap_UI * (flags_Widget(w) & tight_WidgetFlag ? 1 : 2), 0)); 378 shrink_Rect(&bounds, init_I2(gap_UI * (flags_Widget(w) & tight_WidgetFlag ? 1 : 2), 0));
333 bounds.pos.y += padding_().y / 2; 379 bounds.pos.y += padding_().y / 2;
334 if (flags_Widget(w) & extraPadding_WidgetFlag) { 380 if (flags_Widget(w) & extraPadding_WidgetFlag) {
335 bounds.pos.y += extraPaddingHeight_ / 2; 381 if (d->sysCtrl && !cmp_String(id_Widget(w), "url")) {
382 /* TODO: This is super hacky: the native UI control would be offset incorrectly.
383 These paddings/offsets are getting a bit ridiculous, should rethink the whole thing.
384 Use the Widget paddings! */
385 bounds.pos.y += 1.25f * gap_UI / 2;
386 }
387 else {
388 bounds.pos.y += extraPaddingHeight_InputWidget_(d) / 2;
389 }
336 } 390 }
337 return bounds; 391 return bounds;
338} 392}
339 393
394static iWrapText wrap_InputWidget_(const iInputWidget *d, int y) {
395#if LAGRANGE_USE_SYSTEM_TEXT_INPUT
396 iUnused(y); /* full text is wrapped always */
397 iRangecc text = range_String(&d->text);
398#else
399 iRangecc text = range_String(&line_InputWidget_(d, y)->text);
400#endif
401 return (iWrapText){
402 .text = text,
403 .maxWidth = d->maxLen == 0 ? iMaxi(minWidth_InputWidget_,
404 width_Rect(contentBounds_InputWidget_(d)))
405 : unlimitedWidth_InputWidget_,
406 .mode =
407 (d->inFlags & isUrl_InputWidgetFlag ? anyCharacter_WrapTextMode : word_WrapTextMode),
408 .overrideChar = (d->inFlags & isSensitive_InputWidgetFlag ? sensitiveChar_ : 0),
409 };
410}
411
412#if !LAGRANGE_USE_SYSTEM_TEXT_INPUT
413
340iLocalDef iBool isLastLine_InputWidget_(const iInputWidget *d, const iInputLine *line) { 414iLocalDef iBool isLastLine_InputWidget_(const iInputWidget *d, const iInputLine *line) {
341 return (const void *) line == constBack_Array(&d->lines); 415 return (const void *) line == constBack_Array(&d->lines);
342} 416}
@@ -350,11 +424,6 @@ static int numWrapLines_InputWidget_(const iInputWidget *d) {
350 return lastLine_InputWidget_(d)->wrapLines.end; 424 return lastLine_InputWidget_(d)->wrapLines.end;
351} 425}
352 426
353static const iInputLine *line_InputWidget_(const iInputWidget *d, size_t index) {
354 iAssert(!isEmpty_Array(&d->lines));
355 return constAt_Array(&d->lines, index);
356}
357
358static const iString *lineString_InputWidget_(const iInputWidget *d, int y) { 427static const iString *lineString_InputWidget_(const iInputWidget *d, int y) {
359 return &line_InputWidget_(d, y)->text; 428 return &line_InputWidget_(d, y)->text;
360} 429}
@@ -455,20 +524,6 @@ static int visLineOffsetY_InputWidget_(const iInputWidget *d) {
455 d->wheelAccum; 524 d->wheelAccum;
456} 525}
457 526
458static const iChar sensitiveChar_ = 0x25cf; /* black circle */
459static const char *sensitive_ = "\u25cf";
460
461static iWrapText wrap_InputWidget_(const iInputWidget *d, int y) {
462 return (iWrapText){
463 .text = range_String(&line_InputWidget_(d, y)->text),
464 .maxWidth = d->maxLen == 0 ? width_Rect(contentBounds_InputWidget_(d))
465 : unlimitedWidth_InputWidget_,
466 .mode =
467 (d->inFlags & isUrl_InputWidgetFlag ? anyCharacter_WrapTextMode : word_WrapTextMode),
468 .overrideChar = (d->inFlags & isSensitive_InputWidgetFlag ? sensitiveChar_ : 0),
469 };
470}
471
472static iRangei visibleLineRange_InputWidget_(const iInputWidget *d) { 527static iRangei visibleLineRange_InputWidget_(const iInputWidget *d) {
473 iRangei vis = { -1, -1 }; 528 iRangei vis = { -1, -1 };
474 /* Determine which lines are in the potentially visible range. */ 529 /* Determine which lines are in the potentially visible range. */
@@ -518,6 +573,9 @@ static iInt2 relativeCursorCoord_InputWidget_(const iInputWidget *d) {
518} 573}
519 574
520static void updateVisible_InputWidget_(iInputWidget *d) { 575static void updateVisible_InputWidget_(iInputWidget *d) {
576 if (width_Widget(d) == 0) {
577 return; /* Nothing to do yet. */
578 }
521 const int totalWraps = numWrapLines_InputWidget_(d); 579 const int totalWraps = numWrapLines_InputWidget_(d);
522 const int visWraps = iClamp(totalWraps, d->minWrapLines, d->maxWrapLines); 580 const int visWraps = iClamp(totalWraps, d->minWrapLines, d->maxWrapLines);
523 /* Resize the height of the editor. */ 581 /* Resize the height of the editor. */
@@ -535,15 +593,24 @@ static void updateVisible_InputWidget_(iInputWidget *d) {
535 else if (cursorY < d->visWrapLines.start) { 593 else if (cursorY < d->visWrapLines.start) {
536 delta = cursorY - d->visWrapLines.start; 594 delta = cursorY - d->visWrapLines.start;
537 } 595 }
596 if (d->visWrapLines.end + delta > totalWraps) {
597 /* Don't scroll past the bottom. */
598 delta = totalWraps - d->visWrapLines.end;
599 }
600 if (d->visWrapLines.start + delta < 0) {
601 /* Don't ever scroll above the top. */
602 delta = -d->visWrapLines.start;
603 }
538 d->visWrapLines.start += delta; 604 d->visWrapLines.start += delta;
539 d->visWrapLines.end += delta; 605 d->visWrapLines.end += delta;
540 iAssert(contains_Range(&d->visWrapLines, cursorY)); 606// iAssert(contains_Range(&d->visWrapLines, cursorY));
541 if (!isFocused_Widget(d) && d->maxWrapLines == 1) { 607 if (!isFocused_Widget(d) && d->maxWrapLines == 1) {
542 d->visWrapLines.start = 0; 608 d->visWrapLines.start = 0;
543 d->visWrapLines.end = 1; 609 d->visWrapLines.end = 1;
544 } 610 }
545// printf("[InputWidget %p] total:%d viswrp:%d cur:%d vis:%d..%d\n", 611// printf("[InputWidget %p] total:%d viswrp:%d cur:%d vis:%d..%d\n",
546// d, totalWraps, visWraps, d->cursor.y, d->visWrapLines.start, d->visWrapLines.end); 612// d, totalWraps, visWraps, d->cursor.y, d->visWrapLines.start, d->visWrapLines.end);
613// fflush(stdout);
547} 614}
548 615
549static void showCursor_InputWidget_(iInputWidget *d) { 616static void showCursor_InputWidget_(iInputWidget *d) {
@@ -551,6 +618,19 @@ static void showCursor_InputWidget_(iInputWidget *d) {
551 updateVisible_InputWidget_(d); 618 updateVisible_InputWidget_(d);
552} 619}
553 620
621#else /* if LAGRANGE_USE_SYSTEM_TEXT_INPUT */
622
623static int visLineOffsetY_InputWidget_(const iInputWidget *d) {
624 return 0; /* offset for the buffered text */
625}
626
627static void updateVisible_InputWidget_(iInputWidget *d) {
628 iUnused(d);
629 /* TODO: Anything to do? */
630}
631
632#endif
633
554static void invalidateBuffered_InputWidget_(iInputWidget *d) { 634static void invalidateBuffered_InputWidget_(iInputWidget *d) {
555 if (d->buffered) { 635 if (d->buffered) {
556 delete_TextBuf(d->buffered); 636 delete_TextBuf(d->buffered);
@@ -563,7 +643,7 @@ static void updateSizeForFixedLength_InputWidget_(iInputWidget *d) {
563 /* Set a fixed size based on maximum possible width of the text. */ 643 /* Set a fixed size based on maximum possible width of the text. */
564 iBlock *content = new_Block(d->maxLen); 644 iBlock *content = new_Block(d->maxLen);
565 fill_Block(content, 'M'); 645 fill_Block(content, 'M');
566 int extraHeight = (flags_Widget(as_Widget(d)) & extraPadding_WidgetFlag ? extraPaddingHeight_ : 0); 646 int extraHeight = (flags_Widget(as_Widget(d)) & extraPadding_WidgetFlag ? extraPaddingHeight_InputWidget_(d) : 0);
567 setFixedSize_Widget( 647 setFixedSize_Widget(
568 as_Widget(d), 648 as_Widget(d),
569 add_I2(measure_Text(d->font, cstr_Block(content)).bounds.size, 649 add_I2(measure_Text(d->font, cstr_Block(content)).bounds.size,
@@ -574,11 +654,16 @@ static void updateSizeForFixedLength_InputWidget_(iInputWidget *d) {
574} 654}
575 655
576static iString *text_InputWidget_(const iInputWidget *d) { 656static iString *text_InputWidget_(const iInputWidget *d) {
657#if LAGRANGE_USE_SYSTEM_TEXT_INPUT
658 return copy_String(&d->text);
659#else
577 iString *text = new_String(); 660 iString *text = new_String();
578 mergeLines_(&d->lines, text); 661 mergeLines_(&d->lines, text);
579 return text; 662 return text;
663#endif
580} 664}
581 665
666#if !LAGRANGE_USE_SYSTEM_TEXT_INPUT
582static size_t length_InputWidget_(const iInputWidget *d) { 667static size_t length_InputWidget_(const iInputWidget *d) {
583 /* Note: `d->length` is kept up to date, so don't call this normally. */ 668 /* Note: `d->length` is kept up to date, so don't call this normally. */
584 size_t len = 0; 669 size_t len = 0;
@@ -589,37 +674,10 @@ static size_t length_InputWidget_(const iInputWidget *d) {
589 return len; 674 return len;
590} 675}
591 676
592static int contentHeight_InputWidget_(const iInputWidget *d) {
593 return size_Range(&d->visWrapLines) * lineHeight_Text(d->font);
594}
595
596static void updateTextInputRect_InputWidget_(const iInputWidget *d) {
597#if !defined (iPlatformAppleMobile)
598 const iRect bounds = bounds_Widget(constAs_Widget(d));
599 SDL_SetTextInputRect(&(SDL_Rect){ bounds.pos.x, bounds.pos.y, bounds.size.x, bounds.size.y });
600#endif
601}
602
603static void updateMetrics_InputWidget_(iInputWidget *d) {
604 iWidget *w = as_Widget(d);
605 updateSizeForFixedLength_InputWidget_(d);
606 /* Caller must arrange the width, but the height is set here. */
607 const int oldHeight = height_Rect(w->rect);
608 w->rect.size.y = contentHeight_InputWidget_(d) + 3.0f * padding_().y; /* TODO: Why 3x? */
609 if (flags_Widget(w) & extraPadding_WidgetFlag) {
610 w->rect.size.y += extraPaddingHeight_;
611 }
612 invalidateBuffered_InputWidget_(d);
613 if (height_Rect(w->rect) != oldHeight) {
614 postCommand_Widget(d, "input.resized");
615 updateTextInputRect_InputWidget_(d);
616 }
617}
618
619static void updateLine_InputWidget_(iInputWidget *d, iInputLine *line) { 677static void updateLine_InputWidget_(iInputWidget *d, iInputLine *line) {
620 iAssert(endsWith_String(&line->text, "\n") || isLastLine_InputWidget_(d, line)); 678 iAssert(endsWith_String(&line->text, "\n") || isLastLine_InputWidget_(d, line));
621 iWrapText wrapText = wrap_InputWidget_(d, indexOf_Array(&d->lines, line)); 679 iWrapText wrapText = wrap_InputWidget_(d, indexOf_Array(&d->lines, line));
622 if (wrapText.maxWidth <= 0) { 680 if (wrapText.maxWidth <= minWidth_InputWidget_) {
623 line->wrapLines.end = line->wrapLines.start + 1; 681 line->wrapLines.end = line->wrapLines.start + 1;
624 return; 682 return;
625 } 683 }
@@ -668,7 +726,10 @@ static uint32_t cursorTimer_(uint32_t interval, void *w) {
668 return interval; 726 return interval;
669} 727}
670 728
671static void startOrStopCursorTimer_InputWidget_(iInputWidget *d, iBool doStart) { 729static void startOrStopCursorTimer_InputWidget_(iInputWidget *d, int doStart) {
730 if (!prefs_App()->blinkingCursor && doStart == 1) {
731 doStart = iFalse;
732 }
672 if (doStart && !d->timer) { 733 if (doStart && !d->timer) {
673 d->timer = SDL_AddTimer(refreshInterval_InputWidget_, cursorTimer_, d); 734 d->timer = SDL_AddTimer(refreshInterval_InputWidget_, cursorTimer_, d);
674 } 735 }
@@ -678,6 +739,66 @@ static void startOrStopCursorTimer_InputWidget_(iInputWidget *d, iBool doStart)
678 } 739 }
679} 740}
680 741
742#else /* using a system-provided text control */
743
744static void updateAllLinesAndResizeHeight_InputWidget_(iInputWidget *d) {
745 /* Rewrap the buffered text and resize accordingly. */
746 iWrapText wt = wrap_InputWidget_(d, 0);
747 /* TODO: Set max lines limit for WrapText. */
748 const int height = measure_WrapText(&wt, d->font).bounds.size.y;
749 /* We use this to store the number wrapped lines for determining widget height. */
750 d->visWrapLines.start = 0;
751 d->visWrapLines.end = iMax(d->minWrapLines,
752 iMin(d->maxWrapLines, height / lineHeight_Text(d->font)));
753 updateMetrics_InputWidget_(d);
754}
755
756#endif
757
758static int contentHeight_InputWidget_(const iInputWidget *d) {
759 const int lineHeight = lineHeight_Text(d->font);
760#if LAGRANGE_USE_SYSTEM_TEXT_INPUT
761 const int minHeight = d->minWrapLines * lineHeight;
762 const int maxHeight = d->maxWrapLines * lineHeight;
763 if (d->sysCtrl) {
764 const int preferred = (preferredHeight_SystemTextInput(d->sysCtrl) + gap_UI) / lineHeight;
765 return iClamp(preferred * lineHeight, minHeight, maxHeight);
766 }
767 if (d->buffered && ~d->inFlags & needUpdateBuffer_InputWidgetFlag) {
768 return iClamp(d->buffered->size.y, minHeight, maxHeight);
769 }
770#endif
771 return (int) size_Range(&d->visWrapLines) * lineHeight;
772}
773
774static void updateTextInputRect_InputWidget_(const iInputWidget *d) {
775#if LAGRANGE_USE_SYSTEM_TEXT_INPUT
776 if (d->sysCtrl) {
777 setRect_SystemTextInput(d->sysCtrl, contentBounds_InputWidget_(d));
778 }
779#endif
780#if !defined (iPlatformAppleMobile)
781 const iRect bounds = bounds_Widget(constAs_Widget(d));
782 SDL_SetTextInputRect(&(SDL_Rect){ bounds.pos.x, bounds.pos.y, bounds.size.x, bounds.size.y });
783#endif
784}
785
786static void updateMetrics_InputWidget_(iInputWidget *d) {
787 iWidget *w = as_Widget(d);
788 updateSizeForFixedLength_InputWidget_(d);
789 /* Caller must arrange the width, but the height is set here. */
790 const int oldHeight = height_Rect(w->rect);
791 w->rect.size.y = contentHeight_InputWidget_(d) + 3.0f * padding_().y; /* TODO: Why 3x? */
792 if (flags_Widget(w) & extraPadding_WidgetFlag) {
793 w->rect.size.y += extraPaddingHeight_InputWidget_(d);
794 }
795 invalidateBuffered_InputWidget_(d);
796 if (height_Rect(w->rect) != oldHeight) {
797 postCommand_Widget(d, "input.resized");
798 updateTextInputRect_InputWidget_(d);
799 }
800}
801
681void init_InputWidget(iInputWidget *d, size_t maxLen) { 802void init_InputWidget(iInputWidget *d, size_t maxLen) {
682 iWidget *w = &d->widget; 803 iWidget *w = &d->widget;
683 init_Widget(w); 804 init_Widget(w);
@@ -687,40 +808,44 @@ void init_InputWidget(iInputWidget *d, size_t maxLen) {
687#if defined (iPlatformMobile) 808#if defined (iPlatformMobile)
688 setFlags_Widget(w, extraPadding_WidgetFlag, iTrue); 809 setFlags_Widget(w, extraPadding_WidgetFlag, iTrue);
689#endif 810#endif
811#if LAGRANGE_USE_SYSTEM_TEXT_INPUT
812 init_String(&d->text);
813#else
690 init_Array(&d->lines, sizeof(iInputLine)); 814 init_Array(&d->lines, sizeof(iInputLine));
815 init_Array(&d->undoStack, sizeof(iInputUndo));
816 d->cursor = zero_I2();
817 d->prevCursor = zero_I2();
818 d->lastTapTime = 0;
819 d->tapCount = 0;
820 d->timer = 0;
821 d->cursorVis = 0;
822 iZap(d->mark);
823 splitToLines_(&iStringLiteral(""), &d->lines);
824#endif
691 init_String(&d->oldText); 825 init_String(&d->oldText);
692 init_Array(&d->lines, sizeof(iInputLine));
693 init_String(&d->srcHint); 826 init_String(&d->srcHint);
694 init_String(&d->hint); 827 init_String(&d->hint);
695 init_Array(&d->undoStack, sizeof(iInputUndo));
696 d->font = uiInput_FontId | alwaysVariableFlag_FontId; 828 d->font = uiInput_FontId | alwaysVariableFlag_FontId;
697 d->leftPadding = 0; 829 d->leftPadding = 0;
698 d->rightPadding = 0; 830 d->rightPadding = 0;
699 d->cursor = zero_I2();
700 d->prevCursor = zero_I2();
701 d->lastUpdateWidth = 0; 831 d->lastUpdateWidth = 0;
702 d->inFlags = eatEscape_InputWidgetFlag | enterKeyEnabled_InputWidgetFlag | 832 d->inFlags = eatEscape_InputWidgetFlag | enterKeyEnabled_InputWidgetFlag |
703 lineBreaksEnabled_InputWidgetFlag | useReturnKeyBehavior_InputWidgetFlag; 833 lineBreaksEnabled_InputWidgetFlag | useReturnKeyBehavior_InputWidgetFlag;
704 // if (deviceType_App() != desktop_AppDeviceType) { 834 // if (deviceType_App() != desktop_AppDeviceType) {
705 // d->inFlags |= enterKeyInsertsLineFeed_InputWidgetFlag; 835 // d->inFlags |= enterKeyInsertsLineFeed_InputWidgetFlag;
706 // } 836 // }
707 iZap(d->mark);
708 setMaxLen_InputWidget(d, maxLen); 837 setMaxLen_InputWidget(d, maxLen);
709 d->visWrapLines.start = 0; 838 d->visWrapLines.start = 0;
710 d->visWrapLines.end = 1; 839 d->visWrapLines.end = 1;
711 d->maxWrapLines = maxLen > 0 ? 1 : 20; /* TODO: Choose maximum dynamically? */ 840 d->maxWrapLines = maxLen > 0 ? 1 : 20; /* TODO: Choose maximum dynamically? */
712 d->minWrapLines = 1; 841 d->minWrapLines = 1;
713 splitToLines_(&iStringLiteral(""), &d->lines);
714 setFlags_Widget(w, fixedHeight_WidgetFlag, iTrue); /* resizes its own height */ 842 setFlags_Widget(w, fixedHeight_WidgetFlag, iTrue); /* resizes its own height */
715 init_Click(&d->click, d, SDL_BUTTON_LEFT); 843 init_Click(&d->click, d, SDL_BUTTON_LEFT);
716 d->lastTapTime = 0;
717 d->tapCount = 0;
718 d->wheelAccum = 0; 844 d->wheelAccum = 0;
719 d->timer = 0;
720 d->cursorVis = 0;
721 d->buffered = NULL; 845 d->buffered = NULL;
722 d->backupPath = NULL; 846 d->backupPath = NULL;
723 d->backupTimer = 0; 847 d->backupTimer = 0;
848 d->sysCtrl = NULL;
724 updateMetrics_InputWidget_(d); 849 updateMetrics_InputWidget_(d);
725} 850}
726 851
@@ -733,26 +858,48 @@ void deinit_InputWidget(iInputWidget *d) {
733 } 858 }
734 delete_String(d->backupPath); 859 delete_String(d->backupPath);
735 d->backupPath = NULL; 860 d->backupPath = NULL;
861 delete_TextBuf(d->buffered);
862 deinit_String(&d->srcHint);
863 deinit_String(&d->hint);
864 deinit_String(&d->oldText);
865#if LAGRANGE_USE_SYSTEM_TEXT_INPUT
866 delete_SystemTextInput(d->sysCtrl);
867 deinit_String(&d->text);
868#else
869 startOrStopCursorTimer_InputWidget_(d, iFalse);
736 clearInputLines_(&d->lines); 870 clearInputLines_(&d->lines);
737 if (isSelected_Widget(d)) { 871 if (isSelected_Widget(d)) {
738 SDL_StopTextInput(); 872 SDL_StopTextInput();
739 enableEditorKeysInMenus_(iTrue); 873 enableEditorKeysInMenus_(iTrue);
740 } 874 }
741 delete_TextBuf(d->buffered);
742 clearUndo_InputWidget_(d); 875 clearUndo_InputWidget_(d);
743 deinit_Array(&d->undoStack); 876 deinit_Array(&d->undoStack);
744 startOrStopCursorTimer_InputWidget_(d, iFalse);
745 deinit_String(&d->srcHint);
746 deinit_String(&d->hint);
747 deinit_String(&d->oldText);
748 deinit_Array(&d->lines); 877 deinit_Array(&d->lines);
878#endif
879}
880
881static iBool isAllowedToInsertNewline_InputWidget_(const iInputWidget *d) {
882 return ~d->inFlags & isSensitive_InputWidgetFlag &&
883 ~d->inFlags & isUrl_InputWidgetFlag &&
884 d->inFlags & lineBreaksEnabled_InputWidgetFlag && d->maxLen == 0;
749} 885}
750 886
887#if LAGRANGE_USE_SYSTEM_TEXT_INPUT
888static void updateAfterVisualOffsetChange_InputWidget_(iInputWidget *d, iRoot *root) {
889 iAssert(as_Widget(d)->root == root);
890 iUnused(root);
891 if (d->sysCtrl) {
892 setRect_SystemTextInput(d->sysCtrl, contentBounds_InputWidget_(d));
893 }
894}
895#endif
896
751void setFont_InputWidget(iInputWidget *d, int fontId) { 897void setFont_InputWidget(iInputWidget *d, int fontId) {
752 d->font = fontId; 898 d->font = fontId;
753 updateMetrics_InputWidget_(d); 899 updateMetrics_InputWidget_(d);
754} 900}
755 901
902#if !LAGRANGE_USE_SYSTEM_TEXT_INPUT
756static void pushUndo_InputWidget_(iInputWidget *d) { 903static void pushUndo_InputWidget_(iInputWidget *d) {
757 iInputUndo undo; 904 iInputUndo undo;
758 init_InputUndo_(&undo, &d->lines, d->cursor); 905 init_InputUndo_(&undo, &d->lines, d->cursor);
@@ -766,7 +913,6 @@ static void pushUndo_InputWidget_(iInputWidget *d) {
766static iBool popUndo_InputWidget_(iInputWidget *d) { 913static iBool popUndo_InputWidget_(iInputWidget *d) {
767 if (!isEmpty_Array(&d->undoStack)) { 914 if (!isEmpty_Array(&d->undoStack)) {
768 iInputUndo *undo = back_Array(&d->undoStack); 915 iInputUndo *undo = back_Array(&d->undoStack);
769 //setCopy_Array(&d->text, &undo->text);
770 splitToLines_(&undo->text, &d->lines); 916 splitToLines_(&undo->text, &d->lines);
771 d->cursor = undo->cursor; 917 d->cursor = undo->cursor;
772 deinit_InputUndo_(undo); 918 deinit_InputUndo_(undo);
@@ -778,6 +924,43 @@ static iBool popUndo_InputWidget_(iInputWidget *d) {
778 return iFalse; 924 return iFalse;
779} 925}
780 926
927iLocalDef iInputLine *cursorLine_InputWidget_(iInputWidget *d) {
928 return at_Array(&d->lines, d->cursor.y);
929}
930
931iLocalDef const iInputLine *constCursorLine_InputWidget_(const iInputWidget *d) {
932 return constAt_Array(&d->lines, d->cursor.y);
933}
934
935iLocalDef iInt2 cursorMax_InputWidget_(const iInputWidget *d) {
936 const int yLast = size_Array(&d->lines) - 1;
937 return init_I2(endX_InputWidget_(d, yLast), yLast);
938}
939
940static size_t cursorToIndex_InputWidget_(const iInputWidget *d, iInt2 pos) {
941 if (pos.y < 0) {
942 return 0;
943 }
944 if (pos.y >= size_Array(&d->lines)) {
945 return lastLine_InputWidget_(d)->range.end;
946 }
947 const iInputLine *line = line_InputWidget_(d, pos.y);
948 pos.x = iClamp(pos.x, 0, endX_InputWidget_(d, pos.y));
949 return line->range.start + pos.x;
950}
951
952static iInt2 indexToCursor_InputWidget_(const iInputWidget *d, size_t index) {
953 /* TODO: The lines are sorted; this could use a binary search. */
954 iConstForEach(Array, i, &d->lines) {
955 const iInputLine *line = i.value;
956 if (contains_Range(&line->range, index)) {
957 return init_I2(index - line->range.start, index_ArrayConstIterator(&i));
958 }
959 }
960 return cursorMax_InputWidget_(d);
961}
962#endif
963
781void setMode_InputWidget(iInputWidget *d, enum iInputMode mode) { 964void setMode_InputWidget(iInputWidget *d, enum iInputMode mode) {
782 d->mode = mode; 965 d->mode = mode;
783} 966}
@@ -876,7 +1059,11 @@ void setContentPadding_InputWidget(iInputWidget *d, int left, int right) {
876} 1059}
877 1060
878iLocalDef iBool isEmpty_InputWidget_(const iInputWidget *d) { 1061iLocalDef iBool isEmpty_InputWidget_(const iInputWidget *d) {
1062#if LAGRANGE_USE_SYSTEM_TEXT_INPUT
1063 return isEmpty_String(&d->text);
1064#else
879 return size_Array(&d->lines) == 1 && isEmpty_String(&line_InputWidget_(d, 0)->text); 1065 return size_Array(&d->lines) == 1 && isEmpty_String(&line_InputWidget_(d, 0)->text);
1066#endif
880} 1067}
881 1068
882static iBool isHintVisible_InputWidget_(const iInputWidget *d) { 1069static iBool isHintVisible_InputWidget_(const iInputWidget *d) {
@@ -890,11 +1077,15 @@ static void updateBuffered_InputWidget_(iInputWidget *d) {
890 } 1077 }
891 else { 1078 else {
892 /* Draw all the potentially visible lines to a buffer. */ 1079 /* Draw all the potentially visible lines to a buffer. */
1080#if LAGRANGE_USE_SYSTEM_TEXT_INPUT
1081 iString *visText = copy_String(&d->text);
1082#else
893 iString *visText = new_String(); 1083 iString *visText = new_String();
894 const iRangei visRange = visibleLineRange_InputWidget_(d); 1084 const iRangei visRange = visibleLineRange_InputWidget_(d);
895 for (int i = visRange.start; i < visRange.end; i++) { 1085 for (int i = visRange.start; i < visRange.end; i++) {
896 append_String(visText, &line_InputWidget_(d, i)->text); 1086 append_String(visText, &line_InputWidget_(d, i)->text);
897 } 1087 }
1088#endif
898 if (d->inFlags & isUrl_InputWidgetFlag) { 1089 if (d->inFlags & isUrl_InputWidgetFlag) {
899 /* Highlight the host name. */ 1090 /* Highlight the host name. */
900 iUrl parts; 1091 iUrl parts;
@@ -912,6 +1103,7 @@ static void updateBuffered_InputWidget_(iInputWidget *d) {
912 } 1103 }
913 } 1104 }
914 iWrapText wt = wrap_InputWidget_(d, 0); 1105 iWrapText wt = wrap_InputWidget_(d, 0);
1106 wt.maxLines = d->maxWrapLines;
915 wt.text = range_String(visText); 1107 wt.text = range_String(visText);
916 const int fg = uiInputText_ColorId; 1108 const int fg = uiInputText_ColorId;
917 d->buffered = new_TextBuf(&wt, d->font, fg); 1109 d->buffered = new_TextBuf(&wt, d->font, fg);
@@ -920,25 +1112,18 @@ static void updateBuffered_InputWidget_(iInputWidget *d) {
920 d->inFlags &= ~needUpdateBuffer_InputWidgetFlag; 1112 d->inFlags &= ~needUpdateBuffer_InputWidgetFlag;
921} 1113}
922 1114
923iLocalDef iInputLine *cursorLine_InputWidget_(iInputWidget *d) {
924 return at_Array(&d->lines, d->cursor.y);
925}
926
927iLocalDef const iInputLine *constCursorLine_InputWidget_(const iInputWidget *d) {
928 return constAt_Array(&d->lines, d->cursor.y);
929}
930
931iLocalDef iInt2 cursorMax_InputWidget_(const iInputWidget *d) {
932 const int yLast = size_Array(&d->lines) - 1;
933 return init_I2(endX_InputWidget_(d, yLast), yLast);
934}
935
936void setText_InputWidget(iInputWidget *d, const iString *text) { 1115void setText_InputWidget(iInputWidget *d, const iString *text) {
937 if (!d) return; 1116 if (!d) return;
938 if (d->inFlags & isUrl_InputWidgetFlag) { 1117 if (d->inFlags & isUrl_InputWidgetFlag) {
939 /* If user wants URLs encoded, also Punycode the domain. */ 1118 if (prefs_App()->decodeUserVisibleURLs) {
940 if (!prefs_App()->decodeUserVisibleURLs) { 1119 iString *enc = collect_String(copy_String(text));
1120 urlDecodePath_String(enc);
1121 text = enc;
1122 }
1123 else {
1124 /* The user wants URLs encoded, also Punycode the domain. */
941 iString *enc = collect_String(copy_String(text)); 1125 iString *enc = collect_String(copy_String(text));
1126 urlEncodePath_String(enc);
942 /* Prevent address bar spoofing (mentioned as IDN homograph attack in 1127 /* Prevent address bar spoofing (mentioned as IDN homograph attack in
943 https://github.com/skyjake/lagrange/issues/73) */ 1128 https://github.com/skyjake/lagrange/issues/73) */
944 punyEncodeUrlHost_String(enc); 1129 punyEncodeUrlHost_String(enc);
@@ -949,9 +1134,10 @@ void setText_InputWidget(iInputWidget *d, const iString *text) {
949 text = omitDefaultScheme_(collect_String(copy_String(text))); 1134 text = omitDefaultScheme_(collect_String(copy_String(text)));
950 } 1135 }
951 } 1136 }
952 clearUndo_InputWidget_(d);
953 iString *nfcText = collect_String(copy_String(text)); 1137 iString *nfcText = collect_String(copy_String(text));
954 normalize_String(nfcText); 1138 normalize_String(nfcText);
1139#if !LAGRANGE_USE_SYSTEM_TEXT_INPUT
1140 clearUndo_InputWidget_(d);
955 splitToLines_(nfcText, &d->lines); 1141 splitToLines_(nfcText, &d->lines);
956 iAssert(!isEmpty_Array(&d->lines)); 1142 iAssert(!isEmpty_Array(&d->lines));
957 iForEach(Array, i, &d->lines) { 1143 iForEach(Array, i, &d->lines) {
@@ -962,12 +1148,23 @@ void setText_InputWidget(iInputWidget *d, const iString *text) {
962 if (!isFocused_Widget(d)) { 1148 if (!isFocused_Widget(d)) {
963 iZap(d->mark); 1149 iZap(d->mark);
964 } 1150 }
1151#else
1152 set_String(&d->text, nfcText);
1153 if (d->sysCtrl) {
1154 setText_SystemTextInput(d->sysCtrl, nfcText, iTrue);
1155 }
1156 else {
1157 updateAllLinesAndResizeHeight_InputWidget_(d); /* need to know the new height */
1158 }
1159#endif
965 if (!isFocused_Widget(d)) { 1160 if (!isFocused_Widget(d)) {
966 d->inFlags |= needUpdateBuffer_InputWidgetFlag; 1161 d->inFlags |= needUpdateBuffer_InputWidgetFlag;
967 } 1162 }
968 updateVisible_InputWidget_(d); 1163 updateVisible_InputWidget_(d);
969 updateMetrics_InputWidget_(d); 1164 updateMetrics_InputWidget_(d);
970 refresh_Widget(as_Widget(d)); 1165 if (!d->sysCtrl) {
1166 refresh_Widget(as_Widget(d));
1167 }
971} 1168}
972 1169
973void setTextCStr_InputWidget(iInputWidget *d, const char *cstr) { 1170void setTextCStr_InputWidget(iInputWidget *d, const char *cstr) {
@@ -976,38 +1173,39 @@ void setTextCStr_InputWidget(iInputWidget *d, const char *cstr) {
976 delete_String(str); 1173 delete_String(str);
977} 1174}
978 1175
979static size_t cursorToIndex_InputWidget_(const iInputWidget *d, iInt2 pos) {
980 if (pos.y < 0) {
981 return 0;
982 }
983 if (pos.y >= size_Array(&d->lines)) {
984 return lastLine_InputWidget_(d)->range.end;
985 }
986 const iInputLine *line = line_InputWidget_(d, pos.y);
987 pos.x = iClamp(pos.x, 0, endX_InputWidget_(d, pos.y));
988 return line->range.start + pos.x;
989}
990
991static iInt2 indexToCursor_InputWidget_(const iInputWidget *d, size_t index) {
992 /* TODO: The lines are sorted; this could use a binary search. */
993 iConstForEach(Array, i, &d->lines) {
994 const iInputLine *line = i.value;
995 if (contains_Range(&line->range, index)) {
996 return init_I2(index - line->range.start, index_ArrayConstIterator(&i));
997 }
998 }
999 return cursorMax_InputWidget_(d);
1000}
1001
1002void selectAll_InputWidget(iInputWidget *d) { 1176void selectAll_InputWidget(iInputWidget *d) {
1177#if LAGRANGE_USE_SYSTEM_TEXT_INPUT
1178 if (d->sysCtrl) {
1179 selectAll_SystemTextInput(d->sysCtrl);
1180 }
1181#else
1003 d->mark = (iRanges){ 0, lastLine_InputWidget_(d)->range.end }; 1182 d->mark = (iRanges){ 0, lastLine_InputWidget_(d)->range.end };
1004 refresh_Widget(as_Widget(d)); 1183 refresh_Widget(as_Widget(d));
1184#endif
1185}
1186
1187void validate_InputWidget(iInputWidget *d) {
1188 if (d->validator) {
1189 d->validator(d, d->validatorContext); /* this may change the contents */
1190 }
1005} 1191}
1006 1192
1007iLocalDef iBool isEditing_InputWidget_(const iInputWidget *d) { 1193iLocalDef iBool isEditing_InputWidget_(const iInputWidget *d) {
1008 return (flags_Widget(constAs_Widget(d)) & selected_WidgetFlag) != 0; 1194 return (flags_Widget(constAs_Widget(d)) & selected_WidgetFlag) != 0;
1009} 1195}
1010 1196
1197static void contentsWereChanged_InputWidget_(iInputWidget *);
1198
1199#if LAGRANGE_USE_SYSTEM_TEXT_INPUT
1200void systemInputChanged_InputWidget_(iSystemTextInput *sysCtrl, void *widget) {
1201 iInputWidget *d = widget;
1202 set_String(&d->text, text_SystemTextInput(sysCtrl));
1203 restartBackupTimer_InputWidget_(d);
1204 contentsWereChanged_InputWidget_(d);
1205 updateMetrics_InputWidget_(d);
1206}
1207#endif
1208
1011void begin_InputWidget(iInputWidget *d) { 1209void begin_InputWidget(iInputWidget *d) {
1012 iWidget *w = as_Widget(d); 1210 iWidget *w = as_Widget(d);
1013 if (isEditing_InputWidget_(d)) { 1211 if (isEditing_InputWidget_(d)) {
@@ -1016,6 +1214,29 @@ void begin_InputWidget(iInputWidget *d) {
1016 } 1214 }
1017 invalidateBuffered_InputWidget_(d); 1215 invalidateBuffered_InputWidget_(d);
1018 setFlags_Widget(w, hidden_WidgetFlag | disabled_WidgetFlag, iFalse); 1216 setFlags_Widget(w, hidden_WidgetFlag | disabled_WidgetFlag, iFalse);
1217 setFlags_Widget(w, selected_WidgetFlag, iTrue);
1218 d->inFlags &= ~enterPressed_InputWidgetFlag;
1219#if LAGRANGE_USE_SYSTEM_TEXT_INPUT
1220 set_String(&d->oldText, &d->text);
1221 d->sysCtrl = new_SystemTextInput(
1222 contentBounds_InputWidget_(d),
1223 (d->maxWrapLines > 1 ? multiLine_SystemTextInputFlags : 0) |
1224 (d->inFlags & isUrl_InputWidgetFlag ? (disableAutocorrect_SystemTextInputFlag |
1225 disableAutocapitalize_SystemTextInputFlag)
1226 : 0) |
1227 /* widget-specific tweaks (hacks) */
1228 (!cmp_String(id_Widget(w), "url") ? returnGo_SystemTextInputFlags : 0) |
1229 (!cmp_String(id_Widget(w), "upload.text") ? extraPadding_SystemTextInputFlag : 0) |
1230 (flags_Widget(w) & alignRight_WidgetFlag ? alignRight_SystemTextInputFlag : 0) |
1231 (isAllowedToInsertNewline_InputWidget_(d) ? insertNewlines_SystemTextInputFlag : 0) |
1232 (d->inFlags & selectAllOnFocus_InputWidgetFlag ? selectAll_SystemTextInputFlags : 0));
1233 setFont_SystemTextInput(d->sysCtrl, d->font);
1234 setText_SystemTextInput(d->sysCtrl, &d->oldText, iFalse);
1235 setTextChangedFunc_SystemTextInput(d->sysCtrl, systemInputChanged_InputWidget_, d);
1236 iConnect(Root, w->root, visualOffsetsChanged, d, updateAfterVisualOffsetChange_InputWidget_);
1237 updateTextInputRect_InputWidget_(d);
1238 updateMetrics_InputWidget_(d);
1239#else
1019 mergeLines_(&d->lines, &d->oldText); 1240 mergeLines_(&d->lines, &d->oldText);
1020 if (d->mode == overwrite_InputMode) { 1241 if (d->mode == overwrite_InputMode) {
1021 d->cursor = zero_I2(); 1242 d->cursor = zero_I2();
@@ -1025,11 +1246,9 @@ void begin_InputWidget(iInputWidget *d) {
1025 d->cursor.x = iMin(d->cursor.x, cursorLine_InputWidget_(d)->range.end); 1246 d->cursor.x = iMin(d->cursor.x, cursorLine_InputWidget_(d)->range.end);
1026 } 1247 }
1027 SDL_StartTextInput(); 1248 SDL_StartTextInput();
1028 setFlags_Widget(w, selected_WidgetFlag, iTrue);
1029 showCursor_InputWidget_(d); 1249 showCursor_InputWidget_(d);
1030 refresh_Widget(w); 1250 refresh_Widget(w);
1031 startOrStopCursorTimer_InputWidget_(d, iTrue); 1251 startOrStopCursorTimer_InputWidget_(d, iTrue);
1032 d->inFlags &= ~enterPressed_InputWidgetFlag;
1033 if (d->inFlags & selectAllOnFocus_InputWidgetFlag) { 1252 if (d->inFlags & selectAllOnFocus_InputWidgetFlag) {
1034 d->mark = (iRanges){ 0, lastLine_InputWidget_(d)->range.end }; 1253 d->mark = (iRanges){ 0, lastLine_InputWidget_(d)->range.end };
1035 d->cursor = cursorMax_InputWidget_(d); 1254 d->cursor = cursorMax_InputWidget_(d);
@@ -1040,6 +1259,7 @@ void begin_InputWidget(iInputWidget *d) {
1040 enableEditorKeysInMenus_(iFalse); 1259 enableEditorKeysInMenus_(iFalse);
1041 updateTextInputRect_InputWidget_(d); 1260 updateTextInputRect_InputWidget_(d);
1042 updateVisible_InputWidget_(d); 1261 updateVisible_InputWidget_(d);
1262#endif
1043} 1263}
1044 1264
1045void end_InputWidget(iInputWidget *d, iBool accept) { 1265void end_InputWidget(iInputWidget *d, iBool accept) {
@@ -1048,15 +1268,29 @@ void end_InputWidget(iInputWidget *d, iBool accept) {
1048 /* Was not active. */ 1268 /* Was not active. */
1049 return; 1269 return;
1050 } 1270 }
1051 enableEditorKeysInMenus_(iTrue); 1271#if LAGRANGE_USE_SYSTEM_TEXT_INPUT
1272 if (d->sysCtrl) {
1273 iDisconnect(Root, w->root, visualOffsetsChanged, d, updateAfterVisualOffsetChange_InputWidget_);
1274 if (accept) {
1275 set_String(&d->text, text_SystemTextInput(d->sysCtrl));
1276 }
1277 else {
1278 set_String(&d->text, &d->oldText);
1279 }
1280 delete_SystemTextInput(d->sysCtrl);
1281 d->sysCtrl = NULL;
1282 }
1283#else
1052 if (!accept) { 1284 if (!accept) {
1053 /* Overwrite the edited lines. */ 1285 /* Overwrite the edited lines. */
1054 splitToLines_(&d->oldText, &d->lines); 1286 splitToLines_(&d->oldText, &d->lines);
1055 } 1287 }
1056 d->inFlags |= needUpdateBuffer_InputWidgetFlag; 1288 SDL_StopTextInput();
1289 enableEditorKeysInMenus_(iTrue);
1057 d->inFlags &= ~isMarking_InputWidgetFlag; 1290 d->inFlags &= ~isMarking_InputWidgetFlag;
1058 startOrStopCursorTimer_InputWidget_(d, iFalse); 1291 startOrStopCursorTimer_InputWidget_(d, iFalse);
1059 SDL_StopTextInput(); 1292#endif
1293 d->inFlags |= needUpdateBuffer_InputWidgetFlag;
1060 setFlags_Widget(w, selected_WidgetFlag | keepOnTop_WidgetFlag | touchDrag_WidgetFlag, iFalse); 1294 setFlags_Widget(w, selected_WidgetFlag | keepOnTop_WidgetFlag | touchDrag_WidgetFlag, iFalse);
1061 const char *id = cstr_String(id_Widget(as_Widget(d))); 1295 const char *id = cstr_String(id_Widget(as_Widget(d)));
1062 if (!*id) id = "_"; 1296 if (!*id) id = "_";
@@ -1068,6 +1302,7 @@ void end_InputWidget(iInputWidget *d, iBool accept) {
1068 accept ? 1 : 0); 1302 accept ? 1 : 0);
1069} 1303}
1070 1304
1305#if !LAGRANGE_USE_SYSTEM_TEXT_INPUT
1071static void textOfLinesWasChanged_InputWidget_(iInputWidget *d, iRangei lineRange) { 1306static void textOfLinesWasChanged_InputWidget_(iInputWidget *d, iRangei lineRange) {
1072 for (int i = lineRange.start; i < lineRange.end; i++) { 1307 for (int i = lineRange.start; i < lineRange.end; i++) {
1073 updateLine_InputWidget_(d, at_Array(&d->lines, i)); 1308 updateLine_InputWidget_(d, at_Array(&d->lines, i));
@@ -1201,27 +1436,6 @@ static iBool moveCursorByLine_InputWidget_(iInputWidget *d, int dir, int horiz)
1201 return iTrue; 1436 return iTrue;
1202} 1437}
1203 1438
1204void setSensitiveContent_InputWidget(iInputWidget *d, iBool isSensitive) {
1205 iChangeFlags(d->inFlags, isSensitive_InputWidgetFlag, isSensitive);
1206}
1207
1208void setUrlContent_InputWidget(iInputWidget *d, iBool isUrl) {
1209 iChangeFlags(d->inFlags, isUrl_InputWidgetFlag, isUrl);
1210 d->inFlags |= needUpdateBuffer_InputWidgetFlag;
1211}
1212
1213void setSelectAllOnFocus_InputWidget(iInputWidget *d, iBool selectAllOnFocus) {
1214 iChangeFlags(d->inFlags, selectAllOnFocus_InputWidgetFlag, selectAllOnFocus);
1215}
1216
1217void setNotifyEdits_InputWidget(iInputWidget *d, iBool notifyEdits) {
1218 iChangeFlags(d->inFlags, notifyEdits_InputWidgetFlag, notifyEdits);
1219}
1220
1221void setEatEscape_InputWidget(iInputWidget *d, iBool eatEscape) {
1222 iChangeFlags(d->inFlags, eatEscape_InputWidgetFlag, eatEscape);
1223}
1224
1225static iRanges mark_InputWidget_(const iInputWidget *d) { 1439static iRanges mark_InputWidget_(const iInputWidget *d) {
1226 iRanges m = { iMin(d->mark.start, d->mark.end), iMax(d->mark.start, d->mark.end) }; 1440 iRanges m = { iMin(d->mark.start, d->mark.end), iMax(d->mark.start, d->mark.end) };
1227 const iInputLine *last = lastLine_InputWidget_(d); 1441 const iInputLine *last = lastLine_InputWidget_(d);
@@ -1230,15 +1444,6 @@ static iRanges mark_InputWidget_(const iInputWidget *d) {
1230 return m; 1444 return m;
1231} 1445}
1232 1446
1233static void contentsWereChanged_InputWidget_(iInputWidget *d) {
1234 if (d->validator) {
1235 d->validator(d, d->validatorContext); /* this may change the contents */
1236 }
1237 if (d->inFlags & notifyEdits_InputWidgetFlag) {
1238 postCommand_Widget(d, "input.edited id:%s", cstr_String(id_Widget(constAs_Widget(d))));
1239 }
1240}
1241
1242static void deleteIndexRange_InputWidget_(iInputWidget *d, iRanges deleted) { 1447static void deleteIndexRange_InputWidget_(iInputWidget *d, iRanges deleted) {
1243 size_t firstModified = iInvalidPos; 1448 size_t firstModified = iInvalidPos;
1244 restartBackupTimer_InputWidget_(d); 1449 restartBackupTimer_InputWidget_(d);
@@ -1364,7 +1569,7 @@ static iInt2 coordCursor_InputWidget_(const iInputWidget *d, iInt2 coord) {
1364// return cursorMax_InputWidget_(d); 1569// return cursorMax_InputWidget_(d);
1365// } 1570// }
1366 iWrapText wrapText = { 1571 iWrapText wrapText = {
1367 .maxWidth = d->maxLen == 0 ? width_Rect(bounds) : unlimitedWidth_InputWidget_, 1572 .maxWidth = d->maxLen == 0 ? iMaxi(minWidth_InputWidget_, width_Rect(bounds)) : unlimitedWidth_InputWidget_,
1368 .mode = (d->inFlags & isUrl_InputWidgetFlag ? anyCharacter_WrapTextMode : word_WrapTextMode), 1573 .mode = (d->inFlags & isUrl_InputWidgetFlag ? anyCharacter_WrapTextMode : word_WrapTextMode),
1369 .hitPoint = relCoord, 1574 .hitPoint = relCoord,
1370 .overrideChar = (d->inFlags & isSensitive_InputWidgetFlag ? sensitiveChar_ : 0), 1575 .overrideChar = (d->inFlags & isSensitive_InputWidgetFlag ? sensitiveChar_ : 0),
@@ -1447,6 +1652,40 @@ static void extendRange_InputWidget_(iInputWidget *d, size_t *index, int dir) {
1447 *index = cursorToIndex_InputWidget_(d, pos); 1652 *index = cursorToIndex_InputWidget_(d, pos);
1448} 1653}
1449 1654
1655static void lineTextWasChanged_InputWidget_(iInputWidget *d, iInputLine *line) {
1656 const int y = indexOf_Array(&d->lines, line);
1657 textOfLinesWasChanged_InputWidget_(d, (iRangei){ y, y + 1 });
1658}
1659#endif
1660
1661void setSensitiveContent_InputWidget(iInputWidget *d, iBool isSensitive) {
1662 iChangeFlags(d->inFlags, isSensitive_InputWidgetFlag, isSensitive);
1663}
1664
1665void setUrlContent_InputWidget(iInputWidget *d, iBool isUrl) {
1666 iChangeFlags(d->inFlags, isUrl_InputWidgetFlag, isUrl);
1667 d->inFlags |= needUpdateBuffer_InputWidgetFlag;
1668}
1669
1670void setSelectAllOnFocus_InputWidget(iInputWidget *d, iBool selectAllOnFocus) {
1671 iChangeFlags(d->inFlags, selectAllOnFocus_InputWidgetFlag, selectAllOnFocus);
1672}
1673
1674void setNotifyEdits_InputWidget(iInputWidget *d, iBool notifyEdits) {
1675 iChangeFlags(d->inFlags, notifyEdits_InputWidgetFlag, notifyEdits);
1676}
1677
1678void setEatEscape_InputWidget(iInputWidget *d, iBool eatEscape) {
1679 iChangeFlags(d->inFlags, eatEscape_InputWidgetFlag, eatEscape);
1680}
1681
1682static void contentsWereChanged_InputWidget_(iInputWidget *d) {
1683 validate_InputWidget(d);
1684 if (d->inFlags & notifyEdits_InputWidgetFlag) {
1685 postCommand_Widget(d, "input.edited id:%s", cstr_String(id_Widget(constAs_Widget(d))));
1686 }
1687}
1688
1450static iRect bounds_InputWidget_(const iInputWidget *d) { 1689static iRect bounds_InputWidget_(const iInputWidget *d) {
1451 const iWidget *w = constAs_Widget(d); 1690 const iWidget *w = constAs_Widget(d);
1452 iRect bounds = bounds_Widget(w); 1691 iRect bounds = bounds_Widget(w);
@@ -1456,7 +1695,7 @@ static iRect bounds_InputWidget_(const iInputWidget *d) {
1456 /* There may be more visible lines than fits in the widget bounds. */ 1695 /* There may be more visible lines than fits in the widget bounds. */
1457 bounds.size.y = contentHeight_InputWidget_(d) + 3 * padding_().y; 1696 bounds.size.y = contentHeight_InputWidget_(d) + 3 * padding_().y;
1458 if (w->flags & extraPadding_WidgetFlag) { 1697 if (w->flags & extraPadding_WidgetFlag) {
1459 bounds.size.y += extraPaddingHeight_; 1698 bounds.size.y += extraPaddingHeight_InputWidget_(d);
1460 } 1699 }
1461 return bounds; 1700 return bounds;
1462} 1701}
@@ -1465,11 +1704,6 @@ static iBool contains_InputWidget_(const iInputWidget *d, iInt2 coord) {
1465 return contains_Rect(bounds_InputWidget_(d), coord); 1704 return contains_Rect(bounds_InputWidget_(d), coord);
1466} 1705}
1467 1706
1468static void lineTextWasChanged_InputWidget_(iInputWidget *d, iInputLine *line) {
1469 const int y = indexOf_Array(&d->lines, line);
1470 textOfLinesWasChanged_InputWidget_(d, (iRangei){ y, y + 1 });
1471}
1472
1473static iBool isArrowUpDownConsumed_InputWidget_(const iInputWidget *d) { 1707static iBool isArrowUpDownConsumed_InputWidget_(const iInputWidget *d) {
1474 return d->maxWrapLines > 1; 1708 return d->maxWrapLines > 1;
1475} 1709}
@@ -1494,6 +1728,7 @@ enum iEventResult {
1494 true_EventResult = 2, /* event was processed and should not be passed on */ 1728 true_EventResult = 2, /* event was processed and should not be passed on */
1495}; 1729};
1496 1730
1731#if !LAGRANGE_USE_SYSTEM_TEXT_INPUT
1497static void markWordAtCursor_InputWidget_(iInputWidget *d) { 1732static void markWordAtCursor_InputWidget_(iInputWidget *d) {
1498 d->mark.start = d->mark.end = cursorToIndex_InputWidget_(d, d->cursor); 1733 d->mark.start = d->mark.end = cursorToIndex_InputWidget_(d, d->cursor);
1499 extendRange_InputWidget_(d, &d->mark.start, -1); 1734 extendRange_InputWidget_(d, &d->mark.start, -1);
@@ -1510,8 +1745,10 @@ static void showClipMenu_(iInt2 coord) {
1510 openMenuFlags_Widget(clipMenu, coord, iFalse); 1745 openMenuFlags_Widget(clipMenu, coord, iFalse);
1511 } 1746 }
1512} 1747}
1748#endif
1513 1749
1514static enum iEventResult processPointerEvents_InputWidget_(iInputWidget *d, const SDL_Event *ev) { 1750static enum iEventResult processPointerEvents_InputWidget_(iInputWidget *d, const SDL_Event *ev) {
1751#if !LAGRANGE_USE_SYSTEM_TEXT_INPUT
1515 iWidget *w = as_Widget(d); 1752 iWidget *w = as_Widget(d);
1516 if (ev->type == SDL_MOUSEMOTION && (isHover_Widget(d) || flags_Widget(w) & keepOnTop_WidgetFlag)) { 1753 if (ev->type == SDL_MOUSEMOTION && (isHover_Widget(d) || flags_Widget(w) & keepOnTop_WidgetFlag)) {
1517 const iInt2 coord = init_I2(ev->motion.x, ev->motion.y); 1754 const iInt2 coord = init_I2(ev->motion.x, ev->motion.y);
@@ -1585,9 +1822,11 @@ static enum iEventResult processPointerEvents_InputWidget_(iInputWidget *d, cons
1585 return true_EventResult; 1822 return true_EventResult;
1586 } 1823 }
1587 } 1824 }
1825#endif
1588 return ignored_EventResult; 1826 return ignored_EventResult;
1589} 1827}
1590 1828
1829#if !LAGRANGE_USE_SYSTEM_TEXT_INPUT
1591static iInt2 touchCoordCursor_InputWidget_(const iInputWidget *d, iInt2 coord) { 1830static iInt2 touchCoordCursor_InputWidget_(const iInputWidget *d, iInt2 coord) {
1592 /* Clamp to the bounds so the cursor doesn't wrap at the ends. */ 1831 /* Clamp to the bounds so the cursor doesn't wrap at the ends. */
1593 iRect bounds = shrunk_Rect(contentBounds_InputWidget_(d), one_I2()); 1832 iRect bounds = shrunk_Rect(contentBounds_InputWidget_(d), one_I2());
@@ -1609,9 +1848,11 @@ static int distanceToPos_InputWidget_(const iInputWidget *d, iInt2 uiCoord, iInt
1609 } 1848 }
1610 return dist_I2(addY_I2(winCoord, lineHeight_Text(d->font) / 2), uiCoord); 1849 return dist_I2(addY_I2(winCoord, lineHeight_Text(d->font) / 2), uiCoord);
1611} 1850}
1851#endif
1612 1852
1613static enum iEventResult processTouchEvents_InputWidget_(iInputWidget *d, const SDL_Event *ev) { 1853static enum iEventResult processTouchEvents_InputWidget_(iInputWidget *d, const SDL_Event *ev) {
1614 iWidget *w = as_Widget(d); 1854 iWidget *w = as_Widget(d);
1855#if !LAGRANGE_USE_SYSTEM_TEXT_INPUT
1615 /* 1856 /*
1616 + first tap to focus & select all/place cursor 1857 + first tap to focus & select all/place cursor
1617 + focused tap to place cursor 1858 + focused tap to place cursor
@@ -1853,9 +2094,22 @@ static enum iEventResult processTouchEvents_InputWidget_(iInputWidget *d, const
1853// /* Eat all mouse clicks on the widget. */ 2094// /* Eat all mouse clicks on the widget. */
1854// return true_EventResult; 2095// return true_EventResult;
1855// } 2096// }
2097#else
2098 /* Just a tap to activate the system-provided text input control. */
2099 switch (processEvent_Click(&d->click, ev)) {
2100 case none_ClickResult:
2101 break;
2102 case started_ClickResult:
2103 setFocus_Widget(w);
2104 return true_EventResult;
2105 default:
2106 return true_EventResult;
2107 }
2108#endif
1856 return ignored_EventResult; 2109 return ignored_EventResult;
1857} 2110}
1858 2111
2112#if !LAGRANGE_USE_SYSTEM_TEXT_INPUT
1859static void clampWheelAccum_InputWidget_(iInputWidget *d, int wheel) { 2113static void clampWheelAccum_InputWidget_(iInputWidget *d, int wheel) {
1860 if (wheel > 0 && d->visWrapLines.start == 0) { 2114 if (wheel > 0 && d->visWrapLines.start == 0) {
1861 d->wheelAccum = 0; 2115 d->wheelAccum = 0;
@@ -1866,12 +2120,41 @@ static void clampWheelAccum_InputWidget_(iInputWidget *d, int wheel) {
1866 refresh_Widget(d); 2120 refresh_Widget(d);
1867 } 2121 }
1868} 2122}
2123#endif
2124
2125static void overflowScrollToKeepVisible_InputWidget_(iAny *widget) {
2126 iInputWidget *d = widget;
2127 iWidget *w = as_Widget(d);
2128 if (!isFocused_Widget(w) || isAffectedByVisualOffset_Widget(w)) {
2129 return;
2130 }
2131 iRect rect = boundsWithoutVisualOffset_Widget(w);
2132 iRect visible = visibleRect_Root(w->root);
2133 const uint32_t nowTime = SDL_GetTicks();
2134 const double elapsed = (nowTime - d->lastOverflowScrollTime) / 1000.0;
2135 int dist = bottom_Rect(rect) + gap_UI - bottom_Rect(visible);
2136 const int step = iRound(10 * dist * elapsed);
2137 if (step > 0) {
2138 iWidget *scrollable = findOverflowScrollable_Widget(w);
2139 if (scrollable) {
2140 scrollOverflow_Widget(scrollable, -iClamp(step, 1, dist));
2141 d->lastOverflowScrollTime = nowTime;
2142 }
2143 }
2144 if (dist > 0) {
2145 addTicker_App(overflowScrollToKeepVisible_InputWidget_, widget);
2146 }
2147}
1869 2148
1870static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) { 2149static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) {
1871 iWidget *w = as_Widget(d); 2150 iWidget *w = as_Widget(d);
1872 /* Resize according to width immediately. */ 2151 /* Resize according to width immediately. */
1873 if (d->lastUpdateWidth != w->rect.size.x) { 2152 if (d->lastUpdateWidth != w->rect.size.x) {
1874 d->inFlags |= needUpdateBuffer_InputWidgetFlag; 2153 d->inFlags |= needUpdateBuffer_InputWidgetFlag;
2154 if (contentBounds_InputWidget_(d).size.x < minWidth_InputWidget_) {
2155 setFocus_Widget(NULL);
2156 return iFalse;
2157 }
1875 if (d->inFlags & isUrl_InputWidgetFlag) { 2158 if (d->inFlags & isUrl_InputWidgetFlag) {
1876 /* Restore/omit the default scheme if necessary. */ 2159 /* Restore/omit the default scheme if necessary. */
1877 setText_InputWidget(d, text_InputWidget(d)); 2160 setText_InputWidget(d, text_InputWidget(d));
@@ -1879,15 +2162,24 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) {
1879 updateAllLinesAndResizeHeight_InputWidget_(d); 2162 updateAllLinesAndResizeHeight_InputWidget_(d);
1880 d->lastUpdateWidth = w->rect.size.x; 2163 d->lastUpdateWidth = w->rect.size.x;
1881 } 2164 }
1882 if (isCommand_Widget(w, ev, "focus.gained")) { 2165#if LAGRANGE_USE_SYSTEM_TEXT_INPUT
1883 begin_InputWidget(d); 2166 if (isResize_UserEvent(ev)) {
2167 if (d->sysCtrl) {
2168 updateAfterVisualOffsetChange_InputWidget_(d, w->root);
2169 }
2170 }
2171#endif
2172 if (deviceType_App() != desktop_AppDeviceType && isCommand_UserEvent(ev, "menu.opened")) {
2173 setFocus_Widget(NULL);
1884 return iFalse; 2174 return iFalse;
1885 } 2175 }
1886 else if (isEditing_InputWidget_(d) && (isCommand_UserEvent(ev, "window.focus.lost") || 2176 if (isCommand_Widget(w, ev, "focus.gained")) {
1887 isCommand_UserEvent(ev, "window.focus.gained"))) { 2177 if (contentBounds_InputWidget_(d).size.x < minWidth_InputWidget_) {
1888 startOrStopCursorTimer_InputWidget_(d, isCommand_UserEvent(ev, "window.focus.gained")); 2178 setFocus_Widget(NULL);
1889 d->cursorVis = 1; 2179 }
1890 refresh_Widget(d); 2180 else {
2181 begin_InputWidget(d);
2182 }
1891 return iFalse; 2183 return iFalse;
1892 } 2184 }
1893 else if (isCommand_UserEvent(ev, "keyroot.changed")) { 2185 else if (isCommand_UserEvent(ev, "keyroot.changed")) {
@@ -1902,11 +2194,29 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) {
1902 end_InputWidget(d, iTrue); 2194 end_InputWidget(d, iTrue);
1903 return iFalse; 2195 return iFalse;
1904 } 2196 }
2197#if !LAGRANGE_USE_SYSTEM_TEXT_INPUT
2198 else if (isCommand_UserEvent(ev, "prefs.blink.changed")) {
2199 if (isEditing_InputWidget_(d) && arg_Command(command_UserEvent(ev))) {
2200 startOrStopCursorTimer_InputWidget_(d, 2);
2201 }
2202 return iFalse;
2203 }
2204 else if (isEditing_InputWidget_(d) && (isCommand_UserEvent(ev, "window.focus.lost") ||
2205 isCommand_UserEvent(ev, "window.focus.gained"))) {
2206 startOrStopCursorTimer_InputWidget_(d, isCommand_UserEvent(ev, "window.focus.gained"));
2207 d->cursorVis = 1;
2208 refresh_Widget(d);
2209 return iFalse;
2210 }
1905 else if ((isCommand_UserEvent(ev, "copy") || isCommand_UserEvent(ev, "input.copy")) && 2211 else if ((isCommand_UserEvent(ev, "copy") || isCommand_UserEvent(ev, "input.copy")) &&
1906 isEditing_InputWidget_(d)) { 2212 isEditing_InputWidget_(d)) {
1907 copy_InputWidget_(d, argLabel_Command(command_UserEvent(ev), "cut")); 2213 copy_InputWidget_(d, argLabel_Command(command_UserEvent(ev), "cut"));
1908 return iTrue; 2214 return iTrue;
1909 } 2215 }
2216// else if (isFocused_Widget(d) && isCommand_UserEvent(ev, "copy")) {
2217// copy_InputWidget_(d, iFalse);
2218// return iTrue;
2219// }
1910 else if (isCommand_UserEvent(ev, "input.paste") && isEditing_InputWidget_(d)) { 2220 else if (isCommand_UserEvent(ev, "input.paste") && isEditing_InputWidget_(d)) {
1911 paste_InputWidget_(d); 2221 paste_InputWidget_(d);
1912 return iTrue; 2222 return iTrue;
@@ -1918,6 +2228,14 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) {
1918 } 2228 }
1919 return iTrue; 2229 return iTrue;
1920 } 2230 }
2231 else if (isCommand_UserEvent(ev, "text.insert")) {
2232 pushUndo_InputWidget_(d);
2233 deleteMarked_InputWidget_(d);
2234 insertChar_InputWidget_(d, arg_Command(command_UserEvent(ev)));
2235 contentsWereChanged_InputWidget_(d);
2236 return iTrue;
2237 }
2238#endif
1921 else if (isCommand_UserEvent(ev, "input.selectall") && isEditing_InputWidget_(d)) { 2239 else if (isCommand_UserEvent(ev, "input.selectall") && isEditing_InputWidget_(d)) {
1922 selectAll_InputWidget(d); 2240 selectAll_InputWidget(d);
1923 return iTrue; 2241 return iTrue;
@@ -1928,24 +2246,19 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) {
1928 } 2246 }
1929 return iFalse; 2247 return iFalse;
1930 } 2248 }
1931 /* TODO: Scroll to keep widget visible when keyboard appears. */ 2249 else if (isCommand_UserEvent(ev, "keyboard.changed")) {
1932// else if (isCommand_UserEvent(ev, "keyboard.changed")) { 2250 const iBool isKeyboardVisible = (arg_Command(command_UserEvent(ev)) != 0);
1933// if (isFocused_Widget(d) && arg_Command(command_UserEvent(ev))) { 2251 /* Scroll to keep widget visible when keyboard appears. */
1934// iRect rect = bounds_Widget(w); 2252 if (isFocused_Widget(d)) {
1935// rect.pos.y -= value_Anim(&get_Window()->rootOffset); 2253 if (isKeyboardVisible) {
1936// const iInt2 visRoot = visibleSize_Root(w->root); 2254 d->lastOverflowScrollTime = SDL_GetTicks();
1937// if (bottom_Rect(rect) > visRoot.y) { 2255 overflowScrollToKeepVisible_InputWidget_(d);
1938// setValue_Anim(&get_Window()->rootOffset, -(bottom_Rect(rect) - visRoot.y), 250); 2256 }
1939// } 2257 else {
1940// } 2258 setFocus_Widget(NULL); /* stop editing */
1941// return iFalse; 2259 }
1942// } 2260 }
1943 else if (isCommand_UserEvent(ev, "text.insert")) { 2261 return iFalse;
1944 pushUndo_InputWidget_(d);
1945 deleteMarked_InputWidget_(d);
1946 insertChar_InputWidget_(d, arg_Command(command_UserEvent(ev)));
1947 contentsWereChanged_InputWidget_(d);
1948 return iTrue;
1949 } 2262 }
1950 else if (isCommand_Widget(w, ev, "input.backup")) { 2263 else if (isCommand_Widget(w, ev, "input.backup")) {
1951 if (d->inFlags & needBackup_InputWidgetFlag) { 2264 if (d->inFlags & needBackup_InputWidgetFlag) {
@@ -1957,10 +2270,7 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) {
1957 updateMetrics_InputWidget_(d); 2270 updateMetrics_InputWidget_(d);
1958 // updateLinesAndResize_InputWidget_(d); 2271 // updateLinesAndResize_InputWidget_(d);
1959 } 2272 }
1960 else if (isFocused_Widget(d) && isCommand_UserEvent(ev, "copy")) { 2273#if !LAGRANGE_USE_SYSTEM_TEXT_INPUT
1961 copy_InputWidget_(d, iFalse);
1962 return iTrue;
1963 }
1964 if (ev->type == SDL_MOUSEWHEEL && contains_Widget(w, coord_MouseWheelEvent(&ev->wheel))) { 2274 if (ev->type == SDL_MOUSEWHEEL && contains_Widget(w, coord_MouseWheelEvent(&ev->wheel))) {
1965 if (numWrapLines_InputWidget_(d) <= size_Range(&d->visWrapLines)) { 2275 if (numWrapLines_InputWidget_(d) <= size_Range(&d->visWrapLines)) {
1966 return ignored_EventResult; 2276 return ignored_EventResult;
@@ -1994,6 +2304,17 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) {
1994 } 2304 }
1995 return false_EventResult; 2305 return false_EventResult;
1996 } 2306 }
2307 if (ev->type == SDL_TEXTINPUT && isFocused_Widget(w)) {
2308 pushUndo_InputWidget_(d);
2309 deleteMarked_InputWidget_(d);
2310 insertRange_InputWidget_(d, range_CStr(ev->text.text));
2311 contentsWereChanged_InputWidget_(d);
2312 return iTrue;
2313 }
2314 const iInt2 curMax = cursorMax_InputWidget_(d);
2315 const iInt2 lineFirst = init_I2(0, d->cursor.y);
2316 const iInt2 lineLast = init_I2(endX_InputWidget_(d, d->cursor.y), d->cursor.y);
2317#endif
1997 /* Click behavior depends on device type. */ { 2318 /* Click behavior depends on device type. */ {
1998 const int mbResult = (deviceType_App() == desktop_AppDeviceType 2319 const int mbResult = (deviceType_App() == desktop_AppDeviceType
1999 ? processPointerEvents_InputWidget_(d, ev) 2320 ? processPointerEvents_InputWidget_(d, ev)
@@ -2005,12 +2326,10 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) {
2005 if (ev->type == SDL_KEYUP && isFocused_Widget(w)) { 2326 if (ev->type == SDL_KEYUP && isFocused_Widget(w)) {
2006 return iTrue; 2327 return iTrue;
2007 } 2328 }
2008 const iInt2 curMax = cursorMax_InputWidget_(d);
2009 const iInt2 lineFirst = init_I2(0, d->cursor.y);
2010 const iInt2 lineLast = init_I2(endX_InputWidget_(d, d->cursor.y), d->cursor.y);
2011 if (ev->type == SDL_KEYDOWN && isFocused_Widget(w)) { 2329 if (ev->type == SDL_KEYDOWN && isFocused_Widget(w)) {
2012 const int key = ev->key.keysym.sym; 2330 const int key = ev->key.keysym.sym;
2013 const int mods = keyMods_Sym(ev->key.keysym.mod); 2331 const int mods = keyMods_Sym(ev->key.keysym.mod);
2332#if !LAGRANGE_USE_SYSTEM_TEXT_INPUT
2014 if (mods == KMOD_PRIMARY) { 2333 if (mods == KMOD_PRIMARY) {
2015 switch (key) { 2334 switch (key) {
2016 case 'c': 2335 case 'c':
@@ -2028,7 +2347,7 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) {
2028 return iTrue; 2347 return iTrue;
2029 } 2348 }
2030 } 2349 }
2031#if defined (iPlatformApple) 2350# if defined (iPlatformApple)
2032 if (mods == KMOD_PRIMARY || mods == (KMOD_PRIMARY | KMOD_SHIFT)) { 2351 if (mods == KMOD_PRIMARY || mods == (KMOD_PRIMARY | KMOD_SHIFT)) {
2033 switch (key) { 2352 switch (key) {
2034 case SDLK_UP: 2353 case SDLK_UP:
@@ -2038,19 +2357,14 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) {
2038 return iTrue; 2357 return iTrue;
2039 } 2358 }
2040 } 2359 }
2041#endif 2360# endif
2042 d->prevCursor = d->cursor; 2361 d->prevCursor = d->cursor;
2362#endif
2043 switch (key) { 2363 switch (key) {
2044 case SDLK_INSERT:
2045 if (mods == KMOD_SHIFT) {
2046 paste_InputWidget_(d);
2047 }
2048 return iTrue;
2049 case SDLK_RETURN: 2364 case SDLK_RETURN:
2050 case SDLK_KP_ENTER: 2365 case SDLK_KP_ENTER:
2051 if (~d->inFlags & isSensitive_InputWidgetFlag && 2366#if !LAGRANGE_USE_SYSTEM_TEXT_INPUT
2052 ~d->inFlags & isUrl_InputWidgetFlag && 2367 if (isAllowedToInsertNewline_InputWidget_(d)) {
2053 d->inFlags & lineBreaksEnabled_InputWidgetFlag && d->maxLen == 0) {
2054 if (checkLineBreakMods_InputWidget_(d, mods)) { 2368 if (checkLineBreakMods_InputWidget_(d, mods)) {
2055 pushUndo_InputWidget_(d); 2369 pushUndo_InputWidget_(d);
2056 deleteMarked_InputWidget_(d); 2370 deleteMarked_InputWidget_(d);
@@ -2059,6 +2373,7 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) {
2059 return iTrue; 2373 return iTrue;
2060 } 2374 }
2061 } 2375 }
2376#endif
2062 if (d->inFlags & enterKeyEnabled_InputWidgetFlag && 2377 if (d->inFlags & enterKeyEnabled_InputWidgetFlag &&
2063 (checkAcceptMods_InputWidget_(d, mods) || 2378 (checkAcceptMods_InputWidget_(d, mods) ||
2064 (~d->inFlags & lineBreaksEnabled_InputWidgetFlag))) { 2379 (~d->inFlags & lineBreaksEnabled_InputWidgetFlag))) {
@@ -2071,6 +2386,12 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) {
2071 end_InputWidget(d, iTrue); 2386 end_InputWidget(d, iTrue);
2072 setFocus_Widget(NULL); 2387 setFocus_Widget(NULL);
2073 return (d->inFlags & eatEscape_InputWidgetFlag) != 0; 2388 return (d->inFlags & eatEscape_InputWidgetFlag) != 0;
2389#if !LAGRANGE_USE_SYSTEM_TEXT_INPUT
2390 case SDLK_INSERT:
2391 if (mods == KMOD_SHIFT) {
2392 paste_InputWidget_(d);
2393 }
2394 return iTrue;
2074 case SDLK_BACKSPACE: 2395 case SDLK_BACKSPACE:
2075 if (!isEmpty_Range(&d->mark)) { 2396 if (!isEmpty_Range(&d->mark)) {
2076 pushUndo_InputWidget_(d); 2397 pushUndo_InputWidget_(d);
@@ -2170,7 +2491,7 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) {
2170 refresh_Widget(w); 2491 refresh_Widget(w);
2171 return iTrue; 2492 return iTrue;
2172 } 2493 }
2173#if defined (iPlatformApple) 2494# if defined (iPlatformApple)
2174 /* fall through for Emacs-style Home/End */ 2495 /* fall through for Emacs-style Home/End */
2175 case SDLK_e: 2496 case SDLK_e:
2176 if (mods == KMOD_CTRL || mods == (KMOD_CTRL | KMOD_SHIFT)) { 2497 if (mods == KMOD_CTRL || mods == (KMOD_CTRL | KMOD_SHIFT)) {
@@ -2178,7 +2499,7 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) {
2178 refresh_Widget(w); 2499 refresh_Widget(w);
2179 return iTrue; 2500 return iTrue;
2180 } 2501 }
2181#endif 2502# endif
2182 break; 2503 break;
2183 case SDLK_LEFT: 2504 case SDLK_LEFT:
2184 case SDLK_RIGHT: { 2505 case SDLK_RIGHT: {
@@ -2228,22 +2549,17 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) {
2228 } 2549 }
2229 refresh_Widget(d); 2550 refresh_Widget(d);
2230 return iTrue; 2551 return iTrue;
2552#endif
2231 } 2553 }
2232 if (mods & (KMOD_PRIMARY | KMOD_SECONDARY)) { 2554 if (mods & (KMOD_PRIMARY | KMOD_SECONDARY)) {
2233 return iFalse; 2555 return iFalse;
2234 } 2556 }
2235 return iTrue; 2557 return iTrue;
2236 } 2558 }
2237 else if (ev->type == SDL_TEXTINPUT && isFocused_Widget(w)) {
2238 pushUndo_InputWidget_(d);
2239 deleteMarked_InputWidget_(d);
2240 insertRange_InputWidget_(d, range_CStr(ev->text.text));
2241 contentsWereChanged_InputWidget_(d);
2242 return iTrue;
2243 }
2244 return processEvent_Widget(w, ev); 2559 return processEvent_Widget(w, ev);
2245} 2560}
2246 2561
2562#if !LAGRANGE_USE_SYSTEM_TEXT_INPUT
2247iDeclareType(MarkPainter) 2563iDeclareType(MarkPainter)
2248 2564
2249struct Impl_MarkPainter { 2565struct Impl_MarkPainter {
@@ -2302,6 +2618,7 @@ static iBool draw_MarkPainter_(iWrapText *wrapText, iRangecc wrappedText, iTextA
2302 } 2618 }
2303 return iTrue; 2619 return iTrue;
2304} 2620}
2621#endif
2305 2622
2306static void draw_InputWidget_(const iInputWidget *d) { 2623static void draw_InputWidget_(const iInputWidget *d) {
2307 const iWidget *w = constAs_Widget(d); 2624 const iWidget *w = constAs_Widget(d);
@@ -2324,13 +2641,22 @@ static void draw_InputWidget_(const iInputWidget *d) {
2324 isFocused ? gap_UI / 4 : 1, 2641 isFocused ? gap_UI / 4 : 1,
2325 isFocused ? uiInputFrameFocused_ColorId 2642 isFocused ? uiInputFrameFocused_ColorId
2326 : isHover ? uiInputFrameHover_ColorId : uiInputFrame_ColorId); 2643 : isHover ? uiInputFrameHover_ColorId : uiInputFrame_ColorId);
2327 setClip_Paint(&p, adjusted_Rect(bounds, init_I2(d->leftPadding, 0), 2644 if (d->sysCtrl) {
2328 init_I2(-d->rightPadding, w->flags & extraPadding_WidgetFlag ? -gap_UI / 2 : 0))); 2645 /* The system-provided control is drawing the text. */
2646 drawChildren_Widget(w);
2647 return;
2648 }
2329 const iRect contentBounds = contentBounds_InputWidget_(d); 2649 const iRect contentBounds = contentBounds_InputWidget_(d);
2330 iInt2 drawPos = topLeft_Rect(contentBounds); 2650 iInt2 drawPos = topLeft_Rect(contentBounds);
2331 const int fg = isHint ? uiAnnotation_ColorId 2651 const int fg = isHint ? uiAnnotation_ColorId
2332 : isFocused /*&& !isEmpty_Array(&d->lines)*/ ? uiInputTextFocused_ColorId 2652 : isFocused ? uiInputTextFocused_ColorId
2333 : uiInputText_ColorId; 2653 : uiInputText_ColorId;
2654#if !LAGRANGE_USE_SYSTEM_TEXT_INPUT
2655 setClip_Paint(&p,
2656 adjusted_Rect(bounds,
2657 init_I2(d->leftPadding, 0),
2658 init_I2(-d->rightPadding,
2659 w->flags & extraPadding_WidgetFlag ? -gap_UI / 2 : 0)));
2334 iWrapText wrapText = { 2660 iWrapText wrapText = {
2335 .maxWidth = d->maxLen == 0 ? width_Rect(contentBounds) : unlimitedWidth_InputWidget_, 2661 .maxWidth = d->maxLen == 0 ? width_Rect(contentBounds) : unlimitedWidth_InputWidget_,
2336 .mode = (d->inFlags & isUrl_InputWidgetFlag ? anyCharacter_WrapTextMode 2662 .mode = (d->inFlags & isUrl_InputWidgetFlag ? anyCharacter_WrapTextMode
@@ -2338,16 +2664,37 @@ static void draw_InputWidget_(const iInputWidget *d) {
2338 .overrideChar = (d->inFlags & isSensitive_InputWidgetFlag ? sensitiveChar_ : 0), 2664 .overrideChar = (d->inFlags & isSensitive_InputWidgetFlag ? sensitiveChar_ : 0),
2339 }; 2665 };
2340 const iRangei visLines = visibleLineRange_InputWidget_(d); 2666 const iRangei visLines = visibleLineRange_InputWidget_(d);
2341 const int visLineOffsetY = visLineOffsetY_InputWidget_(d);
2342 iRect markerRects[2] = { zero_Rect(), zero_Rect() }; 2667 iRect markerRects[2] = { zero_Rect(), zero_Rect() };
2668#endif
2669 const int visLineOffsetY = visLineOffsetY_InputWidget_(d);
2343 /* If buffered, just draw the buffered copy. */ 2670 /* If buffered, just draw the buffered copy. */
2344 if (d->buffered && !isFocused) { 2671 if (d->buffered && !isFocused) {
2345 /* Most input widgets will use this, since only one is focused at a time. */ 2672 /* Most input widgets will use this, since only one is focused at a time. */
2346 draw_TextBuf(d->buffered, addY_I2(drawPos, visLineOffsetY), white_ColorId); 2673 if (flags_Widget(w) & alignRight_WidgetFlag) {
2674 draw_TextBuf(
2675 d->buffered,
2676 addY_I2(init_I2(right_Rect(contentBounds) - d->buffered->size.x, drawPos.y),
2677 visLineOffsetY),
2678 white_ColorId);
2679 }
2680 else {
2681 draw_TextBuf(d->buffered, addY_I2(drawPos, visLineOffsetY), white_ColorId);
2682 }
2347 } 2683 }
2348 else if (isHint) { 2684 else if (isHint) {
2349 drawRange_Text(d->font, drawPos, uiAnnotation_ColorId, range_String(&d->hint)); 2685 if (flags_Widget(w) & alignRight_WidgetFlag) {
2686 drawAlign_Text(d->font,
2687 init_I2(right_Rect(contentBounds), drawPos.y),
2688 uiAnnotation_ColorId,
2689 right_Alignment,
2690 "%s",
2691 cstr_String(&d->hint));
2692 }
2693 else {
2694 drawRange_Text(d->font, drawPos, uiAnnotation_ColorId, range_String(&d->hint));
2695 }
2350 } 2696 }
2697#if !LAGRANGE_USE_SYSTEM_TEXT_INPUT
2351 else { 2698 else {
2352 iAssert(~d->inFlags & isSensitive_InputWidgetFlag || size_Range(&visLines) == 1); 2699 iAssert(~d->inFlags & isSensitive_InputWidgetFlag || size_Range(&visLines) == 1);
2353 drawPos.y += visLineOffsetY; 2700 drawPos.y += visLineOffsetY;
@@ -2372,7 +2719,8 @@ static void draw_InputWidget_(const iInputWidget *d) {
2372 wrapText.context = NULL; 2719 wrapText.context = NULL;
2373 } 2720 }
2374 /* Draw the insertion point. */ 2721 /* Draw the insertion point. */
2375 if (isFocused && d->cursorVis && contains_Range(&visLines, d->cursor.y) && 2722 if (isFocused && (d->cursorVis || !prefs_App()->blinkingCursor) &&
2723 contains_Range(&visLines, d->cursor.y) &&
2376 (deviceType_App() == desktop_AppDeviceType || isEmpty_Range(&d->mark))) { 2724 (deviceType_App() == desktop_AppDeviceType || isEmpty_Range(&d->mark))) {
2377 iInt2 curSize; 2725 iInt2 curSize;
2378 iRangecc cursorChar = iNullRange; 2726 iRangecc cursorChar = iNullRange;
@@ -2426,6 +2774,7 @@ static void draw_InputWidget_(const iInputWidget *d) {
2426 drawPin_Paint(&p, markerRects[i], i, uiTextCaution_ColorId); 2774 drawPin_Paint(&p, markerRects[i], i, uiTextCaution_ColorId);
2427 } 2775 }
2428 } 2776 }
2777#endif
2429 drawChildren_Widget(w); 2778 drawChildren_Widget(w);
2430} 2779}
2431 2780
diff --git a/src/ui/inputwidget.h b/src/ui/inputwidget.h
index f70c81af..5a61ec22 100644
--- a/src/ui/inputwidget.h
+++ b/src/ui/inputwidget.h
@@ -57,6 +57,7 @@ void setBackupFileName_InputWidget (iInputWidget *, const char *fileName);
57void begin_InputWidget (iInputWidget *); 57void begin_InputWidget (iInputWidget *);
58void end_InputWidget (iInputWidget *, iBool accept); 58void end_InputWidget (iInputWidget *, iBool accept);
59void selectAll_InputWidget (iInputWidget *); 59void selectAll_InputWidget (iInputWidget *);
60void validate_InputWidget (iInputWidget *);
60 61
61void setSelectAllOnFocus_InputWidget (iInputWidget *, iBool selectAllOnFocus); 62void setSelectAllOnFocus_InputWidget (iInputWidget *, iBool selectAllOnFocus);
62void setSensitiveContent_InputWidget (iInputWidget *, iBool isSensitive); 63void setSensitiveContent_InputWidget (iInputWidget *, iBool isSensitive);
diff --git a/src/ui/keys.c b/src/ui/keys.c
index 30072572..d4d9320e 100644
--- a/src/ui/keys.c
+++ b/src/ui/keys.c
@@ -240,6 +240,7 @@ static const struct { int id; iMenuItem bind; int flags; } defaultBindings_[] =
240 { 100,{ "${keys.hoverurl}", '/', KMOD_PRIMARY, "prefs.hoverlink.toggle" }, 0 }, 240 { 100,{ "${keys.hoverurl}", '/', KMOD_PRIMARY, "prefs.hoverlink.toggle" }, 0 },
241 { 110,{ "${menu.save.downloads}", SDLK_s, KMOD_PRIMARY, "document.save" }, 0 }, 241 { 110,{ "${menu.save.downloads}", SDLK_s, KMOD_PRIMARY, "document.save" }, 0 },
242 { 120,{ "${keys.upload}", SDLK_u, KMOD_PRIMARY, "document.upload" }, 0 }, 242 { 120,{ "${keys.upload}", SDLK_u, KMOD_PRIMARY, "document.upload" }, 0 },
243 { 121,{ "${keys.upload.edit}", SDLK_e, KMOD_PRIMARY, "document.upload copy:1" }, 0 },
243 /* The following cannot currently be changed (built-in duplicates). */ 244 /* The following cannot currently be changed (built-in duplicates). */
244#if defined (iPlatformApple) 245#if defined (iPlatformApple)
245 { 1002, { NULL, SDLK_LEFTBRACKET, KMOD_PRIMARY, "navigate.back" }, 0 }, 246 { 1002, { NULL, SDLK_LEFTBRACKET, KMOD_PRIMARY, "navigate.back" }, 0 },
diff --git a/src/ui/labelwidget.c b/src/ui/labelwidget.c
index 4dd66a28..3454014a 100644
--- a/src/ui/labelwidget.c
+++ b/src/ui/labelwidget.c
@@ -36,6 +36,7 @@ struct Impl_LabelWidget {
36 iWidget widget; 36 iWidget widget;
37 iString srcLabel; 37 iString srcLabel;
38 iString label; 38 iString label;
39 iInt2 labelOffset;
39 int font; 40 int font;
40 int key; 41 int key;
41 int kmods; 42 int kmods;
@@ -44,14 +45,15 @@ struct Impl_LabelWidget {
44 iString command; 45 iString command;
45 iClick click; 46 iClick click;
46 struct { 47 struct {
47 uint8_t alignVisual : 1; /* align according to visible bounds, not font metrics */ 48 uint16_t alignVisual : 1; /* align according to visible bounds, not font metrics */
48 uint8_t noAutoMinHeight : 1; /* minimum height is not set automatically */ 49 uint16_t noAutoMinHeight : 1; /* minimum height is not set automatically */
49 uint8_t drawAsOutline : 1; /* draw as outline, filled with background color */ 50 uint16_t drawAsOutline : 1; /* draw as outline, filled with background color */
50 uint8_t noTopFrame : 1; 51 uint16_t noTopFrame : 1;
51 uint8_t wrap : 1; 52 uint16_t wrap : 1;
52 uint8_t allCaps : 1; 53 uint16_t allCaps : 1;
53 uint8_t removeTrailingColon : 1; 54 uint16_t removeTrailingColon : 1;
54 uint8_t chevron : 1; 55 uint16_t chevron : 1;
56 uint16_t checkMark : 1;
55 } flags; 57 } flags;
56}; 58};
57 59
@@ -132,6 +134,10 @@ static iBool processEvent_LabelWidget_(iLabelWidget *d, const SDL_Event *ev) {
132 refresh_Widget(d); 134 refresh_Widget(d);
133 return iFalse; 135 return iFalse;
134 } 136 }
137 else if (isCommand_Widget(w, ev, "trigger")) {
138 trigger_LabelWidget_(d);
139 return iTrue;
140 }
135 if (!isEmpty_String(&d->command)) { 141 if (!isEmpty_String(&d->command)) {
136#if 0 && defined (iPlatformAppleMobile) 142#if 0 && defined (iPlatformAppleMobile)
137 /* Touch allows activating any button on release. */ 143 /* Touch allows activating any button on release. */
@@ -193,6 +199,7 @@ static void getColors_LabelWidget_(const iLabelWidget *d, int *bg, int *fg, int
193 int *icon, int *meta) { 199 int *icon, int *meta) {
194 const iWidget *w = constAs_Widget(d); 200 const iWidget *w = constAs_Widget(d);
195 const int64_t flags = flags_Widget(w); 201 const int64_t flags = flags_Widget(w);
202 const iBool isHover = isHover_LabelWidget_(d);
196 const iBool isFocus = (flags & focusable_WidgetFlag && isFocused_Widget(d)); 203 const iBool isFocus = (flags & focusable_WidgetFlag && isFocused_Widget(d));
197 const iBool isPress = (flags & pressed_WidgetFlag) != 0; 204 const iBool isPress = (flags & pressed_WidgetFlag) != 0;
198 const iBool isSel = (flags & selected_WidgetFlag) != 0; 205 const iBool isSel = (flags & selected_WidgetFlag) != 0;
@@ -205,6 +212,9 @@ static void getColors_LabelWidget_(const iLabelWidget *d, int *bg, int *fg, int
205 *bg = isButton && ~flags & noBackground_WidgetFlag ? (d->widget.bgColor != none_ColorId ? 212 *bg = isButton && ~flags & noBackground_WidgetFlag ? (d->widget.bgColor != none_ColorId ?
206 d->widget.bgColor : uiBackground_ColorId) 213 d->widget.bgColor : uiBackground_ColorId)
207 : none_ColorId; 214 : none_ColorId;
215 if (d->flags.checkMark) {
216 *bg = none_ColorId;
217 }
208 *fg = uiText_ColorId; 218 *fg = uiText_ColorId;
209 *frame1 = isButton ? uiEmboss1_ColorId : d->widget.frameColor; 219 *frame1 = isButton ? uiEmboss1_ColorId : d->widget.frameColor;
210 *frame2 = isButton ? uiEmboss2_ColorId : *frame1; 220 *frame2 = isButton ? uiEmboss2_ColorId : *frame1;
@@ -216,18 +226,17 @@ static void getColors_LabelWidget_(const iLabelWidget *d, int *bg, int *fg, int
216 *meta = uiTextDisabled_ColorId; 226 *meta = uiTextDisabled_ColorId;
217 } 227 }
218 if (isSel) { 228 if (isSel) {
219 if (isMenuItem) { 229 if (!d->flags.checkMark) {
220 *bg = uiBackgroundUnfocusedSelection_ColorId; 230 if (isMenuItem) {
221 } 231 *bg = uiBackgroundUnfocusedSelection_ColorId;
222 else { 232 }
223 *bg = uiBackgroundSelected_ColorId; 233 else {
224 } 234 *bg = uiBackgroundSelected_ColorId;
225// if (!isKeyRoot) { 235 }
226// *bg = uiEmbossSelected1_ColorId; //uiBackgroundUnfocusedSelection_ColorId; 236 if (!isKeyRoot) {
227// } 237 *bg = isDark_ColorTheme(colorTheme_App()) ? uiBackgroundUnfocusedSelection_ColorId
228 if (!isKeyRoot) { 238 : uiMarked_ColorId;
229 *bg = isDark_ColorTheme(colorTheme_App()) ? uiBackgroundUnfocusedSelection_ColorId 239 }
230 : uiMarked_ColorId ;
231 } 240 }
232 *fg = uiTextSelected_ColorId; 241 *fg = uiTextSelected_ColorId;
233 if (isButton) { 242 if (isButton) {
@@ -248,7 +257,7 @@ static void getColors_LabelWidget_(const iLabelWidget *d, int *bg, int *fg, int
248 if (colorEscape == uiTextCaution_ColorId) { 257 if (colorEscape == uiTextCaution_ColorId) {
249 *icon = *meta = colorEscape; 258 *icon = *meta = colorEscape;
250 } 259 }
251 if (isHover_LabelWidget_(d)) { 260 if (isHover) {
252 if (isFrameless) { 261 if (isFrameless) {
253 *bg = uiBackgroundFramelessHover_ColorId; 262 *bg = uiBackgroundFramelessHover_ColorId;
254 *fg = uiTextFramelessHover_ColorId; 263 *fg = uiTextFramelessHover_ColorId;
@@ -274,7 +283,7 @@ static void getColors_LabelWidget_(const iLabelWidget *d, int *bg, int *fg, int
274 } 283 }
275 } 284 }
276 if (d->forceFg >= 0) { 285 if (d->forceFg >= 0) {
277 *fg = /* *icon = */ *meta = d->forceFg; 286 *fg = *meta = d->forceFg;
278 } 287 }
279 if (isPress) { 288 if (isPress) {
280 if (colorEscape == uiTextAction_ColorId || colorEscape == uiTextCaution_ColorId) { 289 if (colorEscape == uiTextAction_ColorId || colorEscape == uiTextCaution_ColorId) {
@@ -289,13 +298,12 @@ static void getColors_LabelWidget_(const iLabelWidget *d, int *bg, int *fg, int
289 *frame1 = uiEmbossPressed1_ColorId; 298 *frame1 = uiEmbossPressed1_ColorId;
290 *frame2 = colorEscape != none_ColorId ? colorEscape : uiEmbossPressed2_ColorId; 299 *frame2 = colorEscape != none_ColorId ? colorEscape : uiEmbossPressed2_ColorId;
291 } 300 }
292 //if (colorEscape == none_ColorId || colorEscape == uiTextAction_ColorId) {
293 *fg = *icon = *meta = uiTextPressed_ColorId | permanent_ColorId; 301 *fg = *icon = *meta = uiTextPressed_ColorId | permanent_ColorId;
294 // }
295 // else {
296 // *fg = (isDark_ColorTheme(colorTheme_App()) ? white_ColorId : black_ColorId) | permanent_ColorId;
297 // }
298 } 302 }
303 }
304 if (((isSel || isHover) && isFrameless) || isPress) {
305 /* Ensure that the full label text remains readable. */
306 *fg |= permanent_ColorId;
299 } 307 }
300} 308}
301 309
@@ -328,6 +336,7 @@ static void draw_LabelWidget_(const iLabelWidget *d) {
328 init_Paint(&p); 336 init_Paint(&p);
329 int bg, fg, frame, frame2, iconColor, metaColor; 337 int bg, fg, frame, frame2, iconColor, metaColor;
330 getColors_LabelWidget_(d, &bg, &fg, &frame, &frame2, &iconColor, &metaColor); 338 getColors_LabelWidget_(d, &bg, &fg, &frame, &frame2, &iconColor, &metaColor);
339 setBaseAttributes_Text(d->font, fg);
331 const enum iColorId colorEscape = parseEscape_Color(cstr_String(&d->label), NULL); 340 const enum iColorId colorEscape = parseEscape_Color(cstr_String(&d->label), NULL);
332 const iBool isCaution = (colorEscape == uiTextCaution_ColorId); 341 const iBool isCaution = (colorEscape == uiTextCaution_ColorId);
333 if (bg >= 0) { 342 if (bg >= 0) {
@@ -347,7 +356,7 @@ static void draw_LabelWidget_(const iLabelWidget *d) {
347 bottomRight_Rect(frameRect), 356 bottomRight_Rect(frameRect),
348 bottomLeft_Rect(frameRect) 357 bottomLeft_Rect(frameRect)
349 }; 358 };
350#if SDL_VERSION_ATLEAST(2, 0, 16) 359#if SDL_COMPILEDVERSION == SDL_VERSIONNUM(2, 0, 16)
351 if (isOpenGLRenderer_Window()) { 360 if (isOpenGLRenderer_Window()) {
352 /* A very curious regression in SDL 2.0.16. */ 361 /* A very curious regression in SDL 2.0.16. */
353 points[3].x--; 362 points[3].x--;
@@ -362,10 +371,6 @@ static void draw_LabelWidget_(const iLabelWidget *d) {
362 } 371 }
363 setClip_Paint(&p, rect); 372 setClip_Paint(&p, rect);
364 const int iconPad = iconPadding_LabelWidget_(d); 373 const int iconPad = iconPadding_LabelWidget_(d);
365// const int iconColor = isCaution ? uiTextCaution_ColorId
366// : flags & (disabled_WidgetFlag | pressed_WidgetFlag) ? fg
367// : isHover ? uiIconHover_ColorId
368// : uiIcon_ColorId;
369 if (d->icon && d->icon != 0x20) { /* no need to draw an empty icon */ 374 if (d->icon && d->icon != 0x20) { /* no need to draw an empty icon */
370 iString str; 375 iString str;
371 initUnicodeN_String(&str, &d->icon, 1); 376 initUnicodeN_String(&str, &d->icon, 1);
@@ -427,22 +432,35 @@ static void draw_LabelWidget_(const iLabelWidget *d) {
427 else { 432 else {
428 drawCenteredOutline_Text( 433 drawCenteredOutline_Text(
429 d->font, 434 d->font,
430 adjusted_Rect(bounds, init_I2(iconPad * (flags & tight_WidgetFlag ? 1.0f : 1.5f), 0), 435 moved_Rect(
436 adjusted_Rect(bounds,
437 init_I2(iconPad * (flags & tight_WidgetFlag ? 1.0f : 1.5f), 0),
431 init_I2(-iconPad * (flags & tight_WidgetFlag ? 0.5f : 1.0f), 0)), 438 init_I2(-iconPad * (flags & tight_WidgetFlag ? 0.5f : 1.0f), 0)),
439 d->labelOffset),
432 d->flags.alignVisual, 440 d->flags.alignVisual,
433 d->flags.drawAsOutline ? fg : none_ColorId, 441 d->flags.drawAsOutline ? fg : none_ColorId,
434 d->flags.drawAsOutline ? d->widget.bgColor : fg, 442 d->flags.drawAsOutline ? d->widget.bgColor : fg,
435 "%s", 443 "%s",
436 cstr_String(&d->label)); 444 cstr_String(&d->label));
437 } 445 }
438 if (d->flags.chevron) { 446 if (d->flags.chevron || (flags & selected_WidgetFlag && d->flags.checkMark)) {
439 const iRect chRect = rect; 447 const iRect chRect = rect;
440 const int chSize = lineHeight_Text(d->font); 448 const int chSize = lineHeight_Text(d->font);
449 int offset = 0;
450 if (d->flags.chevron) {
451 offset = -iconPad;
452 }
453 else {
454 offset = -10 * gap_UI;
455 }
441 drawCentered_Text(d->font, 456 drawCentered_Text(d->font,
442 (iRect){ addX_I2(topRight_Rect(chRect), -iconPad), 457 (iRect){ addX_I2(topRight_Rect(chRect), offset),
443 init_I2(chSize, height_Rect(chRect)) }, 458 init_I2(chSize, height_Rect(chRect)) },
444 iTrue, iconColor, rightAngle_Icon); 459 iTrue,
460 iconColor,
461 d->flags.chevron ? rightAngle_Icon : check_Icon);
445 } 462 }
463 setBaseAttributes_Text(-1, -1);
446 unsetClip_Paint(&p); 464 unsetClip_Paint(&p);
447 drawChildren_Widget(w); 465 drawChildren_Widget(w);
448} 466}
@@ -482,9 +500,10 @@ int font_LabelWidget(const iLabelWidget *d) {
482} 500}
483 501
484void updateSize_LabelWidget(iLabelWidget *d) { 502void updateSize_LabelWidget(iLabelWidget *d) {
485 iWidget *w = as_Widget(d); 503 if (!d) return;
504 iWidget *w = as_Widget(d);
486 const int64_t flags = flags_Widget(w); 505 const int64_t flags = flags_Widget(w);
487 const iInt2 size = defaultSize_LabelWidget(d); 506 const iInt2 size = defaultSize_LabelWidget(d);
488 if (!d->flags.noAutoMinHeight) { 507 if (!d->flags.noAutoMinHeight) {
489 w->minSize.y = size.y; /* vertically text must remain visible */ 508 w->minSize.y = size.y; /* vertically text must remain visible */
490 } 509 }
@@ -514,6 +533,7 @@ void init_LabelWidget(iLabelWidget *d, const char *label, const char *cmd) {
514 d->font = uiLabel_FontId; 533 d->font = uiLabel_FontId;
515 d->forceFg = none_ColorId; 534 d->forceFg = none_ColorId;
516 d->icon = 0; 535 d->icon = 0;
536 d->labelOffset = zero_I2();
517 initCStr_String(&d->srcLabel, label); 537 initCStr_String(&d->srcLabel, label);
518 initCopy_String(&d->label, &d->srcLabel); 538 initCopy_String(&d->label, &d->srcLabel);
519 replaceVariables_LabelWidget_(d); 539 replaceVariables_LabelWidget_(d);
@@ -551,18 +571,22 @@ void setTextColor_LabelWidget(iLabelWidget *d, int color) {
551} 571}
552 572
553void setText_LabelWidget(iLabelWidget *d, const iString *text) { 573void setText_LabelWidget(iLabelWidget *d, const iString *text) {
574 if (d) {
554 updateText_LabelWidget(d, text); 575 updateText_LabelWidget(d, text);
555 updateSize_LabelWidget(d); 576 updateSize_LabelWidget(d);
556 if (isWrapped_LabelWidget(d)) { 577 if (isWrapped_LabelWidget(d)) {
557 sizeChanged_LabelWidget_(d); 578 sizeChanged_LabelWidget_(d);
579}
558 } 580 }
559} 581}
560 582
561void setTextCStr_LabelWidget(iLabelWidget *d, const char *text) { 583void setTextCStr_LabelWidget(iLabelWidget *d, const char *text) {
584 if (d) {
562 updateTextCStr_LabelWidget(d, text); 585 updateTextCStr_LabelWidget(d, text);
563 updateSize_LabelWidget(d); 586 updateSize_LabelWidget(d);
564 if (isWrapped_LabelWidget(d)) { 587 if (isWrapped_LabelWidget(d)) {
565 sizeChanged_LabelWidget_(d); 588 sizeChanged_LabelWidget_(d);
589 }
566 } 590 }
567} 591}
568 592
@@ -586,6 +610,10 @@ void setChevron_LabelWidget(iLabelWidget *d, iBool chevron) {
586 d->flags.chevron = chevron; 610 d->flags.chevron = chevron;
587} 611}
588 612
613void setCheckMark_LabelWidget(iLabelWidget *d, iBool checkMark) {
614 d->flags.checkMark = checkMark;
615}
616
589void setWrap_LabelWidget(iLabelWidget *d, iBool wrap) { 617void setWrap_LabelWidget(iLabelWidget *d, iBool wrap) {
590 d->flags.wrap = wrap; 618 d->flags.wrap = wrap;
591} 619}
@@ -610,6 +638,10 @@ void setRemoveTrailingColon_LabelWidget(iLabelWidget *d, iBool removeTrailingCol
610 } 638 }
611} 639}
612 640
641void setTextOffset_LabelWidget(iLabelWidget *d, iInt2 offset) {
642 d->labelOffset = offset;
643}
644
613void updateText_LabelWidget(iLabelWidget *d, const iString *text) { 645void updateText_LabelWidget(iLabelWidget *d, const iString *text) {
614 set_String(&d->label, text); 646 set_String(&d->label, text);
615 set_String(&d->srcLabel, text); 647 set_String(&d->srcLabel, text);
diff --git a/src/ui/labelwidget.h b/src/ui/labelwidget.h
index 6542ae12..4f605d6b 100644
--- a/src/ui/labelwidget.h
+++ b/src/ui/labelwidget.h
@@ -33,10 +33,12 @@ void setAlignVisually_LabelWidget(iLabelWidget *, iBool alignVisual);
33void setNoAutoMinHeight_LabelWidget (iLabelWidget *, iBool noAutoMinHeight); 33void setNoAutoMinHeight_LabelWidget (iLabelWidget *, iBool noAutoMinHeight);
34void setNoTopFrame_LabelWidget (iLabelWidget *, iBool noTopFrame); 34void setNoTopFrame_LabelWidget (iLabelWidget *, iBool noTopFrame);
35void setChevron_LabelWidget (iLabelWidget *, iBool chevron); 35void setChevron_LabelWidget (iLabelWidget *, iBool chevron);
36void setCheckMark_LabelWidget (iLabelWidget *, iBool checkMark);
36void setWrap_LabelWidget (iLabelWidget *, iBool wrap); 37void setWrap_LabelWidget (iLabelWidget *, iBool wrap);
37void setOutline_LabelWidget (iLabelWidget *, iBool drawAsOutline); 38void setOutline_LabelWidget (iLabelWidget *, iBool drawAsOutline);
38void setAllCaps_LabelWidget (iLabelWidget *, iBool allCaps); 39void setAllCaps_LabelWidget (iLabelWidget *, iBool allCaps);
39void setRemoveTrailingColon_LabelWidget (iLabelWidget *, iBool removeTrailingColon); 40void setRemoveTrailingColon_LabelWidget (iLabelWidget *, iBool removeTrailingColon);
41void setTextOffset_LabelWidget (iLabelWidget *, iInt2 offset);
40void setFont_LabelWidget (iLabelWidget *, int fontId); 42void setFont_LabelWidget (iLabelWidget *, int fontId);
41void setTextColor_LabelWidget (iLabelWidget *, int color); 43void setTextColor_LabelWidget (iLabelWidget *, int color);
42void setText_LabelWidget (iLabelWidget *, const iString *text); /* resizes widget */ 44void setText_LabelWidget (iLabelWidget *, const iString *text); /* resizes widget */
diff --git a/src/ui/linkinfo.c b/src/ui/linkinfo.c
new file mode 100644
index 00000000..5102f9b3
--- /dev/null
+++ b/src/ui/linkinfo.c
@@ -0,0 +1,176 @@
1/* Copyright 2021 Jaakko Keränen <jaakko.keranen@iki.fi>
2
3Redistribution and use in source and binary forms, with or without
4modification, are permitted provided that the following conditions are met:
5
61. Redistributions of source code must retain the above copyright notice, this
7 list of conditions and the following disclaimer.
82. Redistributions in binary form must reproduce the above copyright notice,
9 this list of conditions and the following disclaimer in the documentation
10 and/or other materials provided with the distribution.
11
12THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
13ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
14WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
15DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
16ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
17(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
18LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
19ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
20(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
21SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22
23#include "linkinfo.h"
24#include "metrics.h"
25#include "paint.h"
26#include "../gmcerts.h"
27#include "../app.h"
28
29#include <SDL_render.h>
30
31iDefineTypeConstruction(LinkInfo)
32
33#define minWidth_LinkInfo_ (40 * gap_UI)
34#define hPad_LinkInfo_ (2 * gap_UI)
35#define vPad_LinkInfo_ (1 * gap_UI)
36
37void init_LinkInfo(iLinkInfo *d) {
38 d->buf = NULL;
39 init_Anim(&d->opacity, 0.0f);
40 d->isAltPos = iFalse;
41}
42
43void deinit_LinkInfo(iLinkInfo *d) {
44 delete_TextBuf(d->buf);
45}
46
47iInt2 size_LinkInfo(const iLinkInfo *d) {
48 if (!d->buf) {
49 return zero_I2();
50 }
51 return add_I2(d->buf->size, init_I2(2 * hPad_LinkInfo_, 2 * vPad_LinkInfo_));
52}
53
54void infoText_LinkInfo(const iGmDocument *doc, iGmLinkId linkId, iString *text_out) {
55 const iString *url = linkUrl_GmDocument(doc, linkId);
56 iUrl parts;
57 init_Url(&parts, url);
58 const int flags = linkFlags_GmDocument(doc, linkId);
59 const enum iGmLinkScheme scheme = scheme_GmLinkFlag(flags);
60 const iBool isImage = (flags & imageFileExtension_GmLinkFlag) != 0;
61 const iBool isAudio = (flags & audioFileExtension_GmLinkFlag) != 0;
62 /* Most important info first: the identity that will be used. */
63 const iGmIdentity *ident = identityForUrl_GmCerts(certs_App(), url);
64 if (ident) {
65 appendFormat_String(text_out, person_Icon " %s",
66 //escape_Color(tmBannerItemTitle_ColorId),
67 cstr_String(name_GmIdentity(ident)));
68 }
69 /* Possibly inlined content. */
70 if (isImage || isAudio) {
71 if (!isEmpty_String(text_out)) {
72 appendCStr_String(text_out, "\n");
73 }
74 appendCStr_String(
75 text_out,
76 format_CStr(isImage ? photo_Icon " %s " : "\U0001f3b5 %s",
77 cstr_Lang(isImage ? "link.hint.image" : "link.hint.audio")));
78 }
79 if (!isEmpty_String(text_out)) {
80 appendCStr_String(text_out, " \u2014 ");
81 }
82 /* Indicate non-Gemini schemes. */
83 if (scheme == mailto_GmLinkScheme) {
84 appendCStr_String(text_out, envelope_Icon " ");
85 append_String(text_out, url);
86 }
87 else if (scheme != gemini_GmLinkScheme && !isEmpty_Range(&parts.host)) {
88 appendCStr_String(text_out, globe_Icon " \x1b[1m");
89 appendRange_String(text_out, (iRangecc){ constBegin_String(url),
90 parts.host.end });
91 appendCStr_String(text_out, "\x1b[0m");
92 appendRange_String(text_out, (iRangecc){ parts.path.start, constEnd_String(url) });
93 }
94 else if (scheme != gemini_GmLinkScheme) {
95 appendCStr_String(text_out, scheme == file_GmLinkScheme ? "" : globe_Icon " ");
96 append_String(text_out, url);
97 }
98 else {
99 appendCStr_String(text_out, "\x1b[1m");
100 appendRange_String(text_out, parts.host);
101 if (!isEmpty_Range(&parts.port)) {
102 appendCStr_String(text_out, ":");
103 appendRange_String(text_out, parts.port);
104 }
105 appendCStr_String(text_out, "\x1b[0m");
106 appendRange_String(text_out, (iRangecc){ parts.path.start, constEnd_String(url) });
107 }
108 /* Date of last visit. */
109 if (flags & visited_GmLinkFlag) {
110 iDate date;
111 init_Date(&date, linkTime_GmDocument(doc, linkId));
112 if (!isEmpty_String(text_out)) {
113 appendCStr_String(text_out, " \u2014 ");
114 }
115 iString *dateStr = format_Date(&date, "%b %d");
116 append_String(text_out, dateStr);
117 delete_String(dateStr);
118 }
119}
120
121iBool update_LinkInfo(iLinkInfo *d, const iGmDocument *doc, iGmLinkId linkId, int maxWidth) {
122 if (!d) {
123 return iFalse;
124 }
125 const iBool isAnimated = prefs_App()->uiAnimations;
126 if (d->linkId != linkId || d->maxWidth != maxWidth) {
127 d->linkId = linkId;
128 d->maxWidth = maxWidth;
129 invalidate_LinkInfo(d);
130 if (linkId) {
131 iString str;
132 init_String(&str);
133 infoText_LinkInfo(doc, linkId, &str);
134 if (targetValue_Anim(&d->opacity) < 1) {
135 setValue_Anim(&d->opacity, 1, isAnimated ? 75 : 0);
136 }
137 /* Draw to a buffer, wrapped. */
138 const int avail = iMax(minWidth_LinkInfo_, maxWidth) - 2 * hPad_LinkInfo_;
139 iWrapText wt = { .text = range_String(&str), .maxWidth = avail, .mode = word_WrapTextMode };
140 d->buf = new_TextBuf(&wt, uiLabel_FontId, tmQuote_ColorId);
141 deinit_String(&str);
142 }
143 else {
144 if (targetValue_Anim(&d->opacity) > 0) {
145 setValue_Anim(&d->opacity, 0, isAnimated ? 150 : 0);
146 }
147 }
148 return iTrue;
149 }
150 return iFalse;
151}
152
153void invalidate_LinkInfo(iLinkInfo *d) {
154 if (targetValue_Anim(&d->opacity) > 0) {
155 setValue_Anim(&d->opacity, 0, prefs_App()->uiAnimations ? 150 : 0);
156 }
157}
158
159void draw_LinkInfo(const iLinkInfo *d, iInt2 topLeft) {
160 const float opacity = value_Anim(&d->opacity);
161 if (!d->buf || opacity <= 0.01f) {
162 return;
163 }
164 iPaint p;
165 init_Paint(&p);
166 iInt2 size = size_LinkInfo(d);
167 iRect rect = { topLeft, size };
168 SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_BLEND);
169 p.alpha = 255 * opacity;
170 fillRect_Paint(&p, rect, tmBackgroundAltText_ColorId);
171 drawRect_Paint(&p, rect, tmFrameAltText_ColorId);
172 SDL_SetTextureAlphaMod(d->buf->texture, p.alpha);
173 draw_TextBuf(d->buf, add_I2(topLeft, init_I2(hPad_LinkInfo_, vPad_LinkInfo_)),
174 white_ColorId);
175 SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_NONE);
176}
diff --git a/src/ui/linkinfo.h b/src/ui/linkinfo.h
new file mode 100644
index 00000000..38b90b87
--- /dev/null
+++ b/src/ui/linkinfo.h
@@ -0,0 +1,47 @@
1/* Copyright 2021 Jaakko Keränen <jaakko.keranen@iki.fi>
2
3Redistribution and use in source and binary forms, with or without
4modification, are permitted provided that the following conditions are met:
5
61. Redistributions of source code must retain the above copyright notice, this
7 list of conditions and the following disclaimer.
82. Redistributions in binary form must reproduce the above copyright notice,
9 this list of conditions and the following disclaimer in the documentation
10 and/or other materials provided with the distribution.
11
12THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
13ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
14WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
15DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
16ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
17(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
18LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
19ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
20(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
21SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22
23#pragma once
24
25#include "text.h"
26#include "util.h"
27#include "../gmdocument.h"
28
29iDeclareType(LinkInfo)
30iDeclareTypeConstruction(LinkInfo)
31
32struct Impl_LinkInfo {
33 iGmLinkId linkId;
34 int maxWidth;
35 iTextBuf *buf;
36 iAnim opacity;
37 iBool isAltPos;
38};
39
40iBool update_LinkInfo (iLinkInfo *, const iGmDocument *doc, iGmLinkId linkId,
41 int maxWidth); /* returns true if changed */
42void invalidate_LinkInfo (iLinkInfo *);
43
44void infoText_LinkInfo (const iGmDocument *doc, iGmLinkId linkId, iString *text_out);
45
46iInt2 size_LinkInfo (const iLinkInfo *);
47void draw_LinkInfo (const iLinkInfo *, iInt2 topLeft);
diff --git a/src/ui/listwidget.c b/src/ui/listwidget.c
index c2ba5581..1d0f1729 100644
--- a/src/ui/listwidget.c
+++ b/src/ui/listwidget.c
@@ -49,21 +49,6 @@ iDefineClass(ListItem)
49 49
50iDefineObjectConstruction(ListWidget) 50iDefineObjectConstruction(ListWidget)
51 51
52struct Impl_ListWidget {
53 iWidget widget;
54 iScrollWidget *scroll;
55 iSmoothScroll scrollY;
56 int itemHeight;
57 iPtrArray items;
58 size_t hoverItem;
59 size_t dragItem;
60 iInt2 dragOrigin; /* offset from mouse to drag item's top-left corner */
61 iClick click;
62 iIntSet invalidItems;
63 iVisBuf *visBuf;
64 iBool noHoverWhileScrolling;
65};
66
67static void refreshWhileScrolling_ListWidget_(iAnyObject *any) { 52static void refreshWhileScrolling_ListWidget_(iAnyObject *any) {
68 iListWidget *d = any; 53 iListWidget *d = any;
69 updateVisible_ListWidget(d); 54 updateVisible_ListWidget(d);
@@ -96,11 +81,13 @@ void init_ListWidget(iListWidget *d) {
96 setThumb_ScrollWidget(d->scroll, 0, 0); 81 setThumb_ScrollWidget(d->scroll, 0, 0);
97 init_SmoothScroll(&d->scrollY, w, scrollBegan_ListWidget_); 82 init_SmoothScroll(&d->scrollY, w, scrollBegan_ListWidget_);
98 d->itemHeight = 0; 83 d->itemHeight = 0;
84 d->scrollMode = normal_ScrollMode;
99 d->noHoverWhileScrolling = iFalse; 85 d->noHoverWhileScrolling = iFalse;
100 init_PtrArray(&d->items); 86 init_PtrArray(&d->items);
101 d->hoverItem = iInvalidPos; 87 d->hoverItem = iInvalidPos;
102 d->dragItem = iInvalidPos; 88 d->dragItem = iInvalidPos;
103 d->dragOrigin = zero_I2(); 89 d->dragOrigin = zero_I2();
90 d->dragHandleWidth = 0;
104 init_Click(&d->click, d, SDL_BUTTON_LEFT); 91 init_Click(&d->click, d, SDL_BUTTON_LEFT);
105 init_IntSet(&d->invalidItems); 92 init_IntSet(&d->invalidItems);
106 d->visBuf = new_VisBuf(); 93 d->visBuf = new_VisBuf();
@@ -202,6 +189,17 @@ void setScrollPos_ListWidget(iListWidget *d, int pos) {
202 refresh_Widget(as_Widget(d)); 189 refresh_Widget(as_Widget(d));
203} 190}
204 191
192void setScrollMode_ListWidget(iListWidget *d, enum iScrollMode mode) {
193 d->scrollMode = mode;
194}
195
196void setDragHandleWidth_ListWidget(iListWidget *d, int dragHandleWidth) {
197 d->dragHandleWidth = dragHandleWidth;
198 if (dragHandleWidth == 0) {
199 setFlags_Widget(as_Widget(d), touchDrag_WidgetFlag, iFalse); /* mobile drag handles */
200 }
201}
202
205void scrollOffset_ListWidget(iListWidget *d, int offset) { 203void scrollOffset_ListWidget(iListWidget *d, int offset) {
206 moveSpan_SmoothScroll(&d->scrollY, offset, 0); 204 moveSpan_SmoothScroll(&d->scrollY, offset, 0);
207} 205}
@@ -363,6 +361,7 @@ static iBool endDrag_ListWidget_(iListWidget *d, iInt2 endPos) {
363 if (d->dragItem == iInvalidPos) { 361 if (d->dragItem == iInvalidPos) {
364 return iFalse; 362 return iFalse;
365 } 363 }
364 setFlags_Widget(as_Widget(d), touchDrag_WidgetFlag, iFalse); /* mobile drag handles */
366 stop_Anim(&d->scrollY.pos); 365 stop_Anim(&d->scrollY.pos);
367 enum iDragDestination dstKind; 366 enum iDragDestination dstKind;
368 const size_t index = resolveDragDestination_ListWidget_(d, endPos, &dstKind); 367 const size_t index = resolveDragDestination_ListWidget_(d, endPos, &dstKind);
@@ -381,12 +380,31 @@ static iBool endDrag_ListWidget_(iListWidget *d, iInt2 endPos) {
381 return iTrue; 380 return iTrue;
382} 381}
383 382
383static iBool isScrollDisabled_ListWidget_(const iListWidget *d, const SDL_Event *ev) {
384 int dir = 0;
385 if (ev->type == SDL_MOUSEWHEEL) {
386 dir = iSign(ev->wheel.y);
387 }
388 switch (d->scrollMode) {
389 case disabled_ScrollMode:
390 return iTrue;
391 case disabledAtTopBothDirections_ScrollMode:
392 return scrollPos_ListWidget(d) <= 0;
393 case disabledAtTopUpwards_ScrollMode:
394 return scrollPos_ListWidget(d) <= 0 && dir > 0;
395 default:
396 break;
397 }
398 return iFalse;
399}
400
384static iBool processEvent_ListWidget_(iListWidget *d, const SDL_Event *ev) { 401static iBool processEvent_ListWidget_(iListWidget *d, const SDL_Event *ev) {
385 iWidget *w = as_Widget(d); 402 iWidget *w = as_Widget(d);
386 if (isMetricsChange_UserEvent(ev)) { 403 if (isMetricsChange_UserEvent(ev)) {
387 invalidate_ListWidget(d); 404 invalidate_ListWidget(d);
388 } 405 }
389 else if (processEvent_SmoothScroll(&d->scrollY, ev)) { 406 else if (!isScrollDisabled_ListWidget_(d, ev) &&
407 processEvent_SmoothScroll(&d->scrollY, ev)) {
390 return iTrue; 408 return iTrue;
391 } 409 }
392 else if (isCommand_SDLEvent(ev)) { 410 else if (isCommand_SDLEvent(ev)) {
@@ -435,6 +453,29 @@ static iBool processEvent_ListWidget_(iListWidget *d, const SDL_Event *ev) {
435 } 453 }
436 } 454 }
437 if (ev->type == SDL_MOUSEWHEEL && isHover_Widget(w)) { 455 if (ev->type == SDL_MOUSEWHEEL && isHover_Widget(w)) {
456 if (d->dragHandleWidth) {
457 if (d->dragItem == iInvalidPos) {
458 const iInt2 wpos = coord_MouseWheelEvent(&ev->wheel);
459 if (contains_Widget(w, wpos) &&
460 wpos.x >= right_Rect(boundsWithoutVisualOffset_Widget(w)) - d->dragHandleWidth) {
461 setFlags_Widget(w, touchDrag_WidgetFlag, iTrue);
462 printf("[%p] touch drag started\n", d);
463 return iTrue;
464 }
465 }
466 }
467 if (isScrollDisabled_ListWidget_(d, ev)) {
468 if (ev->wheel.which == SDL_TOUCH_MOUSEID) {
469 /* TODO: Could generalize this selection of the scrollable parent. */
470 extern iWidgetClass Class_SidebarWidget;
471 iWidget *sidebar = findParentClass_Widget(w, &Class_SidebarWidget);
472 if (sidebar) {
473 transferAffinity_Touch(w, sidebar);
474 d->noHoverWhileScrolling = iTrue;
475 }
476 }
477 return iFalse;
478 }
438 int amount = -ev->wheel.y; 479 int amount = -ev->wheel.y;
439 if (isPerPixel_MouseWheelEvent(&ev->wheel)) { 480 if (isPerPixel_MouseWheelEvent(&ev->wheel)) {
440 stop_Anim(&d->scrollY.pos); 481 stop_Anim(&d->scrollY.pos);
@@ -480,7 +521,9 @@ static iBool processEvent_ListWidget_(iListWidget *d, const SDL_Event *ev) {
480 return iTrue; 521 return iTrue;
481 } 522 }
482 redrawHoverItem_ListWidget_(d); 523 redrawHoverItem_ListWidget_(d);
483 if (contains_Rect(itemRect_ListWidget(d, d->hoverItem), pos_Click(&d->click)) && 524 if (contains_Rect(adjusted_Rect(itemRect_ListWidget(d, d->hoverItem),
525 zero_I2(), init_I2(-d->dragHandleWidth, 0)),
526 pos_Click(&d->click)) &&
484 d->hoverItem != iInvalidPos) { 527 d->hoverItem != iInvalidPos) {
485 postCommand_Widget(w, "list.clicked arg:%zu item:%p", 528 postCommand_Widget(w, "list.clicked arg:%zu item:%p",
486 d->hoverItem, constHoverItem_ListWidget(d)); 529 d->hoverItem, constHoverItem_ListWidget(d));
@@ -580,8 +623,9 @@ static void draw_ListWidget_(const iListWidget *d) {
580 } 623 }
581 setClip_Paint(&p, bounds_Widget(w)); 624 setClip_Paint(&p, bounds_Widget(w));
582 draw_VisBuf(d->visBuf, addY_I2(topLeft_Rect(bounds), -scrollY), ySpan_Rect(bounds)); 625 draw_VisBuf(d->visBuf, addY_I2(topLeft_Rect(bounds), -scrollY), ySpan_Rect(bounds));
583 const iInt2 mousePos = mouseCoord_Window(get_Window(), 0); 626 const iBool isMobile = (deviceType_App() != desktop_AppDeviceType);
584 if (d->dragItem != iInvalidPos && contains_Rect(bounds, mousePos)) { 627 const iInt2 mousePos = mouseCoord_Window(get_Window(), isMobile ? SDL_TOUCH_MOUSEID : 0);
628 if (d->dragItem != iInvalidPos && (isMobile || contains_Rect(bounds, mousePos))) {
585 iInt2 pos = add_I2(mousePos, d->dragOrigin); 629 iInt2 pos = add_I2(mousePos, d->dragOrigin);
586 const iListItem *item = constAt_PtrArray(&d->items, d->dragItem); 630 const iListItem *item = constAt_PtrArray(&d->items, d->dragItem);
587 const iRect itemRect = { init_I2(left_Rect(bounds), pos.y), 631 const iRect itemRect = { init_I2(left_Rect(bounds), pos.y),
diff --git a/src/ui/listwidget.h b/src/ui/listwidget.h
index 8adf6ac3..da215b19 100644
--- a/src/ui/listwidget.h
+++ b/src/ui/listwidget.h
@@ -25,6 +25,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
25#include "scrollwidget.h" 25#include "scrollwidget.h"
26#include "paint.h" 26#include "paint.h"
27 27
28#include <the_Foundation/intset.h>
28#include <the_Foundation/ptrarray.h> 29#include <the_Foundation/ptrarray.h>
29 30
30iDeclareType(ListWidget) 31iDeclareType(ListWidget)
@@ -48,6 +49,34 @@ iDeclareObjectConstruction(ListItem)
48iDeclareWidgetClass(ListWidget) 49iDeclareWidgetClass(ListWidget)
49iDeclareObjectConstruction(ListWidget) 50iDeclareObjectConstruction(ListWidget)
50 51
52iDeclareType(VisBuf)
53
54enum iScrollMode {
55 normal_ScrollMode,
56 disabledAtTopBothDirections_ScrollMode,
57 disabledAtTopUpwards_ScrollMode,
58 disabled_ScrollMode,
59};
60
61struct Impl_ListWidget {
62 iWidget widget;
63 iScrollWidget *scroll;
64 iSmoothScroll scrollY;
65 int itemHeight;
66 iPtrArray items;
67 size_t hoverItem;
68 size_t dragItem;
69 iInt2 dragOrigin; /* offset from mouse to drag item's top-left corner */
70 int dragHandleWidth;
71 iClick click;
72 iIntSet invalidItems;
73 iVisBuf *visBuf;
74 enum iScrollMode scrollMode;
75 iBool noHoverWhileScrolling;
76};
77
78void init_ListWidget (iListWidget *);
79
51void setItemHeight_ListWidget (iListWidget *, int itemHeight); 80void setItemHeight_ListWidget (iListWidget *, int itemHeight);
52 81
53void invalidate_ListWidget (iListWidget *); 82void invalidate_ListWidget (iListWidget *);
@@ -62,6 +91,8 @@ int itemHeight_ListWidget (const iListWidget *);
62int scrollPos_ListWidget (const iListWidget *); 91int scrollPos_ListWidget (const iListWidget *);
63 92
64void setScrollPos_ListWidget (iListWidget *, int pos); 93void setScrollPos_ListWidget (iListWidget *, int pos);
94void setScrollMode_ListWidget (iListWidget *, enum iScrollMode mode);
95void setDragHandleWidth_ListWidget(iListWidget *, int dragHandleWidth);
65void scrollToItem_ListWidget (iListWidget *, size_t index, uint32_t span); 96void scrollToItem_ListWidget (iListWidget *, size_t index, uint32_t span);
66void scrollOffset_ListWidget (iListWidget *, int offset); 97void scrollOffset_ListWidget (iListWidget *, int offset);
67void scrollOffsetSpan_ListWidget (iListWidget *, int offset, uint32_t span); 98void scrollOffsetSpan_ListWidget (iListWidget *, int offset, uint32_t span);
diff --git a/src/ui/lookupwidget.c b/src/ui/lookupwidget.c
index da0113ce..f14170ad 100644
--- a/src/ui/lookupwidget.c
+++ b/src/ui/lookupwidget.c
@@ -658,23 +658,37 @@ static iBool processEvent_LookupWidget_(iLookupWidget *d, const SDL_Event *ev) {
658 (equal_Command(cmd, "layout.changed") && 658 (equal_Command(cmd, "layout.changed") &&
659 equal_Rangecc(range_Command(cmd, "id"), "navbar"))) { 659 equal_Rangecc(range_Command(cmd, "id"), "navbar"))) {
660 /* Position the lookup popup under the URL bar. */ { 660 /* Position the lookup popup under the URL bar. */ {
661 iRoot *root = w->root; 661 iRoot *root = w->root;
662 iWidget *url = findChild_Widget(root->widget, "url");
663 const int minWidth = iMin(120 * gap_UI, width_Rect(safeRect_Root(root)));
664 const int urlWidth = width_Widget(url);
665 int extraWidth = 0;
666 if (urlWidth < minWidth) {
667 extraWidth = minWidth - urlWidth;
668 }
662 const iRect navBarBounds = bounds_Widget(findChild_Widget(root->widget, "navbar")); 669 const iRect navBarBounds = bounds_Widget(findChild_Widget(root->widget, "navbar"));
663 iWidget *url = findChild_Widget(root->widget, "url"); 670 setFixedSize_Widget(
664 setFixedSize_Widget(w, init_I2(width_Widget(url), 671 w,
665 (bottom_Rect(rect_Root(root)) - bottom_Rect(navBarBounds)) / 2)); 672 init_I2(width_Widget(url) + extraWidth,
666 setPos_Widget(w, windowToLocal_Widget(w, bottomLeft_Rect(bounds_Widget(url)))); 673 (bottom_Rect(rect_Root(root)) - bottom_Rect(navBarBounds)) / 2));
667#if defined (iPlatformAppleMobile) 674 setPos_Widget(w,
675 windowToLocal_Widget(w,
676 max_I2(zero_I2(),
677 addX_I2(bottomLeft_Rect(bounds_Widget(url)),
678 -extraWidth / 2))));
679#if defined(iPlatformMobile)
668 /* TODO: Check this again. */ 680 /* TODO: Check this again. */
669 /* Adjust height based on keyboard size. */ { 681 /* Adjust height based on keyboard size. */ {
670 w->rect.size.y = bottom_Rect(visibleRect_Root(root)) - top_Rect(bounds_Widget(w)); 682 w->rect.size.y = bottom_Rect(visibleRect_Root(root)) - top_Rect(bounds_Widget(w));
683# if defined (iPlatformAppleMobile)
671 if (deviceType_App() == phone_AppDeviceType) { 684 if (deviceType_App() == phone_AppDeviceType) {
672 float l, r; 685 float l = 0.0f, r = 0.0f;
673 safeAreaInsets_iOS(&l, NULL, &r, NULL); 686 safeAreaInsets_iOS(&l, NULL, &r, NULL);
674 w->rect.size.x = size_Root(root).x - l - r; 687 w->rect.size.x = size_Root(root).x - l - r;
675 w->rect.pos.x = l; 688 w->rect.pos.x = l;
676 /* TODO: Need to use windowToLocal_Widget? */ 689 /* TODO: Need to use windowToLocal_Widget? */
677 } 690 }
691# endif
678 } 692 }
679#endif 693#endif
680 arrange_Widget(w); 694 arrange_Widget(w);
diff --git a/src/ui/mediaui.c b/src/ui/mediaui.c
index 4f2499b0..f0070688 100644
--- a/src/ui/mediaui.c
+++ b/src/ui/mediaui.c
@@ -104,7 +104,7 @@ static int drawSevenSegmentTime_(iInt2 pos, int color, int align, int seconds) {
104 if (align == right_Alignment) { 104 if (align == right_Alignment) {
105 pos.x -= size.x; 105 pos.x -= size.x;
106 } 106 }
107 drawRange_Text(font, addY_I2(pos, -gap_UI / 8), color, range_String(&num)); 107 drawRange_Text(font, addY_I2(pos, gap_UI / 2), color, range_String(&num));
108 deinit_String(&num); 108 deinit_String(&num);
109 return size.x; 109 return size.x;
110} 110}
@@ -238,6 +238,56 @@ void init_DownloadUI(iDownloadUI *d, const iMedia *media, uint16_t mediaId, iRec
238/*----------------------------------------------------------------------------------------------*/ 238/*----------------------------------------------------------------------------------------------*/
239 239
240iBool processEvent_DownloadUI(iDownloadUI *d, const SDL_Event *ev) { 240iBool processEvent_DownloadUI(iDownloadUI *d, const SDL_Event *ev) {
241 if (ev->type == SDL_MOUSEBUTTONDOWN || ev->type == SDL_MOUSEBUTTONUP) {
242 const iInt2 mouse = init_I2(ev->button.x, ev->button.y);
243 if (!contains_Rect(d->bounds, mouse)) {
244 return iFalse;
245 }
246 float bytesPerSecond;
247 const iString *path;
248 iBool isFinished;
249 downloadStats_Media(d->media, (iMediaId){ download_MediaType, d->mediaId },
250 &path, &bytesPerSecond, &isFinished);
251 if (isFinished) {
252 if (ev->button.button == SDL_BUTTON_RIGHT && ev->type == SDL_MOUSEBUTTONDOWN) {
253 const iMenuItem items[] = {
254 /* Items related to the file */
255 { openTab_Icon " ${menu.opentab}",
256 0,
257 0,
258 format_CStr("!open newtab:1 url:%s",
259 cstrCollect_String(makeFileUrl_String(path))) },
260#if defined (iPlatformAppleDesktop)
261 { "${menu.reveal.macos}",
262 0,
263 0,
264 format_CStr("!reveal path:%s", cstr_String(path)) },
265#endif
266#if defined (iPlatformAppleMobile)
267 { export_Icon " ${menu.share}",
268 0,
269 0,
270 format_CStr("!reveal path:%s", cstr_String(path)) },
271#endif
272#if defined (iPlatformLinux)
273 { "${menu.reveal.filemgr}",
274 0,
275 0,
276 format_CStr("!reveal path:%s", cstr_String(path)) },
277#endif
278 { "---" },
279 /* Generic items */
280 { "${menu.downloads}", 0, 0, "downloads.open newtab:1" },
281 };
282 openMenu_Widget(makeMenu_Widget(get_Root()->widget, items, iElemCount(items)),
283 mouse);
284 return iTrue;
285 }
286 else if (ev->button.button == SDL_BUTTON_LEFT && ev->type == SDL_MOUSEBUTTONUP) {
287 postCommandf_App("open default:1 url:%s", cstrCollect_String(makeFileUrl_String(path)));
288 }
289 }
290 }
241 return iFalse; 291 return iFalse;
242} 292}
243 293
diff --git a/src/ui/mobile.c b/src/ui/mobile.c
index abc91218..cf955423 100644
--- a/src/ui/mobile.c
+++ b/src/ui/mobile.c
@@ -23,6 +23,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
23#include "mobile.h" 23#include "mobile.h"
24 24
25#include "app.h" 25#include "app.h"
26#include "certlistwidget.h"
26#include "command.h" 27#include "command.h"
27#include "defs.h" 28#include "defs.h"
28#include "inputwidget.h" 29#include "inputwidget.h"
@@ -36,14 +37,39 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
36# include "ios.h" 37# include "ios.h"
37#endif 38#endif
38 39
40const iToolbarActionSpec toolbarActions_Mobile[max_ToolbarAction] = {
41 { backArrow_Icon, "${menu.back}", "navigate.back" },
42 { forwardArrow_Icon, "${menu.forward}", "navigate.forward" },
43 { home_Icon, "${menu.home}", "navigate.home" },
44 { upArrow_Icon, "${menu.parent}", "navigate.parent" },
45 { reload_Icon, "${menu.reload}", "navigate.reload" },
46 { openTab_Icon, "${menu.newtab}", "tabs.new" },
47 { close_Icon, "${menu.closetab}", "tabs.close" },
48 { bookmark_Icon, "${menu.page.bookmark}", "bookmark.add" },
49 { globe_Icon, "${menu.page.translate}", "document.translate" },
50 { upload_Icon, "${menu.page.upload}", "document.upload" },
51 { edit_Icon, "${menu.page.upload.edit}", "document.upload copy:1" },
52 { magnifyingGlass_Icon, "${menu.find}", "focus.set id:find.input" },
53 { gear_Icon, "${menu.settings}", "preferences" },
54 { leftHalf_Icon, "${menu.sidebar.left}", "sidebar.toggle" },
55};
56
39iBool isUsingPanelLayout_Mobile(void) { 57iBool isUsingPanelLayout_Mobile(void) {
40 return deviceType_App() != desktop_AppDeviceType; 58 return deviceType_App() != desktop_AppDeviceType;
41} 59}
42 60
61#define topPanelMinWidth_Mobile (80 * gap_UI)
62
43static iBool isSideBySideLayout_(void) { 63static iBool isSideBySideLayout_(void) {
64 /* Minimum is an even split. */
65 const int safeWidth = safeRect_Root(get_Root()).size.x;
66 if (safeWidth / 2 < topPanelMinWidth_Mobile) {
67 return iFalse;
68 }
44 if (deviceType_App() == phone_AppDeviceType) { 69 if (deviceType_App() == phone_AppDeviceType) {
45 return isLandscape_App(); 70 return isLandscape_App();
46 } 71 }
72 /* Tablet may still be too narrow. */
47 return numRoots_Window(get_Window()) == 1; 73 return numRoots_Window(get_Window()) == 1;
48} 74}
49 75
@@ -99,6 +125,16 @@ static iWidget *findTitleLabel_(iWidget *panel) {
99 return NULL; 125 return NULL;
100} 126}
101 127
128static void updateCertListHeight_(iWidget *detailStack) {
129 iWidget *certList = findChild_Widget(detailStack, "certlist");
130 if (certList) {
131 setFixedSize_Widget(certList,
132 init_I2(-1,
133 -1 * gap_UI + bottom_Rect(safeRect_Root(certList->root)) -
134 top_Rect(boundsWithoutVisualOffset_Widget(certList))));
135 }
136}
137
102static iBool mainDetailSplitHandler_(iWidget *mainDetailSplit, const char *cmd) { 138static iBool mainDetailSplitHandler_(iWidget *mainDetailSplit, const char *cmd) {
103 if (equal_Command(cmd, "window.resized")) { 139 if (equal_Command(cmd, "window.resized")) {
104 const iBool isPortrait = (deviceType_App() == phone_AppDeviceType && isPortrait_App()); 140 const iBool isPortrait = (deviceType_App() == phone_AppDeviceType && isPortrait_App());
@@ -117,11 +153,13 @@ static iBool mainDetailSplitHandler_(iWidget *mainDetailSplit, const char *cmd)
117 const int pad = isPortrait ? 0 : 3 * gap_UI; 153 const int pad = isPortrait ? 0 : 3 * gap_UI;
118 if (isSideBySide) { 154 if (isSideBySide) {
119 iAssert(topPanel); 155 iAssert(topPanel);
120 topPanel->rect.size.x = (deviceType_App() == phone_AppDeviceType ? 156 topPanel->rect.size.x = iMax(topPanelMinWidth_Mobile,
121 safeRoot.size.x * 2 / 5 : (safeRoot.size.x / 3)); 157 (deviceType_App() == phone_AppDeviceType ?
122 } 158 safeRoot.size.x * 2 / 5 : safeRoot.size.x / 3));
159 }
123 if (deviceType_App() == tablet_AppDeviceType) { 160 if (deviceType_App() == tablet_AppDeviceType) {
124 setPadding_Widget(topPanel, pad, 0, pad, pad); 161 setPadding_Widget(topPanel, pad, 0, pad, pad);
162#if 0
125 if (numPanels == 0) { 163 if (numPanels == 0) {
126 setFlags_Widget(sheet, centerHorizontal_WidgetFlag, iTrue); 164 setFlags_Widget(sheet, centerHorizontal_WidgetFlag, iTrue);
127 const int sheetWidth = iMin(safeRoot.size.x, safeRoot.size.y); 165 const int sheetWidth = iMin(safeRoot.size.x, safeRoot.size.y);
@@ -129,6 +167,7 @@ static iBool mainDetailSplitHandler_(iWidget *mainDetailSplit, const char *cmd)
129 setFixedSize_Widget(sheet, init_I2(sheetWidth, -1)); 167 setFixedSize_Widget(sheet, init_I2(sheetWidth, -1));
130 setFixedSize_Widget(navi, init_I2(sheetWidth, -1)); 168 setFixedSize_Widget(navi, init_I2(sheetWidth, -1));
131 } 169 }
170#endif
132 } 171 }
133 iWidget *detailTitle = findChild_Widget(navi, "detailtitle"); { 172 iWidget *detailTitle = findChild_Widget(navi, "detailtitle"); {
134 setPos_Widget(detailTitle, init_I2(width_Widget(topPanel), 0)); 173 setPos_Widget(detailTitle, init_I2(width_Widget(topPanel), 0));
@@ -143,9 +182,10 @@ static iBool mainDetailSplitHandler_(iWidget *mainDetailSplit, const char *cmd)
143 if (isSideBySide) { 182 if (isSideBySide) {
144 setVisualOffset_Widget(panel, 0, 0, 0); 183 setVisualOffset_Widget(panel, 0, 0, 0);
145 } 184 }
146 setPadding_Widget(panel, pad, 0, pad, pad); 185 setPadding_Widget(panel, pad, 0, pad, pad + bottomSafeInset_Mobile());
147 } 186 }
148 arrange_Widget(mainDetailSplit); 187 arrange_Widget(mainDetailSplit);
188 updateCertListHeight_(detailStack);
149 } 189 }
150 else if (equal_Command(cmd, "mouse.clicked") && arg_Command(cmd)) { 190 else if (equal_Command(cmd, "mouse.clicked") && arg_Command(cmd)) {
151 if (focus_Widget() && class_Widget(focus_Widget()) == &Class_InputWidget) { 191 if (focus_Widget() && class_Widget(focus_Widget()) == &Class_InputWidget) {
@@ -168,13 +208,16 @@ size_t currentPanelIndex_Mobile(const iWidget *panels) {
168 return iInvalidPos; 208 return iInvalidPos;
169} 209}
170 210
211iWidget *panel_Mobile(const iWidget *panels, size_t index) {
212 return child_Widget(findChild_Widget(panels, "detailstack"), index);
213}
214
171static iBool topPanelHandler_(iWidget *topPanel, const char *cmd) { 215static iBool topPanelHandler_(iWidget *topPanel, const char *cmd) {
172 const iBool isPortrait = !isSideBySideLayout_(); 216 const iBool isPortrait = !isSideBySideLayout_();
173 if (equal_Command(cmd, "panel.open")) { 217 if (equal_Command(cmd, "panel.open")) {
174 iWidget *button = pointer_Command(cmd); 218 /* This command is sent by the button that opens the panel. */
219 iWidget *button = pointer_Command(cmd);
175 iWidget *panel = userData_Object(button); 220 iWidget *panel = userData_Object(button);
176// openMenu_Widget(panel, innerToWindow_Widget(panel, zero_I2()));
177// setFlags_Widget(panel, hidden_WidgetFlag, iFalse);
178 unselectAllPanelButtons_(topPanel); 221 unselectAllPanelButtons_(topPanel);
179 int panelIndex = -1; 222 int panelIndex = -1;
180 size_t childIndex = 0; 223 size_t childIndex = 0;
@@ -184,7 +227,7 @@ static iBool topPanelHandler_(iWidget *topPanel, const char *cmd) {
184 /* Animate the current panel in. */ 227 /* Animate the current panel in. */
185 if (child == panel && isPortrait) { 228 if (child == panel && isPortrait) {
186 setupSheetTransition_Mobile(panel, iTrue); 229 setupSheetTransition_Mobile(panel, iTrue);
187 panelIndex = childIndex; 230 panelIndex = (int) childIndex;
188 } 231 }
189 childIndex++; 232 childIndex++;
190 } 233 }
@@ -196,6 +239,7 @@ static iBool topPanelHandler_(iWidget *topPanel, const char *cmd) {
196 setText_LabelWidget(detailTitle, text_LabelWidget((iLabelWidget *) findTitleLabel_(panel))); 239 setText_LabelWidget(detailTitle, text_LabelWidget((iLabelWidget *) findTitleLabel_(panel)));
197 setFlags_Widget(button, selected_WidgetFlag, iTrue); 240 setFlags_Widget(button, selected_WidgetFlag, iTrue);
198 postCommand_Widget(topPanel, "panel.changed arg:%d", panelIndex); 241 postCommand_Widget(topPanel, "panel.changed arg:%d", panelIndex);
242 updateCertListHeight_(findDetailStack_(topPanel));
199 return iTrue; 243 return iTrue;
200 } 244 }
201 if (equal_Command(cmd, "swipe.back")) { 245 if (equal_Command(cmd, "swipe.back")) {
@@ -227,6 +271,10 @@ static iBool topPanelHandler_(iWidget *topPanel, const char *cmd) {
227 else if (findWidget_App("upload")) { 271 else if (findWidget_App("upload")) {
228 postCommand_App("upload.cancel"); 272 postCommand_App("upload.cancel");
229 } 273 }
274 else if (findWidget_App("bmed.sidebar") || findWidget_App("bmed.create") ||
275 findWidget_App("bmed")) {
276 postCommand_App("bmed.cancel");
277 }
230 else if (findWidget_App("ident")) { 278 else if (findWidget_App("ident")) {
231 postCommand_Widget(topPanel, "ident.cancel"); 279 postCommand_Widget(topPanel, "ident.cancel");
232 } 280 }
@@ -468,7 +516,7 @@ void makePanelItem_Mobile(iWidget *panel, const iMenuItem *item) {
468 iLabelWidget *heading = NULL; 516 iLabelWidget *heading = NULL;
469 iWidget * value = NULL; 517 iWidget * value = NULL;
470 const char * spec = item->label; 518 const char * spec = item->label;
471 const char * id = cstr_Rangecc(range_Command(spec, "id")); 519 const char * id = cstr_Command(spec, "id");
472 const char * label = hasLabel_Command(spec, "text") 520 const char * label = hasLabel_Command(spec, "text")
473 ? suffixPtr_Command(spec, "text") 521 ? suffixPtr_Command(spec, "text")
474 : format_CStr("${%s}", id); 522 : format_CStr("${%s}", id);
@@ -482,7 +530,7 @@ void makePanelItem_Mobile(iWidget *panel, const iMenuItem *item) {
482 collapse_WidgetFlag); 530 collapse_WidgetFlag);
483 setFont_LabelWidget(title, uiLabelLargeBold_FontId); 531 setFont_LabelWidget(title, uiLabelLargeBold_FontId);
484 setTextColor_LabelWidget(title, uiHeading_ColorId); 532 setTextColor_LabelWidget(title, uiHeading_ColorId);
485 setAllCaps_LabelWidget(title, iTrue); 533// setAllCaps_LabelWidget(title, iTrue);
486 setId_Widget(as_Widget(title), id); 534 setId_Widget(as_Widget(title), id);
487 } 535 }
488 else if (equal_Command(spec, "heading")) { 536 else if (equal_Command(spec, "heading")) {
@@ -511,10 +559,12 @@ void makePanelItem_Mobile(iWidget *panel, const iMenuItem *item) {
511 setId_Widget(as_Widget(drop), id); 559 setId_Widget(as_Widget(drop), id);
512 widget = makeValuePaddingWithHeading_(heading = makeHeading_Widget(label), as_Widget(drop)); 560 widget = makeValuePaddingWithHeading_(heading = makeHeading_Widget(label), as_Widget(drop));
513 setCommandHandler_Widget(widget, dropdownHeadingHandler_); 561 setCommandHandler_Widget(widget, dropdownHeadingHandler_);
562 widget->padding[2] = gap_UI;
514 setUserData_Object(widget, drop); 563 setUserData_Object(widget, drop);
515 } 564 }
516 else if (equal_Command(spec, "radio") || equal_Command(spec, "buttons")) { 565 else if (equal_Command(spec, "radio") || equal_Command(spec, "buttons")) {
517 const iBool isRadio = equal_Command(spec, "radio"); 566 const iBool isRadio = equal_Command(spec, "radio");
567 const iBool isHorizontal = argLabel_Command(spec, "horizontal");
518 addChild_Widget(panel, iClob(makePadding_Widget(lineHeight_Text(labelFont_())))); 568 addChild_Widget(panel, iClob(makePadding_Widget(lineHeight_Text(labelFont_()))));
519 iLabelWidget *head = makeHeading_Widget(label); 569 iLabelWidget *head = makeHeading_Widget(label);
520 setAllCaps_LabelWidget(head, iTrue); 570 setAllCaps_LabelWidget(head, iTrue);
@@ -522,25 +572,42 @@ void makePanelItem_Mobile(iWidget *panel, const iMenuItem *item) {
522 addChild_Widget(panel, iClob(head)); 572 addChild_Widget(panel, iClob(head));
523 widget = new_Widget(); 573 widget = new_Widget();
524 setBackgroundColor_Widget(widget, uiBackgroundSidebar_ColorId); 574 setBackgroundColor_Widget(widget, uiBackgroundSidebar_ColorId);
525 setPadding_Widget(widget, 4 * gap_UI, 2 * gap_UI, 4 * gap_UI, 2 * gap_UI); 575 const int hPad = (isHorizontal ? 0 : 1);
576 setPadding_Widget(widget, hPad * gap_UI, 2 * gap_UI, hPad * gap_UI, 2 * gap_UI);
526 setFlags_Widget(widget, 577 setFlags_Widget(widget,
527 borderTop_WidgetFlag | 578 borderTop_WidgetFlag |
528 borderBottom_WidgetFlag | 579 borderBottom_WidgetFlag |
529 arrangeHorizontal_WidgetFlag | 580 (isHorizontal ? arrangeHorizontal_WidgetFlag : arrangeVertical_WidgetFlag) |
530 arrangeHeight_WidgetFlag | 581 arrangeHeight_WidgetFlag |
531 resizeToParentWidth_WidgetFlag | 582 resizeToParentWidth_WidgetFlag |
532 resizeWidthOfChildren_WidgetFlag, 583 resizeWidthOfChildren_WidgetFlag,
533 iTrue); 584 iTrue);
534 setId_Widget(widget, id); 585 setId_Widget(widget, id);
586 iBool isFirst = iTrue;
535 for (const iMenuItem *radioItem = item->data; radioItem->label; radioItem++) { 587 for (const iMenuItem *radioItem = item->data; radioItem->label; radioItem++) {
536 const char * radId = cstr_Rangecc(range_Command(radioItem->label, "id")); 588 if (!isHorizontal && !isFirst) {
537 int64_t flags = noBackground_WidgetFlag; 589 /* The separator is padded from the left so we need two. */
590 iWidget *sep = new_Widget();
591 iWidget *sep2 = new_Widget();
592 addChildFlags_Widget(sep, iClob(sep2), 0);
593 setFlags_Widget(sep, arrangeHeight_WidgetFlag | resizeWidthOfChildren_WidgetFlag, iTrue);
594 setBackgroundColor_Widget(sep2, uiSeparator_ColorId);
595 setFixedSize_Widget(sep2, init_I2(-1, gap_UI / 4));
596 setPadding_Widget(sep, 5 * gap_UI, 0, 0, 0);
597 addChildFlags_Widget(widget, iClob(sep), 0);
598 }
599 isFirst = iFalse;
600 const char * radId = cstr_Command(radioItem->label, "id");
601 int64_t flags = noBackground_WidgetFlag | frameless_WidgetFlag;
602 if (!isHorizontal) {
603 flags |= alignLeft_WidgetFlag;
604 }
538 iLabelWidget *button; 605 iLabelWidget *button;
539 if (isRadio) { 606 if (isRadio) {
540 const char *radLabel = 607 const char *radLabel =
541 hasLabel_Command(radioItem->label, "label") 608 hasLabel_Command(radioItem->label, "label")
542 ? format_CStr("${%s}", 609 ? format_CStr("${%s}",
543 cstr_Rangecc(range_Command(radioItem->label, "label"))) 610 cstr_Command(radioItem->label, "label"))
544 : suffixPtr_Command(radioItem->label, "text"); 611 : suffixPtr_Command(radioItem->label, "text");
545 button = new_LabelWidget(radLabel, radioItem->command); 612 button = new_LabelWidget(radLabel, radioItem->command);
546 flags |= radio_WidgetFlag; 613 flags |= radio_WidgetFlag;
@@ -549,17 +616,21 @@ void makePanelItem_Mobile(iWidget *panel, const iMenuItem *item) {
549 button = (iLabelWidget *) makeToggle_Widget(radId); 616 button = (iLabelWidget *) makeToggle_Widget(radId);
550 setTextCStr_LabelWidget(button, format_CStr("${%s}", radId)); 617 setTextCStr_LabelWidget(button, format_CStr("${%s}", radId));
551 setFlags_Widget(as_Widget(button), fixedWidth_WidgetFlag, iFalse); 618 setFlags_Widget(as_Widget(button), fixedWidth_WidgetFlag, iFalse);
552 updateSize_LabelWidget(button);
553 } 619 }
554 setId_Widget(as_Widget(button), radId); 620 setId_Widget(as_Widget(button), radId);
555 setFont_LabelWidget(button, uiLabelMedium_FontId); 621 setFont_LabelWidget(button, deviceType_App() == phone_AppDeviceType ?
622 (isHorizontal ? uiLabelMedium_FontId : uiLabelBig_FontId) : labelFont_());
623 setCheckMark_LabelWidget(button, !isHorizontal);
624 setPadding_Widget(as_Widget(button), gap_UI, 1 * gap_UI, 0, 1 * gap_UI);
625 updateSize_LabelWidget(button);
626 setPadding_Widget(widget, 0, 0, 0, 0);
556 addChildFlags_Widget(widget, iClob(button), flags); 627 addChildFlags_Widget(widget, iClob(button), flags);
557 } 628 }
558 } 629 }
559 else if (equal_Command(spec, "input")) { 630 else if (equal_Command(spec, "input")) {
560 iInputWidget *input = new_InputWidget(argU32Label_Command(spec, "maxlen")); 631 iInputWidget *input = new_InputWidget(argU32Label_Command(spec, "maxlen"));
561 if (hasLabel_Command(spec, "hint")) { 632 if (hasLabel_Command(spec, "hint")) {
562 setHint_InputWidget(input, cstr_Lang(cstr_Rangecc(range_Command(spec, "hint")))); 633 setHint_InputWidget(input, cstr_Lang(cstr_Command(spec, "hint")));
563 } 634 }
564 setId_Widget(as_Widget(input), id); 635 setId_Widget(as_Widget(input), id);
565 setUrlContent_InputWidget(input, argLabel_Command(spec, "url")); 636 setUrlContent_InputWidget(input, argLabel_Command(spec, "url"));
@@ -570,12 +641,13 @@ void makePanelItem_Mobile(iWidget *panel, const iMenuItem *item) {
570 setFlags_Widget(widget, expand_WidgetFlag, iTrue); 641 setFlags_Widget(widget, expand_WidgetFlag, iTrue);
571 } 642 }
572 else { 643 else {
644 setFlags_Widget(as_Widget(input), alignRight_WidgetFlag, iTrue);
573 setContentPadding_InputWidget(input, 3 * gap_UI, 0); 645 setContentPadding_InputWidget(input, 3 * gap_UI, 0);
574 if (hasLabel_Command(spec, "unit")) { 646 if (hasLabel_Command(spec, "unit")) {
575 iWidget *unit = addChildFlags_Widget( 647 iWidget *unit = addChildFlags_Widget(
576 as_Widget(input), 648 as_Widget(input),
577 iClob(new_LabelWidget( 649 iClob(new_LabelWidget(
578 format_CStr("${%s}", cstr_Rangecc(range_Command(spec, "unit"))), NULL)), 650 format_CStr("${%s}", cstr_Command(spec, "unit")), NULL)),
579 frameless_WidgetFlag | moveToParentRightEdge_WidgetFlag | 651 frameless_WidgetFlag | moveToParentRightEdge_WidgetFlag |
580 resizeToParentHeight_WidgetFlag); 652 resizeToParentHeight_WidgetFlag);
581 setContentPadding_InputWidget(input, -1, width_Widget(unit) - 4 * gap_UI); 653 setContentPadding_InputWidget(input, -1, width_Widget(unit) - 4 * gap_UI);
@@ -586,6 +658,14 @@ void makePanelItem_Mobile(iWidget *panel, const iMenuItem *item) {
586 setUserData_Object(widget, input); 658 setUserData_Object(widget, input);
587 } 659 }
588 } 660 }
661 else if (equal_Command(spec, "certlist")) {
662 iCertListWidget *certList = new_CertListWidget();
663 iListWidget *list = (iListWidget *) certList;
664 setBackgroundColor_Widget(as_Widget(list), uiBackgroundSidebar_ColorId);
665 widget = as_Widget(certList);
666 updateItems_CertListWidget(certList);
667 invalidate_ListWidget(list);
668 }
589 else if (equal_Command(spec, "button")) { 669 else if (equal_Command(spec, "button")) {
590 widget = as_Widget(heading = makePanelButton_(label, item->command)); 670 widget = as_Widget(heading = makePanelButton_(label, item->command));
591 setFlags_Widget(widget, selected_WidgetFlag, argLabel_Command(spec, "selected") != 0); 671 setFlags_Widget(widget, selected_WidgetFlag, argLabel_Command(spec, "selected") != 0);
@@ -599,6 +679,9 @@ void makePanelItem_Mobile(iWidget *panel, const iMenuItem *item) {
599 fixedHeight_WidgetFlag | 679 fixedHeight_WidgetFlag |
600 (!argLabel_Command(spec, "frame") ? frameless_WidgetFlag : 0), 680 (!argLabel_Command(spec, "frame") ? frameless_WidgetFlag : 0),
601 iTrue); 681 iTrue);
682 if (argLabel_Command(spec, "font")) {
683 setFont_LabelWidget(lab, argLabel_Command(spec, "font"));
684 }
602 } 685 }
603 else if (equal_Command(spec, "padding")) { 686 else if (equal_Command(spec, "padding")) {
604 float height = 1.5f; 687 float height = 1.5f;
@@ -670,8 +753,10 @@ void initPanels_Mobile(iWidget *panels, iWidget *parentWidget,
670 setFlags_Widget(panels, 753 setFlags_Widget(panels,
671 resizeToParentWidth_WidgetFlag | resizeToParentHeight_WidgetFlag | 754 resizeToParentWidth_WidgetFlag | resizeToParentHeight_WidgetFlag |
672 frameless_WidgetFlag | focusRoot_WidgetFlag | commandOnClick_WidgetFlag | 755 frameless_WidgetFlag | focusRoot_WidgetFlag | commandOnClick_WidgetFlag |
756 horizontalOffset_WidgetFlag |
673 /*overflowScrollable_WidgetFlag |*/ leftEdgeDraggable_WidgetFlag, 757 /*overflowScrollable_WidgetFlag |*/ leftEdgeDraggable_WidgetFlag,
674 iTrue); 758 iTrue);
759 panels->flags2 |= fadeBackground_WidgetFlag2;
675 setFlags_Widget(panels, overflowScrollable_WidgetFlag, iFalse); 760 setFlags_Widget(panels, overflowScrollable_WidgetFlag, iFalse);
676 /* The top-level split between main and detail panels. */ 761 /* The top-level split between main and detail panels. */
677 iWidget *mainDetailSplit = makeHDiv_Widget(); { 762 iWidget *mainDetailSplit = makeHDiv_Widget(); {
@@ -733,7 +818,7 @@ void initPanels_Mobile(iWidget *panels, iWidget *parentWidget,
733 const iMenuItem *item = &itemsNullTerminated[i]; 818 const iMenuItem *item = &itemsNullTerminated[i];
734 if (equal_Command(item->label, "panel")) { 819 if (equal_Command(item->label, "panel")) {
735 haveDetailPanels = iTrue; 820 haveDetailPanels = iTrue;
736 const char *id = cstr_Rangecc(range_Command(item->label, "id")); 821 const char *id = cstr_Command(item->label, "id");
737 const iString *label = hasLabel_Command(item->label, "text") 822 const iString *label = hasLabel_Command(item->label, "text")
738 ? collect_String(suffix_Command(item->label, "text")) 823 ? collect_String(suffix_Command(item->label, "text"))
739 : collectNewFormat_String("${%s}", id); 824 : collectNewFormat_String("${%s}", id);
@@ -849,18 +934,21 @@ void setupMenuTransition_Mobile(iWidget *sheet, iBool isIncoming) {
849 if (!isUsingPanelLayout_Mobile()) { 934 if (!isUsingPanelLayout_Mobile()) {
850 return; 935 return;
851 } 936 }
852 const iBool isSlidePanel = (flags_Widget(sheet) & horizontalOffset_WidgetFlag) != 0; 937 const iBool isHorizPanel = (flags_Widget(sheet) & horizontalOffset_WidgetFlag) != 0;
853 if (isSlidePanel && isLandscape_App()) { 938 if (isHorizPanel && isLandscape_App()) {
854 return; 939 return;
855 } 940 }
941 const int maxOffset = isHorizPanel ? width_Widget(sheet)
942 : isPortraitPhone_App() ? height_Widget(sheet)
943 : (12 * gap_UI);
856 if (isIncoming) { 944 if (isIncoming) {
857 setVisualOffset_Widget(sheet, isSlidePanel ? width_Widget(sheet) : height_Widget(sheet), 0, 0); 945 setVisualOffset_Widget(sheet, maxOffset, 0, 0);
858 setVisualOffset_Widget(sheet, 0, 330, easeOut_AnimFlag | softer_AnimFlag); 946 setVisualOffset_Widget(sheet, 0, 330, easeOut_AnimFlag | softer_AnimFlag);
859 } 947 }
860 else { 948 else {
861 const iBool wasDragged = iAbs(value_Anim(&sheet->visualOffset) - 0) > 1; 949 const iBool wasDragged = iAbs(value_Anim(&sheet->visualOffset) - 0) > 1;
862 setVisualOffset_Widget(sheet, 950 setVisualOffset_Widget(sheet,
863 isSlidePanel ? width_Widget(sheet) : height_Widget(sheet), 951 maxOffset,
864 wasDragged ? 100 : 200, 952 wasDragged ? 100 : 200,
865 wasDragged ? 0 : easeIn_AnimFlag | softer_AnimFlag); 953 wasDragged ? 0 : easeIn_AnimFlag | softer_AnimFlag);
866 } 954 }
@@ -930,3 +1018,13 @@ void setupSheetTransition_Mobile(iWidget *sheet, int flags) {
930 } 1018 }
931 } 1019 }
932} 1020}
1021
1022int bottomSafeInset_Mobile(void) {
1023#if defined (iPlatformAppleMobile)
1024 float bot;
1025 safeAreaInsets_iOS(NULL, NULL, NULL, &bot);
1026 return iRound(bot);
1027#else
1028 return 0;
1029#endif
1030}
diff --git a/src/ui/mobile.h b/src/ui/mobile.h
index 9d7ac8e4..a719f20b 100644
--- a/src/ui/mobile.h
+++ b/src/ui/mobile.h
@@ -22,8 +22,19 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22 22
23#pragma once 23#pragma once
24 24
25#include "defs.h"
25#include <the_Foundation/rect.h> 26#include <the_Foundation/rect.h>
26 27
28iDeclareType(ToolbarActionSpec)
29
30struct Impl_ToolbarActionSpec {
31 const char *icon;
32 const char *label;
33 const char *command;
34};
35
36extern const iToolbarActionSpec toolbarActions_Mobile[max_ToolbarAction];
37
27iDeclareType(Widget) 38iDeclareType(Widget)
28iDeclareType(MenuItem) 39iDeclareType(MenuItem)
29 40
@@ -39,6 +50,7 @@ void initPanels_Mobile (iWidget *panels, iWidget *parentWidget,
39 const iMenuItem *itemsNullTerminated, 50 const iMenuItem *itemsNullTerminated,
40 const iMenuItem *actions, size_t numActions); 51 const iMenuItem *actions, size_t numActions);
41 52
53iWidget * panel_Mobile (const iWidget *panels, size_t index);
42size_t currentPanelIndex_Mobile (const iWidget *panels); 54size_t currentPanelIndex_Mobile (const iWidget *panels);
43 55
44enum iTransitionFlags { 56enum iTransitionFlags {
@@ -55,3 +67,5 @@ enum iTransitionDir {
55 67
56void setupMenuTransition_Mobile (iWidget *menu, iBool isIncoming); 68void setupMenuTransition_Mobile (iWidget *menu, iBool isIncoming);
57void setupSheetTransition_Mobile (iWidget *sheet, int flags); 69void setupSheetTransition_Mobile (iWidget *sheet, int flags);
70
71int bottomSafeInset_Mobile (void);
diff --git a/src/ui/paint.c b/src/ui/paint.c
index b92be27e..5e66f521 100644
--- a/src/ui/paint.c
+++ b/src/ui/paint.c
@@ -41,6 +41,7 @@ void init_Paint(iPaint *d) {
41 d->dst = get_Window(); 41 d->dst = get_Window();
42 d->setTarget = NULL; 42 d->setTarget = NULL;
43 d->oldTarget = NULL; 43 d->oldTarget = NULL;
44 d->oldOrigin = zero_I2();
44 d->alpha = 255; 45 d->alpha = 255;
45} 46}
46 47
@@ -48,6 +49,8 @@ void beginTarget_Paint(iPaint *d, SDL_Texture *target) {
48 SDL_Renderer *rend = renderer_Paint_(d); 49 SDL_Renderer *rend = renderer_Paint_(d);
49 if (!d->setTarget) { 50 if (!d->setTarget) {
50 d->oldTarget = SDL_GetRenderTarget(rend); 51 d->oldTarget = SDL_GetRenderTarget(rend);
52 d->oldOrigin = origin_Paint;
53 origin_Paint = zero_I2();
51 SDL_SetRenderTarget(rend, target); 54 SDL_SetRenderTarget(rend, target);
52 d->setTarget = target; 55 d->setTarget = target;
53 } 56 }
@@ -59,8 +62,10 @@ void beginTarget_Paint(iPaint *d, SDL_Texture *target) {
59void endTarget_Paint(iPaint *d) { 62void endTarget_Paint(iPaint *d) {
60 if (d->setTarget) { 63 if (d->setTarget) {
61 SDL_SetRenderTarget(renderer_Paint_(d), d->oldTarget); 64 SDL_SetRenderTarget(renderer_Paint_(d), d->oldTarget);
65 origin_Paint = d->oldOrigin;
66 d->oldOrigin = zero_I2();
62 d->oldTarget = NULL; 67 d->oldTarget = NULL;
63 d->setTarget = NULL; 68 d->setTarget = NULL;
64 } 69 }
65} 70}
66 71
@@ -108,7 +113,7 @@ void drawRect_Paint(const iPaint *d, iRect rect, int color) {
108 { left_Rect(rect), br.y }, 113 { left_Rect(rect), br.y },
109 { left_Rect(rect), top_Rect(rect) } 114 { left_Rect(rect), top_Rect(rect) }
110 }; 115 };
111#if SDL_VERSION_ATLEAST(2, 0, 16) 116#if SDL_COMPILEDVERSION == SDL_VERSIONNUM(2, 0, 16)
112 if (isOpenGLRenderer_Window()) { 117 if (isOpenGLRenderer_Window()) {
113 /* A very curious regression in SDL 2.0.16. */ 118 /* A very curious regression in SDL 2.0.16. */
114 edges[3].y--; 119 edges[3].y--;
@@ -170,7 +175,7 @@ void drawLines_Paint(const iPaint *d, const iInt2 *points, size_t n, int color)
170 for (size_t i = 0; i < n; i++) { 175 for (size_t i = 0; i < n; i++) {
171 offsetPoints[i] = add_I2(points[i], origin_Paint); 176 offsetPoints[i] = add_I2(points[i], origin_Paint);
172 } 177 }
173 SDL_RenderDrawLines(renderer_Paint_(d), (const SDL_Point *) offsetPoints, n); 178 SDL_RenderDrawLines(renderer_Paint_(d), (const SDL_Point *) offsetPoints, (int) n);
174 free(offsetPoints); 179 free(offsetPoints);
175} 180}
176 181
diff --git a/src/ui/paint.h b/src/ui/paint.h
index e894b62f..dfc9260d 100644
--- a/src/ui/paint.h
+++ b/src/ui/paint.h
@@ -33,6 +33,7 @@ struct Impl_Paint {
33 iWindow * dst; 33 iWindow * dst;
34 SDL_Texture *setTarget; 34 SDL_Texture *setTarget;
35 SDL_Texture *oldTarget; 35 SDL_Texture *oldTarget;
36 iInt2 oldOrigin;
36 uint8_t alpha; 37 uint8_t alpha;
37}; 38};
38 39
diff --git a/src/ui/root.c b/src/ui/root.c
index cf13169d..5c4296cf 100644
--- a/src/ui/root.c
+++ b/src/ui/root.c
@@ -38,6 +38,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
38#include "../history.h" 38#include "../history.h"
39#include "../gmcerts.h" 39#include "../gmcerts.h"
40#include "../gmutil.h" 40#include "../gmutil.h"
41#include "../sitespec.h"
41#include "../visited.h" 42#include "../visited.h"
42 43
43#if defined (iPlatformMsys) 44#if defined (iPlatformMsys)
@@ -94,16 +95,16 @@ static const iMenuItem tabletNavMenuItems_[] = {
94 { close_Icon " ${menu.closetab}", 'w', KMOD_PRIMARY, "tabs.close" }, 95 { close_Icon " ${menu.closetab}", 'w', KMOD_PRIMARY, "tabs.close" },
95 { "---" }, 96 { "---" },
96 { magnifyingGlass_Icon " ${menu.find}", 0, 0, "focus.set id:find.input" }, 97 { magnifyingGlass_Icon " ${menu.find}", 0, 0, "focus.set id:find.input" },
97 { leftHalf_Icon " ${menu.sidebar.left}", SDLK_l, KMOD_PRIMARY | KMOD_SHIFT, "sidebar.toggle" }, 98// { leftHalf_Icon " ${menu.sidebar.left}", SDLK_l, KMOD_PRIMARY | KMOD_SHIFT, "sidebar.toggle" },
98 { rightHalf_Icon " ${menu.sidebar.right}", SDLK_p, KMOD_PRIMARY | KMOD_SHIFT, "sidebar2.toggle" }, 99 { rightHalf_Icon " ${menu.sidebar.right}", SDLK_p, KMOD_PRIMARY | KMOD_SHIFT, "sidebar2.toggle" },
99 { "${menu.view.split}", SDLK_j, KMOD_PRIMARY, "splitmenu.open" }, 100 { "${menu.view.split}", SDLK_j, KMOD_PRIMARY, "splitmenu.open" },
100 { "---" }, 101 { "---" },
101 { book_Icon " ${menu.bookmarks.list}", 0, 0, "!open url:about:bookmarks" }, 102 { book_Icon " ${menu.bookmarks.list}", 0, 0, "!open url:about:bookmarks" },
102 { "${menu.bookmarks.bytag}", 0, 0, "!open url:about:bookmarks?tags" }, 103 { "${menu.bookmarks.bytag}", 0, 0, "!open url:about:bookmarks?tags" },
103 { "${menu.feeds.entrylist}", 0, 0, "!open url:about:feeds" }, 104 { "${menu.feeds.entrylist}", 0, 0, "!open url:about:feeds" },
104 { "${menu.downloads}", 0, 0, "downloads.open" }, 105 //{ "${menu.downloads}", 0, 0, "downloads.open" },
105 { "---" }, 106 { "---" },
106 { gear_Icon " ${menu.preferences}", SDLK_COMMA, KMOD_PRIMARY, "preferences" }, 107 { gear_Icon " ${menu.settings}", SDLK_COMMA, KMOD_PRIMARY, "preferences" },
107 { "${menu.help}", SDLK_F1, 0, "!open url:about:help" }, 108 { "${menu.help}", SDLK_F1, 0, "!open url:about:help" },
108 { "${menu.releasenotes}", 0, 0, "!open url:about:version" }, 109 { "${menu.releasenotes}", 0, 0, "!open url:about:version" },
109}; 110};
@@ -115,10 +116,10 @@ static const iMenuItem phoneNavMenuItems_[] = {
115 { close_Icon " ${menu.closetab}", 'w', KMOD_PRIMARY, "tabs.close" }, 116 { close_Icon " ${menu.closetab}", 'w', KMOD_PRIMARY, "tabs.close" },
116 { "---" }, 117 { "---" },
117 { magnifyingGlass_Icon " ${menu.find}", 0, 0, "focus.set id:find.input" }, 118 { magnifyingGlass_Icon " ${menu.find}", 0, 0, "focus.set id:find.input" },
118 { leftHalf_Icon " ${menu.sidebar}", SDLK_l, KMOD_PRIMARY | KMOD_SHIFT, "sidebar.toggle" }, 119// { leftHalf_Icon " ${menu.sidebar}", SDLK_l, KMOD_PRIMARY | KMOD_SHIFT, "sidebar.toggle" },
119 { "---" }, 120 { "---" },
120 { book_Icon " ${menu.bookmarks.list}", 0, 0, "!open url:about:bookmarks" }, 121 { book_Icon " ${menu.bookmarks.list}", 0, 0, "!open url:about:bookmarks" },
121 { "${menu.downloads}", 0, 0, "downloads.open" }, 122 //{ "${menu.downloads}", 0, 0, "downloads.open" },
122 { "${menu.feeds.entrylist}", 0, 0, "!open url:about:feeds" }, 123 { "${menu.feeds.entrylist}", 0, 0, "!open url:about:feeds" },
123 { "---" }, 124 { "---" },
124 { gear_Icon " ${menu.settings}", SDLK_COMMA, KMOD_PRIMARY, "preferences" }, 125 { gear_Icon " ${menu.settings}", SDLK_COMMA, KMOD_PRIMARY, "preferences" },
@@ -240,6 +241,7 @@ static int loadAnimIndex_ = 0;
240static iRoot * activeRoot_ = NULL; 241static iRoot * activeRoot_ = NULL;
241 242
242iDefineTypeConstruction(Root) 243iDefineTypeConstruction(Root)
244iDefineAudienceGetter(Root, visualOffsetsChanged)
243 245
244void init_Root(iRoot *d) { 246void init_Root(iRoot *d) {
245 iZap(*d); 247 iZap(*d);
@@ -249,6 +251,7 @@ void deinit_Root(iRoot *d) {
249 iReleasePtr(&d->widget); 251 iReleasePtr(&d->widget);
250 delete_PtrArray(d->onTop); 252 delete_PtrArray(d->onTop);
251 delete_PtrSet(d->pendingDestruction); 253 delete_PtrSet(d->pendingDestruction);
254 delete_Audience(d->visualOffsetsChanged);
252} 255}
253 256
254void setCurrent_Root(iRoot *root) { 257void setCurrent_Root(iRoot *root) {
@@ -315,9 +318,12 @@ static iBool handleRootCommands_(iWidget *root, const char *cmd) {
315 if (equal_Command(cmd, "menu.open")) { 318 if (equal_Command(cmd, "menu.open")) {
316 iWidget *button = pointer_Command(cmd); 319 iWidget *button = pointer_Command(cmd);
317 iWidget *menu = findChild_Widget(button, "menu"); 320 iWidget *menu = findChild_Widget(button, "menu");
321 const iBool isPlacedUnder = argLabel_Command(cmd, "under");
318 iAssert(menu); 322 iAssert(menu);
319 if (!isVisible_Widget(menu)) { 323 if (!isVisible_Widget(menu)) {
320 openMenu_Widget(menu, topLeft_Rect(bounds_Widget(button))); 324 openMenu_Widget(menu,
325 isPlacedUnder ? bottomLeft_Rect(bounds_Widget(button))
326 : topLeft_Rect(bounds_Widget(button)));
321 } 327 }
322 else { 328 else {
323 closeMenu_Widget(menu); 329 closeMenu_Widget(menu);
@@ -330,6 +336,93 @@ static iBool handleRootCommands_(iWidget *root, const char *cmd) {
330 openMenuFlags_Widget(menu, zero_I2(), postCommands_MenuOpenFlags | center_MenuOpenFlags); 336 openMenuFlags_Widget(menu, zero_I2(), postCommands_MenuOpenFlags | center_MenuOpenFlags);
331 return iTrue; 337 return iTrue;
332 } 338 }
339 else if (deviceType_App() == tablet_AppDeviceType && equal_Command(cmd, "toolbar.showident")) {
340 /* No toolbar on tablet, so we handle this command here. */
341 postCommand_App("preferences idents:1");
342 return iTrue;
343 }
344 else if (equal_Command(cmd, "identmenu.open")) {
345 iWidget *toolBar = findWidget_Root("toolbar");
346 iWidget *button = findWidget_Root(toolBar && isPortraitPhone_App() ? "toolbar.ident" : "navbar.ident");
347 iArray items;
348 init_Array(&items, sizeof(iMenuItem));
349 /* Current identity. */
350 const iString *docUrl = url_DocumentWidget(document_App());
351 const iGmIdentity *ident = identityForUrl_GmCerts(certs_App(), docUrl);
352 const iString *fp = ident ? collect_String(hexEncode_Block(&ident->fingerprint)) : NULL;
353 iString *str = NULL;
354 if (ident) {
355 str = copy_String(name_GmIdentity(ident));
356 if (!isEmpty_String(&ident->notes)) {
357 appendFormat_String(str, "\n\x1b[0m" uiHeading_ColorEscape "%s", cstr_String(&ident->notes));
358 }
359 }
360 pushBack_Array(
361 &items,
362 &(iMenuItem){ format_CStr("```" uiHeading_ColorEscape "\x1b[1m%s",
363 str ? cstr_String(str) : "${menu.identity.notactive}") });
364 if (ident && isUsedOn_GmIdentity(ident, docUrl)) {
365 pushBack_Array(&items,
366 &(iMenuItem){ close_Icon " ${ident.stopuse}",
367 0,
368 0,
369 format_CStr("ident.signout ident:%s url:%s",
370 cstr_String(fp),
371 cstr_String(docUrl)) });
372 }
373 pushBack_Array(&items, &(iMenuItem){ "---" });
374 delete_String(str);
375 /* Alternate identities. */
376 const iString *site = collectNewRange_String(urlRoot_String(docUrl));
377 iBool haveAlts = iFalse;
378 iConstForEach(StringArray, i, strings_SiteSpec(site, usedIdentities_SiteSpecKey)) {
379 if (!fp || !equal_String(i.value, fp)) {
380 const iBlock *otherFp = collect_Block(hexDecode_Rangecc(range_String(i.value)));
381 const iGmIdentity *other = findIdentity_GmCerts(certs_App(), otherFp);
382 if (other && other != ident) {
383 pushBack_Array(
384 &items,
385 &(iMenuItem){
386 format_CStr(translateCStr_Lang("\U0001f816 ${ident.switch}"),
387 format_CStr("\x1b[1m%s",
388 cstr_String(name_GmIdentity(other)))),
389 0,
390 0,
391 format_CStr("ident.switch fp:%s", cstr_String(i.value)) });
392 haveAlts = iTrue;
393 }
394 }
395 }
396 if (haveAlts) {
397 pushBack_Array(&items, &(iMenuItem){ "---" });
398 }
399 iSidebarWidget *sidebar = findWidget_App("sidebar");
400 pushBackN_Array(
401 &items,
402 (iMenuItem[]){
403 { add_Icon " ${menu.identity.new}", newIdentity_KeyShortcut, "ident.new" },
404 { "${menu.identity.import}", SDLK_i, KMOD_PRIMARY | KMOD_SHIFT, "ident.import" },
405 { "---" } }, 3);
406 if (deviceType_App() == desktop_AppDeviceType) {
407 pushBack_Array(&items,
408 &(iMenuItem){ isVisible_Widget(sidebar) && mode_SidebarWidget(sidebar) ==
409 identities_SidebarMode
410 ? leftHalf_Icon " ${menu.hide.identities}"
411 : leftHalf_Icon " ${menu.show.identities}",
412 0,
413 0,
414 "sidebar.mode arg:3 toggle:1" });
415 }
416 else {
417 pushBack_Array(&items, &(iMenuItem){ gear_Icon " ${menu.identities}", 0, 0,
418 "toolbar.showident"});
419 }
420 iWidget *menu =
421 makeMenu_Widget(button, constData_Array(&items), size_Array(&items));
422 openMenu_Widget(menu, bottomLeft_Rect(bounds_Widget(button)));
423 deinit_Array(&items);
424 return iTrue;
425 }
333 else if (equal_Command(cmd, "contextclick")) { 426 else if (equal_Command(cmd, "contextclick")) {
334 iBool showBarMenu = iFalse; 427 iBool showBarMenu = iFalse;
335 if (equal_Rangecc(range_Command(cmd, "id"), "buttons")) { 428 if (equal_Rangecc(range_Command(cmd, "id"), "buttons")) {
@@ -351,7 +444,7 @@ static iBool handleRootCommands_(iWidget *root, const char *cmd) {
351 return iFalse; 444 return iFalse;
352 } 445 }
353 else if (equal_Command(cmd, "focus.set")) { 446 else if (equal_Command(cmd, "focus.set")) {
354 setFocus_Widget(findWidget_App(cstr_Rangecc(range_Command(cmd, "id")))); 447 setFocus_Widget(findWidget_App(cstr_Command(cmd, "id")));
355 return iTrue; 448 return iTrue;
356 } 449 }
357 else if (equal_Command(cmd, "input.resized")) { 450 else if (equal_Command(cmd, "input.resized")) {
@@ -401,14 +494,38 @@ static iBool handleRootCommands_(iWidget *root, const char *cmd) {
401 SDL_PushEvent(&(SDL_Event){ .type = SDL_QUIT }); 494 SDL_PushEvent(&(SDL_Event){ .type = SDL_QUIT });
402 return iTrue; 495 return iTrue;
403 } 496 }
497 else if (deviceType_App() == tablet_AppDeviceType && equal_Command(cmd, "window.resized")) {
498 iSidebarWidget *sidebar = findChild_Widget(root, "sidebar");
499 iSidebarWidget *sidebar2 = findChild_Widget(root, "sidebar2");
500 setWidth_SidebarWidget(sidebar, 73.0f);
501 setWidth_SidebarWidget(sidebar2, 73.0f);
502 return iFalse;
503 }
404 else if (deviceType_App() == phone_AppDeviceType && equal_Command(cmd, "window.resized")) { 504 else if (deviceType_App() == phone_AppDeviceType && equal_Command(cmd, "window.resized")) {
405 /* Place the sidebar next to or under doctabs depending on orientation. */ 505 /* Place the sidebar next to or under doctabs depending on orientation. */
406 iSidebarWidget *sidebar = findChild_Widget(root, "sidebar"); 506 iSidebarWidget *sidebar = findChild_Widget(root, "sidebar");
407 iSidebarWidget *sidebar2 = findChild_Widget(root, "sidebar2");
408 removeChild_Widget(parent_Widget(sidebar), sidebar); 507 removeChild_Widget(parent_Widget(sidebar), sidebar);
409 // setBackgroundColor_Widget(findChild_Widget(as_Widget(sidebar), "buttons"), 508 iChangeFlags(as_Widget(sidebar)->flags2, fadeBackground_WidgetFlag2, isPortrait_App());
410 // isPortrait_App() ? uiBackgroundUnfocusedSelection_ColorId 509 if (isLandscape_App()) {
411 // : uiBackgroundSidebar_ColorId); 510 setVisualOffset_Widget(as_Widget(sidebar), 0, 0, 0);
511 addChildPos_Widget(findChild_Widget(root, "tabs.content"), iClob(sidebar), front_WidgetAddPos);
512 setWidth_SidebarWidget(sidebar, 73.0f);
513 setFlags_Widget(as_Widget(sidebar), fixedHeight_WidgetFlag | fixedPosition_WidgetFlag, iFalse);
514 }
515 else {
516 addChild_Widget(root, iClob(sidebar));
517 setWidth_SidebarWidget(sidebar, (float) width_Widget(root) / (float) gap_UI);
518 int midHeight = height_Widget(root) / 2;// + lineHeight_Text(uiLabelLarge_FontId);
519#if defined (iPlatformAndroidMobile)
520 midHeight += 2 * lineHeight_Text(uiLabelLarge_FontId);
521#endif
522 setMidHeight_SidebarWidget(sidebar, midHeight);
523 setFixedSize_Widget(as_Widget(sidebar), init_I2(-1, midHeight));
524 setPos_Widget(as_Widget(sidebar), init_I2(0, height_Widget(root) - midHeight));
525 }
526#if 0
527 iSidebarWidget *sidebar = findChild_Widget(root, "sidebar");
528 iSidebarWidget *sidebar2 = findChild_Widget(root, "sidebar2");
412 setFlags_Widget(findChild_Widget(as_Widget(sidebar), "buttons"), 529 setFlags_Widget(findChild_Widget(as_Widget(sidebar), "buttons"),
413 borderTop_WidgetFlag, 530 borderTop_WidgetFlag,
414 isPortrait_App()); 531 isPortrait_App());
@@ -424,6 +541,20 @@ static iBool handleRootCommands_(iWidget *root, const char *cmd) {
424 setWidth_SidebarWidget(sidebar, (float) width_Widget(root) / (float) gap_UI); 541 setWidth_SidebarWidget(sidebar, (float) width_Widget(root) / (float) gap_UI);
425 setWidth_SidebarWidget(sidebar2, (float) width_Widget(root) / (float) gap_UI); 542 setWidth_SidebarWidget(sidebar2, (float) width_Widget(root) / (float) gap_UI);
426 } 543 }
544#endif
545 return iFalse;
546 }
547 else if (equal_Command(cmd, "root.arrange")) {
548 iWidget *prefs = findWidget_Root("prefs");
549 if (prefs) {
550 updatePreferencesLayout_Widget(prefs);
551 }
552 root->root->pendingArrange = iFalse;
553 return iTrue;
554 }
555 else if (equal_Command(cmd, "theme.changed")) {
556 /* The phone toolbar is draw-buffered so it needs refreshing. */
557 refresh_Widget(findWidget_App("toolbar"));
427 return iFalse; 558 return iFalse;
428 } 559 }
429 else if (handleCommand_App(cmd)) { 560 else if (handleCommand_App(cmd)) {
@@ -449,22 +580,54 @@ static void updateNavBarIdentity_(iWidget *navBar) {
449 iLabelWidget *toolName = findWidget_App("toolbar.name"); 580 iLabelWidget *toolName = findWidget_App("toolbar.name");
450 if (toolName) { 581 if (toolName) {
451 setOutline_LabelWidget(toolButton, ident == NULL); 582 setOutline_LabelWidget(toolButton, ident == NULL);
452 updateTextCStr_LabelWidget(toolName, subjectName ? cstr_String(subjectName) : ""); 583 /* Fit the name in the widget. */
584 if (subjectName) {
585 const char *endPos;
586 tryAdvanceNoWrap_Text(uiLabelTiny_FontId, range_String(subjectName), width_Widget(toolName),
587 &endPos);
588 updateText_LabelWidget(
589 toolName,
590 collectNewRange_String((iRangecc){ constBegin_String(subjectName), endPos }));
591 }
592 else {
593 updateTextCStr_LabelWidget(toolName, "");
594 }
453 setFont_LabelWidget(toolButton, subjectName ? uiLabelMedium_FontId : uiLabelLarge_FontId); 595 setFont_LabelWidget(toolButton, subjectName ? uiLabelMedium_FontId : uiLabelLarge_FontId);
454 arrange_Widget(parent_Widget(toolButton)); 596 setTextOffset_LabelWidget(toolButton, init_I2(0, subjectName ? -1.5f * gap_UI : 0));
597 arrange_Widget(parent_Widget(toolButton));
455 } 598 }
456} 599}
457 600
458static void updateNavDirButtons_(iWidget *navBar) { 601static void updateNavDirButtons_(iWidget *navBar) {
459 const iHistory *history = history_DocumentWidget(document_App()); 602 iBeginCollect();
460 setFlags_Widget(findChild_Widget(navBar, "navbar.back"), disabled_WidgetFlag, 603 const iHistory *history = history_DocumentWidget(document_App());
461 atOldest_History(history)); 604 const iBool atOldest = atOldest_History(history);
462 setFlags_Widget(findChild_Widget(navBar, "navbar.forward"), disabled_WidgetFlag, 605 const iBool atNewest = atNewest_History(history);
463 atLatest_History(history)); 606 /* Reset button state. */
464 setFlags_Widget(findWidget_App("toolbar.back"), disabled_WidgetFlag, 607 for (size_t i = 0; i < maxNavbarActions_Prefs; i++) {
465 atOldest_History(history)); 608 const char *id = format_CStr("navbar.action%d", i + 1);
466 setFlags_Widget(findWidget_App("toolbar.forward"), disabled_WidgetFlag, 609 setFlags_Widget(findChild_Widget(navBar, id), disabled_WidgetFlag, iFalse);
467 atLatest_History(history)); 610 }
611 setFlags_Widget(as_Widget(findMenuItem_Widget(navBar, "navigate.back")), disabled_WidgetFlag, atOldest);
612 setFlags_Widget(as_Widget(findMenuItem_Widget(navBar, "navigate.forward")), disabled_WidgetFlag, atNewest);
613 iWidget *toolBar = findWidget_App("toolbar");
614 if (toolBar) {
615 /* Reset the state. */
616 for (int i = 0; i < 2; i++) {
617 const char *id = (i == 0 ? "toolbar.action1" : "toolbar.action2");
618 setFlags_Widget(findChild_Widget(toolBar, id), disabled_WidgetFlag, iFalse);
619 setOutline_LabelWidget(findChild_Widget(toolBar, id), iFalse);
620 }
621 /* Disable certain actions. */
622 iLabelWidget *back = findMenuItem_Widget(toolBar, "navigate.back");
623 iLabelWidget *fwd = findMenuItem_Widget(toolBar, "navigate.forward");
624 setFlags_Widget(as_Widget(back), disabled_WidgetFlag, atOldest);
625 setOutline_LabelWidget(back, atOldest);
626 setFlags_Widget(as_Widget(fwd), disabled_WidgetFlag, atNewest);
627 setOutline_LabelWidget(fwd, atNewest);
628 refresh_Widget(toolBar);
629 }
630 iEndCollect();
468} 631}
469 632
470static const char *loadAnimationCStr_(void) { 633static const char *loadAnimationCStr_(void) {
@@ -502,10 +665,10 @@ static void checkLoadAnimation_Root_(iRoot *d) {
502 665
503void updatePadding_Root(iRoot *d) { 666void updatePadding_Root(iRoot *d) {
504 if (d == NULL) return; 667 if (d == NULL) return;
505 iWidget *toolBar = findChild_Widget(d->widget, "toolbar");
506 float bottom = 0.0f;
507#if defined (iPlatformAppleMobile) 668#if defined (iPlatformAppleMobile)
669 iWidget *toolBar = findChild_Widget(d->widget, "toolbar");
508 float left, top, right; 670 float left, top, right;
671 float bottom = 0.0f;
509 safeAreaInsets_iOS(&left, &top, &right, &bottom); 672 safeAreaInsets_iOS(&left, &top, &right, &bottom);
510 /* Respect the safe area insets. */ { 673 /* Respect the safe area insets. */ {
511 setPadding_Widget(findChild_Widget(d->widget, "navdiv"), left, top, right, 0); 674 setPadding_Widget(findChild_Widget(d->widget, "navdiv"), left, top, right, 0);
@@ -514,15 +677,6 @@ void updatePadding_Root(iRoot *d) {
514 } 677 }
515 } 678 }
516#endif 679#endif
517 if (toolBar) {
518 /* TODO: get this from toolBar height, but it's buggy for some reason */
519 const int sidebarBottomPad = isPortrait_App() ? 11 * gap_UI + bottom : 0;
520 setPadding_Widget(findChild_Widget(d->widget, "sidebar"), 0, 0, 0, sidebarBottomPad);
521 setPadding_Widget(findChild_Widget(d->widget, "sidebar2"), 0, 0, 0, sidebarBottomPad);
522 /* TODO: There seems to be unrelated layout glitch in the sidebar where its children
523 are not arranged correctly until it's hidden and reshown. */
524 }
525 /* Note that `handleNavBarCommands_` also adjusts padding and spacing. */
526} 680}
527 681
528void updateToolbarColors_Root(iRoot *d) { 682void updateToolbarColors_Root(iRoot *d) {
@@ -536,17 +690,25 @@ void updateToolbarColors_Root(iRoot *d) {
536 tmBannerBackground_ColorId; 690 tmBannerBackground_ColorId;
537 setBackgroundColor_Widget(toolBar, bg); 691 setBackgroundColor_Widget(toolBar, bg);
538 iForEach(ObjectList, i, children_Widget(toolBar)) { 692 iForEach(ObjectList, i, children_Widget(toolBar)) {
539 iLabelWidget *btn = i.object; 693// iLabelWidget *btn = i.object;
540 setTextColor_LabelWidget(i.object, isSidebarVisible ? uiTextDim_ColorId : 694 setTextColor_LabelWidget(i.object, isSidebarVisible ? uiTextDim_ColorId :
541 tmBannerIcon_ColorId); 695 tmBannerIcon_ColorId);
542 setBackgroundColor_Widget(i.object, bg); /* using noBackground, but ident has outline */ 696 setBackgroundColor_Widget(i.object, bg); /* using noBackground, but ident has outline */
543 } 697 }
698 setTextColor_LabelWidget(findChild_Widget(toolBar, "toolbar.name"),
699 isSidebarVisible ? uiTextDim_ColorId : tmBannerIcon_ColorId);
544 } 700 }
545#else 701#else
546 iUnused(d); 702 iUnused(d);
547#endif 703#endif
548} 704}
549 705
706void notifyVisualOffsetChange_Root(iRoot *d) {
707 if (d && (d->didAnimateVisualOffsets || d->didChangeArrangement)) {
708 iNotifyAudience(d, visualOffsetsChanged, RootVisualOffsetsChanged);
709 }
710}
711
550void dismissPortraitPhoneSidebars_Root(iRoot *d) { 712void dismissPortraitPhoneSidebars_Root(iRoot *d) {
551 if (deviceType_App() == phone_AppDeviceType && isPortrait_App()) { 713 if (deviceType_App() == phone_AppDeviceType && isPortrait_App()) {
552 iWidget *sidebar = findChild_Widget(d->widget, "sidebar"); 714 iWidget *sidebar = findChild_Widget(d->widget, "sidebar");
@@ -617,16 +779,15 @@ static void updateNavBarSize_(iWidget *navBar) {
617 const iBool isPhone = deviceType_App() == phone_AppDeviceType; 779 const iBool isPhone = deviceType_App() == phone_AppDeviceType;
618 const iBool isNarrow = !isPhone && isNarrow_Root(navBar->root); 780 const iBool isNarrow = !isPhone && isNarrow_Root(navBar->root);
619 /* Adjust navbar padding. */ { 781 /* Adjust navbar padding. */ {
620 int hPad = isPhone && isPortrait_App() ? 0 : (isPhone || isNarrow) ? gap_UI / 2 782 int hPad = isPortraitPhone_App() ? 0 : isPhone || isNarrow ? gap_UI / 2 : (gap_UI * 3 / 2);
621 : gap_UI * 3 / 2; 783 int vPad = gap_UI * 3 / 2;
622 int vPad = gap_UI * 3 / 2;
623 int topPad = !findWidget_Root("winbar") ? gap_UI / 2 : 0; 784 int topPad = !findWidget_Root("winbar") ? gap_UI / 2 : 0;
624 setPadding_Widget(navBar, hPad, vPad / 3 + topPad, hPad, vPad / 2); 785 setPadding_Widget(navBar, hPad, vPad / 3 + topPad, hPad, vPad / 2);
625 } 786 }
626 /* Button sizing. */ 787 /* Button sizing. */
627 if (isNarrow ^ ((flags_Widget(navBar) & tight_WidgetFlag) != 0)) { 788 if (isNarrow ^ ((flags_Widget(navBar) & tight_WidgetFlag) != 0)) {
628 setFlags_Widget(navBar, tight_WidgetFlag, isNarrow); 789 setFlags_Widget(navBar, tight_WidgetFlag, isNarrow);
629 showCollapsed_Widget(findChild_Widget(navBar, "navbar.sidebar"), !isNarrow); 790 showCollapsed_Widget(findChild_Widget(navBar, "navbar.action3"), !isNarrow);
630 iObjectList *lists[] = { 791 iObjectList *lists[] = {
631 children_Widget(navBar), 792 children_Widget(navBar),
632 children_Widget(findChild_Widget(navBar, "url")), 793 children_Widget(findChild_Widget(navBar, "url")),
@@ -647,8 +808,8 @@ static void updateNavBarSize_(iWidget *navBar) {
647 updateUrlInputContentPadding_(navBar); 808 updateUrlInputContentPadding_(navBar);
648 } 809 }
649 if (isPhone) { 810 if (isPhone) {
650 static const char *buttons[] = { "navbar.back", "navbar.forward", "navbar.sidebar", 811 static const char *buttons[] = { "navbar.action1", "navbar.action2", "navbar.action3",
651 "navbar.ident", "navbar.home", "navbar.menu" }; 812 "navbar.action4", "navbar.ident", "navbar.menu" };
652 iWidget *toolBar = findWidget_Root("toolbar"); 813 iWidget *toolBar = findWidget_Root("toolbar");
653 setVisualOffset_Widget(toolBar, 0, 0, 0); 814 setVisualOffset_Widget(toolBar, 0, 0, 0);
654 setFlags_Widget(toolBar, hidden_WidgetFlag, isLandscape_App()); 815 setFlags_Widget(toolBar, hidden_WidgetFlag, isLandscape_App());
@@ -673,6 +834,22 @@ static void updateNavBarSize_(iWidget *navBar) {
673 postCommand_Widget(navBar, "layout.changed id:navbar"); 834 postCommand_Widget(navBar, "layout.changed id:navbar");
674} 835}
675 836
837static void updateNavBarActions_(iWidget *navBar) {
838 const iPrefs *prefs = prefs_App();
839 for (size_t i = 0; i < iElemCount(prefs->navbarActions); i++) {
840 iBeginCollect();
841 const int action = prefs->navbarActions[i];
842 iLabelWidget *button =
843 findChild_Widget(navBar, format_CStr("navbar.action%d", i + 1));
844 if (button) {
845 setFlags_Widget(as_Widget(button), disabled_WidgetFlag, iFalse);
846 updateTextCStr_LabelWidget(button, toolbarActions_Mobile[action].icon);
847 setCommand_LabelWidget(button, collectNewCStr_String(toolbarActions_Mobile[action].command));
848 }
849 iEndCollect();
850 }
851}
852
676static iBool handleNavBarCommands_(iWidget *navBar, const char *cmd) { 853static iBool handleNavBarCommands_(iWidget *navBar, const char *cmd) {
677 if (equal_Command(cmd, "window.resized") || equal_Command(cmd, "metrics.changed")) { 854 if (equal_Command(cmd, "window.resized") || equal_Command(cmd, "metrics.changed")) {
678 updateNavBarSize_(navBar); 855 updateNavBarSize_(navBar);
@@ -685,7 +862,41 @@ static iBool handleNavBarCommands_(iWidget *navBar, const char *cmd) {
685 } 862 }
686 return iFalse; 863 return iFalse;
687 } 864 }
865 else if (equal_Command(cmd, "navbar.actions.changed")) {
866 updateNavBarActions_(navBar);
867 return iTrue;
868 }
869 else if (equal_Command(cmd, "contextclick")) {
870 const iRangecc id = range_Command(cmd, "id");
871 if (id.start && startsWith_CStr(id.start, "navbar.action")) {
872 const int buttonIndex = id.end[-1] - '1';
873 iArray items;
874 init_Array(&items, sizeof(iMenuItem));
875 pushBack_Array(&items, &(iMenuItem){ "```${menu.toolbar.setaction}" });
876 for (size_t i = 0; i < max_ToolbarAction; i++) {
877 pushBack_Array(
878 &items,
879 &(iMenuItem){
880 format_CStr(
881 "%s %s", toolbarActions_Mobile[i].icon, toolbarActions_Mobile[i].label),
882 0,
883 0,
884 format_CStr("navbar.action.set arg:%d button:%d", i, buttonIndex) });
885 }
886 openMenu_Widget(
887 makeMenu_Widget(get_Root()->widget, constData_Array(&items), size_Array(&items)),
888 coord_Command(cmd));
889 deinit_Array(&items);
890 return iTrue;
891 }
892 return iFalse;
893 }
688 else if (equal_Command(cmd, "navigate.focus")) { 894 else if (equal_Command(cmd, "navigate.focus")) {
895 /* The upload dialog has its own path field. */
896 if (findWidget_App("upload")) {
897 postCommand_App("focus.set id:upload.path");
898 return iTrue;
899 }
689 iWidget *url = findChild_Widget(navBar, "url"); 900 iWidget *url = findChild_Widget(navBar, "url");
690 if (focus_Widget() != url) { 901 if (focus_Widget() != url) {
691 setFocus_Widget(findChild_Widget(navBar, "url")); 902 setFocus_Widget(findChild_Widget(navBar, "url"));
@@ -710,6 +921,8 @@ static iBool handleNavBarCommands_(iWidget *navBar, const char *cmd) {
710 } 921 }
711 else if (equal_Command(cmd, "navbar.clear")) { 922 else if (equal_Command(cmd, "navbar.clear")) {
712 iInputWidget *url = findChild_Widget(navBar, "url"); 923 iInputWidget *url = findChild_Widget(navBar, "url");
924 setText_InputWidget(url, collectNew_String());
925#if 0
713 selectAll_InputWidget(url); 926 selectAll_InputWidget(url);
714 /* Emulate a Backspace keypress. */ 927 /* Emulate a Backspace keypress. */
715 class_InputWidget(url)->processEvent( 928 class_InputWidget(url)->processEvent(
@@ -718,6 +931,7 @@ static iBool handleNavBarCommands_(iWidget *navBar, const char *cmd) {
718 .timestamp = SDL_GetTicks(), 931 .timestamp = SDL_GetTicks(),
719 .state = SDL_PRESSED, 932 .state = SDL_PRESSED,
720 .keysym = { .sym = SDLK_BACKSPACE } }); 933 .keysym = { .sym = SDLK_BACKSPACE } });
934#endif
721 return iTrue; 935 return iTrue;
722 } 936 }
723 else if (equal_Command(cmd, "navbar.cancel")) { 937 else if (equal_Command(cmd, "navbar.cancel")) {
@@ -780,6 +994,26 @@ static iBool handleNavBarCommands_(iWidget *navBar, const char *cmd) {
780 dismissPortraitPhoneSidebars_Root(get_Root()); 994 dismissPortraitPhoneSidebars_Root(get_Root());
781 updateNavBarIdentity_(navBar); 995 updateNavBarIdentity_(navBar);
782 updateNavDirButtons_(navBar); 996 updateNavDirButtons_(navBar);
997 /* Update site-specific used identities. */ {
998 const iGmIdentity *ident =
999 identityForUrl_GmCerts(certs_App(), url_DocumentWidget(document_App()));
1000 if (ident) {
1001 const iString *site =
1002 collectNewRange_String(urlRoot_String(canonicalUrl_String(urlStr)));
1003 const iStringArray *usedIdents =
1004 strings_SiteSpec(site, usedIdentities_SiteSpecKey);
1005 const iString *fingerprint = collect_String(hexEncode_Block(&ident->fingerprint));
1006 /* Keep this identity at the end of the list. */
1007 removeString_SiteSpec(site, usedIdentities_SiteSpecKey, fingerprint);
1008 insertString_SiteSpec(site, usedIdentities_SiteSpecKey, fingerprint);
1009 /* Keep the list short. */
1010 while (size_StringArray(usedIdents) > 5) {
1011 removeString_SiteSpec(site,
1012 usedIdentities_SiteSpecKey,
1013 constAt_StringArray(usedIdents, 0));
1014 }
1015 }
1016 }
783 /* Icon updates should be limited to automatically chosen icons if the user 1017 /* Icon updates should be limited to automatically chosen icons if the user
784 is allowed to pick their own in the future. */ 1018 is allowed to pick their own in the future. */
785 if (updateBookmarkIcon_Bookmarks(bookmarks_App(), urlStr, 1019 if (updateBookmarkIcon_Bookmarks(bookmarks_App(), urlStr,
@@ -860,7 +1094,14 @@ static iBool handleSearchBarCommands_(iWidget *searchBar, const char *cmd) {
860 else if (equal_Command(cmd, "focus.gained")) { 1094 else if (equal_Command(cmd, "focus.gained")) {
861 if (pointer_Command(cmd) == findChild_Widget(searchBar, "find.input")) { 1095 if (pointer_Command(cmd) == findChild_Widget(searchBar, "find.input")) {
862 if (!isVisible_Widget(searchBar)) { 1096 if (!isVisible_Widget(searchBar)) {
1097 /* InputWidget will unfocus itself if there isn't enough space for editing
1098 text. A collapsed widget will not have been arranged yet, so on the first
1099 time the widget will just be unfocused immediately. */
1100 const iBool wasArranged = area_Rect(bounds_Widget(searchBar)) > 0;
863 showCollapsed_Widget(searchBar, iTrue); 1101 showCollapsed_Widget(searchBar, iTrue);
1102 if (!wasArranged) {
1103 postCommand_App("focus.set id:find.input");
1104 }
864 } 1105 }
865 } 1106 }
866 } 1107 }
@@ -878,14 +1119,21 @@ static iBool handleSearchBarCommands_(iWidget *searchBar, const char *cmd) {
878} 1119}
879 1120
880#if defined (iPlatformMobile) 1121#if defined (iPlatformMobile)
881static void dismissSidebar_(iWidget *sidebar, const char *toolButtonId) { 1122
882 if (isVisible_Widget(sidebar)) { 1123static void updateToolBarActions_(iWidget *toolBar) {
883 postCommandf_App("%s.toggle", cstr_String(id_Widget(sidebar))); 1124 const iPrefs *prefs = prefs_App();
884// if (toolButtonId) { 1125 for (int i = 0; i < 2; i++) {
885 // setFlags_Widget(findWidget_App(toolButtonId), noBackground_WidgetFlag, iTrue); 1126 const int action = prefs->toolbarActions[i];
886// } 1127 iLabelWidget *button =
887 setVisualOffset_Widget(sidebar, height_Widget(sidebar), 250, easeIn_AnimFlag); 1128 findChild_Widget(toolBar, i == 0 ? "toolbar.action1" : "toolbar.action2");
1129 if (button) {
1130 setFlags_Widget(as_Widget(button), disabled_WidgetFlag, iFalse);
1131 setOutline_LabelWidget(button, iFalse);
1132 updateTextCStr_LabelWidget(button, toolbarActions_Mobile[action].icon);
1133 setCommand_LabelWidget(button, collectNewCStr_String(toolbarActions_Mobile[action].command));
1134 }
888 } 1135 }
1136 refresh_Widget(toolBar);
889} 1137}
890 1138
891static iBool handleToolBarCommands_(iWidget *toolBar, const char *cmd) { 1139static iBool handleToolBarCommands_(iWidget *toolBar, const char *cmd) {
@@ -897,57 +1145,20 @@ static iBool handleToolBarCommands_(iWidget *toolBar, const char *cmd) {
897 return iTrue; 1145 return iTrue;
898 } 1146 }
899 else if (equal_Command(cmd, "toolbar.showview")) { 1147 else if (equal_Command(cmd, "toolbar.showview")) {
900 /* TODO: Clean this up. */
901 iWidget *sidebar = findWidget_App("sidebar");
902 iWidget *sidebar2 = findWidget_App("sidebar2");
903 dismissSidebar_(sidebar2, "toolbar.ident");
904 const iBool isVisible = isVisible_Widget(sidebar);
905 // setFlags_Widget(findChild_Widget(toolBar, "toolbar.view"), noBackground_WidgetFlag,
906 // isVisible);
907 /* If a sidebar hasn't been shown yet, it's height is zero. */
908 const int viewHeight = size_Root(get_Root()).y;
909 if (arg_Command(cmd) >= 0) { 1148 if (arg_Command(cmd) >= 0) {
910 postCommandf_App("sidebar.mode arg:%d show:1", arg_Command(cmd)); 1149 postCommandf_App("sidebar.mode arg:%d show:1", arg_Command(cmd));
911// if (!isVisible) {
912// setVisualOffset_Widget(sidebar, viewHeight, 0, 0);
913// setVisualOffset_Widget(sidebar, 0, 400, easeOut_AnimFlag | softer_AnimFlag);
914// }
915 } 1150 }
916 else { 1151 else {
917 postCommandf_App("sidebar.toggle"); 1152 postCommandf_App("sidebar.toggle");
918// if (isVisible) {
919// setVisualOffset_Widget(sidebar, height_Widget(sidebar), 250, easeIn_AnimFlag);
920// }
921// else {
922// setVisualOffset_Widget(sidebar, viewHeight, 0, 0);
923// setVisualOffset_Widget(sidebar, 0, 400, easeOut_AnimFlag | softer_AnimFlag);
924// }
925 } 1153 }
926 return iTrue; 1154 return iTrue;
927 } 1155 }
928 else if (equal_Command(cmd, "toolbar.showident")) { 1156 else if (equal_Command(cmd, "toolbar.showident")) {
929 /* TODO: Clean this up. */ 1157 iWidget *sidebar = findWidget_App("sidebar");
930 iWidget *sidebar = findWidget_App("sidebar");
931 iWidget *sidebar2 = findWidget_App("sidebar2");
932 //dismissSidebar_(sidebar, "toolbar.view");
933 if (isVisible_Widget(sidebar)) { 1158 if (isVisible_Widget(sidebar)) {
934 postCommandf_App("sidebar.toggle"); 1159 postCommandf_App("sidebar.toggle");
935 } 1160 }
936 const iBool isVisible = isVisible_Widget(sidebar2); 1161 postCommand_App("preferences idents:1");
937 // setFlags_Widget(findChild_Widget(toolBar, "toolbar.ident"), noBackground_WidgetFlag,
938 // isVisible);
939 /* If a sidebar hasn't been shown yet, it's height is zero. */
940 const int viewHeight = size_Root(get_Root()).y;
941 if (isVisible) {
942 dismissSidebar_(sidebar2, NULL);
943 }
944 else {
945 postCommand_App("sidebar2.mode arg:3 show:1");
946 int offset = height_Widget(sidebar2);
947 if (offset == 0) offset = size_Root(get_Root()).y;
948 setVisualOffset_Widget(sidebar2, offset, 0, 0);
949 setVisualOffset_Widget(sidebar2, 0, 400, easeOut_AnimFlag | softer_AnimFlag);
950 }
951 return iTrue; 1162 return iTrue;
952 } 1163 }
953 else if (equal_Command(cmd, "sidebar.mode.changed")) { 1164 else if (equal_Command(cmd, "sidebar.mode.changed")) {
@@ -955,8 +1166,13 @@ static iBool handleToolBarCommands_(iWidget *toolBar, const char *cmd) {
955 updateTextCStr_LabelWidget(viewTool, icon_SidebarMode(arg_Command(cmd))); 1166 updateTextCStr_LabelWidget(viewTool, icon_SidebarMode(arg_Command(cmd)));
956 return iFalse; 1167 return iFalse;
957 } 1168 }
1169 else if (equal_Command(cmd, "toolbar.actions.changed")) {
1170 updateToolBarActions_(toolBar);
1171 return iFalse;
1172 }
958 return iFalse; 1173 return iFalse;
959} 1174}
1175
960#endif /* defined (iPlatformMobile) */ 1176#endif /* defined (iPlatformMobile) */
961 1177
962static iLabelWidget *newLargeIcon_LabelWidget(const char *text, const char *cmd) { 1178static iLabelWidget *newLargeIcon_LabelWidget(const char *text, const char *cmd) {
@@ -987,40 +1203,22 @@ void updateMetrics_Root(iRoot *d) {
987 setFixedSize_Widget(appClose, appMin->rect.size); 1203 setFixedSize_Widget(appClose, appMin->rect.size);
988 setFixedSize_Widget(appIcon, init_I2(appIconSize_Root(), appMin->rect.size.y)); 1204 setFixedSize_Widget(appIcon, init_I2(appIconSize_Root(), appMin->rect.size.y));
989 } 1205 }
990 iWidget *navBar = findChild_Widget(d->widget, "navbar"); 1206 iWidget *navBar = findChild_Widget(d->widget, "navbar");
991// iWidget *lock = findChild_Widget(navBar, "navbar.lock"); 1207 iWidget *url = findChild_Widget(d->widget, "url");
992 iWidget *url = findChild_Widget(d->widget, "url"); 1208 iWidget *rightEmbed = findChild_Widget(navBar, "url.rightembed");
993 iWidget *rightEmbed = findChild_Widget(navBar, "url.rightembed"); 1209 iWidget *embedPad = findChild_Widget(navBar, "url.embedpad");
994 iWidget *embedPad = findChild_Widget(navBar, "url.embedpad"); 1210 iWidget *urlButtons = findChild_Widget(navBar, "url.buttons");
995 iWidget *urlButtons = findChild_Widget(navBar, "url.buttons"); 1211 iLabelWidget *idName = findChild_Widget(d->widget, "toolbar.name");
996 setPadding_Widget(as_Widget(url), 0, gap_UI, 0, gap_UI); 1212 setPadding_Widget(as_Widget(url), 0, gap_UI, 0, gap_UI);
997 navBar->rect.size.y = 0; /* recalculate height based on children (FIXME: shouldn't be needed) */ 1213// navBar->rect.size.y = 0; /* recalculate height based on children (FIXME: shouldn't be needed) */
998// updateSize_LabelWidget((iLabelWidget *) lock);
999// updateSize_LabelWidget((iLabelWidget *) findChild_Widget(navBar, "reload"));
1000// arrange_Widget(urlButtons);
1001 setFixedSize_Widget(embedPad, init_I2(width_Widget(urlButtons) + gap_UI / 2, 1)); 1214 setFixedSize_Widget(embedPad, init_I2(width_Widget(urlButtons) + gap_UI / 2, 1));
1002// setContentPadding_InputWidget((iInputWidget *) url, width_Widget(lock) * 0.75,
1003// width_Widget(lock) * 0.75);
1004 rightEmbed->rect.pos.y = gap_UI; 1215 rightEmbed->rect.pos.y = gap_UI;
1005 updatePadding_Root(d); 1216 updatePadding_Root(d);
1006 arrange_Widget(d->widget); 1217 arrange_Widget(d->widget);
1007 updateUrlInputContentPadding_(navBar); 1218 updateUrlInputContentPadding_(navBar);
1008 /* Position the toolbar identity name label manually. */ { 1219 if (idName) {
1009 iLabelWidget *idName = findChild_Widget(d->widget, "toolbar.name"); 1220 setFixedSize_Widget(as_Widget(idName),
1010 if (idName) { 1221 init_I2(-1, 2 * gap_UI + lineHeight_Text(uiLabelTiny_FontId)));
1011 const iWidget *toolBar = findChild_Widget(d->widget, "toolbar");
1012 const iWidget *viewButton = findChild_Widget(d->widget, "toolbar.view");
1013 const iWidget *idButton = findChild_Widget(toolBar, "toolbar.ident");
1014 const int font = uiLabelTiny_FontId;
1015 setFont_LabelWidget(idName, font);
1016 setPos_Widget(as_Widget(idName),
1017 windowToLocal_Widget(as_Widget(idName),
1018 init_I2(left_Rect(bounds_Widget(idButton)),
1019 bottom_Rect(bounds_Widget(viewButton)) -
1020 lineHeight_Text(font) - gap_UI / 2)));
1021 setFixedSize_Widget(as_Widget(idName), init_I2(width_Widget(idButton),
1022 lineHeight_Text(font)));
1023 }
1024 } 1222 }
1025 postRefresh_App(); 1223 postRefresh_App();
1026} 1224}
@@ -1044,11 +1242,9 @@ void createUserInterface_Root(iRoot *d) {
1044 setFlags_Widget( 1242 setFlags_Widget(
1045 root, resizeChildren_WidgetFlag | fixedSize_WidgetFlag | focusRoot_WidgetFlag, iTrue); 1243 root, resizeChildren_WidgetFlag | fixedSize_WidgetFlag | focusRoot_WidgetFlag, iTrue);
1046 setCommandHandler_Widget(root, handleRootCommands_); 1244 setCommandHandler_Widget(root, handleRootCommands_);
1047
1048 iWidget *div = makeVDiv_Widget(); 1245 iWidget *div = makeVDiv_Widget();
1049 setId_Widget(div, "navdiv"); 1246 setId_Widget(div, "navdiv");
1050 addChild_Widget(root, iClob(div)); 1247 addChild_Widget(root, iClob(div));
1051
1052#if defined (LAGRANGE_ENABLE_CUSTOM_FRAME) 1248#if defined (LAGRANGE_ENABLE_CUSTOM_FRAME)
1053 /* Window title bar. */ 1249 /* Window title bar. */
1054 if (prefs_App()->customFrame) { 1250 if (prefs_App()->customFrame) {
@@ -1117,14 +1313,14 @@ void createUserInterface_Root(iRoot *d) {
1117 addUnsplitButton_(navBar); 1313 addUnsplitButton_(navBar);
1118#endif 1314#endif
1119 iWidget *navBack; 1315 iWidget *navBack;
1120 setId_Widget(navBack = addChildFlags_Widget(navBar, iClob(newIcon_LabelWidget(backArrow_Icon, 0, 0, "navigate.back")), collapse_WidgetFlag), "navbar.back"); 1316 setId_Widget(navBack = addChildFlags_Widget(navBar, iClob(newIcon_LabelWidget(backArrow_Icon, 0, 0, "navigate.back")), collapse_WidgetFlag), "navbar.action1");
1121 setId_Widget(addChildFlags_Widget(navBar, iClob(newIcon_LabelWidget(forwardArrow_Icon, 0, 0, "navigate.forward")), collapse_WidgetFlag), "navbar.forward"); 1317 setId_Widget(addChildFlags_Widget(navBar, iClob(newIcon_LabelWidget(forwardArrow_Icon, 0, 0, "navigate.forward")), collapse_WidgetFlag), "navbar.action2");
1122 /* Button for toggling the left sidebar. */ 1318 /* Button for toggling the left sidebar. */
1123 setId_Widget(addChildFlags_Widget( 1319 setId_Widget(addChildFlags_Widget(
1124 navBar, 1320 navBar,
1125 iClob(newIcon_LabelWidget(leftHalf_Icon, 0, 0, "sidebar.toggle")), 1321 iClob(newIcon_LabelWidget(leftHalf_Icon, 0, 0, "sidebar.toggle")),
1126 collapse_WidgetFlag), 1322 collapse_WidgetFlag),
1127 "navbar.sidebar"); 1323 "navbar.action3");
1128 addChildFlags_Widget(navBar, iClob(new_Widget()), expand_WidgetFlag); 1324 addChildFlags_Widget(navBar, iClob(new_Widget()), expand_WidgetFlag);
1129 iInputWidget *url; 1325 iInputWidget *url;
1130 /* URL input field. */ { 1326 /* URL input field. */ {
@@ -1245,22 +1441,25 @@ void createUserInterface_Root(iRoot *d) {
1245 { book_Icon " ${menu.page.import}", 0, 0, "bookmark.links confirm:1" }, 1441 { book_Icon " ${menu.page.import}", 0, 0, "bookmark.links confirm:1" },
1246 { globe_Icon " ${menu.page.translate}", 0, 0, "document.translate" }, 1442 { globe_Icon " ${menu.page.translate}", 0, 0, "document.translate" },
1247 { upload_Icon " ${menu.page.upload}", 0, 0, "document.upload" }, 1443 { upload_Icon " ${menu.page.upload}", 0, 0, "document.upload" },
1444 { "${menu.page.upload.edit}", 0, 0, "document.upload copy:1" },
1248 { "---" }, 1445 { "---" },
1249 { "${menu.page.copyurl}", 0, 0, "document.copylink" }, 1446 { "${menu.page.copyurl}", 0, 0, "document.copylink" },
1250 { "${menu.page.copysource}", 'c', KMOD_PRIMARY, "copy" }, 1447 { "${menu.page.copysource}", 'c', KMOD_PRIMARY, "copy" },
1251 { download_Icon " " saveToDownloads_Label, SDLK_s, KMOD_PRIMARY, "document.save" } }, 1448 { download_Icon " " saveToDownloads_Label, SDLK_s, KMOD_PRIMARY, "document.save" } },
1252 12); 1449 14);
1253 setId_Widget(as_Widget(pageMenuButton), "pagemenubutton"); 1450 setId_Widget(as_Widget(pageMenuButton), "pagemenubutton");
1254 setFont_LabelWidget(pageMenuButton, uiContentBold_FontId); 1451 setFont_LabelWidget(pageMenuButton, uiContentBold_FontId);
1255 setAlignVisually_LabelWidget(pageMenuButton, iTrue); 1452 setAlignVisually_LabelWidget(pageMenuButton, iTrue);
1256 addChildFlags_Widget(urlButtons, iClob(pageMenuButton), 1453 addChildFlags_Widget(urlButtons, iClob(pageMenuButton),
1257 embedFlags | tight_WidgetFlag | collapse_WidgetFlag); 1454 embedFlags | tight_WidgetFlag | collapse_WidgetFlag |
1455 resizeToParentHeight_WidgetFlag);
1258 updateSize_LabelWidget(pageMenuButton); 1456 updateSize_LabelWidget(pageMenuButton);
1259 } 1457 }
1260 /* Reload button. */ { 1458 /* Reload button. */ {
1261 iLabelWidget *reload = newIcon_LabelWidget(reloadCStr_, 0, 0, "navigate.reload"); 1459 iLabelWidget *reload = newIcon_LabelWidget(reloadCStr_, 0, 0, "navigate.reload");
1262 setId_Widget(as_Widget(reload), "reload"); 1460 setId_Widget(as_Widget(reload), "reload");
1263 addChildFlags_Widget(urlButtons, iClob(reload), embedFlags | collapse_WidgetFlag); 1461 addChildFlags_Widget(urlButtons, iClob(reload), embedFlags | collapse_WidgetFlag |
1462 resizeToParentHeight_WidgetFlag);
1264 updateSize_LabelWidget(reload); 1463 updateSize_LabelWidget(reload);
1265 } 1464 }
1266 addChildFlags_Widget(as_Widget(url), iClob(urlButtons), moveToParentRightEdge_WidgetFlag); 1465 addChildFlags_Widget(as_Widget(url), iClob(urlButtons), moveToParentRightEdge_WidgetFlag);
@@ -1268,17 +1467,16 @@ void createUserInterface_Root(iRoot *d) {
1268 setId_Widget(addChild_Widget(rightEmbed, iClob(makePadding_Widget(0))), "url.embedpad"); 1467 setId_Widget(addChild_Widget(rightEmbed, iClob(makePadding_Widget(0))), "url.embedpad");
1269 } 1468 }
1270 /* The active identity menu. */ { 1469 /* The active identity menu. */ {
1271 iLabelWidget *idMenu = makeMenuButton_LabelWidget( 1470 iLabelWidget *idButton = new_LabelWidget(person_Icon, "identmenu.open");
1272 "\U0001f464", identityButtonMenuItems_, iElemCount(identityButtonMenuItems_)); 1471 setAlignVisually_LabelWidget(idButton, iTrue);
1273 setAlignVisually_LabelWidget(idMenu, iTrue); 1472 setId_Widget(addChildFlags_Widget(navBar, iClob(idButton), collapse_WidgetFlag), "navbar.ident");
1274 setId_Widget(addChildFlags_Widget(navBar, iClob(idMenu), collapse_WidgetFlag), "navbar.ident");
1275 } 1473 }
1276 addChildFlags_Widget(navBar, iClob(new_Widget()), expand_WidgetFlag); 1474 addChildFlags_Widget(navBar, iClob(new_Widget()), expand_WidgetFlag);
1277 setId_Widget(addChildFlags_Widget(navBar, 1475 setId_Widget(addChildFlags_Widget(navBar,
1278 iClob(newIcon_LabelWidget( 1476 iClob(newIcon_LabelWidget(
1279 home_Icon, SDLK_h, KMOD_PRIMARY | KMOD_SHIFT, "navigate.home")), 1477 home_Icon, 0, 0, "navigate.home")),
1280 collapse_WidgetFlag), 1478 collapse_WidgetFlag),
1281 "navbar.home"); 1479 "navbar.action4");
1282#if defined (iPlatformMobile) 1480#if defined (iPlatformMobile)
1283 const iBool isPhone = (deviceType_App() == phone_AppDeviceType); 1481 const iBool isPhone = (deviceType_App() == phone_AppDeviceType);
1284#endif 1482#endif
@@ -1291,6 +1489,7 @@ void createUserInterface_Root(iRoot *d) {
1291 iLabelWidget *navMenu = 1489 iLabelWidget *navMenu =
1292 makeMenuButton_LabelWidget(menu_Icon, navMenuItems_, iElemCount(navMenuItems_)); 1490 makeMenuButton_LabelWidget(menu_Icon, navMenuItems_, iElemCount(navMenuItems_));
1293# endif 1491# endif
1492 setCommand_LabelWidget(navMenu, collectNewCStr_String("menu.open under:1"));
1294 setAlignVisually_LabelWidget(navMenu, iTrue); 1493 setAlignVisually_LabelWidget(navMenu, iTrue);
1295 setId_Widget(addChildFlags_Widget(navBar, iClob(navMenu), collapse_WidgetFlag), "navbar.menu"); 1494 setId_Widget(addChildFlags_Widget(navBar, iClob(navMenu), collapse_WidgetFlag), "navbar.menu");
1296#endif 1495#endif
@@ -1322,17 +1521,18 @@ void createUserInterface_Root(iRoot *d) {
1322 "newtab"); 1521 "newtab");
1323 } 1522 }
1324 /* Sidebars. */ { 1523 /* Sidebars. */ {
1325 iWidget *content = findChild_Widget(root, "tabs.content");
1326 iSidebarWidget *sidebar1 = new_SidebarWidget(left_SidebarSide); 1524 iSidebarWidget *sidebar1 = new_SidebarWidget(left_SidebarSide);
1327 addChildPos_Widget(content, iClob(sidebar1), front_WidgetAddPos);
1328 iSidebarWidget *sidebar2 = new_SidebarWidget(right_SidebarSide);
1329 if (deviceType_App() != phone_AppDeviceType) { 1525 if (deviceType_App() != phone_AppDeviceType) {
1526 /* Sidebars are next to the tab content. */
1527 iWidget *content = findChild_Widget(root, "tabs.content");
1528 addChildPos_Widget(content, iClob(sidebar1), front_WidgetAddPos);
1529 iSidebarWidget *sidebar2 = new_SidebarWidget(right_SidebarSide);
1330 addChildPos_Widget(content, iClob(sidebar2), back_WidgetAddPos); 1530 addChildPos_Widget(content, iClob(sidebar2), back_WidgetAddPos);
1331 } 1531 }
1332 else { 1532 else {
1333 /* The identities sidebar is always in the main area. */ 1533 /* Sidebar is a slide-over sheet. */
1334 addChild_Widget(findChild_Widget(root, "stack"), iClob(sidebar2)); 1534 addChild_Widget(root, iClob(sidebar1));
1335 setFlags_Widget(as_Widget(sidebar2), hidden_WidgetFlag, iTrue); 1535 setFlags_Widget(as_Widget(sidebar1), hidden_WidgetFlag, iTrue);
1336 } 1536 }
1337 } 1537 }
1338 /* Lookup results. */ { 1538 /* Lookup results. */ {
@@ -1386,40 +1586,46 @@ void createUserInterface_Root(iRoot *d) {
1386 commandOnClick_WidgetFlag | 1586 commandOnClick_WidgetFlag |
1387 drawBackgroundToBottom_WidgetFlag, iTrue); 1587 drawBackgroundToBottom_WidgetFlag, iTrue);
1388 setId_Widget(addChildFlags_Widget(toolBar, 1588 setId_Widget(addChildFlags_Widget(toolBar,
1389 iClob(newLargeIcon_LabelWidget(backArrow_Icon, "navigate.back")), 1589 iClob(newLargeIcon_LabelWidget("", "...")),
1390 frameless_WidgetFlag), 1590 frameless_WidgetFlag),
1391 "toolbar.back"); 1591 "toolbar.action1");
1392 setId_Widget(addChildFlags_Widget(toolBar, 1592 setId_Widget(addChildFlags_Widget(toolBar,
1393 iClob(newLargeIcon_LabelWidget(forwardArrow_Icon, "navigate.forward")), 1593 iClob(newLargeIcon_LabelWidget("", "...")),
1394 frameless_WidgetFlag),
1395 "toolbar.forward");
1396 setId_Widget(addChildFlags_Widget(toolBar,
1397 iClob(newLargeIcon_LabelWidget("\U0001f464", "toolbar.showident")),
1398 frameless_WidgetFlag), 1594 frameless_WidgetFlag),
1595 "toolbar.action2");
1596 iWidget *identButton;
1597 setId_Widget(identButton = addChildFlags_Widget(
1598 toolBar,
1599 iClob(newLargeIcon_LabelWidget("\U0001f464", "identmenu.open")),
1600 frameless_WidgetFlag | fixedHeight_WidgetFlag),
1399 "toolbar.ident"); 1601 "toolbar.ident");
1400 setId_Widget(addChildFlags_Widget(toolBar, 1602 setId_Widget(addChildFlags_Widget(toolBar,
1401 iClob(newLargeIcon_LabelWidget(book_Icon, "toolbar.showview arg:-1")), 1603 iClob(newLargeIcon_LabelWidget(book_Icon, "toolbar.showview arg:-1")),
1402 frameless_WidgetFlag | commandOnClick_WidgetFlag), 1604 frameless_WidgetFlag | commandOnClick_WidgetFlag),
1403 "toolbar.view"); 1605 "toolbar.view");
1404 setId_Widget(addChildFlags_Widget(toolBar, 1606 iLabelWidget *idName;
1405 iClob(new_LabelWidget("", "toolbar.showident")), 1607 setId_Widget(addChildFlags_Widget(identButton,
1608 iClob(idName = new_LabelWidget("", NULL)),
1406 frameless_WidgetFlag | 1609 frameless_WidgetFlag |
1407 noBackground_WidgetFlag | 1610 noBackground_WidgetFlag |
1408 fixedPosition_WidgetFlag | 1611 moveToParentBottomEdge_WidgetFlag |
1612 resizeToParentWidth_WidgetFlag
1613 /*fixedPosition_WidgetFlag |
1409 fixedSize_WidgetFlag | 1614 fixedSize_WidgetFlag |
1410 ignoreForParentWidth_WidgetFlag | 1615 ignoreForParentWidth_WidgetFlag |
1411 ignoreForParentHeight_WidgetFlag), 1616 ignoreForParentHeight_WidgetFlag*/),
1412 "toolbar.name"); 1617 "toolbar.name");
1618 setFont_LabelWidget(idName, uiLabelTiny_FontId);
1413 iLabelWidget *menuButton = makeMenuButton_LabelWidget(menu_Icon, phoneNavMenuItems_, 1619 iLabelWidget *menuButton = makeMenuButton_LabelWidget(menu_Icon, phoneNavMenuItems_,
1414 iElemCount(phoneNavMenuItems_)); 1620 iElemCount(phoneNavMenuItems_));
1415 setFont_LabelWidget(menuButton, uiLabelLarge_FontId); 1621 setFont_LabelWidget(menuButton, uiLabelLarge_FontId);
1416 setId_Widget(as_Widget(menuButton), "toolbar.navmenu"); 1622 setId_Widget(as_Widget(menuButton), "toolbar.navmenu");
1417 addChildFlags_Widget(toolBar, iClob(menuButton), frameless_WidgetFlag); 1623 addChildFlags_Widget(toolBar, iClob(menuButton), frameless_WidgetFlag);
1418 iForEach(ObjectList, i, children_Widget(toolBar)) { 1624 iForEach(ObjectList, i, children_Widget(toolBar)) {
1419 iLabelWidget *btn = i.object;
1420 setFlags_Widget(i.object, noBackground_WidgetFlag, iTrue); 1625 setFlags_Widget(i.object, noBackground_WidgetFlag, iTrue);
1421 } 1626 }
1422 updateToolbarColors_Root(d); 1627 updateToolbarColors_Root(d);
1628 updateToolBarActions_(toolBar);
1423 const iMenuItem items[] = { 1629 const iMenuItem items[] = {
1424 { book_Icon " ${sidebar.bookmarks}", 0, 0, "toolbar.showview arg:0" }, 1630 { book_Icon " ${sidebar.bookmarks}", 0, 0, "toolbar.showview arg:0" },
1425 { star_Icon " ${sidebar.feeds}", 0, 0, "toolbar.showview arg:1" }, 1631 { star_Icon " ${sidebar.feeds}", 0, 0, "toolbar.showview arg:1" },
@@ -1431,6 +1637,7 @@ void createUserInterface_Root(iRoot *d) {
1431 setId_Widget(menu, "toolbar.menu"); /* view menu */ 1637 setId_Widget(menu, "toolbar.menu"); /* view menu */
1432 } 1638 }
1433#endif 1639#endif
1640 updateNavBarActions_(navBar);
1434 updatePadding_Root(d); 1641 updatePadding_Root(d);
1435 /* Global context menus. */ { 1642 /* Global context menus. */ {
1436 iWidget *tabsMenu = makeMenu_Widget( 1643 iWidget *tabsMenu = makeMenu_Widget(
@@ -1474,6 +1681,12 @@ void createUserInterface_Root(iRoot *d) {
1474 { select_Icon " ${menu.selectall}", 0, 0, "input.selectall" }, 1681 { select_Icon " ${menu.selectall}", 0, 0, "input.selectall" },
1475 }, 8); 1682 }, 8);
1476#endif 1683#endif
1684 if (deviceType_App() == phone_AppDeviceType) {
1685 /* Small screen; conserve space by removing the Cancel item. */
1686 iRelease(removeChild_Widget(clipMenu, lastChild_Widget(clipMenu)));
1687 iRelease(removeChild_Widget(clipMenu, lastChild_Widget(clipMenu)));
1688 iRelease(removeChild_Widget(clipMenu, lastChild_Widget(clipMenu)));
1689 }
1477 iWidget *splitMenu = makeMenu_Widget(root, (iMenuItem[]){ 1690 iWidget *splitMenu = makeMenu_Widget(root, (iMenuItem[]){
1478 { "${menu.split.merge}", '1', 0, "ui.split arg:0" }, 1691 { "${menu.split.merge}", '1', 0, "ui.split arg:0" },
1479 { "${menu.split.swap}", SDLK_x, 0, "ui.split swap:1" }, 1692 { "${menu.split.swap}", SDLK_x, 0, "ui.split swap:1" },
@@ -1493,6 +1706,7 @@ void createUserInterface_Root(iRoot *d) {
1493 setId_Widget(splitMenu, "splitmenu"); 1706 setId_Widget(splitMenu, "splitmenu");
1494 } 1707 }
1495 /* Global keyboard shortcuts. */ { 1708 /* Global keyboard shortcuts. */ {
1709 addAction_Widget(root, SDLK_h, KMOD_PRIMARY | KMOD_SHIFT, "navigate.home");
1496 addAction_Widget(root, 'l', KMOD_PRIMARY, "navigate.focus"); 1710 addAction_Widget(root, 'l', KMOD_PRIMARY, "navigate.focus");
1497 addAction_Widget(root, 'f', KMOD_PRIMARY, "focus.set id:find.input"); 1711 addAction_Widget(root, 'f', KMOD_PRIMARY, "focus.set id:find.input");
1498 addAction_Widget(root, '1', KMOD_PRIMARY, "sidebar.mode arg:0 toggle:1"); 1712 addAction_Widget(root, '1', KMOD_PRIMARY, "sidebar.mode arg:0 toggle:1");
@@ -1558,9 +1772,10 @@ iRect safeRect_Root(const iRoot *d) {
1558 1772
1559iRect visibleRect_Root(const iRoot *d) { 1773iRect visibleRect_Root(const iRoot *d) {
1560 iRect visRect = rect_Root(d); 1774 iRect visRect = rect_Root(d);
1775 float bottom = 0.0f;
1561#if defined (iPlatformAppleMobile) 1776#if defined (iPlatformAppleMobile)
1562 /* TODO: Check this on device... Maybe DisplayUsableBounds would be good here, too? */ 1777 /* TODO: Check this on device... Maybe DisplayUsableBounds would be good here, too? */
1563 float left, top, right, bottom; 1778 float left, top, right;
1564 safeAreaInsets_iOS(&left, &top, &right, &bottom); 1779 safeAreaInsets_iOS(&left, &top, &right, &bottom);
1565 visRect.pos.x = (int) left; 1780 visRect.pos.x = (int) left;
1566 visRect.size.x -= (int) (left + right); 1781 visRect.size.x -= (int) (left + right);
@@ -1585,6 +1800,6 @@ iRect visibleRect_Root(const iRoot *d) {
1585 visRect = intersect_Rect(visRect, init_Rect(usable.x, usable.y, usable.w, usable.h)); 1800 visRect = intersect_Rect(visRect, init_Rect(usable.x, usable.y, usable.w, usable.h));
1586 } 1801 }
1587#endif 1802#endif
1588 adjustEdges_Rect(&visRect, 0, 0, -get_MainWindow()->keyboardHeight, 0); 1803 adjustEdges_Rect(&visRect, 0, 0, -get_MainWindow()->keyboardHeight + bottom, 0);
1589 return visRect; 1804 return visRect;
1590} 1805}
diff --git a/src/ui/root.h b/src/ui/root.h
index 851d927d..7e831be3 100644
--- a/src/ui/root.h
+++ b/src/ui/root.h
@@ -2,11 +2,15 @@
2 2
3#include "widget.h" 3#include "widget.h"
4#include "color.h" 4#include "color.h"
5#include <the_Foundation/audience.h>
5#include <the_Foundation/ptrset.h> 6#include <the_Foundation/ptrset.h>
6#include <the_Foundation/vec2.h> 7#include <the_Foundation/vec2.h>
7 8
8iDeclareType(Root) 9iDeclareType(Root)
9 10
11iDeclareNotifyFunc(Root, VisualOffsetsChanged)
12iDeclareAudienceGetter(Root, visualOffsetsChanged)
13
10struct Impl_Root { 14struct Impl_Root {
11 iWidget * widget; 15 iWidget * widget;
12 iWindow * window; 16 iWindow * window;
@@ -14,6 +18,9 @@ struct Impl_Root {
14 iPtrSet * pendingDestruction; 18 iPtrSet * pendingDestruction;
15 iBool pendingArrange; 19 iBool pendingArrange;
16 int loadAnimTimer; 20 int loadAnimTimer;
21 iBool didAnimateVisualOffsets;
22 iBool didChangeArrangement;
23 iAudience *visualOffsetsChanged; /* called after running tickers */
17 iColor tmPalette[tmMax_ColorId]; /* theme-specific palette */ 24 iColor tmPalette[tmMax_ColorId]; /* theme-specific palette */
18}; 25};
19 26
@@ -36,6 +43,7 @@ void updatePadding_Root (iRoot *); /* TODO: is part of m
36void dismissPortraitPhoneSidebars_Root (iRoot *); 43void dismissPortraitPhoneSidebars_Root (iRoot *);
37void showToolbar_Root (iRoot *, iBool show); 44void showToolbar_Root (iRoot *, iBool show);
38void updateToolbarColors_Root (iRoot *); 45void updateToolbarColors_Root (iRoot *);
46void notifyVisualOffsetChange_Root (iRoot *);
39 47
40iInt2 size_Root (const iRoot *); 48iInt2 size_Root (const iRoot *);
41iRect rect_Root (const iRoot *); 49iRect rect_Root (const iRoot *);
diff --git a/src/ui/scrollwidget.c b/src/ui/scrollwidget.c
index b6f73b6c..651669c6 100644
--- a/src/ui/scrollwidget.c
+++ b/src/ui/scrollwidget.c
@@ -107,7 +107,7 @@ static iRect bounds_ScrollWidget_(const iScrollWidget *d) {
107static iRect thumbRect_ScrollWidget_(const iScrollWidget *d) { 107static iRect thumbRect_ScrollWidget_(const iScrollWidget *d) {
108 const iRect bounds = bounds_ScrollWidget_(d); 108 const iRect bounds = bounds_ScrollWidget_(d);
109 iRect rect = init_Rect(bounds.pos.x, bounds.pos.y, bounds.size.x, 0); 109 iRect rect = init_Rect(bounds.pos.x, bounds.pos.y, bounds.size.x, 0);
110 const int total = size_Range(&d->range); 110 const int total = (int) size_Range(&d->range);
111 if (total > 0) { 111 if (total > 0) {
112 const int tsize = thumbSize_ScrollWidget_(d); 112 const int tsize = thumbSize_ScrollWidget_(d);
113// iAssert(tsize <= height_Rect(bounds)); 113// iAssert(tsize <= height_Rect(bounds));
@@ -197,7 +197,7 @@ static iBool processEvent_ScrollWidget_(iScrollWidget *d, const SDL_Event *ev) {
197 case drag_ClickResult: { 197 case drag_ClickResult: {
198 const iRect bounds = bounds_ScrollWidget_(d); 198 const iRect bounds = bounds_ScrollWidget_(d);
199 const int offset = delta_Click(&d->click).y; 199 const int offset = delta_Click(&d->click).y;
200 const int total = size_Range(&d->range); 200 const int total = (int) size_Range(&d->range);
201 int dpos = (float) offset / (float) (height_Rect(bounds) - thumbSize_ScrollWidget_(d)) * total; 201 int dpos = (float) offset / (float) (height_Rect(bounds) - thumbSize_ScrollWidget_(d)) * total;
202 d->thumb = iClamp(d->startThumb + dpos, d->range.start, d->range.end); 202 d->thumb = iClamp(d->startThumb + dpos, d->range.start, d->range.end);
203 postCommand_Widget(w, "scroll.moved arg:%d", d->thumb); 203 postCommand_Widget(w, "scroll.moved arg:%d", d->thumb);
diff --git a/src/ui/sidebarwidget.c b/src/ui/sidebarwidget.c
index 2219eba9..16677f9e 100644
--- a/src/ui/sidebarwidget.c
+++ b/src/ui/sidebarwidget.c
@@ -25,6 +25,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
25#include "app.h" 25#include "app.h"
26#include "defs.h" 26#include "defs.h"
27#include "bookmarks.h" 27#include "bookmarks.h"
28#include "certlistwidget.h"
28#include "command.h" 29#include "command.h"
29#include "documentwidget.h" 30#include "documentwidget.h"
30#include "feeds.h" 31#include "feeds.h"
@@ -34,10 +35,12 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
34#include "inputwidget.h" 35#include "inputwidget.h"
35#include "labelwidget.h" 36#include "labelwidget.h"
36#include "listwidget.h" 37#include "listwidget.h"
38#include "mobile.h"
37#include "keys.h" 39#include "keys.h"
38#include "paint.h" 40#include "paint.h"
39#include "root.h" 41#include "root.h"
40#include "scrollwidget.h" 42#include "scrollwidget.h"
43#include "touch.h"
41#include "util.h" 44#include "util.h"
42#include "visited.h" 45#include "visited.h"
43 46
@@ -88,6 +91,22 @@ iDefineObjectConstruction(SidebarItem)
88 91
89/*----------------------------------------------------------------------------------------------*/ 92/*----------------------------------------------------------------------------------------------*/
90 93
94static const char *normalModeLabels_[max_SidebarMode] = {
95 book_Icon " ${sidebar.bookmarks}",
96 star_Icon " ${sidebar.feeds}",
97 clock_Icon " ${sidebar.history}",
98 person_Icon " ${sidebar.identities}",
99 page_Icon " ${sidebar.outline}",
100};
101
102static const char *tightModeLabels_[max_SidebarMode] = {
103 book_Icon,
104 star_Icon,
105 clock_Icon,
106 person_Icon,
107 page_Icon,
108};
109
91struct Impl_SidebarWidget { 110struct Impl_SidebarWidget {
92 iWidget widget; 111 iWidget widget;
93 enum iSidebarSide side; 112 enum iSidebarSide side;
@@ -96,7 +115,10 @@ struct Impl_SidebarWidget {
96 iString cmdPrefix; 115 iString cmdPrefix;
97 iWidget * blank; 116 iWidget * blank;
98 iListWidget * list; 117 iListWidget * list;
118 iCertListWidget * certList;
99 iWidget * actions; /* below the list, area for buttons */ 119 iWidget * actions; /* below the list, area for buttons */
120 int midHeight; /* on portrait phone, the height for the middle state */
121 iBool isEditing; /* mobile edit mode */
100 int modeScroll[max_SidebarMode]; 122 int modeScroll[max_SidebarMode];
101 iLabelWidget * modeButtons[max_SidebarMode]; 123 iLabelWidget * modeButtons[max_SidebarMode];
102 int maxButtonLabelWidth; 124 int maxButtonLabelWidth;
@@ -114,6 +136,10 @@ struct Impl_SidebarWidget {
114 136
115iDefineObjectConstructionArgs(SidebarWidget, (enum iSidebarSide side), side) 137iDefineObjectConstructionArgs(SidebarWidget, (enum iSidebarSide side), side)
116 138
139iLocalDef iListWidget *list_SidebarWidget_(iSidebarWidget *d) {
140 return d->mode == identities_SidebarMode ? (iListWidget *) d->certList : d->list;
141}
142
117static iBool isResizing_SidebarWidget_(const iSidebarWidget *d) { 143static iBool isResizing_SidebarWidget_(const iSidebarWidget *d) {
118 return (flags_Widget(d->resizer) & pressed_WidgetFlag) != 0; 144 return (flags_Widget(d->resizer) & pressed_WidgetFlag) != 0;
119} 145}
@@ -181,77 +207,31 @@ int cmpTree_Bookmark(const iBookmark **a, const iBookmark **b) {
181 return cmpStringCase_String(&bm1->title, &bm2->title); 207 return cmpStringCase_String(&bm1->title, &bm2->title);
182} 208}
183 209
210static enum iFontId actionButtonFont_SidebarWidget_(const iSidebarWidget *d) {
211 switch (deviceType_App()) {
212 default:
213 break;
214 case phone_AppDeviceType:
215 return isPortrait_App() ? uiLabelBig_FontId : uiLabelMedium_FontId;
216 case tablet_AppDeviceType:
217 return uiLabelMedium_FontId;
218 }
219 return d->buttonFont;
220}
221
184static iLabelWidget *addActionButton_SidebarWidget_(iSidebarWidget *d, const char *label, 222static iLabelWidget *addActionButton_SidebarWidget_(iSidebarWidget *d, const char *label,
185 const char *command, int64_t flags) { 223 const char *command, int64_t flags) {
186 iLabelWidget *btn = addChildFlags_Widget(d->actions, 224 iLabelWidget *btn = addChildFlags_Widget(d->actions,
187 iClob(new_LabelWidget(label, command)), 225 iClob(new_LabelWidget(label, command)),
188 //(deviceType_App() != desktop_AppDeviceType ?
189 // extraPadding_WidgetFlag : 0) |
190 flags); 226 flags);
191 setFont_LabelWidget(btn, deviceType_App() == phone_AppDeviceType && d->side == right_SidebarSide 227 setFont_LabelWidget(btn, actionButtonFont_SidebarWidget_(d));
192 ? uiLabelBig_FontId
193 : d->buttonFont);
194 checkIcon_LabelWidget(btn); 228 checkIcon_LabelWidget(btn);
195 return btn; 229 if (deviceType_App() != desktop_AppDeviceType) {
196} 230 setFlags_Widget(as_Widget(btn), frameless_WidgetFlag, iTrue);
197 231 setTextColor_LabelWidget(btn, uiTextAction_ColorId);
198static iGmIdentity *menuIdentity_SidebarWidget_(const iSidebarWidget *d) { 232 setBackgroundColor_Widget(as_Widget(btn), uiBackground_ColorId);
199 if (d->mode == identities_SidebarMode) {
200 if (d->contextItem) {
201 return identity_GmCerts(certs_App(), d->contextItem->id);
202 }
203 }
204 return NULL;
205}
206
207static void updateContextMenu_SidebarWidget_(iSidebarWidget *d) {
208 if (d->mode != identities_SidebarMode) {
209 return;
210 } 233 }
211 iArray *items = collectNew_Array(sizeof(iMenuItem)); 234 return btn;
212 pushBackN_Array(items, (iMenuItem[]){
213 { person_Icon " ${ident.use}", 0, 0, "ident.use arg:1" },
214 { close_Icon " ${ident.stopuse}", 0, 0, "ident.use arg:0" },
215 { close_Icon " ${ident.stopuse.all}", 0, 0, "ident.use arg:0 clear:1" },
216 { "---", 0, 0, NULL },
217 { edit_Icon " ${menu.edit.notes}", 0, 0, "ident.edit" },
218 { "${ident.fingerprint}", 0, 0, "ident.fingerprint" },
219 { export_Icon " ${ident.export}", 0, 0, "ident.export" },
220 { "---", 0, 0, NULL },
221 { delete_Icon " " uiTextCaution_ColorEscape "${ident.delete}", 0, 0, "ident.delete confirm:1" },
222 }, 9);
223 /* Used URLs. */
224 const iGmIdentity *ident = menuIdentity_SidebarWidget_(d);
225 if (ident) {
226 size_t insertPos = 3;
227 if (!isEmpty_StringSet(ident->useUrls)) {
228 insert_Array(items, insertPos++, &(iMenuItem){ "---", 0, 0, NULL });
229 }
230 const iString *docUrl = url_DocumentWidget(document_App());
231 iBool usedOnCurrentPage = iFalse;
232 iConstForEach(StringSet, i, ident->useUrls) {
233 const iString *url = i.value;
234 usedOnCurrentPage |= equalCase_String(docUrl, url);
235 iRangecc urlStr = range_String(url);
236 if (startsWith_Rangecc(urlStr, "gemini://")) {
237 urlStr.start += 9; /* omit the default scheme */
238 }
239 if (endsWith_Rangecc(urlStr, "/")) {
240 urlStr.end--; /* looks cleaner */
241 }
242 insert_Array(items,
243 insertPos++,
244 &(iMenuItem){ format_CStr(globe_Icon " %s", cstr_Rangecc(urlStr)),
245 0,
246 0,
247 format_CStr("!open url:%s", cstr_String(url)) });
248 }
249 if (!usedOnCurrentPage) {
250 remove_Array(items, 1);
251 }
252 }
253 destroy_Widget(d->menu);
254 d->menu = makeMenu_Widget(as_Widget(d), data_Array(items), size_Array(items));
255} 235}
256 236
257static iBool isBookmarkFolded_SidebarWidget_(const iSidebarWidget *d, const iBookmark *bm) { 237static iBool isBookmarkFolded_SidebarWidget_(const iSidebarWidget *d, const iBookmark *bm) {
@@ -264,11 +244,29 @@ static iBool isBookmarkFolded_SidebarWidget_(const iSidebarWidget *d, const iBoo
264 return iFalse; 244 return iFalse;
265} 245}
266 246
247static iBool isSlidingSheet_SidebarWidget_(const iSidebarWidget *d) {
248 return isPortraitPhone_App();
249}
250
251static void setMobileEditMode_SidebarWidget_(iSidebarWidget *d, iBool editing) {
252 iWidget *w = as_Widget(d);
253 d->isEditing = editing;
254 if (d->actions) {
255 setFlags_Widget(findChild_Widget(w, "sidebar.close"), hidden_WidgetFlag, editing);
256 setFlags_Widget(child_Widget(d->actions, 0), hidden_WidgetFlag, !editing);
257 setTextCStr_LabelWidget(child_Widget(as_Widget(d->actions), 2),
258 editing ? "${sidebar.close}" : "${sidebar.action.bookmarks.edit}");
259 setDragHandleWidth_ListWidget(d->list, editing ? itemHeight_ListWidget(d->list) * 3 / 2 : 0);
260 arrange_Widget(d->actions);
261 }
262}
263
267static void updateItemsWithFlags_SidebarWidget_(iSidebarWidget *d, iBool keepActions) { 264static void updateItemsWithFlags_SidebarWidget_(iSidebarWidget *d, iBool keepActions) {
265 const iBool isMobile = (deviceType_App() != desktop_AppDeviceType);
268 clear_ListWidget(d->list); 266 clear_ListWidget(d->list);
269 releaseChildren_Widget(d->blank); 267 releaseChildren_Widget(d->blank);
270 if (!keepActions) { 268 if (!keepActions) {
271 releaseChildren_Widget(d->actions); 269 releaseChildren_Widget(d->actions);
272 } 270 }
273 d->actions->rect.size.y = 0; 271 d->actions->rect.size.y = 0;
274 destroy_Widget(d->menu); 272 destroy_Widget(d->menu);
@@ -288,7 +286,8 @@ static void updateItemsWithFlags_SidebarWidget_(iSidebarWidget *d, iBool keepAct
288 iZap(on); 286 iZap(on);
289 size_t numItems = 0; 287 size_t numItems = 0;
290 isEmpty = iTrue; 288 isEmpty = iTrue;
291 iConstForEach(PtrArray, i, listEntries_Feeds()) { 289 const iPtrArray *feedEntries = listEntries_Feeds();
290 iConstForEach(PtrArray, i, feedEntries) {
292 const iFeedEntry *entry = i.ptr; 291 const iFeedEntry *entry = i.ptr;
293 if (isHidden_FeedEntry(entry)) { 292 if (isHidden_FeedEntry(entry)) {
294 continue; /* A hidden entry. */ 293 continue; /* A hidden entry. */
@@ -301,12 +300,12 @@ static void updateItemsWithFlags_SidebarWidget_(iSidebarWidget *d, iBool keepAct
301 if (secondsSince_Time(&now, &entry->discovered) > maxAge_Visited) { 300 if (secondsSince_Time(&now, &entry->discovered) > maxAge_Visited) {
302 break; /* the rest are even older */ 301 break; /* the rest are even older */
303 } 302 }
304 isEmpty = iFalse;
305 const iBool isOpen = equal_String(docUrl, &entry->url); 303 const iBool isOpen = equal_String(docUrl, &entry->url);
306 const iBool isUnread = isUnread_FeedEntry(entry); 304 const iBool isUnread = isUnread_FeedEntry(entry);
307 if (d->feedsMode == unread_FeedsMode && !isUnread && !isOpen) { 305 if (d->feedsMode == unread_FeedsMode && !isUnread && !isOpen) {
308 continue; 306 continue;
309 } 307 }
308 isEmpty = iFalse;
310 /* Insert date separators. */ { 309 /* Insert date separators. */ {
311 iDate entryDate; 310 iDate entryDate;
312 init_Date(&entryDate, &entry->posted); 311 init_Date(&entryDate, &entry->posted);
@@ -351,34 +350,67 @@ static void updateItemsWithFlags_SidebarWidget_(iSidebarWidget *d, iBool keepAct
351 } 350 }
352 } 351 }
353 /* Actions. */ 352 /* Actions. */
354 if (!keepActions) { 353 if (!isMobile) {
355 addActionButton_SidebarWidget_( 354 if (!keepActions && !isEmpty_PtrArray(feedEntries)) {
356 d, check_Icon " ${sidebar.action.feeds.markallread}", "feeds.markallread", expand_WidgetFlag | 355 addActionButton_SidebarWidget_(d,
357 tight_WidgetFlag); 356 check_Icon
358 updateSize_LabelWidget(addChildFlags_Widget(d->actions, 357 " ${sidebar.action.feeds.markallread}",
359 iClob(new_LabelWidget("${sidebar.action.show}", NULL)), 358 "feeds.markallread",
360 frameless_WidgetFlag | tight_WidgetFlag)); 359 expand_WidgetFlag | tight_WidgetFlag);
361 const iMenuItem items[] = { 360 updateSize_LabelWidget(
362 { "${sidebar.action.feeds.showall}", SDLK_u, KMOD_SHIFT, "feeds.mode arg:0" }, 361 addChildFlags_Widget(d->actions,
363 { "${sidebar.action.feeds.showunread}", SDLK_u, 0, "feeds.mode arg:1" }, 362 iClob(new_LabelWidget("${sidebar.action.show}", NULL)),
364 }; 363 frameless_WidgetFlag | tight_WidgetFlag));
365 iWidget *dropButton = addChild_Widget( 364 const iMenuItem items[] = {
366 d->actions, 365 { page_Icon " ${sidebar.action.feeds.showall}",
367 iClob(makeMenuButton_LabelWidget(items[d->feedsMode].label, items, 2))); 366 SDLK_u,
368 setId_Widget(dropButton, "feeds.modebutton"); 367 KMOD_SHIFT,
369 checkIcon_LabelWidget((iLabelWidget *) dropButton); 368 "feeds.mode arg:0" },
370 setFixedSize_Widget( 369 { circle_Icon " ${sidebar.action.feeds.showunread}",
371 dropButton, 370 SDLK_u,
372 init_I2(iMaxi(20 * gap_UI, measure_Text( 371 0,
373 default_FontId, 372 "feeds.mode arg:1" },
374 translateCStr_Lang(items[findWidestLabel_MenuItem(items, 2)].label)) 373 };
375 .advance.x + 374 iWidget *dropButton = addChild_Widget(
376 6 * gap_UI), 375 d->actions,
376 iClob(makeMenuButton_LabelWidget(items[d->feedsMode].label, items, 2)));
377 setId_Widget(dropButton, "feeds.modebutton");
378 checkIcon_LabelWidget((iLabelWidget *) dropButton);
379 setFixedSize_Widget(
380 dropButton,
381 init_I2(
382 iMaxi(20 * gap_UI,
383 measure_Text(default_FontId,
384 translateCStr_Lang(
385 items[findWidestLabel_MenuItem(items, 2)].label))
386 .advance.x +
387 13 * gap_UI),
377 -1)); 388 -1));
389 }
390 else {
391 updateDropdownSelection_LabelWidget(
392 findChild_Widget(d->actions, "feeds.modebutton"),
393 format_CStr(" arg:%d", d->feedsMode));
394 }
378 } 395 }
379 else { 396 else {
380 updateDropdownSelection_LabelWidget(findChild_Widget(d->actions, "feeds.modebutton"), 397 if (!keepActions) {
381 format_CStr(" arg:%d", d->feedsMode)); 398 iLabelWidget *readAll = addActionButton_SidebarWidget_(d,
399 check_Icon,
400 "feeds.markallread confirm:1",
401 0);
402 setTextColor_LabelWidget(readAll, uiTextCaution_ColorId);
403 addActionButton_SidebarWidget_(d,
404 page_Icon,
405 "feeds.mode arg:0",
406 0);
407 addActionButton_SidebarWidget_(d,
408 circle_Icon,
409 "feeds.mode arg:1",
410 0);
411 }
412 setOutline_LabelWidget(child_Widget(d->actions, 1), d->feedsMode != all_FeedsMode);
413 setOutline_LabelWidget(child_Widget(d->actions, 2), d->feedsMode != unread_FeedsMode);
382 } 414 }
383 d->menu = makeMenu_Widget( 415 d->menu = makeMenu_Widget(
384 as_Widget(d), 416 as_Widget(d),
@@ -416,11 +448,6 @@ static void updateItemsWithFlags_SidebarWidget_(iSidebarWidget *d, iBool keepAct
416 break; 448 break;
417 } 449 }
418 case bookmarks_SidebarMode: { 450 case bookmarks_SidebarMode: {
419 iRegExp *homeTag = iClob(new_RegExp("\\b" homepage_BookmarkTag "\\b", caseSensitive_RegExpOption));
420 iRegExp *subTag = iClob(new_RegExp("\\b" subscribed_BookmarkTag "\\b", caseSensitive_RegExpOption));
421 iRegExp *remoteSourceTag = iClob(new_RegExp("\\b" remoteSource_BookmarkTag "\\b", caseSensitive_RegExpOption));
422 iRegExp *remoteTag = iClob(new_RegExp("\\b" remote_BookmarkTag "\\b", caseSensitive_RegExpOption));
423 iRegExp *linkSplitTag = iClob(new_RegExp("\\b" linkSplit_BookmarkTag "\\b", caseSensitive_RegExpOption));
424 iConstForEach(PtrArray, i, list_Bookmarks(bookmarks_App(), cmpTree_Bookmark, NULL, NULL)) { 451 iConstForEach(PtrArray, i, list_Bookmarks(bookmarks_App(), cmpTree_Bookmark, NULL, NULL)) {
425 const iBookmark *bm = i.ptr; 452 const iBookmark *bm = i.ptr;
426 if (isBookmarkFolded_SidebarWidget_(d, bm)) { 453 if (isBookmarkFolded_SidebarWidget_(d, bm)) {
@@ -439,27 +466,21 @@ static void updateItemsWithFlags_SidebarWidget_(iSidebarWidget *d, iBool keepAct
439 } 466 }
440 set_String(&item->url, &bm->url); 467 set_String(&item->url, &bm->url);
441 set_String(&item->label, &bm->title); 468 set_String(&item->label, &bm->title);
442 /* Icons for special tags. */ { 469 /* Icons for special behaviors. */ {
443 iRegExpMatch m; 470 if (bm->flags & subscribed_BookmarkFlag) {
444 init_RegExpMatch(&m);
445 if (matchString_RegExp(subTag, &bm->tags, &m)) {
446 appendChar_String(&item->meta, 0x2605); 471 appendChar_String(&item->meta, 0x2605);
447 } 472 }
448 init_RegExpMatch(&m); 473 if (bm->flags & homepage_BookmarkFlag) {
449 if (matchString_RegExp(homeTag, &bm->tags, &m)) {
450 appendChar_String(&item->meta, 0x1f3e0); 474 appendChar_String(&item->meta, 0x1f3e0);
451 } 475 }
452 init_RegExpMatch(&m); 476 if (bm->flags & remote_BookmarkFlag) {
453 if (matchString_RegExp(remoteTag, &bm->tags, &m)) {
454 item->listItem.isDraggable = iFalse; 477 item->listItem.isDraggable = iFalse;
455 } 478 }
456 init_RegExpMatch(&m); 479 if (bm->flags & remoteSource_BookmarkFlag) {
457 if (matchString_RegExp(remoteSourceTag, &bm->tags, &m)) {
458 appendChar_String(&item->meta, 0x2913); 480 appendChar_String(&item->meta, 0x2913);
459 item->isBold = iTrue; 481 item->isBold = iTrue;
460 } 482 }
461 init_RegExpMatch(&m); 483 if (bm->flags & linkSplit_BookmarkFlag) {
462 if (matchString_RegExp(linkSplitTag, &bm->tags, &m)) {
463 appendChar_String(&item->meta, 0x25e7); 484 appendChar_String(&item->meta, 0x25e7);
464 } 485 }
465 } 486 }
@@ -495,6 +516,14 @@ static void updateItemsWithFlags_SidebarWidget_(iSidebarWidget *d, iBool keepAct
495 { "---", 0, 0, NULL }, 516 { "---", 0, 0, NULL },
496 { reload_Icon " ${bookmarks.reload}", 0, 0, "bookmarks.reload.remote" } }, 517 { reload_Icon " ${bookmarks.reload}", 0, 0, "bookmarks.reload.remote" } },
497 6); 518 6);
519 if (isMobile) {
520 addActionButton_SidebarWidget_(d, "${sidebar.action.bookmarks.newfolder}",
521 "bookmarks.addfolder", !d->isEditing ? hidden_WidgetFlag : 0);
522 addChildFlags_Widget(d->actions, iClob(new_Widget()), expand_WidgetFlag);
523 iLabelWidget *btn = addActionButton_SidebarWidget_(d,
524 d->isEditing ? "${sidebar.close}" : "${sidebar.action.bookmarks.edit}",
525 "sidebar.bookmarks.edit", 0);
526 }
498 break; 527 break;
499 } 528 }
500 case history_SidebarMode: { 529 case history_SidebarMode: {
@@ -554,49 +583,15 @@ static void updateItemsWithFlags_SidebarWidget_(iSidebarWidget *d, iBool keepAct
554 (iMenuItem[]){ 583 (iMenuItem[]){
555 { delete_Icon " " uiTextCaution_ColorEscape "${history.clear}", 0, 0, "history.clear confirm:1" }, 584 { delete_Icon " " uiTextCaution_ColorEscape "${history.clear}", 0, 0, "history.clear confirm:1" },
556 }, 1); 585 }, 1);
586 if (isMobile) {
587 addChildFlags_Widget(d->actions, iClob(new_Widget()), expand_WidgetFlag);
588 iLabelWidget *btn = addActionButton_SidebarWidget_(d, "${sidebar.action.history.clear}",
589 "history.clear confirm:1", 0);
590 }
557 break; 591 break;
558 } 592 }
559 case identities_SidebarMode: { 593 case identities_SidebarMode: {
560 const iString *tabUrl = url_DocumentWidget(document_App()); 594 isEmpty = !updateItems_CertListWidget(d->certList);
561 const iRangecc tabHost = urlHost_String(tabUrl);
562 isEmpty = iTrue;
563 iConstForEach(PtrArray, i, identities_GmCerts(certs_App())) {
564 const iGmIdentity *ident = i.ptr;
565 iSidebarItem *item = new_SidebarItem();
566 item->id = (uint32_t) index_PtrArrayConstIterator(&i);
567 item->icon = 0x1f464; /* person */
568 set_String(&item->label, name_GmIdentity(ident));
569 iDate until;
570 validUntil_TlsCertificate(ident->cert, &until);
571 const iBool isActive = isUsedOn_GmIdentity(ident, tabUrl);
572 format_String(&item->meta,
573 "%s",
574 isActive ? cstr_Lang("ident.using")
575 : isUsed_GmIdentity(ident)
576 ? formatCStrs_Lang("ident.usedonurls.n", size_StringSet(ident->useUrls))
577 : cstr_Lang("ident.notused"));
578 const char *expiry =
579 ident->flags & temporary_GmIdentityFlag
580 ? cstr_Lang("ident.temporary")
581 : cstrCollect_String(format_Date(&until, cstr_Lang("ident.expiry")));
582 if (isEmpty_String(&ident->notes)) {
583 appendFormat_String(&item->meta, "\n%s", expiry);
584 }
585 else {
586 appendFormat_String(&item->meta,
587 " \u2014 %s\n%s%s",
588 expiry,
589 escape_Color(uiHeading_ColorId),
590 cstr_String(&ident->notes));
591 }
592 item->listItem.isSelected = isActive;
593 if (isUsedOnDomain_GmIdentity(ident, tabHost)) {
594 item->indent = 1; /* will be highlighted */
595 }
596 addItem_ListWidget(d->list, item);
597 iRelease(item);
598 isEmpty = iFalse;
599 }
600 /* Actions. */ 595 /* Actions. */
601 if (!isEmpty) { 596 if (!isEmpty) {
602 addActionButton_SidebarWidget_(d, add_Icon " ${sidebar.action.ident.new}", "ident.new", 0); 597 addActionButton_SidebarWidget_(d, add_Icon " ${sidebar.action.ident.new}", "ident.new", 0);
@@ -607,16 +602,27 @@ static void updateItemsWithFlags_SidebarWidget_(iSidebarWidget *d, iBool keepAct
607 default: 602 default:
608 break; 603 break;
609 } 604 }
610 scrollOffset_ListWidget(d->list, 0); 605 setFlags_Widget(as_Widget(d->list), hidden_WidgetFlag, d->mode == identities_SidebarMode);
611 updateVisible_ListWidget(d->list); 606 setFlags_Widget(as_Widget(d->certList), hidden_WidgetFlag, d->mode != identities_SidebarMode);
612 invalidate_ListWidget(d->list); 607 scrollOffset_ListWidget(list_SidebarWidget_(d), 0);
608 updateVisible_ListWidget(list_SidebarWidget_(d));
609 invalidate_ListWidget(list_SidebarWidget_(d));
613 /* Content for a blank tab. */ 610 /* Content for a blank tab. */
614 if (isEmpty) { 611 if (isEmpty) {
615 if (d->mode == feeds_SidebarMode) { 612 if (d->mode == feeds_SidebarMode) {
616 iWidget *div = makeVDiv_Widget(); 613 iWidget *div = makeVDiv_Widget();
617 setPadding_Widget(div, 3 * gap_UI, 0, 3 * gap_UI, 2 * gap_UI); 614 setPadding_Widget(div, 3 * gap_UI, 0, 3 * gap_UI, 2 * gap_UI);
615 arrange_Widget(d->actions);
616// setPadding_Widget(div, 0, 0, 0, height_Widget(d->actions));
618 addChildFlags_Widget(div, iClob(new_Widget()), expand_WidgetFlag); /* pad */ 617 addChildFlags_Widget(div, iClob(new_Widget()), expand_WidgetFlag); /* pad */
619 addChild_Widget(div, iClob(new_LabelWidget("${menu.feeds.refresh}", "feeds.refresh"))); 618 if (d->feedsMode == all_FeedsMode) {
619 addChild_Widget(div, iClob(new_LabelWidget("${menu.feeds.refresh}", "feeds.refresh")));
620 }
621 else {
622 iLabelWidget *msg = addChildFlags_Widget(div, iClob(new_LabelWidget("${sidebar.empty.unread}", NULL)),
623 frameless_WidgetFlag);
624 setFont_LabelWidget(msg, uiLabelLarge_FontId);
625 }
620 addChildFlags_Widget(div, iClob(new_Widget()), expand_WidgetFlag); /* pad */ 626 addChildFlags_Widget(div, iClob(new_Widget()), expand_WidgetFlag); /* pad */
621 addChild_Widget(d->blank, iClob(div)); 627 addChild_Widget(d->blank, iClob(div));
622 } 628 }
@@ -645,7 +651,7 @@ static void updateItemsWithFlags_SidebarWidget_(iSidebarWidget *d, iBool keepAct
645 setWrap_LabelWidget(linkLabel, iTrue); 651 setWrap_LabelWidget(linkLabel, iTrue);
646 addChild_Widget(d->blank, iClob(div)); 652 addChild_Widget(d->blank, iClob(div));
647 } 653 }
648// arrange_Widget(d->blank); 654 arrange_Widget(d->blank);
649 } 655 }
650#if 0 656#if 0
651 if (deviceType_App() != desktop_AppDeviceType) { 657 if (deviceType_App() != desktop_AppDeviceType) {
@@ -659,7 +665,7 @@ static void updateItemsWithFlags_SidebarWidget_(iSidebarWidget *d, iBool keepAct
659#endif 665#endif
660 arrange_Widget(d->actions); 666 arrange_Widget(d->actions);
661 arrange_Widget(as_Widget(d)); 667 arrange_Widget(as_Widget(d));
662 updateMouseHover_ListWidget(d->list); 668 updateMouseHover_ListWidget(list_SidebarWidget_(d));
663} 669}
664 670
665static void updateItems_SidebarWidget_(iSidebarWidget *d) { 671static void updateItems_SidebarWidget_(iSidebarWidget *d) {
@@ -678,35 +684,59 @@ static size_t findItem_SidebarWidget_(const iSidebarWidget *d, int id) {
678} 684}
679 685
680static void updateItemHeight_SidebarWidget_(iSidebarWidget *d) { 686static void updateItemHeight_SidebarWidget_(iSidebarWidget *d) {
687 const float heights[max_SidebarMode] = { 1.333f, 2.333f, 1.333f, 3.5f, 1.2f };
681 if (d->list) { 688 if (d->list) {
682 const float heights[max_SidebarMode] = { 1.333f, 2.333f, 1.333f, 3.5f, 1.2f };
683 setItemHeight_ListWidget(d->list, heights[d->mode] * lineHeight_Text(d->itemFonts[0])); 689 setItemHeight_ListWidget(d->list, heights[d->mode] * lineHeight_Text(d->itemFonts[0]));
684 } 690 }
691 if (d->certList) {
692 updateItemHeight_CertListWidget(d->certList);
693 }
685} 694}
686 695
687iBool setMode_SidebarWidget(iSidebarWidget *d, enum iSidebarMode mode) { 696iBool setMode_SidebarWidget(iSidebarWidget *d, enum iSidebarMode mode) {
688 if (d->mode == mode) { 697 if (d->mode == mode) {
689 return iFalse; 698 return iFalse;
690 } 699 }
700 if (mode == identities_SidebarMode && deviceType_App() != desktop_AppDeviceType) {
701 return iFalse; /* Identities are in Settings. */
702 }
691 if (d->mode >= 0 && d->mode < max_SidebarMode) { 703 if (d->mode >= 0 && d->mode < max_SidebarMode) {
692 d->modeScroll[d->mode] = scrollPos_ListWidget(d->list); /* saved for later */ 704 d->modeScroll[d->mode] = scrollPos_ListWidget(list_SidebarWidget_(d)); /* saved for later */
693 } 705 }
694 d->mode = mode; 706 d->mode = mode;
695 for (enum iSidebarMode i = 0; i < max_SidebarMode; i++) { 707 for (enum iSidebarMode i = 0; i < max_SidebarMode; i++) {
696 setFlags_Widget(as_Widget(d->modeButtons[i]), selected_WidgetFlag, i == d->mode); 708 setFlags_Widget(as_Widget(d->modeButtons[i]), selected_WidgetFlag, i == d->mode);
697 } 709 }
698 setBackgroundColor_Widget(as_Widget(d->list), 710 setBackgroundColor_Widget(as_Widget(list_SidebarWidget_(d)),
699 d->mode == documentOutline_SidebarMode ? tmBannerBackground_ColorId 711 d->mode == documentOutline_SidebarMode ? tmBannerBackground_ColorId
700 : uiBackgroundSidebar_ColorId); 712 : uiBackgroundSidebar_ColorId);
701 updateItemHeight_SidebarWidget_(d); 713 updateItemHeight_SidebarWidget_(d);
714 if (deviceType_App() != desktop_AppDeviceType && mode != bookmarks_SidebarMode) {
715 setMobileEditMode_SidebarWidget_(d, iFalse);
716 }
702 /* Restore previous scroll position. */ 717 /* Restore previous scroll position. */
703 setScrollPos_ListWidget(d->list, d->modeScroll[mode]); 718 setScrollPos_ListWidget(list_SidebarWidget_(d), d->modeScroll[mode]);
719 /* Title of the mobile sliding sheet. */
720 iLabelWidget *sheetTitle = findChild_Widget(&d->widget, "sidebar.title");
721 if (sheetTitle) {
722 iString title;
723 initCStr_String(&title, normalModeLabels_[d->mode]);
724 removeIconPrefix_String(&title);
725 setText_LabelWidget(sheetTitle, &title);
726 deinit_String(&title);
727 }
704 return iTrue; 728 return iTrue;
705} 729}
706 730
707void setClosedFolders_SidebarWidget(iSidebarWidget *d, const iIntSet *closedFolders) { 731void setClosedFolders_SidebarWidget(iSidebarWidget *d, const iIntSet *closedFolders) {
732 if (d) {
708 delete_IntSet(d->closedFolders); 733 delete_IntSet(d->closedFolders);
709 d->closedFolders = copy_IntSet(closedFolders); 734 d->closedFolders = copy_IntSet(closedFolders);
735 }
736}
737
738void setMidHeight_SidebarWidget(iSidebarWidget *d, int midHeight) {
739 d->midHeight = midHeight;
710} 740}
711 741
712enum iSidebarMode mode_SidebarWidget(const iSidebarWidget *d) { 742enum iSidebarMode mode_SidebarWidget(const iSidebarWidget *d) {
@@ -722,25 +752,9 @@ float width_SidebarWidget(const iSidebarWidget *d) {
722} 752}
723 753
724const iIntSet *closedFolders_SidebarWidget(const iSidebarWidget *d) { 754const iIntSet *closedFolders_SidebarWidget(const iSidebarWidget *d) {
725 return d->closedFolders; 755 return d ? d->closedFolders : collect_IntSet(new_IntSet());
726} 756}
727 757
728static const char *normalModeLabels_[max_SidebarMode] = {
729 book_Icon " ${sidebar.bookmarks}",
730 star_Icon " ${sidebar.feeds}",
731 clock_Icon " ${sidebar.history}",
732 person_Icon " ${sidebar.identities}",
733 page_Icon " ${sidebar.outline}",
734};
735
736static const char *tightModeLabels_[max_SidebarMode] = {
737 book_Icon,
738 star_Icon,
739 clock_Icon,
740 person_Icon,
741 page_Icon,
742};
743
744const char *icon_SidebarMode(enum iSidebarMode mode) { 758const char *icon_SidebarMode(enum iSidebarMode mode) {
745 return tightModeLabels_[mode]; 759 return tightModeLabels_[mode];
746} 760}
@@ -762,6 +776,20 @@ static void updateMetrics_SidebarWidget_(iSidebarWidget *d) {
762 updateItemHeight_SidebarWidget_(d); 776 updateItemHeight_SidebarWidget_(d);
763} 777}
764 778
779static void updateSlidingSheetHeight_SidebarWidget_(iSidebarWidget *sidebar, iRoot *root) {
780 if (!isPortraitPhone_App() || !isVisible_Widget(sidebar)) return;
781 iWidget *d = as_Widget(sidebar);
782 const int oldSize = d->rect.size.y;
783 const int newSize = bottom_Rect(safeRect_Root(d->root)) - top_Rect(bounds_Widget(d));
784 if (oldSize != newSize) {
785 d->rect.size.y = newSize;
786 arrange_Widget(d);
787 }
788// printf("[%p] %u: %d animating %d\n", d, window_Widget(d)->frameTime,
789// (flags_Widget(d) & visualOffset_WidgetFlag) != 0,
790// newSize);
791}
792
765void init_SidebarWidget(iSidebarWidget *d, enum iSidebarSide side) { 793void init_SidebarWidget(iSidebarWidget *d, enum iSidebarSide side) {
766 iWidget *w = as_Widget(d); 794 iWidget *w = as_Widget(d);
767 init_Widget(w); 795 init_Widget(w);
@@ -778,6 +806,8 @@ void init_SidebarWidget(iSidebarWidget *d, enum iSidebarSide side) {
778 d->side = side; 806 d->side = side;
779 d->mode = -1; 807 d->mode = -1;
780 d->feedsMode = all_FeedsMode; 808 d->feedsMode = all_FeedsMode;
809 d->midHeight = 0;
810 d->isEditing = iFalse;
781 d->numUnreadEntries = 0; 811 d->numUnreadEntries = 0;
782 d->buttonFont = uiLabel_FontId; /* wiil be changed later */ 812 d->buttonFont = uiLabel_FontId; /* wiil be changed later */
783 d->itemFonts[0] = uiContent_FontId; 813 d->itemFonts[0] = uiContent_FontId;
@@ -795,18 +825,38 @@ void init_SidebarWidget(iSidebarWidget *d, enum iSidebarSide side) {
795 iWidget *vdiv = makeVDiv_Widget(); 825 iWidget *vdiv = makeVDiv_Widget();
796 addChildFlags_Widget(w, vdiv, resizeToParentWidth_WidgetFlag | resizeToParentHeight_WidgetFlag); 826 addChildFlags_Widget(w, vdiv, resizeToParentWidth_WidgetFlag | resizeToParentHeight_WidgetFlag);
797 iZap(d->modeButtons); 827 iZap(d->modeButtons);
798 d->resizer = NULL; 828 d->resizer = NULL;
799 d->list = NULL; 829 d->list = NULL;
800 d->actions = NULL; 830 d->certList = NULL;
831 d->actions = NULL;
801 d->closedFolders = new_IntSet(); 832 d->closedFolders = new_IntSet();
802 /* On a phone, the right sidebar is used exclusively for Identities. */ 833 /* On a phone, the right sidebar is not used. */
803 const iBool isPhone = deviceType_App() == phone_AppDeviceType; 834 const iBool isPhone = (deviceType_App() == phone_AppDeviceType);
804 if (!isPhone || d->side == left_SidebarSide) { 835 if (isPhone) {
836 iLabelWidget *sheetTitle =
837 addChildFlags_Widget(vdiv,
838 iClob(new_LabelWidget("", NULL)),
839 collapse_WidgetFlag |
840 extraPadding_WidgetFlag | frameless_WidgetFlag);
841 setBackgroundColor_Widget(as_Widget(sheetTitle), uiBackground_ColorId);
842 iLabelWidget *closeButton = addChildFlags_Widget(as_Widget(sheetTitle),
843 iClob(new_LabelWidget(uiTextAction_ColorEscape "${sidebar.close}", "sidebar.toggle")),
844 extraPadding_WidgetFlag | frameless_WidgetFlag |
845 alignRight_WidgetFlag | moveToParentRightEdge_WidgetFlag);
846 as_Widget(sheetTitle)->flags2 |= slidingSheetDraggable_WidgetFlag2; /* phone */
847 as_Widget(closeButton)->flags2 |= slidingSheetDraggable_WidgetFlag2; /* phone */
848 setId_Widget(as_Widget(sheetTitle), "sidebar.title");
849 setId_Widget(as_Widget(closeButton), "sidebar.close");
850 setFont_LabelWidget(sheetTitle, uiLabelBig_FontId);
851 setFont_LabelWidget(closeButton, uiLabelBigBold_FontId);
852 iConnect(Root, get_Root(), visualOffsetsChanged, d, updateSlidingSheetHeight_SidebarWidget_);
853 }
805 iWidget *buttons = new_Widget(); 854 iWidget *buttons = new_Widget();
806 setId_Widget(buttons, "buttons"); 855 setId_Widget(buttons, "buttons");
807 setDrawBufferEnabled_Widget(buttons, iTrue); 856 setDrawBufferEnabled_Widget(buttons, iTrue);
808 for (int i = 0; i < max_SidebarMode; i++) { 857 for (int i = 0; i < max_SidebarMode; i++) {
809 if (deviceType_App() == phone_AppDeviceType && i == identities_SidebarMode) { 858 if (i == identities_SidebarMode && deviceType_App() != desktop_AppDeviceType) {
859 /* On mobile, identities are managed via Settings. */
810 continue; 860 continue;
811 } 861 }
812 d->modeButtons[i] = addChildFlags_Widget( 862 d->modeButtons[i] = addChildFlags_Widget(
@@ -815,51 +865,54 @@ void init_SidebarWidget(iSidebarWidget *d, enum iSidebarSide side) {
815 tightModeLabels_[i], 865 tightModeLabels_[i],
816 format_CStr("%s.mode arg:%d", cstr_String(id_Widget(w)), i))), 866 format_CStr("%s.mode arg:%d", cstr_String(id_Widget(w)), i))),
817 frameless_WidgetFlag | noBackground_WidgetFlag); 867 frameless_WidgetFlag | noBackground_WidgetFlag);
868 as_Widget(d->modeButtons[i])->flags2 |= slidingSheetDraggable_WidgetFlag2; /* phone */
818 } 869 }
819 setButtonFont_SidebarWidget(d, isPhone ? uiLabelBig_FontId : uiLabel_FontId); 870 setButtonFont_SidebarWidget(d, isPhone ? uiLabelBig_FontId : uiLabel_FontId);
820 addChildFlags_Widget(vdiv, 871 addChildFlags_Widget(vdiv,
821 iClob(buttons), 872 iClob(buttons),
822 arrangeHorizontal_WidgetFlag | 873 arrangeHorizontal_WidgetFlag | resizeWidthOfChildren_WidgetFlag |
823 resizeWidthOfChildren_WidgetFlag | 874 arrangeHeight_WidgetFlag | resizeToParentWidth_WidgetFlag);
824 arrangeHeight_WidgetFlag | resizeToParentWidth_WidgetFlag); // |
825// drawBackgroundToHorizontalSafeArea_WidgetFlag);
826 setBackgroundColor_Widget(buttons, uiBackgroundSidebar_ColorId); 875 setBackgroundColor_Widget(buttons, uiBackgroundSidebar_ColorId);
827 }
828 else {
829 iLabelWidget *heading = new_LabelWidget(person_Icon " ${sidebar.identities}", NULL);
830 checkIcon_LabelWidget(heading);
831 setBackgroundColor_Widget(as_Widget(heading), uiBackgroundSidebar_ColorId);
832 setTextColor_LabelWidget(heading, uiTextSelected_ColorId);
833 setFont_LabelWidget(addChildFlags_Widget(vdiv, iClob(heading), borderTop_WidgetFlag |
834 alignLeft_WidgetFlag | frameless_WidgetFlag |
835 drawBackgroundToHorizontalSafeArea_WidgetFlag),
836 uiLabelLargeBold_FontId);
837 }
838 iWidget *content = new_Widget(); 876 iWidget *content = new_Widget();
839 setFlags_Widget(content, resizeChildren_WidgetFlag, iTrue); 877 setFlags_Widget(content, resizeChildren_WidgetFlag, iTrue);
840 iWidget *listAndActions = makeVDiv_Widget(); 878 iWidget *listAndActions = makeVDiv_Widget();
841 addChild_Widget(content, iClob(listAndActions)); 879 addChild_Widget(content, iClob(listAndActions));
880 iWidget *listArea = new_Widget();
881 setFlags_Widget(listArea, resizeChildren_WidgetFlag, iTrue);
842 d->list = new_ListWidget(); 882 d->list = new_ListWidget();
843 setPadding_Widget(as_Widget(d->list), 0, gap_UI, 0, gap_UI); 883 setPadding_Widget(as_Widget(d->list), 0, gap_UI, 0, gap_UI);
884 addChild_Widget(listArea, iClob(d->list));
885 if (!isPhone) {
886 d->certList = new_CertListWidget();
887 setPadding_Widget(as_Widget(d->certList), 0, gap_UI, 0, gap_UI);
888 addChild_Widget(listArea, iClob(d->certList));
889 }
844 addChildFlags_Widget(listAndActions, 890 addChildFlags_Widget(listAndActions,
845 iClob(d->list), 891 iClob(listArea),
846 expand_WidgetFlag); // | drawBackgroundToHorizontalSafeArea_WidgetFlag); 892 expand_WidgetFlag); // | drawBackgroundToHorizontalSafeArea_WidgetFlag);
847 setId_Widget(addChildPosFlags_Widget(listAndActions, 893 setId_Widget(addChildPosFlags_Widget(listAndActions,
848 iClob(d->actions = new_Widget()), 894 iClob(d->actions = new_Widget()),
849 isPhone ? front_WidgetAddPos : back_WidgetAddPos, 895 /*isPhone ? front_WidgetAddPos :*/ back_WidgetAddPos,
850 arrangeHorizontal_WidgetFlag | arrangeHeight_WidgetFlag | 896 arrangeHorizontal_WidgetFlag | arrangeHeight_WidgetFlag |
851 resizeWidthOfChildren_WidgetFlag), // | 897 resizeWidthOfChildren_WidgetFlag), // |
852// drawBackgroundToHorizontalSafeArea_WidgetFlag), 898// drawBackgroundToHorizontalSafeArea_WidgetFlag),
853 "actions"); 899 "actions");
900 if (deviceType_App() != desktop_AppDeviceType) {
901 setFlags_Widget(findChild_Widget(w, "sidebar.title"), borderTop_WidgetFlag, iTrue);
902 setFlags_Widget(d->actions, drawBackgroundToBottom_WidgetFlag, iTrue);
903 setBackgroundColor_Widget(d->actions, uiBackground_ColorId);
904 }
905 else {
854 setBackgroundColor_Widget(d->actions, uiBackgroundSidebar_ColorId); 906 setBackgroundColor_Widget(d->actions, uiBackgroundSidebar_ColorId);
907 }
855 d->contextItem = NULL; 908 d->contextItem = NULL;
856 d->contextIndex = iInvalidPos; 909 d->contextIndex = iInvalidPos;
857 d->blank = new_Widget(); 910 d->blank = new_Widget();
858 addChildFlags_Widget(content, iClob(d->blank), resizeChildren_WidgetFlag); 911 addChildFlags_Widget(content, iClob(d->blank), resizeChildren_WidgetFlag);
859 addChildFlags_Widget(vdiv, iClob(content), expand_WidgetFlag); 912 addChildFlags_Widget(vdiv, iClob(content), expand_WidgetFlag);
860 setMode_SidebarWidget(d, 913 setMode_SidebarWidget(d,
861 deviceType_App() == phone_AppDeviceType && d->side == right_SidebarSide ? 914 /*deviceType_App() == phone_AppDeviceType && d->side == right_SidebarSide ?
862 identities_SidebarMode : bookmarks_SidebarMode); 915 identities_SidebarMode :*/ bookmarks_SidebarMode);
863 d->resizer = 916 d->resizer =
864 addChildFlags_Widget(w, 917 addChildFlags_Widget(w,
865 iClob(new_Widget()), 918 iClob(new_Widget()),
@@ -867,7 +920,7 @@ void init_SidebarWidget(iSidebarWidget *d, enum iSidebarSide side) {
867 resizeToParentHeight_WidgetFlag | 920 resizeToParentHeight_WidgetFlag |
868 (side == left_SidebarSide ? moveToParentRightEdge_WidgetFlag 921 (side == left_SidebarSide ? moveToParentRightEdge_WidgetFlag
869 : moveToParentLeftEdge_WidgetFlag)); 922 : moveToParentLeftEdge_WidgetFlag));
870 if (deviceType_App() == phone_AppDeviceType) { 923 if (deviceType_App() != desktop_AppDeviceType) {
871 setFlags_Widget(d->resizer, hidden_WidgetFlag | disabled_WidgetFlag, iTrue); 924 setFlags_Widget(d->resizer, hidden_WidgetFlag | disabled_WidgetFlag, iTrue);
872 } 925 }
873 setId_Widget(d->resizer, side == left_SidebarSide ? "sidebar.grab" : "sidebar2.grab"); 926 setId_Widget(d->resizer, side == left_SidebarSide ? "sidebar.grab" : "sidebar2.grab");
@@ -902,16 +955,16 @@ iBool setButtonFont_SidebarWidget(iSidebarWidget *d, int font) {
902 955
903static const iGmIdentity *constHoverIdentity_SidebarWidget_(const iSidebarWidget *d) { 956static const iGmIdentity *constHoverIdentity_SidebarWidget_(const iSidebarWidget *d) {
904 if (d->mode == identities_SidebarMode) { 957 if (d->mode == identities_SidebarMode) {
905 const iSidebarItem *hoverItem = constHoverItem_ListWidget(d->list); 958 return constHoverIdentity_CertListWidget(d->certList);
906 if (hoverItem) {
907 return identity_GmCerts(certs_App(), hoverItem->id);
908 } 959 }
909 }
910 return NULL; 960 return NULL;
911} 961}
912 962
913static iGmIdentity *hoverIdentity_SidebarWidget_(const iSidebarWidget *d) { 963static iGmIdentity *hoverIdentity_SidebarWidget_(const iSidebarWidget *d) {
914 return iConstCast(iGmIdentity *, constHoverIdentity_SidebarWidget_(d)); 964 if (d->mode == identities_SidebarMode) {
965 return hoverIdentity_CertListWidget(d->certList);
966 }
967 return NULL;
915} 968}
916 969
917static void itemClicked_SidebarWidget_(iSidebarWidget *d, iSidebarItem *item, size_t itemIndex) { 970static void itemClicked_SidebarWidget_(iSidebarWidget *d, iSidebarItem *item, size_t itemIndex) {
@@ -923,7 +976,6 @@ static void itemClicked_SidebarWidget_(iSidebarWidget *d, iSidebarItem *item, si
923 const iGmHeading *head = constAt_Array(headings_GmDocument(doc), item->id); 976 const iGmHeading *head = constAt_Array(headings_GmDocument(doc), item->id);
924 postCommandf_App("document.goto loc:%p", head->text.start); 977 postCommandf_App("document.goto loc:%p", head->text.start);
925 dismissPortraitPhoneSidebars_Root(as_Widget(d)->root); 978 dismissPortraitPhoneSidebars_Root(as_Widget(d)->root);
926 setOpenedFromSidebar_DocumentWidget(document_App(), iTrue);
927 } 979 }
928 break; 980 break;
929 } 981 }
@@ -945,6 +997,12 @@ static void itemClicked_SidebarWidget_(iSidebarWidget *d, iSidebarItem *item, si
945 updateItems_SidebarWidget_(d); 997 updateItems_SidebarWidget_(d);
946 break; 998 break;
947 } 999 }
1000 if (d->isEditing) {
1001 d->contextItem = item;
1002 d->contextIndex = itemIndex;
1003 postCommand_Widget(d, "bookmark.edit");
1004 break;
1005 }
948 /* fall through */ 1006 /* fall through */
949 case history_SidebarMode: { 1007 case history_SidebarMode: {
950 if (!isEmpty_String(&item->url)) { 1008 if (!isEmpty_String(&item->url)) {
@@ -954,23 +1012,6 @@ static void itemClicked_SidebarWidget_(iSidebarWidget *d, iSidebarItem *item, si
954 } 1012 }
955 break; 1013 break;
956 } 1014 }
957 case identities_SidebarMode: {
958 d->contextItem = item;
959 if (d->contextIndex != iInvalidPos) {
960 invalidateItem_ListWidget(d->list, d->contextIndex);
961 }
962 d->contextIndex = itemIndex;
963 if (itemIndex < numItems_ListWidget(d->list)) {
964 updateContextMenu_SidebarWidget_(d);
965 arrange_Widget(d->menu);
966 openMenu_Widget(d->menu,
967 d->side == left_SidebarSide
968 ? topRight_Rect(itemRect_ListWidget(d->list, itemIndex))
969 : addX_I2(topLeft_Rect(itemRect_ListWidget(d->list, itemIndex)),
970 -width_Widget(d->menu)));
971 }
972 break;
973 }
974 default: 1015 default:
975 break; 1016 break;
976 } 1017 }
@@ -990,7 +1031,7 @@ static void checkModeButtonLayout_SidebarWidget_(iSidebarWidget *d) {
990// updateMetrics_SidebarWidget_(d); 1031// updateMetrics_SidebarWidget_(d);
991 updateItemHeight_SidebarWidget_(d); 1032 updateItemHeight_SidebarWidget_(d);
992 } 1033 }
993 setButtonFont_SidebarWidget(d, isPortrait_App() ? uiLabelBig_FontId : uiLabel_FontId); 1034 setButtonFont_SidebarWidget(d, isPortrait_App() ? uiLabelMedium_FontId : uiLabel_FontId);
994 } 1035 }
995 const iBool isTight = 1036 const iBool isTight =
996 (width_Rect(bounds_Widget(as_Widget(d->modeButtons[0]))) < d->maxButtonLabelWidth); 1037 (width_Rect(bounds_Widget(as_Widget(d->modeButtons[0]))) < d->maxButtonLabelWidth);
@@ -1018,6 +1059,7 @@ static void checkModeButtonLayout_SidebarWidget_(iSidebarWidget *d) {
1018} 1059}
1019 1060
1020void setWidth_SidebarWidget(iSidebarWidget *d, float widthAsGaps) { 1061void setWidth_SidebarWidget(iSidebarWidget *d, float widthAsGaps) {
1062 if (!d) return;
1021 iWidget *w = as_Widget(d); 1063 iWidget *w = as_Widget(d);
1022 const iBool isFixedWidth = deviceType_App() == phone_AppDeviceType; 1064 const iBool isFixedWidth = deviceType_App() == phone_AppDeviceType;
1023 int width = widthAsGaps * gap_UI; /* in pixels */ 1065 int width = widthAsGaps * gap_UI; /* in pixels */
@@ -1056,19 +1098,16 @@ iBool handleBookmarkEditorCommands_SidebarWidget_(iWidget *editor, const char *c
1056 set_String(&bm->url, url); 1098 set_String(&bm->url, url);
1057 set_String(&bm->tags, tags); 1099 set_String(&bm->tags, tags);
1058 if (isEmpty_String(icon)) { 1100 if (isEmpty_String(icon)) {
1059 removeTag_Bookmark(bm, userIcon_BookmarkTag); 1101 bm->flags &= ~userIcon_BookmarkFlag;
1060 bm->icon = 0; 1102 bm->icon = 0;
1061 } 1103 }
1062 else { 1104 else {
1063 addTagIfMissing_Bookmark(bm, userIcon_BookmarkTag); 1105 bm->flags |= userIcon_BookmarkFlag;
1064 bm->icon = first_String(icon); 1106 bm->icon = first_String(icon);
1065 } 1107 }
1066 addOrRemoveTag_Bookmark(bm, homepage_BookmarkTag, 1108 iChangeFlags(bm->flags, homepage_BookmarkFlag, isSelected_Widget(findChild_Widget(editor, "bmed.tag.home")));
1067 isSelected_Widget(findChild_Widget(editor, "bmed.tag.home"))); 1109 iChangeFlags(bm->flags, remoteSource_BookmarkFlag, isSelected_Widget(findChild_Widget(editor, "bmed.tag.remote")));
1068 addOrRemoveTag_Bookmark(bm, remoteSource_BookmarkTag, 1110 iChangeFlags(bm->flags, linkSplit_BookmarkFlag, isSelected_Widget(findChild_Widget(editor, "bmed.tag.linksplit")));
1069 isSelected_Widget(findChild_Widget(editor, "bmed.tag.remote")));
1070 addOrRemoveTag_Bookmark(bm, linkSplit_BookmarkTag,
1071 isSelected_Widget(findChild_Widget(editor, "bmed.tag.linksplit")));
1072 } 1111 }
1073 const iBookmark *folder = userData_Object(findChild_Widget(editor, "bmed.folder")); 1112 const iBookmark *folder = userData_Object(findChild_Widget(editor, "bmed.folder"));
1074 if (!folder || !hasParent_Bookmark(folder, id_Bookmark(bm))) { 1113 if (!folder || !hasParent_Bookmark(folder, id_Bookmark(bm))) {
@@ -1083,6 +1122,36 @@ iBool handleBookmarkEditorCommands_SidebarWidget_(iWidget *editor, const char *c
1083 return iFalse; 1122 return iFalse;
1084} 1123}
1085 1124
1125enum iSlidingSheetPos {
1126 top_SlidingSheetPos,
1127 middle_SlidingSheetPos,
1128 bottom_SlidingSheetPos,
1129};
1130
1131static void setSlidingSheetPos_SidebarWidget_(iSidebarWidget *d, enum iSlidingSheetPos slide) {
1132 iWidget *w = as_Widget(d);
1133 const int pos = w->rect.pos.y;
1134 const iRect safeRect = safeRect_Root(w->root);
1135 if (slide == top_SlidingSheetPos) {
1136 w->rect.pos.y = top_Rect(safeRect);
1137 w->rect.size.y = height_Rect(safeRect);
1138 setVisualOffset_Widget(w, pos - w->rect.pos.y, 0, 0);
1139 setVisualOffset_Widget(w, 0, 200, easeOut_AnimFlag | softer_AnimFlag);
1140 setScrollMode_ListWidget(d->list, disabledAtTopUpwards_ScrollMode);
1141 }
1142 else if (slide == bottom_SlidingSheetPos) {
1143 postCommand_Widget(w, "sidebar.toggle");
1144 }
1145 else {
1146 w->rect.size.y = d->midHeight;
1147 w->rect.pos.y = height_Rect(safeRect) - w->rect.size.y;
1148 setVisualOffset_Widget(w, pos - w->rect.pos.y, 0, 0);
1149 setVisualOffset_Widget(w, 0, 200, easeOut_AnimFlag | softer_AnimFlag);
1150 setScrollMode_ListWidget(d->list, disabledAtTopBothDirections_ScrollMode);
1151 }
1152// animateSlidingSheetHeight_SidebarWidget_(d);
1153}
1154
1086static iBool handleSidebarCommand_SidebarWidget_(iSidebarWidget *d, const char *cmd) { 1155static iBool handleSidebarCommand_SidebarWidget_(iSidebarWidget *d, const char *cmd) {
1087 iWidget *w = as_Widget(d); 1156 iWidget *w = as_Widget(d);
1088 if (equal_Command(cmd, "width")) { 1157 if (equal_Command(cmd, "width")) {
@@ -1112,13 +1181,18 @@ static iBool handleSidebarCommand_SidebarWidget_(iSidebarWidget *d, const char *
1112 argLabel_Command(cmd, "noanim") == 0 && 1181 argLabel_Command(cmd, "noanim") == 0 &&
1113 (d->side == left_SidebarSide || deviceType_App() != phone_AppDeviceType); 1182 (d->side == left_SidebarSide || deviceType_App() != phone_AppDeviceType);
1114 int visX = 0; 1183 int visX = 0;
1184 int visY = 0;
1115 if (isVisible_Widget(w)) { 1185 if (isVisible_Widget(w)) {
1116 visX = left_Rect(bounds_Widget(w)) - left_Rect(w->root->widget->rect); 1186 visX = left_Rect(bounds_Widget(w)) - left_Rect(w->root->widget->rect);
1187 visY = top_Rect(bounds_Widget(w)) - top_Rect(w->root->widget->rect);
1117 } 1188 }
1118 setFlags_Widget(w, hidden_WidgetFlag, isVisible_Widget(w)); 1189 const iBool isHiding = isVisible_Widget(w);
1190 setFlags_Widget(w, hidden_WidgetFlag, isHiding);
1119 /* Safe area inset for mobile. */ 1191 /* Safe area inset for mobile. */
1120 const int safePad = (d->side == left_SidebarSide ? left_Rect(safeRect_Root(w->root)) : 0); 1192 const int safePad = (d->side == left_SidebarSide ? left_Rect(safeRect_Root(w->root)) : 0);
1121 if (isVisible_Widget(w)) { 1193 const int animFlags = easeOut_AnimFlag | softer_AnimFlag;
1194 if (!isPortraitPhone_App()) {
1195 if (!isHiding) {
1122 setFlags_Widget(w, keepOnTop_WidgetFlag, iFalse); 1196 setFlags_Widget(w, keepOnTop_WidgetFlag, iFalse);
1123 w->rect.size.x = d->widthAsGaps * gap_UI; 1197 w->rect.size.x = d->widthAsGaps * gap_UI;
1124 invalidate_ListWidget(d->list); 1198 invalidate_ListWidget(d->list);
@@ -1126,7 +1200,7 @@ static iBool handleSidebarCommand_SidebarWidget_(iSidebarWidget *d, const char *
1126 setFlags_Widget(w, horizontalOffset_WidgetFlag, iTrue); 1200 setFlags_Widget(w, horizontalOffset_WidgetFlag, iTrue);
1127 setVisualOffset_Widget( 1201 setVisualOffset_Widget(
1128 w, (d->side == left_SidebarSide ? -1 : 1) * (w->rect.size.x + safePad), 0, 0); 1202 w, (d->side == left_SidebarSide ? -1 : 1) * (w->rect.size.x + safePad), 0, 0);
1129 setVisualOffset_Widget(w, 0, 300, easeOut_AnimFlag | softer_AnimFlag); 1203 setVisualOffset_Widget(w, 0, 300, animFlags);
1130 } 1204 }
1131 } 1205 }
1132 else if (isAnimated) { 1206 else if (isAnimated) {
@@ -1134,19 +1208,42 @@ static iBool handleSidebarCommand_SidebarWidget_(iSidebarWidget *d, const char *
1134 if (d->side == right_SidebarSide) { 1208 if (d->side == right_SidebarSide) {
1135 setVisualOffset_Widget(w, visX, 0, 0); 1209 setVisualOffset_Widget(w, visX, 0, 0);
1136 setVisualOffset_Widget( 1210 setVisualOffset_Widget(
1137 w, visX + w->rect.size.x + safePad, 300, easeOut_AnimFlag | softer_AnimFlag); 1211 w, visX + w->rect.size.x + safePad, 300, animFlags);
1138 } 1212 }
1139 else { 1213 else {
1140 setFlags_Widget(w, keepOnTop_WidgetFlag, iTrue); 1214 setFlags_Widget(w, keepOnTop_WidgetFlag, iTrue);
1141 setVisualOffset_Widget( 1215 setVisualOffset_Widget(
1142 w, -w->rect.size.x - safePad, 300, easeOut_AnimFlag | softer_AnimFlag); 1216 w, -w->rect.size.x - safePad, 300, animFlags);
1217 }
1143 } 1218 }
1219 setScrollMode_ListWidget(d->list, normal_ScrollMode);
1220 }
1221 else {
1222 /* Portrait phone sidebar works differently: it slides up from the bottom. */
1223 setFlags_Widget(w, horizontalOffset_WidgetFlag, iFalse);
1224 if (!isHiding) {
1225 invalidate_ListWidget(d->list);
1226 w->rect.pos.y = height_Rect(safeRect_Root(w->root)) - d->midHeight;
1227 setVisualOffset_Widget(w, bottom_Rect(rect_Root(w->root)) - w->rect.pos.y, 0, 0);
1228 setVisualOffset_Widget(w, 0, 300, animFlags);
1229 //animateSlidingSheetHeight_SidebarWidget_(d);
1230 setScrollMode_ListWidget(d->list, disabledAtTopBothDirections_ScrollMode);
1231 }
1232 else {
1233 setVisualOffset_Widget(w, bottom_Rect(rect_Root(w->root)) - w->rect.pos.y, 300, animFlags);
1234 if (d->isEditing) {
1235 setMobileEditMode_SidebarWidget_(d, iFalse);
1236 }
1237 }
1238 showToolbar_Root(w->root, isHiding);
1144 } 1239 }
1145 updateToolbarColors_Root(w->root); 1240 updateToolbarColors_Root(w->root);
1146 arrange_Widget(w->parent); 1241 arrange_Widget(w->parent);
1147 /* BUG: Rearranging because the arrange above didn't fully resolve the height. */ 1242 /* BUG: Rearranging because the arrange above didn't fully resolve the height. */
1148 arrange_Widget(w); 1243 arrange_Widget(w);
1244 if (!isPortraitPhone_App()) {
1149 updateSize_DocumentWidget(document_App()); 1245 updateSize_DocumentWidget(document_App());
1246 }
1150 if (isVisible_Widget(w)) { 1247 if (isVisible_Widget(w)) {
1151 updateItems_SidebarWidget_(d); 1248 updateItems_SidebarWidget_(d);
1152 scrollOffset_ListWidget(d->list, 0); 1249 scrollOffset_ListWidget(d->list, 0);
@@ -1154,6 +1251,10 @@ static iBool handleSidebarCommand_SidebarWidget_(iSidebarWidget *d, const char *
1154 refresh_Widget(w->parent); 1251 refresh_Widget(w->parent);
1155 return iTrue; 1252 return iTrue;
1156 } 1253 }
1254 else if (equal_Command(cmd, "bookmarks.edit")) {
1255 setMobileEditMode_SidebarWidget_(d, !d->isEditing);
1256 invalidate_ListWidget(d->list);
1257 }
1157 return iFalse; 1258 return iFalse;
1158} 1259}
1159 1260
@@ -1166,7 +1267,7 @@ static void bookmarkMoved_SidebarWidget_(iSidebarWidget *d, size_t index, size_t
1166 : dstIndex); 1267 : dstIndex);
1167 if (isLast && isBefore) isBefore = iFalse; 1268 if (isLast && isBefore) isBefore = iFalse;
1168 const iBookmark *dst = get_Bookmarks(bookmarks_App(), dstItem->id); 1269 const iBookmark *dst = get_Bookmarks(bookmarks_App(), dstItem->id);
1169 if (hasParent_Bookmark(dst, movingItem->id) || hasTag_Bookmark(dst, remote_BookmarkTag)) { 1270 if (hasParent_Bookmark(dst, movingItem->id) || dst->flags & remote_BookmarkFlag) {
1170 /* Can't move a folder inside itself, and remote bookmarks cannot be reordered. */ 1271 /* Can't move a folder inside itself, and remote bookmarks cannot be reordered. */
1171 return; 1272 return;
1172 } 1273 }
@@ -1190,20 +1291,41 @@ static void bookmarkMovedOntoFolder_SidebarWidget_(iSidebarWidget *d, size_t ind
1190static size_t numBookmarks_(const iPtrArray *bmList) { 1291static size_t numBookmarks_(const iPtrArray *bmList) {
1191 size_t num = 0; 1292 size_t num = 0;
1192 iConstForEach(PtrArray, i, bmList) { 1293 iConstForEach(PtrArray, i, bmList) {
1193 if (!isFolder_Bookmark(i.ptr) && !hasTag_Bookmark(i.ptr, remote_BookmarkTag)) { 1294 const iBookmark *bm = i.ptr;
1295 if (!isFolder_Bookmark(bm) && ~bm->flags & remote_BookmarkFlag) {
1194 num++; 1296 num++;
1195 } 1297 }
1196 } 1298 }
1197 return num; 1299 return num;
1198} 1300}
1199 1301
1302static iRangei SlidingSheetMiddleRegion_SidebarWidget_(const iSidebarWidget *d) {
1303 const iWidget *w = constAs_Widget(d);
1304 const iRect safeRect = safeRect_Root(w->root);
1305 const int midY = bottom_Rect(safeRect) - d->midHeight;
1306 const int topHalf = (top_Rect(safeRect) + midY) / 2;
1307 const int bottomHalf = (bottom_Rect(safeRect) + midY * 2) / 3;
1308 return (iRangei){ topHalf, bottomHalf };
1309}
1310
1311static void gotoNearestSlidingSheetPos_SidebarWidget_(iSidebarWidget *d) {
1312 const iRangei midRegion = SlidingSheetMiddleRegion_SidebarWidget_(d);
1313 const int pos = top_Rect(d->widget.rect);
1314 setSlidingSheetPos_SidebarWidget_(d, pos < midRegion.start
1315 ? top_SlidingSheetPos
1316 : pos > midRegion.end ? bottom_SlidingSheetPos
1317 : middle_SlidingSheetPos);
1318}
1319
1200static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev) { 1320static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev) {
1201 iWidget *w = as_Widget(d); 1321 iWidget *w = as_Widget(d);
1202 /* Handle commands. */ 1322 /* Handle commands. */
1203 if (isResize_UserEvent(ev)) { 1323 if (isResize_UserEvent(ev)) {
1204 checkModeButtonLayout_SidebarWidget_(d); 1324 checkModeButtonLayout_SidebarWidget_(d);
1205 if (deviceType_App() == phone_AppDeviceType && d->side == left_SidebarSide) { 1325 if (deviceType_App() == phone_AppDeviceType) {
1206 setFlags_Widget(w, rightEdgeDraggable_WidgetFlag, isPortrait_App()); 1326 setPadding_Widget(d->actions, 0, 0, 0, 0);
1327 setFlags_Widget(findChild_Widget(w, "sidebar.title"), hidden_WidgetFlag, isLandscape_App());
1328 setFlags_Widget(findChild_Widget(w, "sidebar.close"), hidden_WidgetFlag, isLandscape_App());
1207 /* In landscape, visibility of the toolbar is controlled separately. */ 1329 /* In landscape, visibility of the toolbar is controlled separately. */
1208 if (isVisible_Widget(w)) { 1330 if (isVisible_Widget(w)) {
1209 postCommand_Widget(w, "sidebar.toggle"); 1331 postCommand_Widget(w, "sidebar.toggle");
@@ -1217,8 +1339,16 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev)
1217 setFlags_Widget(as_Widget(d->list), 1339 setFlags_Widget(as_Widget(d->list),
1218 drawBackgroundToHorizontalSafeArea_WidgetFlag, 1340 drawBackgroundToHorizontalSafeArea_WidgetFlag,
1219 isLandscape_App()); 1341 isLandscape_App());
1220 return iFalse; 1342 setFlags_Widget(w,
1343 drawBackgroundToBottom_WidgetFlag,
1344 isPortrait_App());
1345 setBackgroundColor_Widget(w, isPortrait_App() ? uiBackgroundSidebar_ColorId : none_ColorId);
1346 }
1347 if (!isPortraitPhone_App()) {
1348 /* In sliding sheet mode, sidebar is resized to fit in the safe area. */
1349 setPadding_Widget(d->actions, 0, 0, 0, bottomSafeInset_Mobile());
1221 } 1350 }
1351 return iFalse;
1222 } 1352 }
1223 else if (isMetricsChange_UserEvent(ev)) { 1353 else if (isMetricsChange_UserEvent(ev)) {
1224 if (isVisible_Widget(w)) { 1354 if (isVisible_Widget(w)) {
@@ -1230,7 +1360,7 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev)
1230 } 1360 }
1231 else if (ev->type == SDL_USEREVENT && ev->user.code == command_UserEventCode) { 1361 else if (ev->type == SDL_USEREVENT && ev->user.code == command_UserEventCode) {
1232 const char *cmd = command_UserEvent(ev); 1362 const char *cmd = command_UserEvent(ev);
1233 if (equal_Command(cmd, "tabs.changed") || equal_Command(cmd, "document.changed")) { 1363 if (startsWith_CStr(cmd, "tabs.changed id:doc") || equal_Command(cmd, "document.changed")) {
1234 updateItems_SidebarWidget_(d); 1364 updateItems_SidebarWidget_(d);
1235 scrollOffset_ListWidget(d->list, 0); 1365 scrollOffset_ListWidget(d->list, 0);
1236 } 1366 }
@@ -1257,13 +1387,6 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev)
1257 } 1387 }
1258 } 1388 }
1259 } 1389 }
1260 else if (equal_Command(cmd, "idents.changed") && d->mode == identities_SidebarMode) {
1261 updateItems_SidebarWidget_(d);
1262 }
1263 else if (deviceType_App() == tablet_AppDeviceType && equal_Command(cmd, "toolbar.showident")) {
1264 postCommandf_App("sidebar.mode arg:%d toggle:1", identities_SidebarMode);
1265 return iTrue;
1266 }
1267 else if (isPortraitPhone_App() && isVisible_Widget(w) && d->side == left_SidebarSide && 1390 else if (isPortraitPhone_App() && isVisible_Widget(w) && d->side == left_SidebarSide &&
1268 equal_Command(cmd, "swipe.forward")) { 1391 equal_Command(cmd, "swipe.forward")) {
1269 postCommand_App("sidebar.toggle"); 1392 postCommand_App("sidebar.toggle");
@@ -1328,9 +1451,6 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev)
1328 } 1451 }
1329 return iTrue; 1452 return iTrue;
1330 } 1453 }
1331// else if (isCommand_Widget(w, ev, "menu.closed")) {
1332 // invalidateItem_ListWidget(d->list, d->contextIndex);
1333// }
1334 else if (isCommand_Widget(w, ev, "bookmark.open")) { 1454 else if (isCommand_Widget(w, ev, "bookmark.open")) {
1335 const iSidebarItem *item = d->contextItem; 1455 const iSidebarItem *item = d->contextItem;
1336 if (d->mode == bookmarks_SidebarMode && item) { 1456 if (d->mode == bookmarks_SidebarMode && item) {
@@ -1363,13 +1483,13 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev)
1363 if (!isFolder_Bookmark(bm)) { 1483 if (!isFolder_Bookmark(bm)) {
1364 setText_InputWidget(urlInput, &bm->url); 1484 setText_InputWidget(urlInput, &bm->url);
1365 setText_InputWidget(tagsInput, &bm->tags); 1485 setText_InputWidget(tagsInput, &bm->tags);
1366 if (hasTag_Bookmark(bm, userIcon_BookmarkTag)) { 1486 if (bm->flags & userIcon_BookmarkFlag) {
1367 setText_InputWidget(iconInput, 1487 setText_InputWidget(iconInput,
1368 collect_String(newUnicodeN_String(&bm->icon, 1))); 1488 collect_String(newUnicodeN_String(&bm->icon, 1)));
1369 } 1489 }
1370 setToggle_Widget(homeTag, hasTag_Bookmark(bm, homepage_BookmarkTag)); 1490 setToggle_Widget(homeTag, bm->flags & homepage_BookmarkFlag);
1371 setToggle_Widget(remoteSourceTag, hasTag_Bookmark(bm, remoteSource_BookmarkTag)); 1491 setToggle_Widget(remoteSourceTag, bm->flags & remoteSource_BookmarkFlag);
1372 setToggle_Widget(linkSplitTag, hasTag_Bookmark(bm, linkSplit_BookmarkTag)); 1492 setToggle_Widget(linkSplitTag, bm->flags & linkSplit_BookmarkFlag);
1373 } 1493 }
1374 else { 1494 else {
1375 setFlags_Widget(findChild_Widget(dlg, "bmed.special"), 1495 setFlags_Widget(findChild_Widget(dlg, "bmed.special"),
@@ -1390,7 +1510,7 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev)
1390 const iSidebarItem *item = d->contextItem; 1510 const iSidebarItem *item = d->contextItem;
1391 if (d->mode == bookmarks_SidebarMode && item) { 1511 if (d->mode == bookmarks_SidebarMode && item) {
1392 iBookmark *bm = get_Bookmarks(bookmarks_App(), item->id); 1512 iBookmark *bm = get_Bookmarks(bookmarks_App(), item->id);
1393 const iBool isRemote = hasTag_Bookmark(bm, remote_BookmarkTag); 1513 const iBool isRemote = (bm->flags & remote_BookmarkFlag) != 0;
1394 iChar icon = isRemote ? 0x1f588 : bm->icon; 1514 iChar icon = isRemote ? 0x1f588 : bm->icon;
1395 iWidget *dlg = makeBookmarkCreation_Widget(&bm->url, &bm->title, icon); 1515 iWidget *dlg = makeBookmarkCreation_Widget(&bm->url, &bm->title, icon);
1396 setId_Widget(dlg, format_CStr("bmed.%s", cstr_String(id_Widget(w)))); 1516 setId_Widget(dlg, format_CStr("bmed.%s", cstr_String(id_Widget(w))));
@@ -1404,17 +1524,16 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev)
1404 else if (isCommand_Widget(w, ev, "bookmark.tag")) { 1524 else if (isCommand_Widget(w, ev, "bookmark.tag")) {
1405 const iSidebarItem *item = d->contextItem; 1525 const iSidebarItem *item = d->contextItem;
1406 if (d->mode == bookmarks_SidebarMode && item) { 1526 if (d->mode == bookmarks_SidebarMode && item) {
1407 const char *tag = cstr_String(string_Command(cmd, "tag")); 1527 const iRangecc tag = range_Command(cmd, "tag");
1528 const int flag =
1529 (equal_Rangecc(tag, "homepage") ? homepage_BookmarkFlag : 0) |
1530 (equal_Rangecc(tag, "subscribed") ? subscribed_BookmarkFlag : 0) |
1531 (equal_Rangecc(tag, "remotesource") ? remoteSource_BookmarkFlag : 0);
1408 iBookmark *bm = get_Bookmarks(bookmarks_App(), item->id); 1532 iBookmark *bm = get_Bookmarks(bookmarks_App(), item->id);
1409 if (hasTag_Bookmark(bm, tag)) { 1533 if (flag == subscribed_BookmarkFlag && (bm->flags & flag)) {
1410 removeTag_Bookmark(bm, tag); 1534 removeEntries_Feeds(item->id); /* get rid of unsubscribed entries */
1411 if (!iCmpStr(tag, subscribed_BookmarkTag)) {
1412 removeEntries_Feeds(item->id);
1413 }
1414 }
1415 else {
1416 addTag_Bookmark(bm, tag);
1417 } 1535 }
1536 bm->flags ^= flag;
1418 postCommand_App("bookmarks.changed"); 1537 postCommand_App("bookmarks.changed");
1419 } 1538 }
1420 return iTrue; 1539 return iTrue;
@@ -1491,6 +1610,15 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev)
1491 return iTrue; 1610 return iTrue;
1492 } 1611 }
1493 else if (equal_Command(cmd, "feeds.markallread") && d->mode == feeds_SidebarMode) { 1612 else if (equal_Command(cmd, "feeds.markallread") && d->mode == feeds_SidebarMode) {
1613 if (argLabel_Command(cmd, "confirm")) {
1614 /* This is used on mobile. */
1615 iWidget *menu = makeMenu_Widget(w->root->widget, (iMenuItem[]){
1616 check_Icon " " uiTextCaution_ColorEscape "${feeds.markallread}", 0, 0,
1617 "feeds.markallread"
1618 }, 1);
1619 openMenu_Widget(menu, topLeft_Rect(bounds_Widget(d->actions)));
1620 return iTrue;
1621 }
1494 iConstForEach(PtrArray, i, listEntries_Feeds()) { 1622 iConstForEach(PtrArray, i, listEntries_Feeds()) {
1495 const iFeedEntry *entry = i.ptr; 1623 const iFeedEntry *entry = i.ptr;
1496 const iString *url = url_FeedEntry(entry); 1624 const iString *url = url_FeedEntry(entry);
@@ -1539,7 +1667,7 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev)
1539 } 1667 }
1540 if (isCommand_Widget(w, ev, "feed.entry.unsubscribe")) { 1668 if (isCommand_Widget(w, ev, "feed.entry.unsubscribe")) {
1541 if (arg_Command(cmd)) { 1669 if (arg_Command(cmd)) {
1542 removeTag_Bookmark(feedBookmark, subscribed_BookmarkTag); 1670 feedBookmark->flags &= ~subscribed_BookmarkFlag;
1543 removeEntries_Feeds(id_Bookmark(feedBookmark)); 1671 removeEntries_Feeds(id_Bookmark(feedBookmark));
1544 updateItems_SidebarWidget_(d); 1672 updateItems_SidebarWidget_(d);
1545 } 1673 }
@@ -1555,108 +1683,11 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev)
1555 0, 1683 0,
1556 format_CStr("!feed.entry.unsubscribe arg:1 ptr:%p", d) } }, 1684 format_CStr("!feed.entry.unsubscribe arg:1 ptr:%p", d) } },
1557 2); 1685 2);
1558 }
1559 return iTrue;
1560 }
1561 }
1562 }
1563 }
1564 else if (isCommand_Widget(w, ev, "ident.use")) {
1565 iGmIdentity * ident = menuIdentity_SidebarWidget_(d);
1566 const iString *tabUrl = url_DocumentWidget(document_App());
1567 if (ident) {
1568 if (argLabel_Command(cmd, "clear")) {
1569 clearUse_GmIdentity(ident);
1570 }
1571 else if (arg_Command(cmd)) {
1572 signIn_GmCerts(certs_App(), ident, tabUrl);
1573 postCommand_App("navigate.reload");
1574 }
1575 else {
1576 signOut_GmCerts(certs_App(), tabUrl);
1577 postCommand_App("navigate.reload");
1578 }
1579 saveIdentities_GmCerts(certs_App());
1580 updateItems_SidebarWidget_(d);
1581 }
1582 return iTrue;
1583 }
1584 else if (isCommand_Widget(w, ev, "ident.edit")) {
1585 const iGmIdentity *ident = menuIdentity_SidebarWidget_(d);
1586 if (ident) {
1587 makeValueInput_Widget(get_Root()->widget,
1588 &ident->notes,
1589 uiHeading_ColorEscape "${heading.ident.notes}",
1590 format_CStr(cstr_Lang("dlg.ident.notes"), cstr_String(name_GmIdentity(ident))),
1591 uiTextAction_ColorEscape "${dlg.default}",
1592 format_CStr("!ident.setnotes ident:%p ptr:%p", ident, d));
1593 }
1594 return iTrue;
1595 }
1596 else if (isCommand_Widget(w, ev, "ident.fingerprint")) {
1597 const iGmIdentity *ident = menuIdentity_SidebarWidget_(d);
1598 if (ident) {
1599 const iString *fps = collect_String(
1600 hexEncode_Block(collect_Block(fingerprint_TlsCertificate(ident->cert))));
1601 SDL_SetClipboardText(cstr_String(fps));
1602 } 1686 }
1603 return iTrue; 1687 return iTrue;
1604 } 1688 }
1605 else if (isCommand_Widget(w, ev, "ident.export")) {
1606 const iGmIdentity *ident = menuIdentity_SidebarWidget_(d);
1607 if (ident) {
1608 iString *pem = collect_String(pem_TlsCertificate(ident->cert));
1609 append_String(pem, collect_String(privateKeyPem_TlsCertificate(ident->cert)));
1610 iDocumentWidget *expTab = newTab_App(NULL, iTrue);
1611 setUrlAndSource_DocumentWidget(
1612 expTab,
1613 collectNewFormat_String("file:%s.pem", cstr_String(name_GmIdentity(ident))),
1614 collectNewCStr_String("text/plain"),
1615 utf8_String(pem));
1616 } 1689 }
1617 return iTrue;
1618 }
1619 else if (isCommand_Widget(w, ev, "ident.setnotes")) {
1620 iGmIdentity *ident = pointerLabel_Command(cmd, "ident");
1621 if (ident) {
1622 setCStr_String(&ident->notes, suffixPtr_Command(cmd, "value"));
1623 updateItems_SidebarWidget_(d);
1624 }
1625 return iTrue;
1626 }
1627 else if (isCommand_Widget(w, ev, "ident.pickicon")) {
1628 return iTrue;
1629 }
1630 else if (isCommand_Widget(w, ev, "ident.reveal")) {
1631 const iGmIdentity *ident = menuIdentity_SidebarWidget_(d);
1632 if (ident) {
1633 const iString *crtPath = certificatePath_GmCerts(certs_App(), ident);
1634 if (crtPath) {
1635 revealPath_App(crtPath);
1636 }
1637 } 1690 }
1638 return iTrue;
1639 }
1640 else if (isCommand_Widget(w, ev, "ident.delete")) {
1641 iSidebarItem *item = d->contextItem;
1642 if (argLabel_Command(cmd, "confirm")) {
1643 makeQuestion_Widget(
1644 uiTextCaution_ColorEscape "${heading.ident.delete}",
1645 format_CStr(cstr_Lang("dlg.confirm.ident.delete"),
1646 uiTextAction_ColorEscape,
1647 cstr_String(&item->label),
1648 uiText_ColorEscape),
1649 (iMenuItem[]){ { "${cancel}", 0, 0, NULL },
1650 { uiTextCaution_ColorEscape "${dlg.ident.delete}",
1651 0,
1652 0,
1653 format_CStr("!ident.delete confirm:0 ptr:%p", d) } },
1654 2);
1655 return iTrue;
1656 }
1657 deleteIdentity_GmCerts(certs_App(), menuIdentity_SidebarWidget_(d));
1658 postCommand_App("idents.changed");
1659 return iTrue;
1660 } 1691 }
1661 else if (isCommand_Widget(w, ev, "history.delete")) { 1692 else if (isCommand_Widget(w, ev, "history.delete")) {
1662 if (d->contextItem && !isEmpty_String(&d->contextItem->url)) { 1693 if (d->contextItem && !isEmpty_String(&d->contextItem->url)) {
@@ -1711,14 +1742,10 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev)
1711 /* Update cursor. */ 1742 /* Update cursor. */
1712 else if (contains_Widget(w, mouse)) { 1743 else if (contains_Widget(w, mouse)) {
1713 const iSidebarItem *item = constHoverItem_ListWidget(d->list); 1744 const iSidebarItem *item = constHoverItem_ListWidget(d->list);
1714 if (item && d->mode != identities_SidebarMode) {
1715 setCursor_Window(get_Window(), 1745 setCursor_Window(get_Window(),
1716 item->listItem.isSeparator ? SDL_SYSTEM_CURSOR_ARROW 1746 item ? (item->listItem.isSeparator ? SDL_SYSTEM_CURSOR_ARROW
1717 : SDL_SYSTEM_CURSOR_HAND); 1747 : SDL_SYSTEM_CURSOR_HAND)
1718 } 1748 : SDL_SYSTEM_CURSOR_ARROW);
1719 else {
1720 setCursor_Window(get_Window(), SDL_SYSTEM_CURSOR_ARROW);
1721 }
1722 } 1749 }
1723 if (d->contextIndex != iInvalidPos) { 1750 if (d->contextIndex != iInvalidPos) {
1724 invalidateItem_ListWidget(d->list, d->contextIndex); 1751 invalidateItem_ListWidget(d->list, d->contextIndex);
@@ -1726,7 +1753,14 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev)
1726 } 1753 }
1727 } 1754 }
1728 /* Update context menu items. */ 1755 /* Update context menu items. */
1729 if ((d->menu || d->mode == identities_SidebarMode) && ev->type == SDL_MOUSEBUTTONDOWN) { 1756 if (d->menu && ev->type == SDL_MOUSEBUTTONDOWN) {
1757 if (isSlidingSheet_SidebarWidget_(d) &&
1758 ev->button.button == SDL_BUTTON_LEFT &&
1759 isVisible_Widget(d) &&
1760 !contains_Widget(w, init_I2(ev->button.x, ev->button.y))) {
1761 setSlidingSheetPos_SidebarWidget_(d, bottom_SlidingSheetPos);
1762 return iTrue;
1763 }
1730 if (ev->button.button == SDL_BUTTON_RIGHT) { 1764 if (ev->button.button == SDL_BUTTON_RIGHT) {
1731 d->contextItem = NULL; 1765 d->contextItem = NULL;
1732 if (!isVisible_Widget(d->menu)) { 1766 if (!isVisible_Widget(d->menu)) {
@@ -1739,7 +1773,6 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev)
1739 invalidateItem_ListWidget(d->list, d->contextIndex); 1773 invalidateItem_ListWidget(d->list, d->contextIndex);
1740 } 1774 }
1741 d->contextIndex = hoverItemIndex_ListWidget(d->list); 1775 d->contextIndex = hoverItemIndex_ListWidget(d->list);
1742 updateContextMenu_SidebarWidget_(d);
1743 /* TODO: Some callback-based mechanism would be nice for updating menus right 1776 /* TODO: Some callback-based mechanism would be nice for updating menus right
1744 before they open? At least move these to `updateContextMenu_ */ 1777 before they open? At least move these to `updateContextMenu_ */
1745 if (d->mode == bookmarks_SidebarMode && d->contextItem) { 1778 if (d->mode == bookmarks_SidebarMode && d->contextItem) {
@@ -1747,17 +1780,17 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev)
1747 if (bm) { 1780 if (bm) {
1748 setMenuItemLabel_Widget(d->menu, 1781 setMenuItemLabel_Widget(d->menu,
1749 "bookmark.tag tag:homepage", 1782 "bookmark.tag tag:homepage",
1750 hasTag_Bookmark(bm, homepage_BookmarkTag) 1783 bm->flags & homepage_BookmarkFlag
1751 ? home_Icon " ${bookmark.untag.home}" 1784 ? home_Icon " ${bookmark.untag.home}"
1752 : home_Icon " ${bookmark.tag.home}"); 1785 : home_Icon " ${bookmark.tag.home}");
1753 setMenuItemLabel_Widget(d->menu, 1786 setMenuItemLabel_Widget(d->menu,
1754 "bookmark.tag tag:subscribed", 1787 "bookmark.tag tag:subscribed",
1755 hasTag_Bookmark(bm, subscribed_BookmarkTag) 1788 bm->flags & subscribed_BookmarkFlag
1756 ? star_Icon " ${bookmark.untag.sub}" 1789 ? star_Icon " ${bookmark.untag.sub}"
1757 : star_Icon " ${bookmark.tag.sub}"); 1790 : star_Icon " ${bookmark.tag.sub}");
1758 setMenuItemLabel_Widget(d->menu, 1791 setMenuItemLabel_Widget(d->menu,
1759 "bookmark.tag tag:remotesource", 1792 "bookmark.tag tag:remotesource",
1760 hasTag_Bookmark(bm, remoteSource_BookmarkTag) 1793 bm->flags & remoteSource_BookmarkFlag
1761 ? downArrowBar_Icon " ${bookmark.untag.remote}" 1794 ? downArrowBar_Icon " ${bookmark.untag.remote}"
1762 : downArrowBar_Icon " ${bookmark.tag.remote}"); 1795 : downArrowBar_Icon " ${bookmark.tag.remote}");
1763 } 1796 }
@@ -1769,29 +1802,9 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev)
1769 isRead ? circle_Icon " ${feeds.entry.markunread}" 1802 isRead ? circle_Icon " ${feeds.entry.markunread}"
1770 : circleWhite_Icon " ${feeds.entry.markread}"); 1803 : circleWhite_Icon " ${feeds.entry.markread}");
1771 } 1804 }
1772 else if (d->mode == identities_SidebarMode) {
1773 const iGmIdentity *ident = constHoverIdentity_SidebarWidget_(d);
1774 const iString * docUrl = url_DocumentWidget(document_App());
1775 iForEach(ObjectList, i, children_Widget(d->menu)) {
1776 if (isInstance_Object(i.object, &Class_LabelWidget)) {
1777 iLabelWidget *menuItem = i.object;
1778 const char * cmdItem = cstr_String(command_LabelWidget(menuItem));
1779 if (equal_Command(cmdItem, "ident.use")) {
1780 const iBool cmdUse = arg_Command(cmdItem) != 0;
1781 const iBool cmdClear = argLabel_Command(cmdItem, "clear") != 0;
1782 setFlags_Widget(
1783 as_Widget(menuItem),
1784 disabled_WidgetFlag,
1785 (cmdClear && !isUsed_GmIdentity(ident)) ||
1786 (!cmdClear && cmdUse && isUsedOn_GmIdentity(ident, docUrl)) ||
1787 (!cmdClear && !cmdUse && !isUsedOn_GmIdentity(ident, docUrl)));
1788 } 1805 }
1789 } 1806 }
1790 } 1807 }
1791 }
1792 }
1793 }
1794 }
1795 if (ev->type == SDL_KEYDOWN) { 1808 if (ev->type == SDL_KEYDOWN) {
1796 const int key = ev->key.keysym.sym; 1809 const int key = ev->key.keysym.sym;
1797 const int kmods = keyMods_Sym(ev->key.keysym.mod); 1810 const int kmods = keyMods_Sym(ev->key.keysym.mod);
@@ -1801,6 +1814,61 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev)
1801 return iTrue; 1814 return iTrue;
1802 } 1815 }
1803 } 1816 }
1817 if (isSlidingSheet_SidebarWidget_(d)) {
1818 if (ev->type == SDL_MOUSEWHEEL) {
1819 enum iWidgetTouchMode touchMode = widgetMode_Touch(w);
1820 if (touchMode == momentum_WidgetTouchMode) {
1821 /* We don't do momentum. */
1822 float swipe = stopWidgetMomentum_Touch(w) / gap_UI;
1823// printf("swipe: %f\n", swipe);
1824 const iRangei midRegion = SlidingSheetMiddleRegion_SidebarWidget_(d);
1825 const int pos = top_Rect(w->rect);
1826 if (swipe < 170) {
1827 gotoNearestSlidingSheetPos_SidebarWidget_(d);
1828 }
1829 else if (swipe > 500 && ev->wheel.y > 0) {
1830 /* Fast swipe down will dismiss. */
1831 setSlidingSheetPos_SidebarWidget_(d, bottom_SlidingSheetPos);
1832 }
1833 else if (ev->wheel.y < 0) {
1834 setSlidingSheetPos_SidebarWidget_(d, top_SlidingSheetPos);
1835 }
1836 else if (pos < (midRegion.start + midRegion.end) / 2) {
1837 setSlidingSheetPos_SidebarWidget_(d, middle_SlidingSheetPos);
1838 }
1839 else {
1840 setSlidingSheetPos_SidebarWidget_(d, bottom_SlidingSheetPos);
1841 }
1842 }
1843 else if (touchMode == touch_WidgetTouchMode) {
1844 /* Move with the finger. */
1845 adjustEdges_Rect(&w->rect, ev->wheel.y, 0, 0, 0);
1846 /* Upon reaching the top, scrolling is switched back to the list. */
1847 const iRect rootRect = safeRect_Root(w->root);
1848 const int top = top_Rect(rootRect);
1849 if (w->rect.pos.y < top) {
1850 setScrollMode_ListWidget(d->list, disabledAtTopUpwards_ScrollMode);
1851 setScrollPos_ListWidget(d->list, top - w->rect.pos.y);
1852 transferAffinity_Touch(w, as_Widget(d->list));
1853 w->rect.pos.y = top;
1854 w->rect.size.y = height_Rect(rootRect);
1855 }
1856 else {
1857 setScrollMode_ListWidget(d->list, disabled_ScrollMode);
1858 }
1859 arrange_Widget(w);
1860 refresh_Widget(w);
1861 }
1862 else {
1863 return iFalse;
1864 }
1865 return iTrue;
1866 }
1867 if (ev->type == SDL_USEREVENT && ev->user.code == widgetTouchEnds_UserEventCode) {
1868 gotoNearestSlidingSheetPos_SidebarWidget_(d);
1869 return iTrue;
1870 }
1871 }
1804 if (ev->type == SDL_MOUSEBUTTONDOWN && 1872 if (ev->type == SDL_MOUSEBUTTONDOWN &&
1805 contains_Widget(as_Widget(d->list), init_I2(ev->button.x, ev->button.y))) { 1873 contains_Widget(as_Widget(d->list), init_I2(ev->button.x, ev->button.y))) {
1806 if (hoverItem_ListWidget(d->list) || isVisible_Widget(d->menu)) { 1874 if (hoverItem_ListWidget(d->list) || isVisible_Widget(d->menu)) {
@@ -1811,13 +1879,12 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev)
1811 const iSidebarItem *hoverItem = hoverItem_ListWidget(d->list); 1879 const iSidebarItem *hoverItem = hoverItem_ListWidget(d->list);
1812 iAssert(hoverItem); 1880 iAssert(hoverItem);
1813 const iBookmark * bm = get_Bookmarks(bookmarks_App(), hoverItem->id); 1881 const iBookmark * bm = get_Bookmarks(bookmarks_App(), hoverItem->id);
1814 const iBool isRemote = hasTag_Bookmark(bm, remote_BookmarkTag); 1882 const iBool isRemote = (bm->flags & remote_BookmarkFlag) != 0;
1815 static const char *localOnlyCmds[] = { "bookmark.edit", 1883 static const char *localOnlyCmds[] = { "bookmark.edit",
1816 "bookmark.delete", 1884 "bookmark.delete",
1817 "bookmark.tag tag:" subscribed_BookmarkTag, 1885 "bookmark.tag tag:subscribed",
1818 "bookmark.tag tag:" homepage_BookmarkTag, 1886 "bookmark.tag tag:homepage",
1819 "bookmark.tag tag:" remoteSource_BookmarkTag, 1887 "bookmark.tag tag:remotesource" };
1820 "bookmark.tag tag:" subscribed_BookmarkTag };
1821 iForIndices(i, localOnlyCmds) { 1888 iForIndices(i, localOnlyCmds) {
1822 setFlags_Widget(as_Widget(findMenuItem_Widget(d->menu, localOnlyCmds[i])), 1889 setFlags_Widget(as_Widget(findMenuItem_Widget(d->menu, localOnlyCmds[i])),
1823 disabled_WidgetFlag, 1890 disabled_WidgetFlag,
@@ -1838,6 +1905,9 @@ static void draw_SidebarWidget_(const iSidebarWidget *d) {
1838 const iRect bounds = bounds_Widget(w); 1905 const iRect bounds = bounds_Widget(w);
1839 iPaint p; 1906 iPaint p;
1840 init_Paint(&p); 1907 init_Paint(&p);
1908 if (d->mode == documentOutline_SidebarMode) {
1909 makePaletteGlobal_GmDocument(document_DocumentWidget(document_App()));
1910 }
1841 if (!isPortraitPhone_App()) { /* this would erase page contents during transition on the phone */ 1911 if (!isPortraitPhone_App()) { /* this would erase page contents during transition on the phone */
1842 if (flags_Widget(w) & visualOffset_WidgetFlag && 1912 if (flags_Widget(w) & visualOffset_WidgetFlag &&
1843 flags_Widget(w) & horizontalOffset_WidgetFlag && isVisible_Widget(w)) { 1913 flags_Widget(w) & horizontalOffset_WidgetFlag && isVisible_Widget(w)) {
@@ -1860,6 +1930,7 @@ static void draw_SidebarItem_(const iSidebarItem *d, iPaint *p, iRect itemRect,
1860 &Class_SidebarWidget); 1930 &Class_SidebarWidget);
1861 const iBool isMenuVisible = isVisible_Widget(sidebar->menu); 1931 const iBool isMenuVisible = isVisible_Widget(sidebar->menu);
1862 const iBool isDragging = constDragItem_ListWidget(list) == d; 1932 const iBool isDragging = constDragItem_ListWidget(list) == d;
1933 const iBool isEditing = sidebar->isEditing; /* only on mobile */
1863 const iBool isPressing = isMouseDown_ListWidget(list) && !isDragging; 1934 const iBool isPressing = isMouseDown_ListWidget(list) && !isDragging;
1864 const iBool isHover = 1935 const iBool isHover =
1865 (!isMenuVisible && 1936 (!isMenuVisible &&
@@ -2013,6 +2084,16 @@ static void draw_SidebarItem_(const iSidebarItem *d, iPaint *p, iRect itemRect,
2013 drawRange_Text(font, textPos, fg, range_String(&d->label)); 2084 drawRange_Text(font, textPos, fg, range_String(&d->label));
2014 const int metaFont = uiLabel_FontId; 2085 const int metaFont = uiLabel_FontId;
2015 const int metaIconWidth = 4.5f * gap_UI; 2086 const int metaIconWidth = 4.5f * gap_UI;
2087 if (isEditing) {
2088 iRect dragRect = {
2089 addX_I2(topRight_Rect(itemRect), -itemHeight * 3 / 2),
2090 init_I2(itemHeight * 3 / 2, itemHeight)
2091 };
2092 fillRect_Paint(p, dragRect, bg);
2093 drawVLine_Paint(p, topLeft_Rect(dragRect), height_Rect(dragRect), uiSeparator_ColorId);
2094 drawCentered_Text(uiContent_FontId, dragRect, iTrue, uiAnnotation_ColorId, menu_Icon);
2095 adjustEdges_Rect(&itemRect, 0, -width_Rect(dragRect), 0, 0);
2096 }
2016 const iInt2 metaPos = 2097 const iInt2 metaPos =
2017 init_I2(right_Rect(itemRect) - 2098 init_I2(right_Rect(itemRect) -
2018 length_String(&d->meta) * 2099 length_String(&d->meta) *
@@ -2091,40 +2172,6 @@ static void draw_SidebarItem_(const iSidebarItem *d, iPaint *p, iRect itemRect,
2091 } 2172 }
2092 iEndCollect(); 2173 iEndCollect();
2093 } 2174 }
2094 else if (sidebar->mode == identities_SidebarMode) {
2095 const int fg = isHover ? (isPressing ? uiTextPressed_ColorId : uiTextFramelessHover_ColorId)
2096 : uiTextStrong_ColorId;
2097 const iBool isUsedOnDomain = (d->indent != 0);
2098 iString icon;
2099 initUnicodeN_String(&icon, &d->icon, 1);
2100 iInt2 cPos = topLeft_Rect(itemRect);
2101 const int indent = 1.4f * lineHeight_Text(font);
2102 addv_I2(&cPos,
2103 init_I2(3 * gap_UI,
2104 (itemHeight - lineHeight_Text(uiLabel_FontId) * 2 - lineHeight_Text(font)) /
2105 2));
2106 const int metaFg = isHover ? permanent_ColorId | (isPressing ? uiTextPressed_ColorId
2107 : uiTextFramelessHover_ColorId)
2108 : uiTextDim_ColorId;
2109 if (!d->listItem.isSelected && !isUsedOnDomain) {
2110 drawOutline_Text(font, cPos, metaFg, none_ColorId, range_String(&icon));
2111 }
2112 drawRange_Text(font,
2113 cPos,
2114 d->listItem.isSelected ? iconColor
2115 : isUsedOnDomain ? altIconColor
2116 : uiBackgroundSidebar_ColorId,
2117 range_String(&icon));
2118 deinit_String(&icon);
2119 drawRange_Text(d->listItem.isSelected ? sidebar->itemFonts[1] : font,
2120 add_I2(cPos, init_I2(indent, 0)),
2121 fg,
2122 range_String(&d->label));
2123 drawRange_Text(uiLabel_FontId,
2124 add_I2(cPos, init_I2(indent, lineHeight_Text(font))),
2125 metaFg,
2126 range_String(&d->meta));
2127 }
2128} 2175}
2129 2176
2130iBeginDefineSubclass(SidebarWidget, Widget) 2177iBeginDefineSubclass(SidebarWidget, Widget)
diff --git a/src/ui/sidebarwidget.h b/src/ui/sidebarwidget.h
index 81c6681f..2a930a60 100644
--- a/src/ui/sidebarwidget.h
+++ b/src/ui/sidebarwidget.h
@@ -54,6 +54,7 @@ iBool setMode_SidebarWidget (iSidebarWidget *, enum iSidebar
54void setWidth_SidebarWidget (iSidebarWidget *, float widthAsGaps); 54void setWidth_SidebarWidget (iSidebarWidget *, float widthAsGaps);
55iBool setButtonFont_SidebarWidget (iSidebarWidget *, int font); 55iBool setButtonFont_SidebarWidget (iSidebarWidget *, int font);
56void setClosedFolders_SidebarWidget (iSidebarWidget *, const iIntSet *closedFolders); 56void setClosedFolders_SidebarWidget (iSidebarWidget *, const iIntSet *closedFolders);
57void setMidHeight_SidebarWidget (iSidebarWidget *, int midHeight); /* phone layout */
57 58
58enum iSidebarMode mode_SidebarWidget (const iSidebarWidget *); 59enum iSidebarMode mode_SidebarWidget (const iSidebarWidget *);
59enum iFeedsMode feedsMode_SidebarWidget (const iSidebarWidget *); 60enum iFeedsMode feedsMode_SidebarWidget (const iSidebarWidget *);
diff --git a/src/ui/text.c b/src/ui/text.c
index 4a4b3776..7bb418eb 100644
--- a/src/ui/text.c
+++ b/src/ui/text.c
@@ -390,14 +390,19 @@ static void deinitCache_Text_(iText *d) {
390 SDL_DestroyTexture(d->cache); 390 SDL_DestroyTexture(d->cache);
391} 391}
392 392
393iRegExp *makeAnsiEscapePattern_Text(void) {
394 return new_RegExp("[[()][?]?([0-9;AB]*?)([ABCDEFGHJKSTfhilmn])", 0);
395}
396
393void init_Text(iText *d, SDL_Renderer *render) { 397void init_Text(iText *d, SDL_Renderer *render) {
394 iText *oldActive = activeText_; 398 iText *oldActive = activeText_;
395 activeText_ = d; 399 activeText_ = d;
396 init_Array(&d->fonts, sizeof(iFont)); 400 init_Array(&d->fonts, sizeof(iFont));
397 d->contentFontSize = contentScale_Text_; 401 d->contentFontSize = contentScale_Text_;
398 d->ansiEscape = new_RegExp("[[()][?]?([0-9;AB]*?)([ABCDEFGHJKSTfhilmn])", 0); 402 d->ansiEscape = makeAnsiEscapePattern_Text();
399 d->baseFontId = -1; 403 d->baseFontId = -1;
400 d->baseFgColorId = -1; 404 d->baseFgColorId = -1;
405 d->missingGlyphs = iFalse;
401 d->render = render; 406 d->render = render;
402 /* A grayscale palette for rasterized glyphs. */ { 407 /* A grayscale palette for rasterized glyphs. */ {
403 SDL_Color colors[256]; 408 SDL_Color colors[256];
@@ -448,6 +453,10 @@ void setAnsiFlags_Text(int ansiFlags) {
448 activeText_->ansiFlags = ansiFlags; 453 activeText_->ansiFlags = ansiFlags;
449} 454}
450 455
456int ansiFlags_Text(void) {
457 return activeText_->ansiFlags;
458}
459
451void setDocumentFontSize_Text(iText *d, float fontSizeFactor) { 460void setDocumentFontSize_Text(iText *d, float fontSizeFactor) {
452 fontSizeFactor *= contentScale_Text_; 461 fontSizeFactor *= contentScale_Text_;
453 iAssert(fontSizeFactor > 0); 462 iAssert(fontSizeFactor > 0);
@@ -642,6 +651,37 @@ static iBool isControl_Char_(iChar c) {
642/*----------------------------------------------------------------------------------------------*/ 651/*----------------------------------------------------------------------------------------------*/
643 652
644iDeclareType(AttributedRun) 653iDeclareType(AttributedRun)
654
655enum iScript {
656 unspecified_Script,
657 arabic_Script,
658 bengali_Script,
659 devanagari_Script,
660 han_Script,
661 hiragana_Script,
662 katakana_Script,
663 oriya_Script,
664 tamil_Script,
665 max_Script
666};
667
668iLocalDef iBool isCJK_Script_(enum iScript d) {
669 return d == han_Script || d == hiragana_Script || d == katakana_Script;
670}
671
672#if defined (LAGRANGE_ENABLE_HARFBUZZ)
673static const hb_script_t hbScripts_[max_Script] = {
674 0,
675 HB_SCRIPT_ARABIC,
676 HB_SCRIPT_BENGALI,
677 HB_SCRIPT_DEVANAGARI,
678 HB_SCRIPT_HAN,
679 HB_SCRIPT_HIRAGANA,
680 HB_SCRIPT_KATAKANA,
681 HB_SCRIPT_ORIYA,
682 HB_SCRIPT_TAMIL,
683};
684#endif
645 685
646struct Impl_AttributedRun { 686struct Impl_AttributedRun {
647 iRangei logical; /* UTF-32 codepoint indices in the logical-order text */ 687 iRangei logical; /* UTF-32 codepoint indices in the logical-order text */
@@ -651,8 +691,7 @@ struct Impl_AttributedRun {
651 iColor bgColor_; /* any RGB color; A > 0 */ 691 iColor bgColor_; /* any RGB color; A > 0 */
652 struct { 692 struct {
653 uint8_t isLineBreak : 1; 693 uint8_t isLineBreak : 1;
654// uint8_t isRTL : 1; 694 uint8_t script : 7; /* if script detected */
655 uint8_t isArabic : 1; /* Arabic script detected */
656 } flags; 695 } flags;
657}; 696};
658 697
@@ -744,7 +783,7 @@ static void finishRun_AttributedText_(iAttributedText *d, iAttributedRun *run, i
744#endif 783#endif
745 pushBack_Array(&d->runs, &finishedRun); 784 pushBack_Array(&d->runs, &finishedRun);
746 run->flags.isLineBreak = iFalse; 785 run->flags.isLineBreak = iFalse;
747 run->flags.isArabic = iFalse; 786 run->flags.script = unspecified_Script;
748 } 787 }
749 run->logical.start = endAt; 788 run->logical.start = endAt;
750} 789}
@@ -789,6 +828,7 @@ static void prepare_AttributedText_(iAttributedText *d, int overrideBaseDir, iCh
789 pushBack_Array(&d->logicalToSourceOffset, &(int){ ch - d->source.start }); 828 pushBack_Array(&d->logicalToSourceOffset, &(int){ ch - d->source.start });
790 ch += len; 829 ch += len;
791 } 830 }
831 iBool bidiOk = iFalse;
792#if defined (LAGRANGE_ENABLE_FRIBIDI) 832#if defined (LAGRANGE_ENABLE_FRIBIDI)
793 /* Use FriBidi to reorder the codepoints. */ 833 /* Use FriBidi to reorder the codepoints. */
794 resize_Array(&d->visual, length); 834 resize_Array(&d->visual, length);
@@ -796,25 +836,25 @@ static void prepare_AttributedText_(iAttributedText *d, int overrideBaseDir, iCh
796 resize_Array(&d->visualToLogical, length); 836 resize_Array(&d->visualToLogical, length);
797 d->bidiLevels = length ? malloc(length) : NULL; 837 d->bidiLevels = length ? malloc(length) : NULL;
798 FriBidiParType baseDir = (FriBidiParType) FRIBIDI_TYPE_ON; 838 FriBidiParType baseDir = (FriBidiParType) FRIBIDI_TYPE_ON;
799 /* TODO: If this returns zero (error occurred), act like everything is LTR. */ 839 bidiOk = fribidi_log2vis(constData_Array(&d->logical),
800 fribidi_log2vis(constData_Array(&d->logical), 840 (FriBidiStrIndex) length,
801 length, 841 &baseDir,
802 &baseDir, 842 data_Array(&d->visual),
803 data_Array(&d->visual), 843 data_Array(&d->logicalToVisual),
804 data_Array(&d->logicalToVisual), 844 data_Array(&d->visualToLogical),
805 data_Array(&d->visualToLogical), 845 (FriBidiLevel *) d->bidiLevels) > 0;
806 (FriBidiLevel *) d->bidiLevels);
807 d->isBaseRTL = (overrideBaseDir == 0 ? FRIBIDI_IS_RTL(baseDir) : (overrideBaseDir < 0)); 846 d->isBaseRTL = (overrideBaseDir == 0 ? FRIBIDI_IS_RTL(baseDir) : (overrideBaseDir < 0));
808#else
809 /* 1:1 mapping. */
810 setCopy_Array(&d->visual, &d->logical);
811 resize_Array(&d->logicalToVisual, length);
812 for (size_t i = 0; i < length; i++) {
813 set_Array(&d->logicalToVisual, i, &(int){ i });
814 }
815 setCopy_Array(&d->visualToLogical, &d->logicalToVisual);
816 d->isBaseRTL = iFalse;
817#endif 847#endif
848 if (!bidiOk) {
849 /* 1:1 mapping. */
850 setCopy_Array(&d->visual, &d->logical);
851 resize_Array(&d->logicalToVisual, length);
852 for (size_t i = 0; i < length; i++) {
853 set_Array(&d->logicalToVisual, i, &(int){ i });
854 }
855 setCopy_Array(&d->visualToLogical, &d->logicalToVisual);
856 d->isBaseRTL = iFalse;
857 }
818 } 858 }
819 /* The mapping needs to include the terminating NULL position. */ { 859 /* The mapping needs to include the terminating NULL position. */ {
820 pushBack_Array(&d->logicalToSourceOffset, &(int){ d->source.end - d->source.start }); 860 pushBack_Array(&d->logicalToSourceOffset, &(int){ d->source.end - d->source.start });
@@ -828,7 +868,6 @@ static void prepare_AttributedText_(iAttributedText *d, int overrideBaseDir, iCh
828 .font = d->font, 868 .font = d->font,
829 }; 869 };
830 const int *logToSource = constData_Array(&d->logicalToSourceOffset); 870 const int *logToSource = constData_Array(&d->logicalToSourceOffset);
831 const int * logToVis = constData_Array(&d->logicalToVisual);
832 const iChar * logicalText = constData_Array(&d->logical); 871 const iChar * logicalText = constData_Array(&d->logical);
833 iBool isRTL = d->isBaseRTL; 872 iBool isRTL = d->isBaseRTL;
834 int numNonSpace = 0; 873 int numNonSpace = 0;
@@ -868,17 +907,34 @@ static void prepare_AttributedText_(iAttributedText *d, int overrideBaseDir, iCh
868 /* Note: This styling is hardcoded to match `typesetOneLine_RunTypesetter_()`. */ 907 /* Note: This styling is hardcoded to match `typesetOneLine_RunTypesetter_()`. */
869 if (ansi & allowFontStyle_AnsiFlag && equal_Rangecc(sequence, "1")) { 908 if (ansi & allowFontStyle_AnsiFlag && equal_Rangecc(sequence, "1")) {
870 run.attrib.bold = iTrue; 909 run.attrib.bold = iTrue;
910 run.attrib.regular = iFalse;
911 run.attrib.light = iFalse;
871 if (d->baseFgColorId == tmParagraph_ColorId) { 912 if (d->baseFgColorId == tmParagraph_ColorId) {
872 setFgColor_AttributedRun_(&run, tmFirstParagraph_ColorId); 913 setFgColor_AttributedRun_(&run, tmFirstParagraph_ColorId);
873 } 914 }
874 attribFont = font_Text_(fontWithStyle_Text(fontId_Text_(d->baseFont), 915 attribFont = font_Text_(fontWithStyle_Text(fontId_Text_(d->baseFont),
875 bold_FontStyle)); 916 bold_FontStyle));
876 } 917 }
918 else if (ansi & allowFontStyle_AnsiFlag && equal_Rangecc(sequence, "2")) {
919 run.attrib.light = iTrue;
920 run.attrib.regular = iFalse;
921 run.attrib.bold = iFalse;
922 attribFont = font_Text_(fontWithStyle_Text(fontId_Text_(d->baseFont),
923 light_FontStyle));
924 }
877 else if (ansi & allowFontStyle_AnsiFlag && equal_Rangecc(sequence, "3")) { 925 else if (ansi & allowFontStyle_AnsiFlag && equal_Rangecc(sequence, "3")) {
878 run.attrib.italic = iTrue; 926 run.attrib.italic = iTrue;
879 attribFont = font_Text_(fontWithStyle_Text(fontId_Text_(d->baseFont), 927 attribFont = font_Text_(fontWithStyle_Text(fontId_Text_(d->baseFont),
880 italic_FontStyle)); 928 italic_FontStyle));
881 } 929 }
930 else if (ansi & allowFontStyle_AnsiFlag && equal_Rangecc(sequence, "10")) {
931 run.attrib.regular = iTrue;
932 run.attrib.bold = iFalse;
933 run.attrib.light = iFalse;
934 run.attrib.italic = iFalse;
935 attribFont = font_Text_(fontWithStyle_Text(fontId_Text_(d->baseFont),
936 regular_FontStyle));
937 }
882 else if (ansi & allowFontStyle_AnsiFlag && equal_Rangecc(sequence, "11")) { 938 else if (ansi & allowFontStyle_AnsiFlag && equal_Rangecc(sequence, "11")) {
883 run.attrib.monospace = iTrue; 939 run.attrib.monospace = iTrue;
884 setFgColor_AttributedRun_(&run, tmPreformatted_ColorId); 940 setFgColor_AttributedRun_(&run, tmPreformatted_ColorId);
@@ -886,7 +942,9 @@ static void prepare_AttributedText_(iAttributedText *d, int overrideBaseDir, iCh
886 monospace_FontId)); 942 monospace_FontId));
887 } 943 }
888 else if (equal_Rangecc(sequence, "0")) { 944 else if (equal_Rangecc(sequence, "0")) {
945 run.attrib.regular = iFalse;
889 run.attrib.bold = iFalse; 946 run.attrib.bold = iFalse;
947 run.attrib.light = iFalse;
890 run.attrib.italic = iFalse; 948 run.attrib.italic = iFalse;
891 run.attrib.monospace = iFalse; 949 run.attrib.monospace = iFalse;
892 attribFont = run.font = d->baseFont; 950 attribFont = run.font = d->baseFont;
@@ -957,16 +1015,44 @@ static void prepare_AttributedText_(iAttributedText *d, int overrideBaseDir, iCh
957 (int)logicalText[pos]); 1015 (int)logicalText[pos]);
958#endif 1016#endif
959 } 1017 }
1018 /* Detect the script. */
960#if defined (LAGRANGE_ENABLE_FRIBIDI) 1019#if defined (LAGRANGE_ENABLE_FRIBIDI)
961 if (fribidi_get_bidi_type(ch) == FRIBIDI_TYPE_AL) { 1020 if (fribidi_get_bidi_type(ch) == FRIBIDI_TYPE_AL) {
962 run.flags.isArabic = iTrue; /* Arabic letter */ 1021 run.flags.script = arabic_Script;
963 } 1022 }
1023 else
964#endif 1024#endif
1025 {
1026 const char *scr = script_Char(ch);
1027// printf("Char %08x %lc => %s\n", ch, (int) ch, scr);
1028 if (!iCmpStr(scr, "Bengali")) {
1029 run.flags.script = bengali_Script;
1030 }
1031 else if (!iCmpStr(scr, "Devanagari")) {
1032 run.flags.script = devanagari_Script;
1033 }
1034 else if (!iCmpStr(scr, "Han")) {
1035 run.flags.script = han_Script;
1036 }
1037 else if (!iCmpStr(scr, "Hiragana")) {
1038 run.flags.script = hiragana_Script;
1039 }
1040 else if (!iCmpStr(scr, "Katakana")) {
1041 run.flags.script = katakana_Script;
1042 }
1043 else if (!iCmpStr(scr, "Oriya")) {
1044 run.flags.script = oriya_Script;
1045 }
1046 else if (!iCmpStr(scr, "Tamil")) {
1047 run.flags.script = tamil_Script;
1048 }
1049 }
965 } 1050 }
966 if (!isEmpty_Range(&run.logical)) { 1051 if (!isEmpty_Range(&run.logical)) {
967 pushBack_Array(&d->runs, &run); 1052 pushBack_Array(&d->runs, &run);
968 } 1053 }
969#if 0 1054#if 0
1055 const int *logToVis = constData_Array(&d->logicalToVisual);
970 printf("[AttributedText] %zu runs:\n", size_Array(&d->runs)); 1056 printf("[AttributedText] %zu runs:\n", size_Array(&d->runs));
971 iConstForEach(Array, i, &d->runs) { 1057 iConstForEach(Array, i, &d->runs) {
972 const iAttributedRun *run = i.value; 1058 const iAttributedRun *run = i.value;
@@ -1021,10 +1107,10 @@ struct Impl_RasterGlyph {
1021 iRect rect; 1107 iRect rect;
1022}; 1108};
1023 1109
1024static void cacheGlyphs_Font_(iFont *d, const iArray *glyphIndices) { 1110static void cacheGlyphs_Font_(iFont *d, const uint32_t *glyphIndices, size_t numGlyphIndices) {
1025 /* TODO: Make this an object so it can be used sequentially without reallocating buffers. */ 1111 /* TODO: Make this an object so it can be used sequentially without reallocating buffers. */
1026 SDL_Surface *buf = NULL; 1112 SDL_Surface *buf = NULL;
1027 const iInt2 bufSize = init_I2(iMin(512, d->height * iMin(2 * size_Array(glyphIndices), 20)), 1113 const iInt2 bufSize = init_I2(iMin(512, d->height * iMin(2 * numGlyphIndices, 20)),
1028 d->height * 4 / 3); 1114 d->height * 4 / 3);
1029 int bufX = 0; 1115 int bufX = 0;
1030 iArray * rasters = NULL; 1116 iArray * rasters = NULL;
@@ -1033,9 +1119,9 @@ static void cacheGlyphs_Font_(iFont *d, const iArray *glyphIndices) {
1033 iAssert(isExposed_Window(get_Window())); 1119 iAssert(isExposed_Window(get_Window()));
1034 /* We'll flush the buffered rasters periodically until everything is cached. */ 1120 /* We'll flush the buffered rasters periodically until everything is cached. */
1035 size_t index = 0; 1121 size_t index = 0;
1036 while (index < size_Array(glyphIndices)) { 1122 while (index < numGlyphIndices) {
1037 for (; index < size_Array(glyphIndices); index++) { 1123 for (; index < numGlyphIndices; index++) {
1038 const uint32_t glyphIndex = constValue_Array(glyphIndices, index, uint32_t); 1124 const uint32_t glyphIndex = glyphIndices[index];
1039 const int lastCacheBottom = activeText_->cacheBottom; 1125 const int lastCacheBottom = activeText_->cacheBottom;
1040 iGlyph *glyph = glyphByIndex_Font_(d, glyphIndex); 1126 iGlyph *glyph = glyphByIndex_Font_(d, glyphIndex);
1041 if (activeText_->cacheBottom < lastCacheBottom) { 1127 if (activeText_->cacheBottom < lastCacheBottom) {
@@ -1137,12 +1223,8 @@ static void cacheGlyphs_Font_(iFont *d, const iArray *glyphIndices) {
1137 } 1223 }
1138} 1224}
1139 1225
1140static void cacheSingleGlyph_Font_(iFont *d, uint32_t glyphIndex) { 1226iLocalDef void cacheSingleGlyph_Font_(iFont *d, uint32_t glyphIndex) {
1141 iArray indices; 1227 cacheGlyphs_Font_(d, &glyphIndex, 1);
1142 init_Array(&indices, sizeof(uint32_t));
1143 pushBack_Array(&indices, &glyphIndex);
1144 cacheGlyphs_Font_(d, &indices);
1145 deinit_Array(&indices);
1146} 1228}
1147 1229
1148static void cacheTextGlyphs_Font_(iFont *d, const iRangecc text) { 1230static void cacheTextGlyphs_Font_(iFont *d, const iRangecc text) {
@@ -1171,7 +1253,7 @@ static void cacheTextGlyphs_Font_(iFont *d, const iRangecc text) {
1171 } 1253 }
1172 deinit_AttributedText(&attrText); 1254 deinit_AttributedText(&attrText);
1173 /* TODO: Cache glyphs from ALL the fonts we encountered above. */ 1255 /* TODO: Cache glyphs from ALL the fonts we encountered above. */
1174 cacheGlyphs_Font_(d, &glyphIndices); 1256 cacheGlyphs_Font_(d, constData_Array(&glyphIndices), size_Array(&glyphIndices));
1175 deinit_Array(&glyphIndices); 1257 deinit_Array(&glyphIndices);
1176} 1258}
1177 1259
@@ -1380,8 +1462,9 @@ static iRect run_Font_(iFont *d, const iRunArgs *args) {
1380 } 1462 }
1381 hb_buffer_set_content_type(buf->hb, HB_BUFFER_CONTENT_TYPE_UNICODE); 1463 hb_buffer_set_content_type(buf->hb, HB_BUFFER_CONTENT_TYPE_UNICODE);
1382 hb_buffer_set_direction(buf->hb, HB_DIRECTION_LTR); /* visual */ 1464 hb_buffer_set_direction(buf->hb, HB_DIRECTION_LTR); /* visual */
1383 if (run->flags.isArabic) { 1465 const hb_script_t script = hbScripts_[run->flags.script];
1384 hb_buffer_set_script(buf->hb, HB_SCRIPT_ARABIC); 1466 if (script) {
1467 hb_buffer_set_script(buf->hb, script);
1385 } 1468 }
1386 } 1469 }
1387 if (isMonospaced) { 1470 if (isMonospaced) {
@@ -1405,6 +1488,7 @@ static iRect run_Font_(iFont *d, const iRunArgs *args) {
1405 iBool isFirst = iTrue; 1488 iBool isFirst = iTrue;
1406 const iBool checkHitPoint = wrap && !isEqual_I2(wrap->hitPoint, zero_I2()); 1489 const iBool checkHitPoint = wrap && !isEqual_I2(wrap->hitPoint, zero_I2());
1407 const iBool checkHitChar = wrap && wrap->hitChar; 1490 const iBool checkHitChar = wrap && wrap->hitChar;
1491 size_t numWrapLines = 0;
1408 while (!isEmpty_Range(&wrapRuns)) { 1492 while (!isEmpty_Range(&wrapRuns)) {
1409 if (isFirst) { 1493 if (isFirst) {
1410 isFirst = iFalse; 1494 isFirst = iFalse;
@@ -1469,8 +1553,11 @@ static iRect run_Font_(iFont *d, const iRunArgs *args) {
1469 const float xOffset = run->font->xScale * buf->glyphPos[i].x_offset; 1553 const float xOffset = run->font->xScale * buf->glyphPos[i].x_offset;
1470 const float xAdvance = run->font->xScale * buf->glyphPos[i].x_advance; 1554 const float xAdvance = run->font->xScale * buf->glyphPos[i].x_advance;
1471 const iChar ch = logicalText[logPos]; 1555 const iChar ch = logicalText[logPos];
1556 const enum iWrapTextMode wrapMode = isCJK_Script_(run->flags.script)
1557 ? anyCharacter_WrapTextMode
1558 : args->wrap->mode;
1472 iAssert(xAdvance >= 0); 1559 iAssert(xAdvance >= 0);
1473 if (args->wrap->mode == word_WrapTextMode) { 1560 if (wrapMode == word_WrapTextMode) {
1474 /* When word wrapping, only consider certain places breakable. */ 1561 /* When word wrapping, only consider certain places breakable. */
1475 if ((prevCh == '-' || prevCh == '/') && !isPunct_Char(ch)) { 1562 if ((prevCh == '-' || prevCh == '/') && !isPunct_Char(ch)) {
1476 safeBreakPos = logPos; 1563 safeBreakPos = logPos;
@@ -1515,7 +1602,7 @@ static iRect run_Font_(iFont *d, const iRunArgs *args) {
1515 wrapPosRange.end = safeBreakPos; 1602 wrapPosRange.end = safeBreakPos;
1516 } 1603 }
1517 else { 1604 else {
1518 if (args->wrap->mode == word_WrapTextMode && run->logical.start > wrapPosRange.start) { 1605 if (wrapMode == word_WrapTextMode && run->logical.start > wrapPosRange.start) {
1519 /* Don't have a word break position, so the whole run needs 1606 /* Don't have a word break position, so the whole run needs
1520 to be cut. */ 1607 to be cut. */
1521 wrapPosRange.end = run->logical.start; 1608 wrapPosRange.end = run->logical.start;
@@ -1529,7 +1616,7 @@ static iRect run_Font_(iFont *d, const iRunArgs *args) {
1529 breakRunIndex = runIndex; 1616 breakRunIndex = runIndex;
1530 } 1617 }
1531 wrapResumePos = wrapPosRange.end; 1618 wrapResumePos = wrapPosRange.end;
1532 if (args->wrap->mode != anyCharacter_WrapTextMode) { 1619 if (wrapMode != anyCharacter_WrapTextMode) {
1533 while (wrapResumePos < textLen && isSpace_Char(logicalText[wrapResumePos])) { 1620 while (wrapResumePos < textLen && isSpace_Char(logicalText[wrapResumePos])) {
1534 wrapResumePos++; /* skip space */ 1621 wrapResumePos++; /* skip space */
1535 } 1622 }
@@ -1634,6 +1721,10 @@ static iRect run_Font_(iFont *d, const iRunArgs *args) {
1634 iRound(wrapAdvance))) { 1721 iRound(wrapAdvance))) {
1635 willAbortDueToWrap = iTrue; 1722 willAbortDueToWrap = iTrue;
1636 } 1723 }
1724 numWrapLines++;
1725 if (wrap && wrap->maxLines && numWrapLines == wrap->maxLines) {
1726 willAbortDueToWrap = iTrue;
1727 }
1637 wrapAttrib = lastAttrib; 1728 wrapAttrib = lastAttrib;
1638 xCursor = origin; 1729 xCursor = origin;
1639 /* We have determined a possible wrap position and alignment for the work runs, 1730 /* We have determined a possible wrap position and alignment for the work runs,
@@ -1664,12 +1755,13 @@ static iRect run_Font_(iFont *d, const iRunArgs *args) {
1664 /* Already handled this part of the run. */ 1755 /* Already handled this part of the run. */
1665 continue; 1756 continue;
1666 } 1757 }
1667 const float xOffset = run->font->xScale * buf->glyphPos[i].x_offset; 1758 const float xOffset = run->font->xScale * buf->glyphPos[i].x_offset;
1668 const float yOffset = run->font->yScale * buf->glyphPos[i].y_offset; 1759 float yOffset = run->font->yScale * buf->glyphPos[i].y_offset;
1669 const float xAdvance = run->font->xScale * buf->glyphPos[i].x_advance; 1760 const float xAdvance = run->font->xScale * buf->glyphPos[i].x_advance;
1670 const float yAdvance = run->font->yScale * buf->glyphPos[i].y_advance; 1761 const float yAdvance = run->font->yScale * buf->glyphPos[i].y_advance;
1671 const iGlyph *glyph = glyphByIndex_Font_(run->font, glyphId); 1762 const iGlyph *glyph = glyphByIndex_Font_(run->font, glyphId);
1672 if (logicalText[logPos] == '\t') { 1763 const iChar ch = logicalText[logPos];
1764 if (ch == '\t') {
1673#if 0 1765#if 0
1674 if (mode & draw_RunMode) { 1766 if (mode & draw_RunMode) {
1675 /* Tab indicator. */ 1767 /* Tab indicator. */
@@ -1688,6 +1780,13 @@ static iRect run_Font_(iFont *d, const iRunArgs *args) {
1688 } 1780 }
1689 const float xf = xCursor + xOffset; 1781 const float xf = xCursor + xOffset;
1690 const int hoff = enableHalfPixelGlyphs_Text ? (xf - ((int) xf) > 0.5f ? 1 : 0) : 0; 1782 const int hoff = enableHalfPixelGlyphs_Text ? (xf - ((int) xf) > 0.5f ? 1 : 0) : 0;
1783 if (ch == 0x3001 || ch == 0x3002) {
1784 /* Vertical misalignment?? */
1785 if (yOffset == 0.0f) {
1786 /* Move down to baseline. Why doesn't HarfBuzz do this? */
1787 yOffset = glyph->d[hoff].y + glyph->rect[hoff].size.y + glyph->d[hoff].y / 4;
1788 }
1789 }
1691 /* Output position for the glyph. */ 1790 /* Output position for the glyph. */
1692 SDL_Rect dst = { orig.x + xCursor + xOffset + glyph->d[hoff].x, 1791 SDL_Rect dst = { orig.x + xCursor + xOffset + glyph->d[hoff].x,
1693 orig.y + yCursor - yOffset + glyph->font->baseline + glyph->d[hoff].y, 1792 orig.y + yCursor - yOffset + glyph->font->baseline + glyph->d[hoff].y,
@@ -2231,6 +2330,7 @@ void init_TextBuf(iTextBuf *d, iWrapText *wrapText, int font, int color) {
2231 SDL_Texture *oldTarget = SDL_GetRenderTarget(render); 2330 SDL_Texture *oldTarget = SDL_GetRenderTarget(render);
2232 const iInt2 oldOrigin = origin_Paint; 2331 const iInt2 oldOrigin = origin_Paint;
2233 origin_Paint = zero_I2(); 2332 origin_Paint = zero_I2();
2333 setBaseAttributes_Text(font, color);
2234 SDL_SetRenderTarget(render, d->texture); 2334 SDL_SetRenderTarget(render, d->texture);
2235 SDL_SetRenderDrawBlendMode(render, SDL_BLENDMODE_NONE); 2335 SDL_SetRenderDrawBlendMode(render, SDL_BLENDMODE_NONE);
2236 SDL_SetRenderDrawColor(render, 255, 255, 255, 0); 2336 SDL_SetRenderDrawColor(render, 255, 255, 255, 0);
@@ -2241,6 +2341,7 @@ void init_TextBuf(iTextBuf *d, iWrapText *wrapText, int font, int color) {
2241 SDL_SetRenderTarget(render, oldTarget); 2341 SDL_SetRenderTarget(render, oldTarget);
2242 origin_Paint = oldOrigin; 2342 origin_Paint = oldOrigin;
2243 SDL_SetTextureBlendMode(d->texture, SDL_BLENDMODE_BLEND); 2343 SDL_SetTextureBlendMode(d->texture, SDL_BLENDMODE_BLEND);
2344 setBaseAttributes_Text(-1, -1);
2244 } 2345 }
2245} 2346}
2246 2347
diff --git a/src/ui/text.h b/src/ui/text.h
index 63499484..c8bb6f85 100644
--- a/src/ui/text.h
+++ b/src/ui/text.h
@@ -29,6 +29,8 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
29 29
30#include "fontpack.h" 30#include "fontpack.h"
31 31
32iDeclareType(RegExp)
33
32/* Content sizes: regular (1x) -> medium (1.2x) -> big (1.33x) -> large (1.67x) -> huge (2x) */ 34/* Content sizes: regular (1x) -> medium (1.2x) -> big (1.33x) -> large (1.67x) -> huge (2x) */
33 35
34#define FONT_ID(name, style, size) ((name) + ((style) * max_FontSize) + (size)) 36#define FONT_ID(name, style, size) ((name) + ((style) * max_FontSize) + (size))
@@ -159,6 +161,7 @@ enum iAnsiFlag {
159void setOpacity_Text (float opacity); 161void setOpacity_Text (float opacity);
160void setBaseAttributes_Text (int fontId, int fgColorId); /* current "normal" text attributes */ 162void setBaseAttributes_Text (int fontId, int fgColorId); /* current "normal" text attributes */
161void setAnsiFlags_Text (int ansiFlags); 163void setAnsiFlags_Text (int ansiFlags);
164int ansiFlags_Text (void);
162 165
163void cache_Text (int fontId, iRangecc text); /* pre-render glyphs */ 166void cache_Text (int fontId, iRangecc text); /* pre-render glyphs */
164 167
@@ -190,7 +193,9 @@ struct Impl_TextAttrib {
190 int16_t fgColorId; 193 int16_t fgColorId;
191 int16_t bgColorId; 194 int16_t bgColorId;
192 struct { 195 struct {
196 uint16_t regular : 1;
193 uint16_t bold : 1; 197 uint16_t bold : 1;
198 uint16_t light : 1;
194 uint16_t italic : 1; 199 uint16_t italic : 1;
195 uint16_t monospace : 1; 200 uint16_t monospace : 1;
196 uint16_t isBaseRTL : 1; 201 uint16_t isBaseRTL : 1;
@@ -202,6 +207,7 @@ struct Impl_WrapText {
202 /* arguments */ 207 /* arguments */
203 iRangecc text; 208 iRangecc text;
204 int maxWidth; 209 int maxWidth;
210 size_t maxLines; /* 0: unlimited */
205 enum iWrapTextMode mode; 211 enum iWrapTextMode mode;
206 iBool (*wrapFunc)(iWrapText *, iRangecc wrappedText, iTextAttrib attrib, int origin, 212 iBool (*wrapFunc)(iWrapText *, iRangecc wrappedText, iTextAttrib attrib, int origin,
207 int advance); 213 int advance);
@@ -229,6 +235,8 @@ enum iTextBlockMode { quadrants_TextBlockMode, shading_TextBlockMode };
229iString * renderBlockChars_Text (const iBlock *fontData, int height, enum iTextBlockMode, 235iString * renderBlockChars_Text (const iBlock *fontData, int height, enum iTextBlockMode,
230 const iString *text); 236 const iString *text);
231 237
238iRegExp * makeAnsiEscapePattern_Text (void);
239
232/*-----------------------------------------------------------------------------------------------*/ 240/*-----------------------------------------------------------------------------------------------*/
233 241
234iDeclareType(TextBuf) 242iDeclareType(TextBuf)
@@ -239,6 +247,5 @@ struct Impl_TextBuf {
239 iInt2 size; 247 iInt2 size;
240}; 248};
241 249
242iTextBuf * newRange_TextBuf (int font, int color, iRangecc text); 250iTextBuf * newRange_TextBuf(int font, int color, iRangecc text);
243
244void draw_TextBuf (const iTextBuf *, iInt2 pos, int color); 251void draw_TextBuf (const iTextBuf *, iInt2 pos, int color);
diff --git a/src/ui/text_simple.c b/src/ui/text_simple.c
index 8560c138..71942cf1 100644
--- a/src/ui/text_simple.c
+++ b/src/ui/text_simple.c
@@ -118,8 +118,9 @@ static iRect runSimple_Font_(iFont *d, const iRunArgs *args) {
118 if (match_RegExp(activeText_->ansiEscape, chPos, args->text.end - chPos, &m)) { 118 if (match_RegExp(activeText_->ansiEscape, chPos, args->text.end - chPos, &m)) {
119 if (mode & draw_RunMode && ~mode & permanentColorFlag_RunMode) { 119 if (mode & draw_RunMode && ~mode & permanentColorFlag_RunMode) {
120 /* Change the color. */ 120 /* Change the color. */
121 iColor clr; 121 iColor clr = get_Color(args->color);
122 ansiColors_Color(capturedRange_RegExpMatch(&m, 1), tmParagraph_ColorId, 122 ansiColors_Color(capturedRange_RegExpMatch(&m, 1),
123 activeText_->baseFgColorId,
123 none_ColorId, &clr, NULL); 124 none_ColorId, &clr, NULL);
124 SDL_SetTextureColorMod(activeText_->cache, clr.r, clr.g, clr.b); 125 SDL_SetTextureColorMod(activeText_->cache, clr.r, clr.g, clr.b);
125 if (args->mode & fillBackground_RunMode) { 126 if (args->mode & fillBackground_RunMode) {
diff --git a/src/ui/touch.c b/src/ui/touch.c
index d6846572..20ccf7b8 100644
--- a/src/ui/touch.c
+++ b/src/ui/touch.c
@@ -111,6 +111,7 @@ struct Impl_TouchState {
111 double momFrictionPerStep; 111 double momFrictionPerStep;
112 double lastMomTime; 112 double lastMomTime;
113 iInt2 currentTouchPos; /* for emulating SDL_GetMouseState() */ 113 iInt2 currentTouchPos; /* for emulating SDL_GetMouseState() */
114 iInt2 latestLongPressStartPos;
114}; 115};
115 116
116static iTouchState *touchState_(void) { 117static iTouchState *touchState_(void) {
@@ -260,6 +261,11 @@ static iFloat3 gestureVector_Touch_(const iTouch *d) {
260 return sub_F3(d->pos[0], d->pos[lastIndex]); 261 return sub_F3(d->pos[0], d->pos[lastIndex]);
261} 262}
262 263
264static uint32_t gestureSpan_Touch_(const iTouch *d) {
265 const size_t lastIndex = iMin(d->posCount - 1, lastIndex_Touch_);
266 return d->posTime[0] - d->posTime[lastIndex];
267}
268
263static void update_TouchState_(void *ptr) { 269static void update_TouchState_(void *ptr) {
264 iWindow *win = get_Window(); 270 iWindow *win = get_Window();
265 const iWidget *oldHover = win->hover; 271 const iWidget *oldHover = win->hover;
@@ -308,6 +314,7 @@ static void update_TouchState_(void *ptr) {
308 } 314 }
309 if (!touch->isTapAndHold && nowTime - touch->startTime >= longPressSpanMs_ && 315 if (!touch->isTapAndHold && nowTime - touch->startTime >= longPressSpanMs_ &&
310 touch->affinity) { 316 touch->affinity) {
317 touchState_()->latestLongPressStartPos = initF3_I2(touch->pos[0]);
311 dispatchClick_Touch_(touch, SDL_BUTTON_RIGHT); 318 dispatchClick_Touch_(touch, SDL_BUTTON_RIGHT);
312 touch->isTapAndHold = iTrue; 319 touch->isTapAndHold = iTrue;
313 touch->hasMoved = iFalse; 320 touch->hasMoved = iFalse;
@@ -593,9 +600,18 @@ iBool processEvent_Touch(const SDL_Event *ev) {
593 divvf_F3(&touch->accum, 6); 600 divvf_F3(&touch->accum, 6);
594 divfv_I2(&pixels, 6); 601 divfv_I2(&pixels, 6);
595 /* Allow scrolling a scrollable widget. */ 602 /* Allow scrolling a scrollable widget. */
596 iWidget *flow = findOverflowScrollable_Widget(touch->affinity); 603 if (touch->affinity && touch->affinity->flags2 & slidingSheetDraggable_WidgetFlag2) {
597 if (flow) { 604 extern iWidgetClass Class_SidebarWidget; /* The only type of sliding sheet for now. */
598 touch->affinity = flow; 605 iWidget *slider = findParentClass_Widget(touch->affinity, &Class_SidebarWidget);
606 if (slider) {
607 touch->affinity = slider;
608 }
609 }
610 else {
611 iWidget *flow = findOverflowScrollable_Widget(touch->affinity);
612 if (flow) {
613 touch->affinity = flow;
614 }
599 } 615 }
600 } 616 }
601 else { 617 else {
@@ -621,11 +637,13 @@ iBool processEvent_Touch(const SDL_Event *ev) {
621 if (touch->axis == y_TouchAxis) { 637 if (touch->axis == y_TouchAxis) {
622 pixels.x = 0; 638 pixels.x = 0;
623 } 639 }
624// printf("%p (%s) py: %i wy: %f acc: %f edge: %d\n", 640#if 0
625// touch->affinity, 641 printf("%p (%s) py: %i wy: %f acc: %f edge: %d\n",
626// class_Widget(touch->affinity)->name, 642 touch->affinity,
627// pixels.y, y_F3(amount), y_F3(touch->accum), 643 class_Widget(touch->affinity)->name,
628// touch->edge); 644 pixels.y, y_F3(amount), y_F3(touch->accum),
645 touch->edge);
646#endif
629 if (pixels.x || pixels.y) { 647 if (pixels.x || pixels.y) {
630 //setFocus_Widget(NULL); 648 //setFocus_Widget(NULL);
631 dispatchMotion_Touch_(touch->startPos /*pos[0]*/, 0); 649 dispatchMotion_Touch_(touch->startPos /*pos[0]*/, 0);
@@ -661,11 +679,14 @@ iBool processEvent_Touch(const SDL_Event *ev) {
661#endif 679#endif
662 if (touch->edge && !isStationary_Touch_(touch)) { 680 if (touch->edge && !isStationary_Touch_(touch)) {
663 const iFloat3 gesture = gestureVector_Touch_(touch); 681 const iFloat3 gesture = gestureVector_Touch_(touch);
682 const uint32_t duration = gestureSpan_Touch_(touch);
664 const float pixel = window->pixelRatio; 683 const float pixel = window->pixelRatio;
665 const int moveDir = x_F3(gesture) < -pixel ? -1 : x_F3(gesture) > pixel ? +1 : 0; 684 const int moveDir = x_F3(gesture) < -pixel ? -1 : x_F3(gesture) > pixel ? +1 : 0;
666 const int didAbort = (touch->edge == left_TouchEdge && moveDir < 0) || 685 const int didAbort = (touch->edge == left_TouchEdge && moveDir < 0) ||
667 (touch->edge == right_TouchEdge && moveDir > 0); 686 (touch->edge == right_TouchEdge && moveDir > 0);
668 postCommandf_App("edgeswipe.ended abort:%d side:%d id:%llu", didAbort, touch->edge, touch->id); 687 postCommandf_App("edgeswipe.ended abort:%d side:%d id:%llu speed:%d", didAbort,
688 touch->edge, touch->id,
689 (int) (duration > 0 ? length_F3(gesture) / (duration / 1000.0f) : 0));
669 remove_ArrayIterator(&i); 690 remove_ArrayIterator(&i);
670 continue; 691 continue;
671 } 692 }
@@ -689,8 +710,8 @@ iBool processEvent_Touch(const SDL_Event *ev) {
689 } 710 }
690 /* Edge swipes do not generate momentum. */ 711 /* Edge swipes do not generate momentum. */
691 const size_t lastIndex = iMin(touch->posCount - 1, lastIndex_Touch_); 712 const size_t lastIndex = iMin(touch->posCount - 1, lastIndex_Touch_);
692 const uint32_t duration = nowTime - touch->startTime;
693 const iFloat3 gestureVector = sub_F3(pos, touch->pos[lastIndex]); 713 const iFloat3 gestureVector = sub_F3(pos, touch->pos[lastIndex]);
714 const uint32_t duration = nowTime - touch->startTime;
694 iFloat3 velocity = zero_F3(); 715 iFloat3 velocity = zero_F3();
695#if 0 716#if 0
696 if (touch->edge && fabsf(2 * x_F3(gestureVector)) > fabsf(y_F3(gestureVector)) && 717 if (touch->edge && fabsf(2 * x_F3(gestureVector)) > fabsf(y_F3(gestureVector)) &&
@@ -805,10 +826,24 @@ void widgetDestroyed_Touch(iWidget *widget) {
805 } 826 }
806} 827}
807 828
829void transferAffinity_Touch(iWidget *src, iWidget *dst) {
830 iTouchState *d = touchState_();
831 iForEach(Array, i, d->touches) {
832 iTouch *touch = i.value;
833 if (touch->affinity == src) {
834 touch->affinity = dst;
835 }
836 }
837}
838
808iInt2 latestPosition_Touch(void) { 839iInt2 latestPosition_Touch(void) {
809 return touchState_()->currentTouchPos; 840 return touchState_()->currentTouchPos;
810} 841}
811 842
843iInt2 latestTapPosition_Touch(void) {
844 return touchState_()->latestLongPressStartPos;
845}
846
812iBool isHovering_Touch(void) { 847iBool isHovering_Touch(void) {
813 iTouchState *d = touchState_(); 848 iTouchState *d = touchState_();
814 if (numFingers_Touch() == 1) { 849 if (numFingers_Touch() == 1) {
diff --git a/src/ui/touch.h b/src/ui/touch.h
index e048224a..15c6da1f 100644
--- a/src/ui/touch.h
+++ b/src/ui/touch.h
@@ -36,10 +36,12 @@ enum iWidgetTouchMode {
36iBool processEvent_Touch (const SDL_Event *); 36iBool processEvent_Touch (const SDL_Event *);
37void update_Touch (void); 37void update_Touch (void);
38 38
39float stopWidgetMomentum_Touch (const iWidget *widget); 39float stopWidgetMomentum_Touch (const iWidget *widget); /* pixels per second */
40enum iWidgetTouchMode widgetMode_Touch (const iWidget *widget); 40enum iWidgetTouchMode widgetMode_Touch (const iWidget *widget);
41void widgetDestroyed_Touch (iWidget *widget); 41void widgetDestroyed_Touch (iWidget *widget);
42void transferAffinity_Touch (iWidget *src, iWidget *dst);
42 43
43iInt2 latestPosition_Touch (void); /* valid during processing of current event */ 44iInt2 latestPosition_Touch (void); /* valid during processing of current event */
45iInt2 latestTapPosition_Touch (void);
44iBool isHovering_Touch (void); /* stationary touch or a long-press drag ongoing */ 46iBool isHovering_Touch (void); /* stationary touch or a long-press drag ongoing */
45size_t numFingers_Touch (void); 47size_t numFingers_Touch (void);
diff --git a/src/ui/uploadwidget.c b/src/ui/uploadwidget.c
index bad00071..ed448768 100644
--- a/src/ui/uploadwidget.c
+++ b/src/ui/uploadwidget.c
@@ -56,8 +56,10 @@ struct Impl_UploadWidget {
56 iDocumentWidget *viewer; 56 iDocumentWidget *viewer;
57 iGmRequest * request; 57 iGmRequest * request;
58 iLabelWidget * info; 58 iLabelWidget * info;
59 iInputWidget * path;
59 iInputWidget * mime; 60 iInputWidget * mime;
60 iInputWidget * token; 61 iInputWidget * token;
62 iLabelWidget * ident;
61 iInputWidget * input; 63 iInputWidget * input;
62 iLabelWidget * filePathLabel; 64 iLabelWidget * filePathLabel;
63 iLabelWidget * fileSizeLabel; 65 iLabelWidget * fileSizeLabel;
@@ -91,17 +93,19 @@ static void updateProgress_UploadWidget_(iGmRequest *request, size_t current, si
91static void updateInputMaxHeight_UploadWidget_(iUploadWidget *d) { 93static void updateInputMaxHeight_UploadWidget_(iUploadWidget *d) {
92 iWidget *w = as_Widget(d); 94 iWidget *w = as_Widget(d);
93 /* Calculate how many lines fits vertically in the view. */ 95 /* Calculate how many lines fits vertically in the view. */
94 const iInt2 inputPos = topLeft_Rect(bounds_Widget(as_Widget(d->input))); 96 const iInt2 inputPos = topLeft_Rect(bounds_Widget(as_Widget(d->input)));
95 const int footerHeight = isUsingPanelLayout_Mobile() ? 0 : 97 int footerHeight = 0;
96 (height_Widget(d->token) + 98 if (!isUsingPanelLayout_Mobile()) {
97 height_Widget(findChild_Widget(w, "dialogbuttons")) + 99 footerHeight = (height_Widget(d->token) +
98 12 * gap_UI); 100 height_Widget(findChild_Widget(w, "dialogbuttons")) +
99 const int avail = bottom_Rect(safeRect_Root(w->root)) - footerHeight - 101 12 * gap_UI);
100 get_MainWindow()->keyboardHeight; 102 }
101 setLineLimits_InputWidget(d->input, 103 const int avail = bottom_Rect(visibleRect_Root(w->root)) - footerHeight - inputPos.y;
102 minLines_InputWidget(d->input), 104 /* On desktop, retain the previously set minLines value. */
103 iMaxi(minLines_InputWidget(d->input), 105 int minLines = isUsingPanelLayout_Mobile() ? 1 : minLines_InputWidget(d->input);
104 (avail - inputPos.y) / lineHeight_Text(font_InputWidget(d->input)))); 106 int maxLines = iMaxi(minLines, avail / lineHeight_Text(font_InputWidget(d->input)));
107 /* On mobile, the height is fixed to the available space. */
108 setLineLimits_InputWidget(d->input, isUsingPanelLayout_Mobile() ? maxLines : minLines, maxLines);
105} 109}
106 110
107static const iGmIdentity *titanIdentityForUrl_(const iString *url) { 111static const iGmIdentity *titanIdentityForUrl_(const iString *url) {
@@ -123,9 +127,15 @@ static const iArray *makeIdentityItems_UploadWidget_(const iUploadWidget *d) {
123 pushBack_Array(items, &(iMenuItem){ "---" }); 127 pushBack_Array(items, &(iMenuItem){ "---" });
124 iConstForEach(PtrArray, i, listIdentities_GmCerts(certs_App(), NULL, NULL)) { 128 iConstForEach(PtrArray, i, listIdentities_GmCerts(certs_App(), NULL, NULL)) {
125 const iGmIdentity *id = i.ptr; 129 const iGmIdentity *id = i.ptr;
130 iString *str = collect_String(copy_String(name_GmIdentity(id)));
131 prependCStr_String(str, "\x1b[1m");
132 if (!isEmpty_String(&id->notes)) {
133 appendFormat_String(
134 str, "\x1b[0m\n%s%s", escape_Color(uiTextDim_ColorId), cstr_String(&id->notes));
135 }
126 pushBack_Array( 136 pushBack_Array(
127 items, 137 items,
128 &(iMenuItem){ cstr_String(name_GmIdentity(id)), 0, 0, 138 &(iMenuItem){ cstr_String(str), 0, 0,
129 format_CStr("upload.setid fp:%s", 139 format_CStr("upload.setid fp:%s",
130 cstrCollect_String(hexEncode_Block(&id->fingerprint))) }); 140 cstrCollect_String(hexEncode_Block(&id->fingerprint))) });
131 } 141 }
@@ -182,15 +192,20 @@ void init_UploadWidget(iUploadWidget *d) {
182 }; 192 };
183 initPanels_Mobile(w, NULL, (iMenuItem[]){ 193 initPanels_Mobile(w, NULL, (iMenuItem[]){
184 { "title id:heading.upload" }, 194 { "title id:heading.upload" },
185 { "label id:upload.info" }, 195 { "heading id:upload.url" },
196 { format_CStr("label id:upload.info font:%d",
197 deviceType_App() == phone_AppDeviceType ? uiLabelBig_FontId : uiLabelMedium_FontId) },
198 { "input id:upload.path hint:hint.upload.path noheading:1 url:1 text:" },
199 { "heading text:${heading.upload.id}" },
200 { "dropdown id:upload.id icon:0x1f464 text:", 0, 0, constData_Array(makeIdentityItems_UploadWidget_(d)) },
201 { "input id:upload.token hint:hint.upload.token.long icon:0x1f516 text:" },
202 { "heading id:upload.content" },
186 { "panel id:dlg.upload.text icon:0x1f5b9 noscroll:1", 0, 0, (const void *) textItems }, 203 { "panel id:dlg.upload.text icon:0x1f5b9 noscroll:1", 0, 0, (const void *) textItems },
187 { "panel id:dlg.upload.file icon:0x1f4c1", 0, 0, (const void *) fileItems }, 204 { "panel id:dlg.upload.file icon:0x1f4c1", 0, 0, (const void *) fileItems },
188 { "padding" },
189 { "dropdown id:upload.id icon:0x1f464", 0, 0, constData_Array(makeIdentityItems_UploadWidget_(d)) },
190 { "input id:upload.token hint:hint.upload.token icon:0x1f511" },
191 { NULL } 205 { NULL }
192 }, actions, iElemCount(actions)); 206 }, actions, iElemCount(actions));
193 d->info = findChild_Widget(w, "upload.info"); 207 d->info = findChild_Widget(w, "upload.info");
208 d->path = findChild_Widget(w, "upload.path");
194 d->input = findChild_Widget(w, "upload.text"); 209 d->input = findChild_Widget(w, "upload.text");
195 d->filePathLabel = findChild_Widget(w, "upload.filepathlabel"); 210 d->filePathLabel = findChild_Widget(w, "upload.filepathlabel");
196 d->fileSizeLabel = findChild_Widget(w, "upload.filesizelabel"); 211 d->fileSizeLabel = findChild_Widget(w, "upload.filesizelabel");
@@ -200,17 +215,28 @@ void init_UploadWidget(iUploadWidget *d) {
200 if (isPortraitPhone_App()) { 215 if (isPortraitPhone_App()) {
201 enableUploadButton_UploadWidget_(d, iFalse); 216 enableUploadButton_UploadWidget_(d, iFalse);
202 } 217 }
218 iWidget *title = findChild_Widget(w, "heading.upload.text");
219 iLabelWidget *menu = new_LabelWidget(midEllipsis_Icon, "upload.editmenu.open");
220 setTextColor_LabelWidget(menu, uiTextAction_ColorId);
221 setFont_LabelWidget(menu, uiLabelBigBold_FontId);
222 addChildFlags_Widget(title, iClob(menu), frameless_WidgetFlag | moveToParentRightEdge_WidgetFlag);
203 } 223 }
204 else { 224 else {
205 useSheetStyle_Widget(w); 225 useSheetStyle_Widget(w);
206 setFlags_Widget(w, overflowScrollable_WidgetFlag, iFalse); 226 setFlags_Widget(w, overflowScrollable_WidgetFlag, iFalse);
207 addChildFlags_Widget(w, 227 addDialogTitle_Widget(w, "${heading.upload}", NULL);
208 iClob(new_LabelWidget(uiHeading_ColorEscape "${heading.upload}", NULL)), 228 iWidget *headings, *values;
209 frameless_WidgetFlag); 229 /* URL path. */ {
210 d->info = addChildFlags_Widget(w, iClob(new_LabelWidget("", NULL)), 230 iWidget *page = makeTwoColumns_Widget(&headings, &values);
211 frameless_WidgetFlag | resizeToParentWidth_WidgetFlag | 231 d->path = new_InputWidget(0);
212 fixedHeight_WidgetFlag); 232 addTwoColumnDialogInputField_Widget(
213 setWrap_LabelWidget(d->info, iTrue); 233 headings, values, "", "upload.path", iClob(d->path));
234 d->info = (iLabelWidget *) lastChild_Widget(headings);
235 setFont_LabelWidget(d->info, uiContent_FontId);
236 setTextColor_LabelWidget(d->info, uiInputTextFocused_ColorId);
237 addChild_Widget(w, iClob(page));
238 addChild_Widget(w, iClob(makePadding_Widget(gap_UI)));
239 }
214 /* Tabs for input data. */ 240 /* Tabs for input data. */
215 iWidget *tabs = makeTabs_Widget(w); 241 iWidget *tabs = makeTabs_Widget(w);
216 /* Make the tabs support vertical expansion based on content. */ { 242 /* Make the tabs support vertical expansion based on content. */ {
@@ -220,7 +246,6 @@ void init_UploadWidget(iUploadWidget *d) {
220 setFlags_Widget(tabPages, resizeHeightOfChildren_WidgetFlag, iFalse); 246 setFlags_Widget(tabPages, resizeHeightOfChildren_WidgetFlag, iFalse);
221 setFlags_Widget(tabPages, arrangeHeight_WidgetFlag, iTrue); 247 setFlags_Widget(tabPages, arrangeHeight_WidgetFlag, iTrue);
222 } 248 }
223 iWidget *headings, *values;
224 setBackgroundColor_Widget(findChild_Widget(tabs, "tabs.buttons"), uiBackgroundSidebar_ColorId); 249 setBackgroundColor_Widget(findChild_Widget(tabs, "tabs.buttons"), uiBackgroundSidebar_ColorId);
225 setId_Widget(tabs, "upload.tabs"); 250 setId_Widget(tabs, "upload.tabs");
226 /* Text input. */ { 251 /* Text input. */ {
@@ -233,7 +258,8 @@ void init_UploadWidget(iUploadWidget *d) {
233 appendFramelessTabPage_Widget(tabs, iClob(page), "${heading.upload.text}", '1', 0); 258 appendFramelessTabPage_Widget(tabs, iClob(page), "${heading.upload.text}", '1', 0);
234 } 259 }
235 /* File content. */ { 260 /* File content. */ {
236 appendTwoColumnTabPage_Widget(tabs, "${heading.upload.file}", '2', &headings, &values); 261 iWidget *page = appendTwoColumnTabPage_Widget(tabs, "${heading.upload.file}", '2', &headings, &values);
262 setBackgroundColor_Widget(page, uiBackgroundSidebar_ColorId);
237 addChildFlags_Widget(headings, iClob(new_LabelWidget("${upload.file.name}", NULL)), frameless_WidgetFlag); 263 addChildFlags_Widget(headings, iClob(new_LabelWidget("${upload.file.name}", NULL)), frameless_WidgetFlag);
238 d->filePathLabel = addChildFlags_Widget(values, iClob(new_LabelWidget(uiTextAction_ColorEscape "${upload.file.drophere}", NULL)), frameless_WidgetFlag); 264 d->filePathLabel = addChildFlags_Widget(values, iClob(new_LabelWidget(uiTextAction_ColorEscape "${upload.file.drophere}", NULL)), frameless_WidgetFlag);
239 addChildFlags_Widget(headings, iClob(new_LabelWidget("${upload.file.size}", NULL)), frameless_WidgetFlag); 265 addChildFlags_Widget(headings, iClob(new_LabelWidget("${upload.file.size}", NULL)), frameless_WidgetFlag);
@@ -245,19 +271,20 @@ void init_UploadWidget(iUploadWidget *d) {
245 /* Identity and Token. */ { 271 /* Identity and Token. */ {
246 addChild_Widget(w, iClob(makePadding_Widget(gap_UI))); 272 addChild_Widget(w, iClob(makePadding_Widget(gap_UI)));
247 iWidget *page = makeTwoColumns_Widget(&headings, &values); 273 iWidget *page = makeTwoColumns_Widget(&headings, &values);
248 /* Token. */
249 d->token = addTwoColumnDialogInputField_Widget(
250 headings, values, "${upload.token}", "upload.token", iClob(new_InputWidget(0)));
251 setHint_InputWidget(d->token, "${hint.upload.token}");
252 setFixedSize_Widget(as_Widget(d->token), init_I2(50 * gap_UI, -1));
253 /* Identity. */ 274 /* Identity. */
254 const iArray * identItems = makeIdentityItems_UploadWidget_(d); 275 const iArray * identItems = makeIdentityItems_UploadWidget_(d);
255 const iMenuItem *items = constData_Array(identItems); 276 const iMenuItem *items = constData_Array(identItems);
256 const size_t numItems = size_Array(identItems); 277 const size_t numItems = size_Array(identItems);
257 iLabelWidget * ident = makeMenuButton_LabelWidget("${upload.id}", items, numItems); 278 d->ident = makeMenuButton_LabelWidget("${upload.id}", items, numItems);
258 setTextCStr_LabelWidget(ident, items[findWidestLabel_MenuItem(items, numItems)].label); 279 setTextCStr_LabelWidget(d->ident, items[findWidestLabel_MenuItem(items, numItems)].label);
280 //setFixedSize_Widget(as_Widget(d->ident), init_I2(50 * gap_UI, ));
259 addChild_Widget(headings, iClob(makeHeading_Widget("${upload.id}"))); 281 addChild_Widget(headings, iClob(makeHeading_Widget("${upload.id}")));
260 setId_Widget(addChildFlags_Widget(values, iClob(ident), alignLeft_WidgetFlag), "upload.id"); 282 setId_Widget(addChildFlags_Widget(values, iClob(d->ident), alignLeft_WidgetFlag), "upload.id");
283 /* Token. */
284 d->token = addTwoColumnDialogInputField_Widget(
285 headings, values, "${upload.token}", "upload.token", iClob(new_InputWidget(0)));
286 setHint_InputWidget(d->token, "${hint.upload.token}");
287 setFixedSize_Widget(as_Widget(d->token), init_I2(50 * gap_UI, -1));
261 addChild_Widget(w, iClob(page)); 288 addChild_Widget(w, iClob(page));
262 } 289 }
263 /* Buttons. */ { 290 /* Buttons. */ {
@@ -272,7 +299,12 @@ void init_UploadWidget(iUploadWidget *d) {
272 } 299 }
273 resizeToLargestPage_Widget(tabs); 300 resizeToLargestPage_Widget(tabs);
274 arrange_Widget(w); 301 arrange_Widget(w);
302 setFixedSize_Widget(as_Widget(d->path), init_I2(width_Widget(tabs) - width_Widget(d->info), -1));
303 setFixedSize_Widget(as_Widget(d->mime), init_I2(width_Widget(tabs) - 3 * gap_UI -
304 left_Rect(parent_Widget(d->mime)->rect), -1));
275 setFixedSize_Widget(as_Widget(d->token), init_I2(width_Widget(tabs) - left_Rect(parent_Widget(d->token)->rect), -1)); 305 setFixedSize_Widget(as_Widget(d->token), init_I2(width_Widget(tabs) - left_Rect(parent_Widget(d->token)->rect), -1));
306 setFixedSize_Widget(as_Widget(d->ident), init_I2(width_Widget(d->token),
307 lineHeight_Text(uiLabel_FontId) + 2 * gap_UI));
276 setFlags_Widget(as_Widget(d->token), expand_WidgetFlag, iTrue); 308 setFlags_Widget(as_Widget(d->token), expand_WidgetFlag, iTrue);
277 setFocus_Widget(as_Widget(d->input)); 309 setFocus_Widget(as_Widget(d->input));
278 } 310 }
@@ -339,8 +371,24 @@ static void setUrlPort_UploadWidget_(iUploadWidget *d, const iString *url, uint1
339 appendRange_String(&d->url, (iRangecc){ parts.scheme.end, parts.host.end }); 371 appendRange_String(&d->url, (iRangecc){ parts.scheme.end, parts.host.end });
340 appendFormat_String(&d->url, ":%u", overridePort ? overridePort : titanPortForUrl_(url)); 372 appendFormat_String(&d->url, ":%u", overridePort ? overridePort : titanPortForUrl_(url));
341 appendRange_String(&d->url, (iRangecc){ parts.path.start, constEnd_String(url) }); 373 appendRange_String(&d->url, (iRangecc){ parts.path.start, constEnd_String(url) });
342 setText_LabelWidget(d->info, &d->url); 374 const iRangecc siteRoot = urlRoot_String(&d->url);
343 arrange_Widget(as_Widget(d)); 375 setTextCStr_LabelWidget(d->info, cstr_Rangecc((iRangecc){ urlHost_String(&d->url).start,
376 siteRoot.end }));
377 /* From root onwards, the URL is editable. */
378 setTextCStr_InputWidget(d->path,
379 cstr_Rangecc((iRangecc){ siteRoot.end, constEnd_String(&d->url) }));
380 if (!cmp_String(text_InputWidget(d->path), "/")) {
381 setTextCStr_InputWidget(d->path, ""); /* might as well show the hint */
382 }
383 if (isUsingPanelLayout_Mobile()) {
384 arrange_Widget(as_Widget(d)); /* a wrapped label */
385 }
386 else {
387 setFixedSize_Widget(as_Widget(d->path),
388 init_I2(width_Widget(findChild_Widget(as_Widget(d), "upload.tabs")) -
389 width_Widget(d->info),
390 -1));
391 }
344} 392}
345 393
346void setUrl_UploadWidget(iUploadWidget *d, const iString *url) { 394void setUrl_UploadWidget(iUploadWidget *d, const iString *url) {
@@ -353,6 +401,10 @@ void setResponseViewer_UploadWidget(iUploadWidget *d, iDocumentWidget *doc) {
353 d->viewer = doc; 401 d->viewer = doc;
354} 402}
355 403
404void setText_UploadWidget(iUploadWidget *d, const iString *text) {
405 setText_InputWidget(findChild_Widget(as_Widget(d), "upload.text"), text);
406}
407
356static iWidget *acceptButton_UploadWidget_(iUploadWidget *d) { 408static iWidget *acceptButton_UploadWidget_(iUploadWidget *d) {
357 return lastChild_Widget(findChild_Widget(as_Widget(d), "dialogbuttons")); 409 return lastChild_Widget(findChild_Widget(as_Widget(d), "dialogbuttons"));
358} 410}
@@ -396,6 +448,18 @@ static void showOrHideUploadButton_UploadWidget_(iUploadWidget *d) {
396 } 448 }
397} 449}
398 450
451static const iString *requestUrl_UploadWidget_(const iUploadWidget *d) {
452 const iRangecc siteRoot = urlRoot_String(&d->url);
453 iString *reqUrl = collectNew_String();
454 setRange_String(reqUrl, (iRangecc){ constBegin_String(&d->url), siteRoot.end });
455 const iString *path = text_InputWidget(d->path);
456 if (!startsWith_String(path, "/")) {
457 appendCStr_String(reqUrl, "/");
458 }
459 append_String(reqUrl, path);
460 return reqUrl;
461}
462
399static iBool processEvent_UploadWidget_(iUploadWidget *d, const SDL_Event *ev) { 463static iBool processEvent_UploadWidget_(iUploadWidget *d, const SDL_Event *ev) {
400 iWidget *w = as_Widget(d); 464 iWidget *w = as_Widget(d);
401 const char *cmd = command_UserEvent(ev); 465 const char *cmd = command_UserEvent(ev);
@@ -405,8 +469,22 @@ static iBool processEvent_UploadWidget_(iUploadWidget *d, const SDL_Event *ev) {
405 } 469 }
406 else if (equal_Command(cmd, "panel.changed")) { 470 else if (equal_Command(cmd, "panel.changed")) {
407 showOrHideUploadButton_UploadWidget_(d); 471 showOrHideUploadButton_UploadWidget_(d);
472 if (currentPanelIndex_Mobile(w) == 0) {
473 setFocus_Widget(as_Widget(d->input));
474 }
475 else {
476 setFocus_Widget(NULL);
477 }
478 refresh_Widget(d->input);
479 return iFalse;
480 }
481#if defined (iPlatformAppleMobile)
482 else if (deviceType_App() != desktop_AppDeviceType && equal_Command(cmd, "menu.opened")) {
483 setFocus_Widget(NULL); /* overlaid text fields! */
484 refresh_Widget(d->input);
408 return iFalse; 485 return iFalse;
409 } 486 }
487#endif
410 else if (equal_Command(cmd, "upload.cancel")) { 488 else if (equal_Command(cmd, "upload.cancel")) {
411 setupSheetTransition_Mobile(w, iFalse); 489 setupSheetTransition_Mobile(w, iFalse);
412 destroy_Widget(w); 490 destroy_Widget(w);
@@ -444,6 +522,43 @@ static iBool processEvent_UploadWidget_(iUploadWidget *d, const SDL_Event *ev) {
444 updateIdentityDropdown_UploadWidget_(d); 522 updateIdentityDropdown_UploadWidget_(d);
445 return iTrue; 523 return iTrue;
446 } 524 }
525 if (isCommand_Widget(w, ev, "upload.editmenu.open")) {
526 setFocus_Widget(NULL);
527 refresh_Widget(as_Widget(d->input));
528 iWidget *editMenu = makeMenu_Widget(root_Widget(w), (iMenuItem[]){
529 { select_Icon " ${menu.selectall}", 0, 0, "upload.text.selectall" },
530 { export_Icon " ${menu.upload.export}", 0, 0, "upload.text.export" },
531 { "---" },
532 { delete_Icon " " uiTextCaution_ColorEscape "${menu.upload.delete}", 0, 0, "upload.text.delete" }
533 }, 4);
534 openMenu_Widget(editMenu, topLeft_Rect(bounds_Widget(as_Widget(d->input))));
535 return iTrue;
536 }
537 if (isCommand_UserEvent(ev, "upload.text.export")) {
538#if defined (iPlatformAppleMobile)
539 openTextActivityView_iOS(text_InputWidget(d->input));
540#endif
541 return iTrue;
542 }
543 if (isCommand_UserEvent(ev, "upload.text.delete")) {
544 if (argLabel_Command(command_UserEvent(ev), "confirmed")) {
545 setTextCStr_InputWidget(d->input, "");
546 setFocus_Widget(as_Widget(d->input));
547 }
548 else {
549 openMenu_Widget(makeMenu_Widget(root_Widget(w), (iMenuItem[]){
550 { delete_Icon " " uiTextCaution_ColorEscape "${menu.upload.delete.confirm}", 0, 0,
551 "upload.text.delete confirmed:1" }
552 }, 1), zero_I2());
553 }
554 return iTrue;
555 }
556 if (isCommand_UserEvent(ev, "upload.text.selectall")) {
557 setFocus_Widget(as_Widget(d->input));
558 refresh_Widget(as_Widget(d->input));
559 postCommand_Widget(d->input, "input.selectall");
560 return iTrue;
561 }
447 if (isCommand_Widget(w, ev, "upload.accept")) { 562 if (isCommand_Widget(w, ev, "upload.accept")) {
448 iBool isText; 563 iBool isText;
449 iWidget *tabs = findChild_Widget(w, "upload.tabs"); 564 iWidget *tabs = findChild_Widget(w, "upload.tabs");
@@ -464,7 +579,7 @@ static iBool processEvent_UploadWidget_(iUploadWidget *d, const SDL_Event *ev) {
464 d->request = new_GmRequest(certs_App()); 579 d->request = new_GmRequest(certs_App());
465 setSendProgressFunc_GmRequest(d->request, updateProgress_UploadWidget_); 580 setSendProgressFunc_GmRequest(d->request, updateProgress_UploadWidget_);
466 setUserData_Object(d->request, d); 581 setUserData_Object(d->request, d);
467 setUrl_GmRequest(d->request, &d->url); 582 setUrl_GmRequest(d->request, requestUrl_UploadWidget_(d));
468 const iString *site = collectNewRange_String(urlRoot_String(&d->url)); 583 const iString *site = collectNewRange_String(urlRoot_String(&d->url));
469 switch (d->idMode) { 584 switch (d->idMode) {
470 case none_UploadIdentity: 585 case none_UploadIdentity:
@@ -540,10 +655,15 @@ static iBool processEvent_UploadWidget_(iUploadWidget *d, const SDL_Event *ev) {
540 return iTrue; 655 return iTrue;
541 } 656 }
542 else if (isCommand_Widget(w, ev, "input.resized")) { 657 else if (isCommand_Widget(w, ev, "input.resized")) {
543 resizeToLargestPage_Widget(findChild_Widget(w, "upload.tabs")); 658 if (!isUsingPanelLayout_Mobile()) {
544 arrange_Widget(w); 659 resizeToLargestPage_Widget(findChild_Widget(w, "upload.tabs"));
545 refresh_Widget(w); 660 arrange_Widget(w);
546 return iTrue; 661 refresh_Widget(w);
662 return iTrue;
663 }
664 else {
665 refresh_Widget(as_Widget(d->input));
666 }
547 } 667 }
548 else if (isCommand_Widget(w, ev, "upload.pickfile")) { 668 else if (isCommand_Widget(w, ev, "upload.pickfile")) {
549#if defined (iPlatformAppleMobile) 669#if defined (iPlatformAppleMobile)
diff --git a/src/ui/uploadwidget.h b/src/ui/uploadwidget.h
index 5a7de45e..1cc1f193 100644
--- a/src/ui/uploadwidget.h
+++ b/src/ui/uploadwidget.h
@@ -31,3 +31,4 @@ iDeclareType(DocumentWidget)
31 31
32void setUrl_UploadWidget (iUploadWidget *, const iString *url); 32void setUrl_UploadWidget (iUploadWidget *, const iString *url);
33void setResponseViewer_UploadWidget (iUploadWidget *, iDocumentWidget *doc); 33void setResponseViewer_UploadWidget (iUploadWidget *, iDocumentWidget *doc);
34void setText_UploadWidget (iUploadWidget *, const iString *text);
diff --git a/src/ui/util.c b/src/ui/util.c
index 912e1d37..31907721 100644
--- a/src/ui/util.c
+++ b/src/ui/util.c
@@ -479,12 +479,15 @@ void init_SmoothScroll(iSmoothScroll *d, iWidget *owner, iSmoothScrollNotifyFunc
479 reset_SmoothScroll(d); 479 reset_SmoothScroll(d);
480 d->widget = owner; 480 d->widget = owner;
481 d->notify = notify; 481 d->notify = notify;
482 d->pullActionTriggered = 0;
483 d->flags = 0;
482} 484}
483 485
484void reset_SmoothScroll(iSmoothScroll *d) { 486void reset_SmoothScroll(iSmoothScroll *d) {
485 init_Anim(&d->pos, 0); 487 init_Anim(&d->pos, 0);
486 d->max = 0; 488 d->max = 0;
487 d->overscroll = (deviceType_App() != desktop_AppDeviceType ? 100 * gap_UI : 0); 489 d->overscroll = (deviceType_App() != desktop_AppDeviceType ? 100 * gap_UI : 0);
490 d->pullActionTriggered = 0;
488} 491}
489 492
490void setMax_SmoothScroll(iSmoothScroll *d, int max) { 493void setMax_SmoothScroll(iSmoothScroll *d, int max) {
@@ -518,6 +521,29 @@ iBool isFinished_SmoothScroll(const iSmoothScroll *d) {
518 return isFinished_Anim(&d->pos); 521 return isFinished_Anim(&d->pos);
519} 522}
520 523
524iLocalDef int pullActionThreshold_SmoothScroll_(const iSmoothScroll *d) {
525 return d->overscroll * 6 / 10;
526}
527
528float pullActionPos_SmoothScroll(const iSmoothScroll *d) {
529 if (d->pullActionTriggered >= 1) {
530 return 1.0f;
531 }
532 float pos = overscroll_SmoothScroll_(d);
533 if (pos >= 0.0f) {
534 return 0.0f;
535 }
536 pos = -pos / (float) pullActionThreshold_SmoothScroll_(d);
537 return iMin(pos, 1.0f);
538}
539
540static void checkPullAction_SmoothScroll_(iSmoothScroll *d) {
541 if (d->pullActionTriggered == 1 && d->widget) {
542 postCommand_Widget(d->widget, "pullaction");
543 d->pullActionTriggered = 2; /* pending handling */
544 }
545}
546
521void moveSpan_SmoothScroll(iSmoothScroll *d, int offset, uint32_t span) { 547void moveSpan_SmoothScroll(iSmoothScroll *d, int offset, uint32_t span) {
522#if !defined (iPlatformMobile) 548#if !defined (iPlatformMobile)
523 if (!prefs_App()->smoothScrolling) { 549 if (!prefs_App()->smoothScrolling) {
@@ -525,16 +551,19 @@ void moveSpan_SmoothScroll(iSmoothScroll *d, int offset, uint32_t span) {
525 } 551 }
526#endif 552#endif
527 int destY = targetValue_Anim(&d->pos) + offset; 553 int destY = targetValue_Anim(&d->pos) + offset;
554 if (d->flags & pullDownAction_SmoothScrollFlag && destY < -pullActionThreshold_SmoothScroll_(d)) {
555 if (d->pullActionTriggered == 0) {
556 d->pullActionTriggered = iTrue;
557#if defined (iPlatformAppleMobile)
558 playHapticEffect_iOS(tap_HapticEffect);
559#endif
560 }
561 }
528 if (destY < -d->overscroll) { 562 if (destY < -d->overscroll) {
529 destY = -d->overscroll; 563 destY = -d->overscroll;
530 } 564 }
531 if (d->max > 0) { 565 if (destY >= d->max + d->overscroll) {
532 if (destY >= d->max + d->overscroll) { 566 destY = d->max + d->overscroll;
533 destY = d->max + d->overscroll;
534 }
535 }
536 else {
537 destY = 0;
538 } 567 }
539 if (span) { 568 if (span) {
540 setValueEased_Anim(&d->pos, destY, span); 569 setValueEased_Anim(&d->pos, destY, span);
@@ -552,6 +581,7 @@ void moveSpan_SmoothScroll(iSmoothScroll *d, int offset, uint32_t span) {
552 // printf("remaining: %f dur: %d\n", remaining, duration); 581 // printf("remaining: %f dur: %d\n", remaining, duration);
553 d->pos.bounce = (osDelta < 0 ? -1 : 1) * 582 d->pos.bounce = (osDelta < 0 ? -1 : 1) *
554 iMini(5 * d->overscroll, remaining * remaining * 0.00005f); 583 iMini(5 * d->overscroll, remaining * remaining * 0.00005f);
584 checkPullAction_SmoothScroll_(d);
555 } 585 }
556 } 586 }
557 if (d->notify) { 587 if (d->notify) {
@@ -570,6 +600,7 @@ iBool processEvent_SmoothScroll(iSmoothScroll *d, const SDL_Event *ev) {
570 moveSpan_SmoothScroll(d, -osDelta, 100 * sqrt(iAbs(osDelta) / gap_UI)); 600 moveSpan_SmoothScroll(d, -osDelta, 100 * sqrt(iAbs(osDelta) / gap_UI));
571 d->pos.flags = easeOut_AnimFlag | muchSofter_AnimFlag; 601 d->pos.flags = easeOut_AnimFlag | muchSofter_AnimFlag;
572 } 602 }
603 checkPullAction_SmoothScroll_(d);
573 return iTrue; 604 return iTrue;
574 } 605 }
575 return iFalse; 606 return iFalse;
@@ -620,7 +651,7 @@ static iBool isCommandIgnoredByMenus_(const char *cmd) {
620 if (equal_Command(cmd, "window.focus.lost") || 651 if (equal_Command(cmd, "window.focus.lost") ||
621 equal_Command(cmd, "window.focus.gained")) return iTrue; 652 equal_Command(cmd, "window.focus.gained")) return iTrue;
622 /* TODO: Perhaps a common way of indicating which commands are notifications and should not 653 /* TODO: Perhaps a common way of indicating which commands are notifications and should not
623 be reacted to by menus? */ 654 be reacted to by menus?! */
624 return equal_Command(cmd, "media.updated") || 655 return equal_Command(cmd, "media.updated") ||
625 equal_Command(cmd, "media.player.update") || 656 equal_Command(cmd, "media.player.update") ||
626 startsWith_CStr(cmd, "feeds.update.") || 657 startsWith_CStr(cmd, "feeds.update.") ||
@@ -640,13 +671,16 @@ static iBool isCommandIgnoredByMenus_(const char *cmd) {
640 equal_Command(cmd, "window.reload.update") || 671 equal_Command(cmd, "window.reload.update") ||
641 equal_Command(cmd, "window.mouse.exited") || 672 equal_Command(cmd, "window.mouse.exited") ||
642 equal_Command(cmd, "window.mouse.entered") || 673 equal_Command(cmd, "window.mouse.entered") ||
674 equal_Command(cmd, "input.backup") ||
675 equal_Command(cmd, "input.ended") ||
676 equal_Command(cmd, "focus.lost") ||
643 (equal_Command(cmd, "mouse.clicked") && !arg_Command(cmd)); /* button released */ 677 (equal_Command(cmd, "mouse.clicked") && !arg_Command(cmd)); /* button released */
644} 678}
645 679
646static iLabelWidget *parentMenuButton_(const iWidget *menu) { 680static iLabelWidget *parentMenuButton_(const iWidget *menu) {
647 if (isInstance_Object(menu->parent, &Class_LabelWidget)) { 681 if (isInstance_Object(menu->parent, &Class_LabelWidget)) {
648 iLabelWidget *button = (iLabelWidget *) menu->parent; 682 iLabelWidget *button = (iLabelWidget *) menu->parent;
649 if (!cmp_String(command_LabelWidget(button), "menu.open")) { 683 if (equal_Command(cstr_String(command_LabelWidget(button)), "menu.open")) {
650 return button; 684 return button;
651 } 685 }
652 } 686 }
@@ -671,6 +705,21 @@ static iBool menuHandler_(iWidget *menu, const char *cmd) {
671 closeMenu_Widget(menu); 705 closeMenu_Widget(menu);
672 return iTrue; 706 return iTrue;
673 } 707 }
708 if (equal_Command(cmd, "cancel") && pointerLabel_Command(cmd, "menu") == menu) {
709 return iFalse;
710 }
711 if (equal_Command(cmd, "contextclick") && pointer_Command(cmd) == menu) {
712 return iFalse;
713 }
714 if (deviceType_App() == phone_AppDeviceType && equal_Command(cmd, "keyboard.changed") &&
715 arg_Command(cmd) == 0) {
716 /* May need to reposition the menu. */
717 menu->rect.pos = windowToLocal_Widget(
718 menu,
719 init_I2(left_Rect(bounds_Widget(menu)),
720 bottom_Rect(safeRect_Root(menu->root)) - menu->rect.size.y));
721 return iFalse;
722 }
674 if (!isCommandIgnoredByMenus_(cmd)) { 723 if (!isCommandIgnoredByMenus_(cmd)) {
675 closeMenu_Widget(menu); 724 closeMenu_Widget(menu);
676 } 725 }
@@ -733,13 +782,16 @@ void makeMenuItems_Widget(iWidget *menu, const iMenuItem *items, size_t n) {
733 noBackground_WidgetFlag | frameless_WidgetFlag | alignLeft_WidgetFlag | 782 noBackground_WidgetFlag | frameless_WidgetFlag | alignLeft_WidgetFlag |
734 drawKey_WidgetFlag | itemFlags); 783 drawKey_WidgetFlag | itemFlags);
735 setWrap_LabelWidget(label, isInfo); 784 setWrap_LabelWidget(label, isInfo);
785 if (!isInfo) {
736 haveIcons |= checkIcon_LabelWidget(label); 786 haveIcons |= checkIcon_LabelWidget(label);
737 updateSize_LabelWidget(label); /* drawKey was set */ 787 }
738 setFlags_Widget(as_Widget(label), disabled_WidgetFlag, isDisabled); 788 setFlags_Widget(as_Widget(label), disabled_WidgetFlag, isDisabled);
739 if (isInfo) { 789 if (isInfo) {
740 setFlags_Widget(as_Widget(label), fixedHeight_WidgetFlag, iTrue); /* wrap changes height */ 790 setFlags_Widget(as_Widget(label), resizeToParentWidth_WidgetFlag |
791 fixedHeight_WidgetFlag, iTrue); /* wrap changes height */
741 setTextColor_LabelWidget(label, uiTextAction_ColorId); 792 setTextColor_LabelWidget(label, uiTextAction_ColorId);
742 } 793 }
794 updateSize_LabelWidget(label); /* drawKey was set */
743 } 795 }
744 } 796 }
745 if (deviceType_App() == phone_AppDeviceType) { 797 if (deviceType_App() == phone_AppDeviceType) {
@@ -861,12 +913,8 @@ iWidget *makeMenu_Widget(iWidget *parent, const iMenuItem *items, size_t n) {
861 setFlags_Widget(menu, 913 setFlags_Widget(menu,
862 keepOnTop_WidgetFlag | collapse_WidgetFlag | hidden_WidgetFlag | 914 keepOnTop_WidgetFlag | collapse_WidgetFlag | hidden_WidgetFlag |
863 arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag | 915 arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag |
864 resizeChildrenToWidestChild_WidgetFlag | overflowScrollable_WidgetFlag | 916 resizeChildrenToWidestChild_WidgetFlag | overflowScrollable_WidgetFlag,
865 (isPortraitPhone_App() ? drawBackgroundToVerticalSafeArea_WidgetFlag : 0),
866 iTrue); 917 iTrue);
867 if (!isPortraitPhone_App()) {
868 setFrameColor_Widget(menu, uiBackgroundSelected_ColorId);
869 }
870 makeMenuItems_Widget(menu, items, n); 918 makeMenuItems_Widget(menu, items, n);
871 addChild_Widget(parent, menu); 919 addChild_Widget(parent, menu);
872 iRelease(menu); /* owned by parent now */ 920 iRelease(menu); /* owned by parent now */
@@ -884,6 +932,7 @@ void openMenu_Widget(iWidget *d, iInt2 windowCoord) {
884 932
885static void updateMenuItemFonts_Widget_(iWidget *d) { 933static void updateMenuItemFonts_Widget_(iWidget *d) {
886 const iBool isPortraitPhone = (deviceType_App() == phone_AppDeviceType && isPortrait_App()); 934 const iBool isPortraitPhone = (deviceType_App() == phone_AppDeviceType && isPortrait_App());
935 const iBool isMobile = (deviceType_App() != desktop_AppDeviceType);
887 const iBool isSlidePanel = (flags_Widget(d) & horizontalOffset_WidgetFlag) != 0; 936 const iBool isSlidePanel = (flags_Widget(d) & horizontalOffset_WidgetFlag) != 0;
888 iForEach(ObjectList, i, children_Widget(d)) { 937 iForEach(ObjectList, i, children_Widget(d)) {
889 if (isInstance_Object(i.object, &Class_LabelWidget)) { 938 if (isInstance_Object(i.object, &Class_LabelWidget)) {
@@ -892,16 +941,16 @@ static void updateMenuItemFonts_Widget_(iWidget *d) {
892 if (isWrapped_LabelWidget(label)) { 941 if (isWrapped_LabelWidget(label)) {
893 continue; 942 continue;
894 } 943 }
895 if (deviceType_App() == desktop_AppDeviceType) { 944 switch (deviceType_App()) {
945 case desktop_AppDeviceType:
896 setFont_LabelWidget(label, isCaution ? uiLabelBold_FontId : uiLabel_FontId); 946 setFont_LabelWidget(label, isCaution ? uiLabelBold_FontId : uiLabel_FontId);
897 } 947 break;
898 else if (isPortraitPhone) { 948 case tablet_AppDeviceType:
899 if (!isSlidePanel) { 949 setFont_LabelWidget(label, isCaution ? uiLabelMediumBold_FontId : uiLabelMedium_FontId);
950 break;
951 case phone_AppDeviceType:
900 setFont_LabelWidget(label, isCaution ? uiLabelBigBold_FontId : uiLabelBig_FontId); 952 setFont_LabelWidget(label, isCaution ? uiLabelBigBold_FontId : uiLabelBig_FontId);
901 } 953 break;
902 }
903 else {
904 setFont_LabelWidget(label, isCaution ? uiContentBold_FontId : uiContent_FontId);
905 } 954 }
906 } 955 }
907 else if (childCount_Widget(i.object)) { 956 else if (childCount_Widget(i.object)) {
@@ -1024,20 +1073,28 @@ void openMenuFlags_Widget(iWidget *d, iInt2 windowCoord, int menuOpenFlags) {
1024#else 1073#else
1025 const iRect rootRect = rect_Root(d->root); 1074 const iRect rootRect = rect_Root(d->root);
1026 const iInt2 rootSize = rootRect.size; 1075 const iInt2 rootSize = rootRect.size;
1027 const iBool isPortraitPhone = (deviceType_App() == phone_AppDeviceType && isPortrait_App()); 1076 const iBool isPhone = (deviceType_App() == phone_AppDeviceType);
1077 const iBool isPortraitPhone = (isPhone && isPortrait_App());
1028 const iBool isSlidePanel = (flags_Widget(d) & horizontalOffset_WidgetFlag) != 0; 1078 const iBool isSlidePanel = (flags_Widget(d) & horizontalOffset_WidgetFlag) != 0;
1029 if (postCommands) { 1079 if (postCommands) {
1030 postCommand_App("cancel"); /* dismiss any other menus */ 1080 postCommandf_App("cancel menu:%p", d); /* dismiss any other menus */
1031 } 1081 }
1032 /* Menu closes when commands are emitted, so handle any pending ones beforehand. */ 1082 /* Menu closes when commands are emitted, so handle any pending ones beforehand. */
1033 processEvents_App(postedEventsOnly_AppEventMode); 1083 processEvents_App(postedEventsOnly_AppEventMode);
1034 setFlags_Widget(d, hidden_WidgetFlag, iFalse); 1084 setFlags_Widget(d, hidden_WidgetFlag, iFalse);
1035 setFlags_Widget(d, commandOnMouseMiss_WidgetFlag, iTrue); 1085 setFlags_Widget(d, commandOnMouseMiss_WidgetFlag, iTrue);
1036 setFlags_Widget(findChild_Widget(d, "menu.cancel"), disabled_WidgetFlag, iFalse); 1086 setFlags_Widget(findChild_Widget(d, "menu.cancel"), disabled_WidgetFlag, iFalse);
1087 if (!isPortraitPhone) {
1088 setFrameColor_Widget(d, uiBackgroundSelected_ColorId);
1089 }
1090 else {
1091 setFrameColor_Widget(d, none_ColorId);
1092 }
1037 arrange_Widget(d); /* need to know the height */ 1093 arrange_Widget(d); /* need to know the height */
1038 iBool allowOverflow = iFalse; 1094 iBool allowOverflow = iFalse;
1039 /* A vertical offset determined by a possible selected label in the menu. */ 1095 /* A vertical offset determined by a possible selected label in the menu. */
1040 if (windowCoord.y < rootSize.y - lineHeight_Text(uiNormal_FontSize) * 3) { 1096 if (deviceType_App() == desktop_AppDeviceType &&
1097 windowCoord.y < rootSize.y - lineHeight_Text(uiNormal_FontSize) * 3) {
1041 iConstForEach(ObjectList, child, children_Widget(d)) { 1098 iConstForEach(ObjectList, child, children_Widget(d)) {
1042 const iWidget *item = constAs_Widget(child.object); 1099 const iWidget *item = constAs_Widget(child.object);
1043 if (flags_Widget(item) & selected_WidgetFlag) { 1100 if (flags_Widget(item) & selected_WidgetFlag) {
@@ -1104,22 +1161,35 @@ void openMenuFlags_Widget(iWidget *d, iInt2 windowCoord, int menuOpenFlags) {
1104 } 1161 }
1105#endif 1162#endif
1106 raise_Widget(d); 1163 raise_Widget(d);
1107 if (isPortraitPhone) { 1164 if (deviceType_App() != desktop_AppDeviceType) {
1108 setFlags_Widget(d, arrangeWidth_WidgetFlag | resizeChildrenToWidestChild_WidgetFlag, iFalse); 1165 setFlags_Widget(d, arrangeWidth_WidgetFlag | resizeChildrenToWidestChild_WidgetFlag,
1109 setFlags_Widget(d, resizeWidthOfChildren_WidgetFlag | drawBackgroundToBottom_WidgetFlag, iTrue); 1166 !isPhone);
1110 if (!isSlidePanel) { 1167 setFlags_Widget(d,
1111 setFlags_Widget(d, borderTop_WidgetFlag, iTrue); 1168 resizeWidthOfChildren_WidgetFlag | drawBackgroundToBottom_WidgetFlag |
1169 drawBackgroundToVerticalSafeArea_WidgetFlag,
1170 isPhone);
1171 if (isPhone) {
1172 setFlags_Widget(d, borderTop_WidgetFlag, !isSlidePanel && isPortrait_App()); /* menu is otherwise frameless */
1173 setFixedSize_Widget(d, init_I2(iMin(rootSize.x, rootSize.y), -1));
1174 }
1175 else {
1176 d->rect.size.x = 0;
1112 } 1177 }
1113 d->rect.size.x = rootSize.x;
1114 } 1178 }
1115 updateMenuItemFonts_Widget_(d); 1179 updateMenuItemFonts_Widget_(d);
1116 arrange_Widget(d); 1180 arrange_Widget(d);
1117 if (isPortraitPhone) { 1181 if (!isSlidePanel) {
1182 /* LAYOUT BUG: Height of wrapped menu items is incorrect with a single arrange! */
1183 arrange_Widget(d);
1184 }
1185 if (deviceType_App() == phone_AppDeviceType) {
1118 if (isSlidePanel) { 1186 if (isSlidePanel) {
1119 d->rect.pos = zero_I2(); 1187 d->rect.pos = zero_I2();
1120 } 1188 }
1121 else { 1189 else {
1122 d->rect.pos = init_I2(0, rootSize.y); 1190 d->rect.pos = windowToLocal_Widget(d,
1191 init_I2(rootSize.x / 2 - d->rect.size.x / 2,
1192 rootSize.y));
1123 } 1193 }
1124 } 1194 }
1125 else if (menuOpenFlags & center_MenuOpenFlags) { 1195 else if (menuOpenFlags & center_MenuOpenFlags) {
@@ -1143,6 +1213,9 @@ void openMenuFlags_Widget(iWidget *d, iInt2 windowCoord, int menuOpenFlags) {
1143 leftExcess += l; 1213 leftExcess += l;
1144 rightExcess += r; 1214 rightExcess += r;
1145 } 1215 }
1216#elif defined (iPlatformMobile)
1217 /* Reserve space for the keyboard. */
1218 bottomExcess += get_MainWindow()->keyboardHeight;
1146#endif 1219#endif
1147 if (!allowOverflow) { 1220 if (!allowOverflow) {
1148 if (bottomExcess > 0 && (!isPortraitPhone || !isSlidePanel)) { 1221 if (bottomExcess > 0 && (!isPortraitPhone || !isSlidePanel)) {
@@ -1203,6 +1276,15 @@ iLabelWidget *findMenuItem_Widget(iWidget *menu, const char *command) {
1203 return NULL; 1276 return NULL;
1204} 1277}
1205 1278
1279iWidget *findUserData_Widget(iWidget *d, void *userData) {
1280 iForEach(ObjectList, i, children_Widget(d)) {
1281 if (userData_Object(i.object) == userData) {
1282 return i.object;
1283 }
1284 }
1285 return NULL;
1286}
1287
1206void setMenuItemDisabled_Widget(iWidget *menu, const char *command, iBool disable) { 1288void setMenuItemDisabled_Widget(iWidget *menu, const char *command, iBool disable) {
1207 if (flags_Widget(menu) & nativeMenu_WidgetFlag) { 1289 if (flags_Widget(menu) & nativeMenu_WidgetFlag) {
1208 setDisabled_NativeMenuItem(findNativeMenuItem_Widget(menu, command), disable); 1290 setDisabled_NativeMenuItem(findNativeMenuItem_Widget(menu, command), disable);
@@ -1269,6 +1351,12 @@ const iString *removeMenuItemLabelPrefixes_String(const iString *d) {
1269 return collect_String(str); 1351 return collect_String(str);
1270} 1352}
1271 1353
1354static const iString *replaceNewlinesWithDash_(const iString *str) {
1355 iString *mod = copy_String(str);
1356 replace_String(mod, "\n", " ");
1357 return collect_String(mod);
1358}
1359
1272void updateDropdownSelection_LabelWidget(iLabelWidget *dropButton, const char *selectedCommand) { 1360void updateDropdownSelection_LabelWidget(iLabelWidget *dropButton, const char *selectedCommand) {
1273 if (!dropButton) { 1361 if (!dropButton) {
1274 return; 1362 return;
@@ -1279,8 +1367,9 @@ void updateDropdownSelection_LabelWidget(iLabelWidget *dropButton, const char *s
1279 iMenuItem *item = findNativeMenuItem_Widget(menu, selectedCommand); 1367 iMenuItem *item = findNativeMenuItem_Widget(menu, selectedCommand);
1280 if (item) { 1368 if (item) {
1281 setSelected_NativeMenuItem(item, iTrue); 1369 setSelected_NativeMenuItem(item, iTrue);
1282 updateText_LabelWidget( 1370 updateText_LabelWidget(dropButton,
1283 dropButton, removeMenuItemLabelPrefixes_String(collectNewCStr_String(item->label))); 1371 replaceNewlinesWithDash_(removeMenuItemLabelPrefixes_String(
1372 collectNewCStr_String(item->label))));
1284 checkIcon_LabelWidget(dropButton); 1373 checkIcon_LabelWidget(dropButton);
1285 } 1374 }
1286 return; 1375 return;
@@ -1291,7 +1380,8 @@ void updateDropdownSelection_LabelWidget(iLabelWidget *dropButton, const char *s
1291 const iBool isSelected = endsWith_String(command_LabelWidget(item), selectedCommand); 1380 const iBool isSelected = endsWith_String(command_LabelWidget(item), selectedCommand);
1292 setFlags_Widget(as_Widget(item), selected_WidgetFlag, isSelected); 1381 setFlags_Widget(as_Widget(item), selected_WidgetFlag, isSelected);
1293 if (isSelected) { 1382 if (isSelected) {
1294 updateText_LabelWidget(dropButton, sourceText_LabelWidget(item)); 1383 updateText_LabelWidget(dropButton,
1384 replaceNewlinesWithDash_(text_LabelWidget(item)));
1295 checkIcon_LabelWidget(dropButton); 1385 checkIcon_LabelWidget(dropButton);
1296 } 1386 }
1297 } 1387 }
@@ -1342,7 +1432,7 @@ static iBool tabSwitcher_(iWidget *tabs, const char *cmd) {
1342 if (equal_Command(cmd, "tabs.switch")) { 1432 if (equal_Command(cmd, "tabs.switch")) {
1343 iWidget *target = pointerLabel_Command(cmd, "page"); 1433 iWidget *target = pointerLabel_Command(cmd, "page");
1344 if (!target) { 1434 if (!target) {
1345 target = findChild_Widget(tabs, cstr_Rangecc(range_Command(cmd, "id"))); 1435 target = findChild_Widget(tabs, cstr_Command(cmd, "id"));
1346 } 1436 }
1347 if (!target) return iFalse; 1437 if (!target) return iFalse;
1348 unfocusFocusInsideTabPage_(currentTabPage_Widget(tabs)); 1438 unfocusFocusInsideTabPage_(currentTabPage_Widget(tabs));
@@ -1377,7 +1467,7 @@ static iBool tabSwitcher_(iWidget *tabs, const char *cmd) {
1377 iWidget *nextTabs = findChild_Widget(otherRoot_Window(get_Window(), tabs->root)->widget, 1467 iWidget *nextTabs = findChild_Widget(otherRoot_Window(get_Window(), tabs->root)->widget,
1378 "doctabs"); 1468 "doctabs");
1379 iWidget *nextPages = findChild_Widget(nextTabs, "tabs.pages"); 1469 iWidget *nextPages = findChild_Widget(nextTabs, "tabs.pages");
1380 tabIndex = (dir < 0 ? childCount_Widget(nextPages) - 1 : 0); 1470 tabIndex = (int) (dir < 0 ? childCount_Widget(nextPages) - 1 : 0);
1381 showTabPage_Widget(nextTabs, child_Widget(nextPages, tabIndex)); 1471 showTabPage_Widget(nextTabs, child_Widget(nextPages, tabIndex));
1382 postCommand_App("keyroot.next"); 1472 postCommand_App("keyroot.next");
1383 } 1473 }
@@ -1602,6 +1692,22 @@ void useSheetStyle_Widget(iWidget *d) {
1602 iTrue); 1692 iTrue);
1603} 1693}
1604 1694
1695static iLabelWidget *addDialogTitle_(iWidget *dlg, const char *text, const char *id) {
1696 iLabelWidget *label = new_LabelWidget(text, NULL);
1697 addChildFlags_Widget(dlg, iClob(label), alignLeft_WidgetFlag | frameless_WidgetFlag |
1698 resizeToParentWidth_WidgetFlag);
1699 setAllCaps_LabelWidget(label, iTrue);
1700 setTextColor_LabelWidget(label, uiHeading_ColorId);
1701 if (id) {
1702 setId_Widget(as_Widget(label), id);
1703 }
1704 return label;
1705}
1706
1707iLabelWidget *addDialogTitle_Widget(iWidget *dlg, const char *text, const char *idOrNull) {
1708 return addDialogTitle_(dlg, text, idOrNull);
1709}
1710
1605static void acceptValueInput_(iWidget *dlg) { 1711static void acceptValueInput_(iWidget *dlg) {
1606 const iInputWidget *input = findChild_Widget(dlg, "input"); 1712 const iInputWidget *input = findChild_Widget(dlg, "input");
1607 if (!isEmpty_String(id_Widget(dlg))) { 1713 if (!isEmpty_String(id_Widget(dlg))) {
@@ -1613,7 +1719,7 @@ static void acceptValueInput_(iWidget *dlg) {
1613 } 1719 }
1614} 1720}
1615 1721
1616static void updateValueInputWidth_(iWidget *dlg) { 1722static void updateValueInputSizing_(iWidget *dlg) {
1617 const iRect safeRoot = safeRect_Root(dlg->root); 1723 const iRect safeRoot = safeRect_Root(dlg->root);
1618 const iInt2 rootSize = safeRoot.size; 1724 const iInt2 rootSize = safeRoot.size;
1619 iWidget * title = findChild_Widget(dlg, "valueinput.title"); 1725 iWidget * title = findChild_Widget(dlg, "valueinput.title");
@@ -1623,18 +1729,19 @@ static void updateValueInputWidth_(iWidget *dlg) {
1623 } 1729 }
1624 else { 1730 else {
1625 dlg->rect.size.x = 1731 dlg->rect.size.x =
1626 iMin(rootSize.x, iMaxi(iMaxi(100 * gap_UI, title->rect.size.x), prompt->rect.size.x)); 1732 iMin(rootSize.x, iMaxi(iMaxi(100 * gap_UI, title ? title->rect.size.x : 0),
1733 prompt->rect.size.x));
1627 } 1734 }
1628 /* Adjust the maximum number of visible lines. */ 1735 /* Adjust the maximum number of visible lines. */
1629 int footer = 6 * gap_UI + get_MainWindow()->keyboardHeight; 1736 int footer = 6 * gap_UI;
1630 iWidget *buttons = findChild_Widget(dlg, "dialogbuttons"); 1737 iWidget *buttons = findChild_Widget(dlg, "dialogbuttons");
1631 if (buttons) { 1738 if (buttons && deviceType_App() == desktop_AppDeviceType) {
1632 footer += height_Widget(buttons); 1739 footer += height_Widget(buttons);
1633 } 1740 }
1634 iInputWidget *input = findChild_Widget(dlg, "input"); 1741 iInputWidget *input = findChild_Widget(dlg, "input");
1635 setLineLimits_InputWidget(input, 1742 setLineLimits_InputWidget(input,
1636 1, 1743 1,
1637 (bottom_Rect(safeRect_Root(dlg->root)) - footer - 1744 (bottom_Rect(visibleRect_Root(dlg->root)) - footer -
1638 top_Rect(boundsWithoutVisualOffset_Widget(as_Widget(input)))) / 1745 top_Rect(boundsWithoutVisualOffset_Widget(as_Widget(input)))) /
1639 lineHeight_Text(font_InputWidget(input))); 1746 lineHeight_Text(font_InputWidget(input)));
1640} 1747}
@@ -1643,11 +1750,17 @@ iBool valueInputHandler_(iWidget *dlg, const char *cmd) {
1643 iWidget *ptr = as_Widget(pointer_Command(cmd)); 1750 iWidget *ptr = as_Widget(pointer_Command(cmd));
1644 if (equal_Command(cmd, "window.resized") || equal_Command(cmd, "keyboard.changed")) { 1751 if (equal_Command(cmd, "window.resized") || equal_Command(cmd, "keyboard.changed")) {
1645 if (isVisible_Widget(dlg)) { 1752 if (isVisible_Widget(dlg)) {
1646 updateValueInputWidth_(dlg); 1753 updateValueInputSizing_(dlg);
1647 arrange_Widget(dlg); 1754 arrange_Widget(dlg);
1648 } 1755 }
1649 return iFalse; 1756 return iFalse;
1650 } 1757 }
1758 if (equal_Command(cmd, "input.resized")) {
1759 /* BUG: A single arrange here is not sufficient, leaving a big gap between prompt and input. Why? */
1760 arrange_Widget(dlg);
1761 arrange_Widget(dlg);
1762 return iTrue;
1763 }
1651 if (equal_Command(cmd, "input.ended")) { 1764 if (equal_Command(cmd, "input.ended")) {
1652 if (argLabel_Command(cmd, "enter") && hasParent_Widget(ptr, dlg)) { 1765 if (argLabel_Command(cmd, "enter") && hasParent_Widget(ptr, dlg)) {
1653 if (arg_Command(cmd)) { 1766 if (arg_Command(cmd)) {
@@ -1663,6 +1776,12 @@ iBool valueInputHandler_(iWidget *dlg, const char *cmd) {
1663 } 1776 }
1664 return iFalse; 1777 return iFalse;
1665 } 1778 }
1779 else if (equal_Command(cmd, "valueinput.set")) {
1780 iInputWidget *input = findChild_Widget(dlg, "input");
1781 setTextCStr_InputWidget(input, suffixPtr_Command(cmd, "text"));
1782 validate_InputWidget(input);
1783 return iTrue;
1784 }
1666 else if (equal_Command(cmd, "valueinput.cancel")) { 1785 else if (equal_Command(cmd, "valueinput.cancel")) {
1667 postCommandf_App("valueinput.cancelled id:%s", cstr_String(id_Widget(dlg))); 1786 postCommandf_App("valueinput.cancelled id:%s", cstr_String(id_Widget(dlg)));
1668 setId_Widget(dlg, ""); /* no further commands to emit */ 1787 setId_Widget(dlg, ""); /* no further commands to emit */
@@ -1699,9 +1818,9 @@ iWidget *makeDialogButtons_Widget(const iMenuItem *actions, size_t numActions) {
1699 addChildFlags_Widget(div, iClob(new_Widget()), expand_WidgetFlag); 1818 addChildFlags_Widget(div, iClob(new_Widget()), expand_WidgetFlag);
1700 } 1819 }
1701 int fonts[2] = { uiLabel_FontId, uiLabelBold_FontId }; 1820 int fonts[2] = { uiLabel_FontId, uiLabelBold_FontId };
1702 if (deviceType_App() == phone_AppDeviceType) { 1821 if (deviceType_App() != desktop_AppDeviceType) {
1703 fonts[0] = uiLabelMedium_FontId; 1822 fonts[0] = uiLabelBig_FontId;
1704 fonts[1] = uiLabelMediumBold_FontId; 1823 fonts[1] = uiLabelBigBold_FontId;
1705 } 1824 }
1706 for (size_t i = 0; i < numActions; i++) { 1825 for (size_t i = 0; i < numActions; i++) {
1707 const char *label = actions[i].label; 1826 const char *label = actions[i].label;
@@ -1743,6 +1862,10 @@ iWidget *makeDialogButtons_Widget(const iMenuItem *actions, size_t numActions) {
1743 setId_Widget(as_Widget(button), "default"); 1862 setId_Widget(as_Widget(button), "default");
1744 } 1863 }
1745 setFlags_Widget(as_Widget(button), alignLeft_WidgetFlag | drawKey_WidgetFlag, isDefault); 1864 setFlags_Widget(as_Widget(button), alignLeft_WidgetFlag | drawKey_WidgetFlag, isDefault);
1865 if (deviceType_App() != desktop_AppDeviceType) {
1866 setFlags_Widget(as_Widget(button), frameless_WidgetFlag | noBackground_WidgetFlag, iTrue);
1867 setTextColor_LabelWidget(button, uiTextAction_ColorId);
1868 }
1746 setFont_LabelWidget(button, isDefault ? fonts[1] : fonts[0]); 1869 setFont_LabelWidget(button, isDefault ? fonts[1] : fonts[0]);
1747 } 1870 }
1748 return div; 1871 return div;
@@ -1758,9 +1881,9 @@ iWidget *makeValueInput_Widget(iWidget *parent, const iString *initialValue, con
1758 if (parent) { 1881 if (parent) {
1759 addChild_Widget(parent, iClob(dlg)); 1882 addChild_Widget(parent, iClob(dlg));
1760 } 1883 }
1761 setId_Widget( 1884 if (deviceType_App() == desktop_AppDeviceType) { /* conserve space on mobile */
1762 addChildFlags_Widget(dlg, iClob(new_LabelWidget(title, NULL)), frameless_WidgetFlag), 1885 addDialogTitle_(dlg, title, "valueinput.title");
1763 "valueinput.title"); 1886 }
1764 iLabelWidget *promptLabel; 1887 iLabelWidget *promptLabel;
1765 setId_Widget(addChildFlags_Widget( 1888 setId_Widget(addChildFlags_Widget(
1766 dlg, iClob(promptLabel = new_LabelWidget(prompt, NULL)), frameless_WidgetFlag 1889 dlg, iClob(promptLabel = new_LabelWidget(prompt, NULL)), frameless_WidgetFlag
@@ -1780,27 +1903,38 @@ iWidget *makeValueInput_Widget(iWidget *parent, const iString *initialValue, con
1780 } 1903 }
1781 setId_Widget(as_Widget(input), "input"); 1904 setId_Widget(as_Widget(input), "input");
1782 addChild_Widget(dlg, iClob(makePadding_Widget(gap_UI))); 1905 addChild_Widget(dlg, iClob(makePadding_Widget(gap_UI)));
1783 addChild_Widget(dlg, 1906 /* On mobile, the actions are laid out a bit differently: buttons on top, on opposite edges. */
1784 iClob(makeDialogButtons_Widget( 1907 iArray actions;
1785 (iMenuItem[]){ { "${cancel}", SDLK_ESCAPE, 0, "valueinput.cancel" }, 1908 init_Array(&actions, sizeof(iMenuItem));
1786 { acceptLabel, 1909 pushBack_Array(&actions, &(iMenuItem){ "${cancel}", SDLK_ESCAPE, 0, "valueinput.cancel" });
1910 if (deviceType_App() != desktop_AppDeviceType) {
1911 pushBack_Array(&actions, &(iMenuItem){ "---" });
1912 }
1913 pushBack_Array(&actions, &(iMenuItem){
1914 acceptLabel,
1787 SDLK_RETURN, 1915 SDLK_RETURN,
1788 acceptKeyMod_ReturnKeyBehavior(prefs_App()->returnKey), 1916 acceptKeyMod_ReturnKeyBehavior(prefs_App()->returnKey),
1789 "valueinput.accept" } }, 1917 "valueinput.accept"
1790 2))); 1918 });
1791// finalizeSheet_Mobile(dlg); 1919 addChildPos_Widget(dlg,
1920 iClob(makeDialogButtons_Widget(constData_Array(&actions),
1921 size_Array(&actions))),
1922 deviceType_App() != desktop_AppDeviceType ?
1923 front_WidgetAddPos : back_WidgetAddPos);
1924 deinit_Array(&actions);
1792 arrange_Widget(dlg); 1925 arrange_Widget(dlg);
1793 if (parent) { 1926 if (parent) {
1794 setFocus_Widget(as_Widget(input)); 1927 setFocus_Widget(as_Widget(input));
1795 } 1928 }
1796 /* Check that the top is in the safe area. */ { 1929 /* Check that the top is in the safe area. */
1797 int top = top_Rect(bounds_Widget(dlg)); 1930 if (deviceType_App() != desktop_AppDeviceType) {
1931 int top = top_Rect(boundsWithoutVisualOffset_Widget(dlg));
1798 int delta = top - top_Rect(safeRect_Root(dlg->root)); 1932 int delta = top - top_Rect(safeRect_Root(dlg->root));
1799 if (delta < 0) { 1933 if (delta < 0) {
1800 dlg->rect.pos.y -= delta; 1934 dlg->rect.pos.y -= delta;
1801 } 1935 }
1802 } 1936 }
1803 updateValueInputWidth_(dlg); 1937 updateValueInputSizing_(dlg);
1804 setupSheetTransition_Mobile(dlg, incoming_TransitionFlag | top_TransitionDir); 1938 setupSheetTransition_Mobile(dlg, incoming_TransitionFlag | top_TransitionDir);
1805 return dlg; 1939 return dlg;
1806} 1940}
@@ -1808,7 +1942,7 @@ iWidget *makeValueInput_Widget(iWidget *parent, const iString *initialValue, con
1808void updateValueInput_Widget(iWidget *d, const char *title, const char *prompt) { 1942void updateValueInput_Widget(iWidget *d, const char *title, const char *prompt) {
1809 setTextCStr_LabelWidget(findChild_Widget(d, "valueinput.title"), title); 1943 setTextCStr_LabelWidget(findChild_Widget(d, "valueinput.title"), title);
1810 setTextCStr_LabelWidget(findChild_Widget(d, "valueinput.prompt"), prompt); 1944 setTextCStr_LabelWidget(findChild_Widget(d, "valueinput.prompt"), prompt);
1811 updateValueInputWidth_(d); 1945 updateValueInputSizing_(d);
1812} 1946}
1813 1947
1814static void updateQuestionWidth_(iWidget *dlg) { 1948static void updateQuestionWidth_(iWidget *dlg) {
@@ -1894,9 +2028,7 @@ iWidget *makeQuestion_Widget(const char *title, const char *msg,
1894 } 2028 }
1895 iWidget *dlg = makeSheet_Widget(""); 2029 iWidget *dlg = makeSheet_Widget("");
1896 setCommandHandler_Widget(dlg, messageHandler_); 2030 setCommandHandler_Widget(dlg, messageHandler_);
1897 setId_Widget( 2031 addDialogTitle_(dlg, title, "question.title");
1898 addChildFlags_Widget(dlg, iClob(new_LabelWidget(title, NULL)), frameless_WidgetFlag),
1899 "question.title");
1900 iLabelWidget *msgLabel; 2032 iLabelWidget *msgLabel;
1901 setId_Widget(addChildFlags_Widget(dlg, 2033 setId_Widget(addChildFlags_Widget(dlg,
1902 iClob(msgLabel = new_LabelWidget(msg, NULL)), 2034 iClob(msgLabel = new_LabelWidget(msg, NULL)),
@@ -2021,9 +2153,11 @@ iWidget *appendTwoColumnTabPage_Widget(iWidget *tabs, const char *title, int sho
2021} 2153}
2022 2154
2023static void makeTwoColumnHeading_(const char *title, iWidget *headings, iWidget *values) { 2155static void makeTwoColumnHeading_(const char *title, iWidget *headings, iWidget *values) {
2024 addChildFlags_Widget(headings, 2156 setFont_LabelWidget(addChildFlags_Widget(headings,
2025 iClob(makeHeading_Widget(format_CStr(uiHeading_ColorEscape "%s", title))), 2157 iClob(makeHeading_Widget(
2026 ignoreForParentWidth_WidgetFlag); 2158 format_CStr(uiHeading_ColorEscape "%s", title))),
2159 ignoreForParentWidth_WidgetFlag),
2160 uiLabelBold_FontId);
2027 addChild_Widget(values, iClob(makeHeading_Widget(""))); 2161 addChild_Widget(values, iClob(makeHeading_Widget("")));
2028} 2162}
2029 2163
@@ -2071,28 +2205,6 @@ static const iArray *makeFontItems_(const char *id) {
2071 0, 2205 0,
2072 format_CStr("!font.set %s:%s", id, cstr_String(&spec->id)) }); 2206 format_CStr("!font.set %s:%s", id, cstr_String(&spec->id)) });
2073 } 2207 }
2074#if 0
2075 const struct {
2076 const char * name;
2077 enum iTextFont cfgId;
2078 } fonts[] = { { "Nunito", nunito_TextFont },
2079 { "Source Sans 3", sourceSans3_TextFont },
2080 { "Fira Sans", firaSans_TextFont },
2081 { "---", -1 },
2082 { "Literata", literata_TextFont },
2083 { "Tinos", tinos_TextFont },
2084 { "---", -1 },
2085 { "Iosevka", iosevka_TextFont } };
2086 iForIndices(i, fonts) {
2087 pushBack_Array(items,
2088 &(iMenuItem){ fonts[i].name,
2089 0,
2090 0,
2091 fonts[i].cfgId >= 0
2092 ? format_CStr("!%s.set arg:%d", id, fonts[i].cfgId)
2093 : NULL });
2094 }
2095#endif
2096 pushBack_Array(items, &(iMenuItem){ NULL }); /* terminator */ 2208 pushBack_Array(items, &(iMenuItem){ NULL }); /* terminator */
2097 return items; 2209 return items;
2098} 2210}
@@ -2108,13 +2220,6 @@ static void addFontButtons_(iWidget *parent, const char *id) {
2108 addChildFlags_Widget(parent, iClob(button), alignLeft_WidgetFlag); 2220 addChildFlags_Widget(parent, iClob(button), alignLeft_WidgetFlag);
2109} 2221}
2110 2222
2111#if 0
2112static int cmp_MenuItem_(const void *e1, const void *e2) {
2113 const iMenuItem *a = e1, *b = e2;
2114 return iCmpStr(a->label, b->label);
2115}
2116#endif
2117
2118void updatePreferencesLayout_Widget(iWidget *prefs) { 2223void updatePreferencesLayout_Widget(iWidget *prefs) {
2119 if (!prefs || deviceType_App() != desktop_AppDeviceType) { 2224 if (!prefs || deviceType_App() != desktop_AppDeviceType) {
2120 return; 2225 return;
@@ -2299,6 +2404,15 @@ iWidget *makePreferences_Widget(void) {
2299 format_CStr("returnkey.set arg:%d", acceptWithPrimaryMod_ReturnKeyBehavior) }, 2404 format_CStr("returnkey.set arg:%d", acceptWithPrimaryMod_ReturnKeyBehavior) },
2300 { NULL } 2405 { NULL }
2301 }; 2406 };
2407 iMenuItem toolbarActionItems[2][max_ToolbarAction];
2408 iZap(toolbarActionItems);
2409 for (int j = 0; j < 2; j++) {
2410 for (int i = 0; i < sidebar_ToolbarAction; i++) {
2411 toolbarActionItems[j][i].label = toolbarActions_Mobile[i].label;
2412 toolbarActionItems[j][i].command =
2413 format_CStr("toolbar.action.set arg:%d button:%d", i, j);
2414 }
2415 }
2302 iMenuItem docThemes[2][max_GmDocumentTheme + 1]; 2416 iMenuItem docThemes[2][max_GmDocumentTheme + 1];
2303 for (int i = 0; i < 2; ++i) { 2417 for (int i = 0; i < 2; ++i) {
2304 const iBool isDark = (i == 0); 2418 const iBool isDark = (i == 0);
@@ -2390,24 +2504,27 @@ iWidget *makePreferences_Widget(void) {
2390 }; 2504 };
2391 const iMenuItem uiPanelItems[] = { 2505 const iMenuItem uiPanelItems[] = {
2392 { "title id:heading.prefs.interface" }, 2506 { "title id:heading.prefs.interface" },
2393 { "dropdown device:1 id:prefs.returnkey", 0, 0, (const void *) returnKeyBehaviors }, 2507 { "dropdown device:0 id:prefs.returnkey", 0, 0, (const void *) returnKeyBehaviors },
2394 { "padding device:1" }, 2508 { "padding device:1" },
2395 { "toggle id:prefs.hoverlink" },
2396 { "toggle device:2 id:prefs.hidetoolbarscroll" }, 2509 { "toggle device:2 id:prefs.hidetoolbarscroll" },
2510 { "heading device:2 id:heading.prefs.toolbaractions" },
2511 { "dropdown device:2 id:prefs.toolbaraction1", 0, 0, (const void *) toolbarActionItems[0] },
2512 { "dropdown device:2 id:prefs.toolbaraction2", 0, 0, (const void *) toolbarActionItems[1] },
2397 { "heading id:heading.prefs.sizing" }, 2513 { "heading id:heading.prefs.sizing" },
2398 { "input id:prefs.uiscale maxlen:8" }, 2514 { "input id:prefs.uiscale maxlen:8" },
2399 { NULL } 2515 { NULL }
2400 }; 2516 };
2401 const iMenuItem colorPanelItems[] = { 2517 const iMenuItem colorPanelItems[] = {
2402 { "title id:heading.prefs.colors" }, 2518 { "title id:heading.prefs.colors" },
2403 { "heading id:heading.prefs.uitheme" }, 2519#if !defined (iPlatformAndroidMobile)
2404 { "toggle id:prefs.ostheme" }, 2520 { "toggle id:prefs.ostheme" },
2521#endif
2405 { "radio id:prefs.theme", 0, 0, (const void *) themeItems }, 2522 { "radio id:prefs.theme", 0, 0, (const void *) themeItems },
2406 { "radio id:prefs.accent", 0, 0, (const void *) accentItems }, 2523 { "radio id:prefs.accent", 0, 0, (const void *) accentItems },
2407 { "heading id:heading.prefs.pagecontent" }, 2524 { "heading id:heading.prefs.pagecontent" },
2408 { "dropdown id:prefs.doctheme.dark", 0, 0, (const void *) docThemes[0] }, 2525 { "dropdown id:prefs.doctheme.dark", 0, 0, (const void *) docThemes[0] },
2409 { "dropdown id:prefs.doctheme.light", 0, 0, (const void *) docThemes[1] }, 2526 { "dropdown id:prefs.doctheme.light", 0, 0, (const void *) docThemes[1] },
2410 { "radio id:prefs.saturation", 0, 0, (const void *) satItems }, 2527 { "radio horizontal:1 id:prefs.saturation", 0, 0, (const void *) satItems },
2411 { "padding" }, 2528 { "padding" },
2412 { "dropdown id:prefs.imagestyle", 0, 0, (const void *) imgStyles }, 2529 { "dropdown id:prefs.imagestyle", 0, 0, (const void *) imgStyles },
2413 { NULL } 2530 { NULL }
@@ -2418,18 +2535,23 @@ iWidget *makePreferences_Widget(void) {
2418 { "dropdown id:prefs.font.body", 0, 0, (const void *) constData_Array(makeFontItems_("body")) }, 2535 { "dropdown id:prefs.font.body", 0, 0, (const void *) constData_Array(makeFontItems_("body")) },
2419 { "dropdown id:prefs.font.mono", 0, 0, (const void *) constData_Array(makeFontItems_("mono")) }, 2536 { "dropdown id:prefs.font.mono", 0, 0, (const void *) constData_Array(makeFontItems_("mono")) },
2420 { "buttons id:prefs.mono", 0, 0, (const void *) monoFontItems }, 2537 { "buttons id:prefs.mono", 0, 0, (const void *) monoFontItems },
2421 { "dropdown id:prefs.font.monodoc", 0, 0, (const void *) constData_Array(makeFontItems_("monodoc")) },
2422 { "padding" }, 2538 { "padding" },
2423 { "toggle id:prefs.font.smooth" }, 2539 { "dropdown id:prefs.font.monodoc", 0, 0, (const void *) constData_Array(makeFontItems_("monodoc")) },
2424 { "padding" }, 2540 { "padding" },
2425 { "dropdown id:prefs.font.ui", 0, 0, (const void *) constData_Array(makeFontItems_("ui")) }, 2541 { "toggle id:prefs.font.warnmissing" },
2542 { "heading id:prefs.gemtext.ansi" },
2543 { "toggle id:prefs.gemtext.ansi.fg" },
2544 { "toggle id:prefs.gemtext.ansi.bg" },
2545 { "toggle id:prefs.gemtext.ansi.fontstyle" },
2546// { "padding" },
2547// { "dropdown id:prefs.font.ui", 0, 0, (const void *) constData_Array(makeFontItems_("ui")) },
2426 { "padding" }, 2548 { "padding" },
2427 { "button text:" fontpack_Icon " ${menu.fonts}", 0, 0, "!open url:about:fonts" }, 2549 { "button text:" fontpack_Icon " " uiTextAction_ColorEscape "${menu.fonts}", 0, 0, "!open url:about:fonts" },
2428 { NULL } 2550 { NULL }
2429 }; 2551 };
2430 const iMenuItem stylePanelItems[] = { 2552 const iMenuItem stylePanelItems[] = {
2431 { "title id:heading.prefs.style" }, 2553 { "title id:heading.prefs.style" },
2432 { "radio id:prefs.linewidth", 0, 0, (const void *) lineWidthItems }, 2554 { "radio horizontal:1 id:prefs.linewidth", 0, 0, (const void *) lineWidthItems },
2433 { "padding" }, 2555 { "padding" },
2434 { "input id:prefs.linespacing maxlen:5" }, 2556 { "input id:prefs.linespacing maxlen:5" },
2435 { "radio id:prefs.quoteicon", 0, 0, (const void *) quoteItems }, 2557 { "radio id:prefs.quoteicon", 0, 0, (const void *) quoteItems },
@@ -2458,6 +2580,7 @@ iWidget *makePreferences_Widget(void) {
2458 }; 2580 };
2459 const iMenuItem identityPanelItems[] = { 2581 const iMenuItem identityPanelItems[] = {
2460 { "title id:sidebar.identities" }, 2582 { "title id:sidebar.identities" },
2583 { "certlist" },
2461 { NULL } 2584 { NULL }
2462 }; 2585 };
2463 iString *aboutText = collectNew_String(); { 2586 iString *aboutText = collectNew_String(); {
@@ -2466,6 +2589,10 @@ iWidget *makePreferences_Widget(void) {
2466 appendFormat_String(aboutText, " (" LAGRANGE_IOS_VERSION ") %s" LAGRANGE_IOS_BUILD_DATE, 2589 appendFormat_String(aboutText, " (" LAGRANGE_IOS_VERSION ") %s" LAGRANGE_IOS_BUILD_DATE,
2467 escape_Color(uiTextDim_ColorId)); 2590 escape_Color(uiTextDim_ColorId));
2468#endif 2591#endif
2592#if defined (iPlatformAndroidMobile)
2593 appendFormat_String(aboutText, " (" LAGRANGE_ANDROID_VERSION ") %s" LAGRANGE_ANDROID_BUILD_DATE,
2594 escape_Color(uiTextDim_ColorId));
2595#endif
2469 } 2596 }
2470 const iMenuItem aboutPanelItems[] = { 2597 const iMenuItem aboutPanelItems[] = {
2471 { format_CStr("heading text:%s", cstr_String(aboutText)) }, 2598 { format_CStr("heading text:%s", cstr_String(aboutText)) },
@@ -2482,7 +2609,7 @@ iWidget *makePreferences_Widget(void) {
2482 { "title id:heading.settings" }, 2609 { "title id:heading.settings" },
2483 { "panel text:" gear_Icon " ${heading.prefs.general}", 0, 0, (const void *) generalPanelItems }, 2610 { "panel text:" gear_Icon " ${heading.prefs.general}", 0, 0, (const void *) generalPanelItems },
2484 { "panel icon:0x1f5a7 id:heading.prefs.network", 0, 0, (const void *) networkPanelItems }, 2611 { "panel icon:0x1f5a7 id:heading.prefs.network", 0, 0, (const void *) networkPanelItems },
2485 { "panel text:" person_Icon " ${sidebar.identities}", 0, 0, (const void *) identityPanelItems }, 2612 { "panel noscroll:1 text:" person_Icon " ${sidebar.identities}", 0, 0, (const void *) identityPanelItems },
2486 { "padding" }, 2613 { "padding" },
2487 { "panel icon:0x1f4f1 id:heading.prefs.interface", 0, 0, (const void *) uiPanelItems }, 2614 { "panel icon:0x1f4f1 id:heading.prefs.interface", 0, 0, (const void *) uiPanelItems },
2488 { "panel icon:0x1f3a8 id:heading.prefs.colors", 0, 0, (const void *) colorPanelItems }, 2615 { "panel icon:0x1f3a8 id:heading.prefs.colors", 0, 0, (const void *) colorPanelItems },
@@ -2498,9 +2625,7 @@ iWidget *makePreferences_Widget(void) {
2498 return dlg; 2625 return dlg;
2499 } 2626 }
2500 iWidget *dlg = makeSheet_Widget("prefs"); 2627 iWidget *dlg = makeSheet_Widget("prefs");
2501 addChildFlags_Widget(dlg, 2628 addDialogTitle_(dlg, "${heading.prefs}", NULL);
2502 iClob(new_LabelWidget(uiHeading_ColorEscape "${heading.prefs}", NULL)),
2503 frameless_WidgetFlag);
2504 iWidget *tabs = makeTabs_Widget(dlg); 2629 iWidget *tabs = makeTabs_Widget(dlg);
2505 setBackgroundColor_Widget(findChild_Widget(tabs, "tabs.buttons"), uiBackgroundSidebar_ColorId); 2630 setBackgroundColor_Widget(findChild_Widget(tabs, "tabs.buttons"), uiBackgroundSidebar_ColorId);
2506 setId_Widget(tabs, "prefs.tabs"); 2631 setId_Widget(tabs, "prefs.tabs");
@@ -2546,9 +2671,8 @@ iWidget *makePreferences_Widget(void) {
2546 } 2671 }
2547 /* User Interface. */ { 2672 /* User Interface. */ {
2548 appendTwoColumnTabPage_Widget(tabs, "${heading.prefs.interface}", '2', &headings, &values); 2673 appendTwoColumnTabPage_Widget(tabs, "${heading.prefs.interface}", '2', &headings, &values);
2549#if defined (LAGRANGE_ENABLE_CUSTOM_FRAME) 2674 addDialogToggle_(headings, values, "${prefs.animate}", "prefs.animate");
2550 addDialogToggle_(headings, values, "${prefs.customframe}", "prefs.customframe"); 2675 addDialogToggle_(headings, values, "${prefs.blink}", "prefs.blink");
2551#endif
2552 addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.returnkey}"))); 2676 addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.returnkey}")));
2553 /* Return key behaviors. */ { 2677 /* Return key behaviors. */ {
2554 iLabelWidget *returnKey = makeMenuButton_LabelWidget( 2678 iLabelWidget *returnKey = makeMenuButton_LabelWidget(
@@ -2562,7 +2686,9 @@ iWidget *makePreferences_Widget(void) {
2562 setId_Widget(addChildFlags_Widget(values, iClob(returnKey), alignLeft_WidgetFlag), 2686 setId_Widget(addChildFlags_Widget(values, iClob(returnKey), alignLeft_WidgetFlag),
2563 "prefs.returnkey"); 2687 "prefs.returnkey");
2564 } 2688 }
2565 addDialogToggle_(headings, values, "${prefs.animate}", "prefs.animate"); 2689#if defined (LAGRANGE_ENABLE_CUSTOM_FRAME)
2690 addDialogToggle_(headings, values, "${prefs.customframe}", "prefs.customframe");
2691#endif
2566 makeTwoColumnHeading_("${heading.prefs.scrolling}", headings, values); 2692 makeTwoColumnHeading_("${heading.prefs.scrolling}", headings, values);
2567 addDialogToggle_(headings, values, "${prefs.smoothscroll}", "prefs.smoothscroll"); 2693 addDialogToggle_(headings, values, "${prefs.smoothscroll}", "prefs.smoothscroll");
2568 /* Scroll speeds. */ { 2694 /* Scroll speeds. */ {
@@ -2827,7 +2953,7 @@ static iBool isBookmarkFolder_(void *context, const iBookmark *bm) {
2827 return isFolder_Bookmark(bm); 2953 return isFolder_Bookmark(bm);
2828} 2954}
2829 2955
2830static const iArray *makeBookmarkFolderItems_(void) { 2956static const iArray *makeBookmarkFolderItems_(iBool withNullTerminator) {
2831 iArray *folders = new_Array(sizeof(iMenuItem)); 2957 iArray *folders = new_Array(sizeof(iMenuItem));
2832 pushBack_Array(folders, &(iMenuItem){ "\u2014", 0, 0, "dlg.bookmark.setfolder arg:0" }); 2958 pushBack_Array(folders, &(iMenuItem){ "\u2014", 0, 0, "dlg.bookmark.setfolder arg:0" });
2833 iConstForEach( 2959 iConstForEach(
@@ -2848,6 +2974,9 @@ static const iArray *makeBookmarkFolderItems_(void) {
2848 0, 2974 0,
2849 format_CStr("dlg.bookmark.setfolder arg:%u", id_Bookmark(bm)) }); 2975 format_CStr("dlg.bookmark.setfolder arg:%u", id_Bookmark(bm)) });
2850 } 2976 }
2977 if (withNullTerminator) {
2978 pushBack_Array(folders, &(iMenuItem){ NULL });
2979 }
2851 return collect_Array(folders); 2980 return collect_Array(folders);
2852} 2981}
2853 2982
@@ -2856,40 +2985,37 @@ iWidget *makeBookmarkEditor_Widget(void) {
2856 { "${cancel}", 0, 0, "bmed.cancel" }, 2985 { "${cancel}", 0, 0, "bmed.cancel" },
2857 { uiTextCaution_ColorEscape "${dlg.bookmark.save}", SDLK_RETURN, KMOD_PRIMARY, "bmed.accept" } 2986 { uiTextCaution_ColorEscape "${dlg.bookmark.save}", SDLK_RETURN, KMOD_PRIMARY, "bmed.accept" }
2858 }; 2987 };
2988 iWidget *dlg = NULL;
2859 if (isUsingPanelLayout_Mobile()) { 2989 if (isUsingPanelLayout_Mobile()) {
2990 const iArray *folderItems = makeBookmarkFolderItems_(iTrue);
2860 const iMenuItem items[] = { 2991 const iMenuItem items[] = {
2861 { "title id:bmed.heading text:${heading.bookmark.edit}" }, 2992 { "title id:bmed.heading text:${heading.bookmark.edit}" },
2862 { "heading id:dlg.bookmark.url" }, 2993 { "heading id:dlg.bookmark.url" },
2863 { "input id:bmed.url url:1 noheading:1" }, 2994 { "input id:bmed.url url:1 noheading:1" },
2864 { "padding" }, 2995 { "padding" },
2865 { "input id:bmed.title text:${dlg.bookmark.title}" }, 2996 { "input id:bmed.title text:${dlg.bookmark.title}" },
2866 { "input id:bmed.tags text:${dlg.bookmark.tags}" }, 2997 { "dropdown id:bmed.folder text:${dlg.bookmark.folder}", 0, 0, (const void *) constData_Array(folderItems) },
2998 { "padding" },
2867 { "input id:bmed.icon maxlen:1 text:${dlg.bookmark.icon}" }, 2999 { "input id:bmed.icon maxlen:1 text:${dlg.bookmark.icon}" },
3000 { "input id:bmed.tags text:${dlg.bookmark.tags}" },
2868 { "heading text:${heading.bookmark.tags}" }, 3001 { "heading text:${heading.bookmark.tags}" },
2869 { "toggle id:bmed.tag.home text:${bookmark.tag.home}" }, 3002 { "toggle id:bmed.tag.home text:${bookmark.tag.home}" },
2870 { "toggle id:bmed.tag.remote text:${bookmark.tag.remote}" }, 3003 { "toggle id:bmed.tag.remote text:${bookmark.tag.remote}" },
2871 { "toggle id:bmed.tag.linksplit text:${bookmark.tag.linksplit}" }, 3004 { "toggle id:bmed.tag.linksplit text:${bookmark.tag.linksplit}" },
2872 { NULL } 3005 { NULL }
2873 }; 3006 };
2874 iWidget *dlg = makePanels_Mobile("bmed", items, actions, iElemCount(actions)); 3007 dlg = makePanels_Mobile("bmed", items, actions, iElemCount(actions));
2875 setupSheetTransition_Mobile(dlg, iTrue); 3008 setupSheetTransition_Mobile(dlg, iTrue);
2876 return dlg;
2877 } 3009 }
2878 iWidget *dlg = makeSheet_Widget("bmed"); 3010 else {
2879 setId_Widget(addChildFlags_Widget( 3011 dlg = makeSheet_Widget("bmed");
2880 dlg, 3012 addDialogTitle_(dlg, "${heading.bookmark.edit}", "bmed.heading");
2881 iClob(new_LabelWidget(uiHeading_ColorEscape "${heading.bookmark.edit}", NULL)),
2882 frameless_WidgetFlag),
2883 "bmed.heading");
2884 iWidget *headings, *values; 3013 iWidget *headings, *values;
2885 addChild_Widget(dlg, iClob(makeTwoColumns_Widget(&headings, &values))); 3014 addChild_Widget(dlg, iClob(makeTwoColumns_Widget(&headings, &values)));
2886 iInputWidget *inputs[4]; 3015 iInputWidget *inputs[4];
2887 addDialogInputWithHeading_(headings, values, "${dlg.bookmark.title}", "bmed.title", iClob(inputs[0] = new_InputWidget(0)));
2888 addDialogInputWithHeading_(headings, values, "${dlg.bookmark.url}", "bmed.url", iClob(inputs[1] = new_InputWidget(0)));
2889 setUrlContent_InputWidget(inputs[1], iTrue);
2890 /* Folder to add to. */ { 3016 /* Folder to add to. */ {
2891 addChild_Widget(headings, iClob(makeHeading_Widget("${dlg.bookmark.folder}"))); 3017 addChild_Widget(headings, iClob(makeHeading_Widget("${dlg.bookmark.folder}")));
2892 const iArray *folderItems = makeBookmarkFolderItems_(); 3018 const iArray *folderItems = makeBookmarkFolderItems_(iFalse);
2893 iLabelWidget *folderButton; 3019 iLabelWidget *folderButton;
2894 setId_Widget(addChildFlags_Widget(values, 3020 setId_Widget(addChildFlags_Widget(values,
2895 iClob(folderButton = makeMenuButton_LabelWidget( 3021 iClob(folderButton = makeMenuButton_LabelWidget(
@@ -2897,11 +3023,10 @@ iWidget *makeBookmarkEditor_Widget(void) {
2897 constData_Array(folderItems), 3023 constData_Array(folderItems),
2898 size_Array(folderItems))), alignLeft_WidgetFlag), 3024 size_Array(folderItems))), alignLeft_WidgetFlag),
2899 "bmed.folder"); 3025 "bmed.folder");
2900 const uint32_t recentFolderId = recentFolder_Bookmarks(bookmarks_App());
2901 updateDropdownSelection_LabelWidget(
2902 folderButton, format_CStr(" arg:%u", recentFolderId));
2903 setUserData_Object(folderButton, get_Bookmarks(bookmarks_App(), recentFolderId));
2904 } 3026 }
3027 addDialogInputWithHeading_(headings, values, "${dlg.bookmark.title}", "bmed.title", iClob(inputs[0] = new_InputWidget(0)));
3028 addDialogInputWithHeading_(headings, values, "${dlg.bookmark.url}", "bmed.url", iClob(inputs[1] = new_InputWidget(0)));
3029 setUrlContent_InputWidget(inputs[1], iTrue);
2905 addDialogInputWithHeading_(headings, values, "${dlg.bookmark.tags}", "bmed.tags", iClob(inputs[2] = new_InputWidget(0))); 3030 addDialogInputWithHeading_(headings, values, "${dlg.bookmark.tags}", "bmed.tags", iClob(inputs[2] = new_InputWidget(0)));
2906 addDialogInputWithHeading_(headings, values, "${dlg.bookmark.icon}", "bmed.icon", iClob(inputs[3] = new_InputWidget(1))); 3031 addDialogInputWithHeading_(headings, values, "${dlg.bookmark.icon}", "bmed.icon", iClob(inputs[3] = new_InputWidget(1)));
2907 /* Buttons for special tags. */ 3032 /* Buttons for special tags. */
@@ -2921,6 +3046,12 @@ iWidget *makeBookmarkEditor_Widget(void) {
2921 addChild_Widget(dlg, iClob(makeDialogButtons_Widget(actions, iElemCount(actions)))); 3046 addChild_Widget(dlg, iClob(makeDialogButtons_Widget(actions, iElemCount(actions))));
2922 addChild_Widget(get_Root()->widget, iClob(dlg)); 3047 addChild_Widget(get_Root()->widget, iClob(dlg));
2923 setupSheetTransition_Mobile(dlg, iTrue); 3048 setupSheetTransition_Mobile(dlg, iTrue);
3049 }
3050 /* Use a recently accessed folder as the default. */
3051 const uint32_t recentFolderId = recentFolder_Bookmarks(bookmarks_App());
3052 iLabelWidget *folderDrop = findChild_Widget(dlg, "bmed.folder");
3053 updateDropdownSelection_LabelWidget(folderDrop, format_CStr(" arg:%u", recentFolderId));
3054 setUserData_Object(folderDrop, get_Bookmarks(bookmarks_App(), recentFolderId));
2924 return dlg; 3055 return dlg;
2925} 3056}
2926 3057
@@ -2945,16 +3076,16 @@ static iBool handleBookmarkCreationCommands_SidebarWidget_(iWidget *editor, cons
2945 const uint32_t id = add_Bookmarks(bookmarks_App(), url, title, tags, first_String(icon)); 3076 const uint32_t id = add_Bookmarks(bookmarks_App(), url, title, tags, first_String(icon));
2946 iBookmark * bm = get_Bookmarks(bookmarks_App(), id); 3077 iBookmark * bm = get_Bookmarks(bookmarks_App(), id);
2947 if (!isEmpty_String(icon)) { 3078 if (!isEmpty_String(icon)) {
2948 addTagIfMissing_Bookmark(bm, userIcon_BookmarkTag); 3079 bm->flags |= userIcon_BookmarkFlag;
2949 } 3080 }
2950 if (isSelected_Widget(findChild_Widget(editor, "bmed.tag.home"))) { 3081 if (isSelected_Widget(findChild_Widget(editor, "bmed.tag.home"))) {
2951 addTag_Bookmark(bm, homepage_BookmarkTag); 3082 bm->flags |= homepage_BookmarkFlag;
2952 } 3083 }
2953 if (isSelected_Widget(findChild_Widget(editor, "bmed.tag.remote"))) { 3084 if (isSelected_Widget(findChild_Widget(editor, "bmed.tag.remote"))) {
2954 addTag_Bookmark(bm, remoteSource_BookmarkTag); 3085 bm->flags |= remoteSource_BookmarkFlag;
2955 } 3086 }
2956 if (isSelected_Widget(findChild_Widget(editor, "bmed.tag.linksplit"))) { 3087 if (isSelected_Widget(findChild_Widget(editor, "bmed.tag.linksplit"))) {
2957 addTag_Bookmark(bm, linkSplit_BookmarkTag); 3088 bm->flags |= linkSplit_BookmarkFlag;
2958 } 3089 }
2959 bm->parentId = folder ? id_Bookmark(folder) : 0; 3090 bm->parentId = folder ? id_Bookmark(folder) : 0;
2960 setRecentFolder_Bookmarks(bookmarks_App(), bm->parentId); 3091 setRecentFolder_Bookmarks(bookmarks_App(), bm->parentId);
@@ -3020,9 +3151,9 @@ static iBool handleFeedSettingCommands_(iWidget *dlg, const char *cmd) {
3020 iBookmark *bm = get_Bookmarks(bookmarks_App(), id); 3151 iBookmark *bm = get_Bookmarks(bookmarks_App(), id);
3021 iAssert(bm); 3152 iAssert(bm);
3022 set_String(&bm->title, feedTitle); 3153 set_String(&bm->title, feedTitle);
3023 addOrRemoveTag_Bookmark(bm, subscribed_BookmarkTag, iTrue); 3154 bm->flags |= subscribed_BookmarkFlag;
3024 addOrRemoveTag_Bookmark(bm, headings_BookmarkTag, headings); 3155 iChangeFlags(bm->flags, headings_BookmarkFlag, headings);
3025 addOrRemoveTag_Bookmark(bm, ignoreWeb_BookmarkTag, ignoreWeb); 3156 iChangeFlags(bm->flags, ignoreWeb_BookmarkFlag, ignoreWeb);
3026 postCommand_App("bookmarks.changed"); 3157 postCommand_App("bookmarks.changed");
3027 setupSheetTransition_Mobile(dlg, iFalse); 3158 setupSheetTransition_Mobile(dlg, iFalse);
3028 destroy_Widget(dlg); 3159 destroy_Widget(dlg);
@@ -3051,15 +3182,14 @@ iWidget *makeFeedSettings_Widget(uint32_t bookmarkId) {
3051 { format_CStr("title id:feedcfg.heading text:%s", headingText) }, 3182 { format_CStr("title id:feedcfg.heading text:%s", headingText) },
3052 { "input id:feedcfg.title text:${dlg.feed.title}" }, 3183 { "input id:feedcfg.title text:${dlg.feed.title}" },
3053 { "radio id:dlg.feed.entrytype", 0, 0, (const void *) typeItems }, 3184 { "radio id:dlg.feed.entrytype", 0, 0, (const void *) typeItems },
3185 { "padding" },
3054 { "toggle id:feedcfg.ignoreweb text:${dlg.feed.ignoreweb}" }, 3186 { "toggle id:feedcfg.ignoreweb text:${dlg.feed.ignoreweb}" },
3055 { NULL } 3187 { NULL }
3056 }, actions, iElemCount(actions)); 3188 }, actions, iElemCount(actions));
3057 } 3189 }
3058 else { 3190 else {
3059 dlg = makeSheet_Widget("feedcfg"); 3191 dlg = makeSheet_Widget("feedcfg");
3060 setId_Widget( 3192 addDialogTitle_(dlg, headingText, "feedcfg.heading");
3061 addChildFlags_Widget(dlg, iClob(new_LabelWidget(headingText, NULL)), frameless_WidgetFlag),
3062 "feedcfg.heading");
3063 iWidget *headings, *values; 3193 iWidget *headings, *values;
3064 addChild_Widget(dlg, iClob(makeTwoColumns_Widget(&headings, &values))); 3194 addChild_Widget(dlg, iClob(makeTwoColumns_Widget(&headings, &values)));
3065 iInputWidget *input = new_InputWidget(0); 3195 iInputWidget *input = new_InputWidget(0);
@@ -3085,13 +3215,13 @@ iWidget *makeFeedSettings_Widget(uint32_t bookmarkId) {
3085 setText_InputWidget(findChild_Widget(dlg, "feedcfg.title"), 3215 setText_InputWidget(findChild_Widget(dlg, "feedcfg.title"),
3086 bm ? &bm->title : feedTitle_DocumentWidget(document_App())); 3216 bm ? &bm->title : feedTitle_DocumentWidget(document_App()));
3087 setFlags_Widget(findChild_Widget(dlg, 3217 setFlags_Widget(findChild_Widget(dlg,
3088 hasTag_Bookmark(bm, headings_BookmarkTag) 3218 bm && bm->flags & headings_BookmarkFlag
3089 ? "feedcfg.type.headings" 3219 ? "feedcfg.type.headings"
3090 : "feedcfg.type.gemini"), 3220 : "feedcfg.type.gemini"),
3091 selected_WidgetFlag, 3221 selected_WidgetFlag,
3092 iTrue); 3222 iTrue);
3093 setToggle_Widget(findChild_Widget(dlg, "feedcfg.ignoreweb"), 3223 setToggle_Widget(findChild_Widget(dlg, "feedcfg.ignoreweb"),
3094 hasTag_Bookmark(bm, ignoreWeb_BookmarkTag)); 3224 bm && bm->flags & ignoreWeb_BookmarkFlag);
3095 setCommandHandler_Widget(dlg, handleFeedSettingCommands_); 3225 setCommandHandler_Widget(dlg, handleFeedSettingCommands_);
3096 } 3226 }
3097 setupSheetTransition_Mobile(dlg, incoming_TransitionFlag); 3227 setupSheetTransition_Mobile(dlg, incoming_TransitionFlag);
@@ -3138,11 +3268,7 @@ iWidget *makeIdentityCreation_Widget(void) {
3138 } 3268 }
3139 else { 3269 else {
3140 dlg = makeSheet_Widget("ident"); 3270 dlg = makeSheet_Widget("ident");
3141 setId_Widget(addChildFlags_Widget( 3271 addDialogTitle_(dlg, "${heading.newident}", "ident.heading");
3142 dlg,
3143 iClob(new_LabelWidget(uiHeading_ColorEscape "${heading.newident}", NULL)),
3144 frameless_WidgetFlag),
3145 "ident.heading");
3146 iWidget *page = new_Widget(); 3272 iWidget *page = new_Widget();
3147 addChildFlags_Widget( 3273 addChildFlags_Widget(
3148 dlg, iClob(new_LabelWidget("${dlg.newident.rsa.selfsign}", NULL)), frameless_WidgetFlag); 3274 dlg, iClob(new_LabelWidget("${dlg.newident.rsa.selfsign}", NULL)), frameless_WidgetFlag);
@@ -3226,7 +3352,7 @@ static const iMenuItem languages[] = {
3226static iBool translationHandler_(iWidget *dlg, const char *cmd) { 3352static iBool translationHandler_(iWidget *dlg, const char *cmd) {
3227 iUnused(dlg); 3353 iUnused(dlg);
3228 if (equal_Command(cmd, "xlt.lang")) { 3354 if (equal_Command(cmd, "xlt.lang")) {
3229 const iMenuItem *langItem = &languages[languageIndex_CStr(cstr_Rangecc(range_Command(cmd, "id")))]; 3355 const iMenuItem *langItem = &languages[languageIndex_CStr(cstr_Command(cmd, "id"))];
3230 iWidget *widget = pointer_Command(cmd); 3356 iWidget *widget = pointer_Command(cmd);
3231 iLabelWidget *drop; 3357 iLabelWidget *drop;
3232 if (flags_Widget(widget) & nativeMenu_WidgetFlag) { 3358 if (flags_Widget(widget) & nativeMenu_WidgetFlag) {
@@ -3246,7 +3372,7 @@ const char *languageId_String(const iString *menuItemLabel) {
3246 iForIndices(i, languages) { 3372 iForIndices(i, languages) {
3247 if (!languages[i].label) break; 3373 if (!languages[i].label) break;
3248 if (!cmp_String(menuItemLabel, translateCStr_Lang(languages[i].label))) { 3374 if (!cmp_String(menuItemLabel, translateCStr_Lang(languages[i].label))) {
3249 return cstr_Rangecc(range_Command(languages[i].command, "id")); 3375 return cstr_Command(languages[i].command, "id");
3250 } 3376 }
3251 } 3377 }
3252 return ""; 3378 return "";
@@ -3280,10 +3406,7 @@ iWidget *makeTranslation_Widget(iWidget *parent) {
3280 dlg = makeSheet_Widget("xlt"); 3406 dlg = makeSheet_Widget("xlt");
3281 setFlags_Widget(dlg, keepOnTop_WidgetFlag, iFalse); 3407 setFlags_Widget(dlg, keepOnTop_WidgetFlag, iFalse);
3282 dlg->minSize.x = 70 * gap_UI; 3408 dlg->minSize.x = 70 * gap_UI;
3283 addChildFlags_Widget( 3409 addDialogTitle_(dlg, "${heading.translate}", NULL);
3284 dlg,
3285 iClob(new_LabelWidget(uiHeading_ColorEscape "${heading.translate}", NULL)),
3286 frameless_WidgetFlag);
3287 addChild_Widget(dlg, iClob(makePadding_Widget(lineHeight_Text(uiLabel_FontId)))); 3410 addChild_Widget(dlg, iClob(makePadding_Widget(lineHeight_Text(uiLabel_FontId))));
3288 iWidget *headings, *values; 3411 iWidget *headings, *values;
3289 iWidget *page; 3412 iWidget *page;
@@ -3316,20 +3439,13 @@ iWidget *makeTranslation_Widget(iWidget *parent) {
3316 addChild_Widget(dlg, iClob(makeDialogButtons_Widget(actions, iElemCount(actions)))); 3439 addChild_Widget(dlg, iClob(makeDialogButtons_Widget(actions, iElemCount(actions))));
3317 addChild_Widget(parent, iClob(dlg)); 3440 addChild_Widget(parent, iClob(dlg));
3318 arrange_Widget(dlg); 3441 arrange_Widget(dlg);
3442 arrange_Widget(dlg); /* TODO: Augh, another layout bug: two arranges required. */
3319 } 3443 }
3320 /* Update choices. */ 3444 /* Update choices. */
3321 updateDropdownSelection_LabelWidget(findChild_Widget(dlg, "xlt.from"), 3445 updateDropdownSelection_LabelWidget(findChild_Widget(dlg, "xlt.from"),
3322 languages[prefs_App()->langFrom].command); 3446 languages[prefs_App()->langFrom].command);
3323 updateDropdownSelection_LabelWidget(findChild_Widget(dlg, "xlt.to"), 3447 updateDropdownSelection_LabelWidget(findChild_Widget(dlg, "xlt.to"),
3324 languages[prefs_App()->langTo].command); 3448 languages[prefs_App()->langTo].command);
3325// updateText_LabelWidget(
3326// findChild_Widget(dlg, "xlt.from"),
3327// text_LabelWidget(child_Widget(findChild_Widget(findChild_Widget(dlg, "xlt.from"), "menu"),
3328// prefs_App()->langFrom)));
3329// updateText_LabelWidget(
3330// findChild_Widget(dlg, "xlt.to"),
3331// text_LabelWidget(child_Widget(findChild_Widget(findChild_Widget(dlg, "xlt.to"), "menu"),
3332// prefs_App()->langTo)));
3333 setCommandHandler_Widget(dlg, translationHandler_); 3449 setCommandHandler_Widget(dlg, translationHandler_);
3334 setupSheetTransition_Mobile(dlg, iTrue); 3450 setupSheetTransition_Mobile(dlg, iTrue);
3335 return dlg; 3451 return dlg;
diff --git a/src/ui/util.h b/src/ui/util.h
index d13d751b..98ce784c 100644
--- a/src/ui/util.h
+++ b/src/ui/util.h
@@ -47,16 +47,31 @@ iLocalDef iBool isMetricsChange_UserEvent(const SDL_Event *d) {
47} 47}
48 48
49enum iMouseWheelFlag { 49enum iMouseWheelFlag {
50 perPixel_MouseWheelFlag = iBit(9), /* e.g., trackpad or finger scroll; applied to `direction` */ 50 /* Note: A future version of SDL may support per-pixel scrolling, but 2.0.x doesn't. */
51 perPixel_MouseWheelFlag = iBit(9), /* e.g., trackpad or finger scroll; applied to `direction` */
52 inertia_MouseWheelFlag = iBit(10),
53 scrollFinished_MouseWheelFlag = iBit(11),
51}; 54};
52 55
53/* Note: A future version of SDL may support per-pixel scrolling, but 2.0.x doesn't. */
54iLocalDef void setPerPixel_MouseWheelEvent(SDL_MouseWheelEvent *ev, iBool set) { 56iLocalDef void setPerPixel_MouseWheelEvent(SDL_MouseWheelEvent *ev, iBool set) {
55 iChangeFlags(ev->direction, perPixel_MouseWheelFlag, set); 57 iChangeFlags(ev->direction, perPixel_MouseWheelFlag, set);
56} 58}
59iLocalDef void setInertia_MouseWheelEvent(SDL_MouseWheelEvent *ev, iBool set) {
60 iChangeFlags(ev->direction, inertia_MouseWheelFlag, set);
61}
62iLocalDef void setScrollFinished_MouseWheelEvent(SDL_MouseWheelEvent *ev, iBool set) {
63 iChangeFlags(ev->direction, scrollFinished_MouseWheelFlag, set);
64}
65
57iLocalDef iBool isPerPixel_MouseWheelEvent(const SDL_MouseWheelEvent *ev) { 66iLocalDef iBool isPerPixel_MouseWheelEvent(const SDL_MouseWheelEvent *ev) {
58 return (ev->direction & perPixel_MouseWheelFlag) != 0; 67 return (ev->direction & perPixel_MouseWheelFlag) != 0;
59} 68}
69iLocalDef iBool isInertia_MouseWheelEvent(const SDL_MouseWheelEvent *ev) {
70 return (ev->direction & inertia_MouseWheelFlag) != 0;
71}
72iLocalDef iBool isScrollFinished_MouseWheelEvent(const SDL_MouseWheelEvent *ev) {
73 return (ev->direction & scrollFinished_MouseWheelFlag) != 0;
74}
60 75
61iInt2 coord_MouseWheelEvent (const SDL_MouseWheelEvent *); 76iInt2 coord_MouseWheelEvent (const SDL_MouseWheelEvent *);
62 77
@@ -179,11 +194,18 @@ iDeclareType(SmoothScroll)
179 194
180typedef void (*iSmoothScrollNotifyFunc)(iAnyObject *, int offset, uint32_t span); 195typedef void (*iSmoothScrollNotifyFunc)(iAnyObject *, int offset, uint32_t span);
181 196
197enum iSmoothScrollFlags {
198 pullDownAction_SmoothScrollFlag = iBit(1),
199 pullUpAction_SmoothScrollFlag = iBit(2),
200};
201
182struct Impl_SmoothScroll { 202struct Impl_SmoothScroll {
183 iAnim pos; 203 iAnim pos;
184 int max; 204 int max;
185 int overscroll; 205 int overscroll;
186 iWidget *widget; 206 iWidget *widget;
207 int flags;
208 int pullActionTriggered;
187 iSmoothScrollNotifyFunc notify; 209 iSmoothScrollNotifyFunc notify;
188}; 210};
189 211
@@ -197,6 +219,7 @@ iBool processEvent_SmoothScroll (iSmoothScroll *, const SDL_Event *ev);
197 219
198float pos_SmoothScroll (const iSmoothScroll *); 220float pos_SmoothScroll (const iSmoothScroll *);
199iBool isFinished_SmoothScroll (const iSmoothScroll *); 221iBool isFinished_SmoothScroll (const iSmoothScroll *);
222float pullActionPos_SmoothScroll (const iSmoothScroll *); /* 0...1 */
200 223
201/*-----------------------------------------------------------------------------------------------*/ 224/*-----------------------------------------------------------------------------------------------*/
202 225
@@ -249,7 +272,8 @@ void setMenuItemDisabled_Widget (iWidget *menu, const char *comm
249void setMenuItemDisabledByIndex_Widget(iWidget *menu, size_t index, iBool disable); 272void setMenuItemDisabledByIndex_Widget(iWidget *menu, size_t index, iBool disable);
250void setMenuItemLabel_Widget (iWidget *menu, const char *command, const char *newLabel); 273void setMenuItemLabel_Widget (iWidget *menu, const char *command, const char *newLabel);
251void setMenuItemLabelByIndex_Widget (iWidget *menu, size_t index, const char *newLabel); 274void setMenuItemLabelByIndex_Widget (iWidget *menu, size_t index, const char *newLabel);
252void setNativeMenuItems_Widget (iWidget *, const iMenuItem *items, size_t n); 275void setNativeMenuItems_Widget (iWidget *menu, const iMenuItem *items, size_t n);
276iWidget * findUserData_Widget (iWidget *, void *userData);
253 277
254int checkContextMenu_Widget (iWidget *, const SDL_Event *ev); /* see macro below */ 278int checkContextMenu_Widget (iWidget *, const SDL_Event *ev); /* see macro below */
255 279
@@ -293,6 +317,7 @@ iWidget * makeTwoColumns_Widget (iWidget **headings, iWidget **values);
293 317
294iLabelWidget *dialogAcceptButton_Widget (const iWidget *); 318iLabelWidget *dialogAcceptButton_Widget (const iWidget *);
295 319
320iLabelWidget *addDialogTitle_Widget (iWidget *, const char *text, const char *idOrNull);
296iInputWidget *addTwoColumnDialogInputField_Widget(iWidget *headings, iWidget *values, 321iInputWidget *addTwoColumnDialogInputField_Widget(iWidget *headings, iWidget *values,
297 const char *labelText, const char *inputId, 322 const char *labelText, const char *inputId,
298 iInputWidget *input); 323 iInputWidget *input);
diff --git a/src/ui/visbuf.c b/src/ui/visbuf.c
index 8f7a4c46..0097b12a 100644
--- a/src/ui/visbuf.c
+++ b/src/ui/visbuf.c
@@ -21,6 +21,7 @@ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
21SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ 21SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22 22
23#include "visbuf.h" 23#include "visbuf.h"
24#include "paint.h"
24#include "window.h" 25#include "window.h"
25#include "util.h" 26#include "util.h"
26 27
@@ -224,6 +225,8 @@ void draw_VisBuf(const iVisBuf *d, const iInt2 topLeft, const iRangei yClipBound
224 continue; /* Outside the clipping area. */ 225 continue; /* Outside the clipping area. */
225#endif 226#endif
226 } 227 }
228 dst.x += origin_Paint.x;
229 dst.y += origin_Paint.y;
227#if defined (DEBUG_SCALE) 230#if defined (DEBUG_SCALE)
228 dst.w *= DEBUG_SCALE; 231 dst.w *= DEBUG_SCALE;
229 dst.h *= DEBUG_SCALE; 232 dst.h *= DEBUG_SCALE;
diff --git a/src/ui/widget.c b/src/ui/widget.c
index cedda461..9f67b1c7 100644
--- a/src/ui/widget.c
+++ b/src/ui/widget.c
@@ -31,6 +31,8 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
31#include "util.h" 31#include "util.h"
32#include "window.h" 32#include "window.h"
33 33
34#include "labelwidget.h"
35
34#include <the_Foundation/ptrarray.h> 36#include <the_Foundation/ptrarray.h>
35#include <the_Foundation/ptrset.h> 37#include <the_Foundation/ptrset.h>
36#include <SDL_mouse.h> 38#include <SDL_mouse.h>
@@ -122,7 +124,9 @@ void init_Widget(iWidget *d) {
122 init_String(&d->id); 124 init_String(&d->id);
123 d->root = get_Root(); /* never changes after this */ 125 d->root = get_Root(); /* never changes after this */
124 d->flags = 0; 126 d->flags = 0;
127 d->flags2 = 0;
125 d->rect = zero_Rect(); 128 d->rect = zero_Rect();
129 d->oldSize = zero_I2();
126 d->minSize = zero_I2(); 130 d->minSize = zero_I2();
127 d->sizeRef = NULL; 131 d->sizeRef = NULL;
128 d->offsetRef = NULL; 132 d->offsetRef = NULL;
@@ -139,8 +143,11 @@ void init_Widget(iWidget *d) {
139static void visualOffsetAnimation_Widget_(void *ptr) { 143static void visualOffsetAnimation_Widget_(void *ptr) {
140 iWidget *d = ptr; 144 iWidget *d = ptr;
141 postRefresh_App(); 145 postRefresh_App();
146 d->root->didAnimateVisualOffsets = iTrue;
147// printf("'%s' visoffanim: fin:%d val:%f\n", cstr_String(&d->id),
148// isFinished_Anim(&d->visualOffset), value_Anim(&d->visualOffset)); fflush(stdout);
142 if (!isFinished_Anim(&d->visualOffset)) { 149 if (!isFinished_Anim(&d->visualOffset)) {
143 addTicker_App(visualOffsetAnimation_Widget_, ptr); 150 addTickerRoot_App(visualOffsetAnimation_Widget_, d->root, ptr);
144 } 151 }
145 else { 152 else {
146 d->flags &= ~visualOffset_WidgetFlag; 153 d->flags &= ~visualOffset_WidgetFlag;
@@ -269,10 +276,12 @@ void setMinSize_Widget(iWidget *d, iInt2 minSize) {
269} 276}
270 277
271void setPadding_Widget(iWidget *d, int left, int top, int right, int bottom) { 278void setPadding_Widget(iWidget *d, int left, int top, int right, int bottom) {
272 d->padding[0] = left; 279 if (d) {
273 d->padding[1] = top; 280 d->padding[0] = left;
274 d->padding[2] = right; 281 d->padding[1] = top;
275 d->padding[3] = bottom; 282 d->padding[2] = right;
283 d->padding[3] = bottom;
284 }
276} 285}
277 286
278iWidget *root_Widget(const iWidget *d) { 287iWidget *root_Widget(const iWidget *d) {
@@ -414,9 +423,10 @@ static iBool setWidth_Widget_(iWidget *d, int width) {
414 if (d->rect.size.x != width) { 423 if (d->rect.size.x != width) {
415 d->rect.size.x = width; 424 d->rect.size.x = width;
416 TRACE(d, "width has changed to %d", width); 425 TRACE(d, "width has changed to %d", width);
417 if (class_Widget(d)->sizeChanged) { 426// if (~d->flags2 & undefinedWidth_WidgetFlag2 && class_Widget(d)->sizeChanged) {
418 class_Widget(d)->sizeChanged(d); 427// class_Widget(d)->sizeChanged(d);
419 } 428// }
429// d->flags2 &= ~undefinedWidth_WidgetFlag2;
420 return iTrue; 430 return iTrue;
421 } 431 }
422 } 432 }
@@ -437,9 +447,10 @@ static iBool setHeight_Widget_(iWidget *d, int height) {
437 if (d->rect.size.y != height) { 447 if (d->rect.size.y != height) {
438 d->rect.size.y = height; 448 d->rect.size.y = height;
439 TRACE(d, "height has changed to %d", height); 449 TRACE(d, "height has changed to %d", height);
440 if (class_Widget(d)->sizeChanged) { 450// if (~d->flags2 & undefinedHeight_WidgetFlag2 && class_Widget(d)->sizeChanged) {
441 class_Widget(d)->sizeChanged(d); 451// class_Widget(d)->sizeChanged(d);
442 } 452// }
453// d->flags2 &= ~undefinedHeight_WidgetFlag2;
443 return iTrue; 454 return iTrue;
444 } 455 }
445 } 456 }
@@ -836,6 +847,13 @@ static void arrange_Widget_(iWidget *d) {
836} 847}
837 848
838static void resetArrangement_Widget_(iWidget *d) { 849static void resetArrangement_Widget_(iWidget *d) {
850 d->oldSize = d->rect.size;
851 if (d->flags & resizeToParentWidth_WidgetFlag) {
852 d->rect.size.x = 0;
853 }
854 if (d->flags & resizeToParentHeight_WidgetFlag) {
855 d->rect.size.y = 0;
856 }
839 iForEach(ObjectList, i, children_Widget(d)) { 857 iForEach(ObjectList, i, children_Widget(d)) {
840 iWidget *child = as_Widget(i.object); 858 iWidget *child = as_Widget(i.object);
841 resetArrangement_Widget_(child); 859 resetArrangement_Widget_(child);
@@ -847,6 +865,14 @@ static void resetArrangement_Widget_(iWidget *d) {
847 ~child->flags & fixedWidth_WidgetFlag) { 865 ~child->flags & fixedWidth_WidgetFlag) {
848 child->rect.size.x = 0; 866 child->rect.size.x = 0;
849 } 867 }
868 if (d->flags & resizeChildrenToWidestChild_WidgetFlag) {
869 if (isInstance_Object(child, &Class_LabelWidget)) {
870 updateSize_LabelWidget((iLabelWidget *) child);
871 }
872 else {
873 child->rect.size.x = 0;
874 }
875 }
850 if (d->flags & arrangeVertical_WidgetFlag) { 876 if (d->flags & arrangeVertical_WidgetFlag) {
851 child->rect.pos.y = 0; 877 child->rect.pos.y = 0;
852 } 878 }
@@ -858,6 +884,15 @@ static void resetArrangement_Widget_(iWidget *d) {
858 } 884 }
859} 885}
860 886
887static void notifySizeChanged_Widget_(iWidget *d) {
888 if (class_Widget(d)->sizeChanged && !isEqual_I2(d->rect.size, d->oldSize)) {
889 class_Widget(d)->sizeChanged(d);
890 }
891 iForEach(ObjectList, child, d->children) {
892 notifySizeChanged_Widget_(child.object);
893 }
894}
895
861void arrange_Widget(iWidget *d) { 896void arrange_Widget(iWidget *d) {
862 if (d) { 897 if (d) {
863#if !defined (NDEBUG) 898#if !defined (NDEBUG)
@@ -867,6 +902,8 @@ void arrange_Widget(iWidget *d) {
867#endif 902#endif
868 resetArrangement_Widget_(d); /* back to initial default sizes */ 903 resetArrangement_Widget_(d); /* back to initial default sizes */
869 arrange_Widget_(d); 904 arrange_Widget_(d);
905 notifySizeChanged_Widget_(d);
906 d->root->didChangeArrangement = iTrue;
870 } 907 }
871} 908}
872 909
@@ -884,6 +921,14 @@ int visualOffsetByReference_Widget(const iWidget *d) {
884// const float factor = width_Widget(d) / (float) size_Root(d->root).x; 921// const float factor = width_Widget(d) / (float) size_Root(d->root).x;
885 const int invOff = width_Widget(d) - iRound(value_Anim(&child->visualOffset)); 922 const int invOff = width_Widget(d) - iRound(value_Anim(&child->visualOffset));
886 offX -= invOff / 4; 923 offX -= invOff / 4;
924#if 0
925 if (invOff) {
926 printf(" [%p] %s (%p, fin:%d visoff:%d drag:%d): invOff %d\n", d, cstr_String(&child->id), child,
927 isFinished_Anim(&child->visualOffset),
928 (child->flags & visualOffset_WidgetFlag) != 0,
929 (child->flags & dragged_WidgetFlag) != 0, invOff); fflush(stdout);
930 }
931#endif
887 } 932 }
888 } 933 }
889 return offX; 934 return offX;
@@ -1183,6 +1228,9 @@ iBool scrollOverflow_Widget(iWidget *d, int delta) {
1183 bounds.pos.y = iMin(bounds.pos.y, validPosRange.end); 1228 bounds.pos.y = iMin(bounds.pos.y, validPosRange.end);
1184 } 1229 }
1185// printf("range: %d ... %d\n", range.start, range.end); 1230// printf("range: %d ... %d\n", range.start, range.end);
1231 if (delta) {
1232 d->root->didChangeArrangement = iTrue; /* ensure that widgets update if needed */
1233 }
1186 } 1234 }
1187 else { 1235 else {
1188 bounds.pos.y = iClamp(bounds.pos.y, validPosRange.start, validPosRange.end); 1236 bounds.pos.y = iClamp(bounds.pos.y, validPosRange.start, validPosRange.end);
@@ -1370,7 +1418,8 @@ void drawLayerEffects_Widget(const iWidget *d) {
1370 shadowBorder = iFalse; 1418 shadowBorder = iFalse;
1371 } 1419 }
1372 } 1420 }
1373 const iBool isFaded = fadeBackground && ~d->flags & noFadeBackground_WidgetFlag; 1421 const iBool isFaded = (fadeBackground && ~d->flags & noFadeBackground_WidgetFlag) ||
1422 (d->flags2 & fadeBackground_WidgetFlag2);
1374 if (shadowBorder && ~d->flags & noShadowBorder_WidgetFlag) { 1423 if (shadowBorder && ~d->flags & noShadowBorder_WidgetFlag) {
1375 iPaint p; 1424 iPaint p;
1376 init_Paint(&p); 1425 init_Paint(&p);
@@ -1381,9 +1430,19 @@ void drawLayerEffects_Widget(const iWidget *d) {
1381 init_Paint(&p); 1430 init_Paint(&p);
1382 p.alpha = 0x50; 1431 p.alpha = 0x50;
1383 if (flags_Widget(d) & (visualOffset_WidgetFlag | dragged_WidgetFlag)) { 1432 if (flags_Widget(d) & (visualOffset_WidgetFlag | dragged_WidgetFlag)) {
1384 const float area = d->rect.size.x * d->rect.size.y; 1433 const float area = d->rect.size.x * d->rect.size.y;
1434 const float rootArea = area_Rect(rect_Root(d->root));
1385 const float visibleArea = area_Rect(intersect_Rect(bounds_Widget(d), rect_Root(d->root))); 1435 const float visibleArea = area_Rect(intersect_Rect(bounds_Widget(d), rect_Root(d->root)));
1386 p.alpha *= (area > 0 ? visibleArea / area : 0.0f); 1436 if (isPortraitPhone_App() && !cmp_String(&d->id, "sidebar")) {
1437 p.alpha *= iClamp(visibleArea / rootArea * 2, 0.0f, 1.0f);
1438 }
1439 else if (area > 0) {
1440 p.alpha *= visibleArea / area;
1441 }
1442 else {
1443 p.alpha = 0;
1444 }
1445 //printf("area:%f visarea:%f alpha:%d\n", rootArea, visibleArea, p.alpha);
1387 } 1446 }
1388 SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_BLEND); 1447 SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_BLEND);
1389 fillRect_Paint(&p, rect_Root(d->root), backgroundFadeColor_Widget()); 1448 fillRect_Paint(&p, rect_Root(d->root), backgroundFadeColor_Widget());
@@ -1851,7 +1910,7 @@ iAny *findParentClass_Widget(const iWidget *d, const iAnyClass *class) {
1851} 1910}
1852 1911
1853iAny *findOverflowScrollable_Widget(iWidget *d) { 1912iAny *findOverflowScrollable_Widget(iWidget *d) {
1854 const iRect rootRect = rect_Root(d->root); 1913 const iRect rootRect = visibleRect_Root(d->root);
1855 for (iWidget *w = d; w; w = parent_Widget(w)) { 1914 for (iWidget *w = d; w; w = parent_Widget(w)) {
1856 if (flags_Widget(w) & overflowScrollable_WidgetFlag) { 1915 if (flags_Widget(w) & overflowScrollable_WidgetFlag) {
1857 const iRect bounds = boundsWithoutVisualOffset_Widget(w); 1916 const iRect bounds = boundsWithoutVisualOffset_Widget(w);
@@ -1956,12 +2015,16 @@ iBool isAffectedByVisualOffset_Widget(const iWidget *d) {
1956 if (w->flags & visualOffset_WidgetFlag) { 2015 if (w->flags & visualOffset_WidgetFlag) {
1957 return iTrue; 2016 return iTrue;
1958 } 2017 }
2018 if (visualOffsetByReference_Widget(w) != 0) {
2019 return iTrue;
2020 }
1959 } 2021 }
1960 return iFalse; 2022 return iFalse;
1961} 2023}
1962 2024
1963void setFocus_Widget(iWidget *d) { 2025void setFocus_Widget(iWidget *d) {
1964 iWindow *win = get_Window(); 2026 iWindow *win = d ? window_Widget(d) : get_Window();
2027 iAssert(win);
1965 if (win->focus != d) { 2028 if (win->focus != d) {
1966 if (win->focus) { 2029 if (win->focus) {
1967 iAssert(!contains_PtrSet(win->focus->root->pendingDestruction, win->focus)); 2030 iAssert(!contains_PtrSet(win->focus->root->pendingDestruction, win->focus));
@@ -1976,6 +2039,13 @@ void setFocus_Widget(iWidget *d) {
1976 } 2039 }
1977} 2040}
1978 2041
2042void setKeyboardGrab_Widget(iWidget *d) {
2043 iWindow *win = d ? window_Widget(d) : get_Window();
2044 iAssert(win);
2045 win->focus = d;
2046 /* no notifications sent */
2047}
2048
1979iWidget *focus_Widget(void) { 2049iWidget *focus_Widget(void) {
1980 return get_Window()->focus; 2050 return get_Window()->focus;
1981} 2051}
diff --git a/src/ui/widget.h b/src/ui/widget.h
index 4025f5c5..fb7eb5e2 100644
--- a/src/ui/widget.h
+++ b/src/ui/widget.h
@@ -123,6 +123,11 @@ enum iWidgetFlag {
123#define refChildrenOffset_WidgetFlag iBit64(63) /* visual offset determined by the offset of referenced children */ 123#define refChildrenOffset_WidgetFlag iBit64(63) /* visual offset determined by the offset of referenced children */
124#define nativeMenu_WidgetFlag iBit64(64) 124#define nativeMenu_WidgetFlag iBit64(64)
125 125
126enum iWidgetFlag2 {
127 slidingSheetDraggable_WidgetFlag2 = iBit(1),
128 fadeBackground_WidgetFlag2 = iBit(2),
129};
130
126enum iWidgetAddPos { 131enum iWidgetAddPos {
127 back_WidgetAddPos, 132 back_WidgetAddPos,
128 front_WidgetAddPos, 133 front_WidgetAddPos,
@@ -139,7 +144,9 @@ struct Impl_Widget {
139 iObject object; 144 iObject object;
140 iString id; 145 iString id;
141 int64_t flags; 146 int64_t flags;
147 int flags2;
142 iRect rect; 148 iRect rect;
149 iInt2 oldSize; /* in previous arrangement; for notification */
143 iInt2 minSize; 150 iInt2 minSize;
144 iWidget * sizeRef; 151 iWidget * sizeRef;
145 iWidget * offsetRef; 152 iWidget * offsetRef;
@@ -230,6 +237,24 @@ iLocalDef int height_Widget(const iAnyObject *d) {
230 } 237 }
231 return 0; 238 return 0;
232} 239}
240iLocalDef int leftPad_Widget(const iWidget *d) {
241 return d->padding[0];
242}
243iLocalDef int topPad_Widget(const iWidget *d) {
244 return d->padding[1];
245}
246iLocalDef int rightPad_Widget(const iWidget *d) {
247 return d->padding[2];
248}
249iLocalDef int bottomPad_Widget(const iWidget *d) {
250 return d->padding[3];
251}
252iLocalDef iInt2 tlPad_Widget(const iWidget *d) {
253 return init_I2(leftPad_Widget(d), topPad_Widget(d));
254}
255iLocalDef iInt2 brPad_Widget(const iWidget *d) {
256 return init_I2(rightPad_Widget(d), bottomPad_Widget(d));
257}
233iLocalDef iObjectList *children_Widget(iAnyObject *d) { 258iLocalDef iObjectList *children_Widget(iAnyObject *d) {
234 if (d == NULL) return NULL; 259 if (d == NULL) return NULL;
235 iAssert(isInstance_Object(d, &Class_Widget)); 260 iAssert(isInstance_Object(d, &Class_Widget));
@@ -302,7 +327,8 @@ void scrollInfo_Widget (const iWidget *, iWidgetScrollInfo *inf
302 327
303int backgroundFadeColor_Widget (void); 328int backgroundFadeColor_Widget (void);
304 329
305void setFocus_Widget (iWidget *); 330void setFocus_Widget (iWidget *); /* widget must be flagged `focusable` */
331void setKeyboardGrab_Widget (iWidget *); /* sets focus on any widget */
306iWidget * focus_Widget (void); 332iWidget * focus_Widget (void);
307void setHover_Widget (iWidget *); 333void setHover_Widget (iWidget *);
308iWidget * hover_Widget (void); 334iWidget * hover_Widget (void);
diff --git a/src/ui/window.c b/src/ui/window.c
index 9f12cabf..af36bb22 100644
--- a/src/ui/window.c
+++ b/src/ui/window.c
@@ -442,7 +442,7 @@ void create_Window_(iWindow *d, iRect rect, uint32_t flags) {
442static SDL_Surface *loadImage_(const iBlock *data, int resized) { 442static SDL_Surface *loadImage_(const iBlock *data, int resized) {
443 int w = 0, h = 0, num = 4; 443 int w = 0, h = 0, num = 4;
444 stbi_uc *pixels = stbi_load_from_memory( 444 stbi_uc *pixels = stbi_load_from_memory(
445 constData_Block(data), size_Block(data), &w, &h, &num, STBI_rgb_alpha); 445 constData_Block(data), (int) size_Block(data), &w, &h, &num, STBI_rgb_alpha);
446 if (resized) { 446 if (resized) {
447 stbi_uc *rsPixels = malloc(num * resized * resized); 447 stbi_uc *rsPixels = malloc(num * resized * resized);
448 stbir_resize_uint8(pixels, w, h, 0, rsPixels, resized, resized, 0, num); 448 stbir_resize_uint8(pixels, w, h, 0, rsPixels, resized, resized, 0, num);
@@ -560,6 +560,7 @@ void init_MainWindow(iMainWindow *d, iRect rect) {
560 d->splitMode = 0; 560 d->splitMode = 0;
561 d->pendingSplitMode = 0; 561 d->pendingSplitMode = 0;
562 d->pendingSplitUrl = new_String(); 562 d->pendingSplitUrl = new_String();
563 d->pendingSplitOrigin = new_String();
563 d->place.initialPos = rect.pos; 564 d->place.initialPos = rect.pos;
564 d->place.normalRect = rect; 565 d->place.normalRect = rect;
565 d->place.lastNotifiedSize = zero_I2(); 566 d->place.lastNotifiedSize = zero_I2();
@@ -605,6 +606,7 @@ void init_MainWindow(iMainWindow *d, iRect rect) {
605#endif 606#endif
606 setCurrent_Text(d->base.text); 607 setCurrent_Text(d->base.text);
607 SDL_GetRendererOutputSize(d->base.render, &d->base.size.x, &d->base.size.y); 608 SDL_GetRendererOutputSize(d->base.render, &d->base.size.x, &d->base.size.y);
609 d->maxDrawableHeight = d->base.size.y;
608 setupUserInterface_MainWindow(d); 610 setupUserInterface_MainWindow(d);
609 postCommand_App("~bindings.changed"); /* update from bindings */ 611 postCommand_App("~bindings.changed"); /* update from bindings */
610 /* Load the border shadow texture. */ { 612 /* Load the border shadow texture. */ {
@@ -642,6 +644,7 @@ void deinit_MainWindow(iMainWindow *d) {
642 if (theMainWindow_ == d) { 644 if (theMainWindow_ == d) {
643 theMainWindow_ = NULL; 645 theMainWindow_ = NULL;
644 } 646 }
647 delete_String(d->pendingSplitOrigin);
645 delete_String(d->pendingSplitUrl); 648 delete_String(d->pendingSplitUrl);
646 deinit_Window(&d->base); 649 deinit_Window(&d->base);
647} 650}
@@ -1006,6 +1009,28 @@ iBool processEvent_Window(iWindow *d, const SDL_Event *ev) {
1006 postCommand_App("media.player.update"); /* in case a player needs updating */ 1009 postCommand_App("media.player.update"); /* in case a player needs updating */
1007 return iTrue; 1010 return iTrue;
1008 } 1011 }
1012 if (event.type == SDL_USEREVENT && isCommand_UserEvent(ev, "window.sysframe") && mw) {
1013 /* This command is sent on Android to update the keyboard height. */
1014 const char *cmd = command_UserEvent(ev);
1015 /*
1016 0
1017 |
1018 top
1019 | |
1020 | bottom (top of keyboard) :
1021 | | : keyboardHeight
1022 maxDrawableHeight :
1023 |
1024 fullheight
1025 */
1026 const int top = argLabel_Command(cmd, "top");
1027 const int bottom = argLabel_Command(cmd, "bottom");
1028 if (!SDL_IsScreenKeyboardShown(mw->base.win)) {
1029 mw->maxDrawableHeight = bottom - top;
1030 }
1031 setKeyboardHeight_MainWindow(mw, top + mw->maxDrawableHeight - bottom);
1032 return iTrue;
1033 }
1009 if (processEvent_Touch(&event)) { 1034 if (processEvent_Touch(&event)) {
1010 return iTrue; 1035 return iTrue;
1011 } 1036 }
@@ -1236,11 +1261,15 @@ void draw_MainWindow(iMainWindow *d) {
1236 isDrawing_ = iTrue; 1261 isDrawing_ = iTrue;
1237 setCurrent_Text(d->base.text); 1262 setCurrent_Text(d->base.text);
1238 /* Check if root needs resizing. */ { 1263 /* Check if root needs resizing. */ {
1264 const iBool wasPortrait = isPortrait_App();
1239 iInt2 renderSize; 1265 iInt2 renderSize;
1240 SDL_GetRendererOutputSize(w->render, &renderSize.x, &renderSize.y); 1266 SDL_GetRendererOutputSize(w->render, &renderSize.x, &renderSize.y);
1241 if (!isEqual_I2(renderSize, w->size)) { 1267 if (!isEqual_I2(renderSize, w->size)) {
1242 updateSize_MainWindow_(d, iTrue); 1268 updateSize_MainWindow_(d, iTrue);
1243 processEvents_App(postedEventsOnly_AppEventMode); 1269 processEvents_App(postedEventsOnly_AppEventMode);
1270 if (isPortrait_App() != wasPortrait) {
1271 d->maxDrawableHeight = renderSize.y;
1272 }
1244 } 1273 }
1245 } 1274 }
1246 const int winFlags = SDL_GetWindowFlags(d->base.win); 1275 const int winFlags = SDL_GetWindowFlags(d->base.win);
@@ -1268,6 +1297,14 @@ void draw_MainWindow(iMainWindow *d) {
1268 } 1297 }
1269 /* Draw widgets. */ 1298 /* Draw widgets. */
1270 w->frameTime = SDL_GetTicks(); 1299 w->frameTime = SDL_GetTicks();
1300 iForIndices(i, d->base.roots) {
1301 iRoot *root = d->base.roots[i];
1302 if (root) {
1303 /* Some widgets may need a just-in-time visual update. */
1304 notifyVisualOffsetChange_Root(root);
1305 root->didChangeArrangement = iFalse;
1306 }
1307 }
1271 if (isExposed_Window(w)) { 1308 if (isExposed_Window(w)) {
1272 w->isInvalidated = iFalse; 1309 w->isInvalidated = iFalse;
1273 extern int drawCount_; 1310 extern int drawCount_;
@@ -1442,6 +1479,7 @@ iBool isOpenGLRenderer_Window(void) {
1442} 1479}
1443 1480
1444void setKeyboardHeight_MainWindow(iMainWindow *d, int height) { 1481void setKeyboardHeight_MainWindow(iMainWindow *d, int height) {
1482 height = iMax(0, height);
1445 if (d->keyboardHeight != height) { 1483 if (d->keyboardHeight != height) {
1446 d->keyboardHeight = height; 1484 d->keyboardHeight = height;
1447 postCommandf_App("keyboard.changed arg:%d", height); 1485 postCommandf_App("keyboard.changed arg:%d", height);
@@ -1528,18 +1566,20 @@ void setSplitMode_MainWindow(iMainWindow *d, int splitFlags) {
1528 } 1566 }
1529 } 1567 }
1530 if (!isEmpty_String(d->pendingSplitUrl)) { 1568 if (!isEmpty_String(d->pendingSplitUrl)) {
1531 postCommandf_Root(w->roots[newRootIndex], "open url:%s", 1569 postCommandf_Root(w->roots[newRootIndex], "open origin:%s url:%s",
1570 cstr_String(d->pendingSplitOrigin),
1532 cstr_String(d->pendingSplitUrl)); 1571 cstr_String(d->pendingSplitUrl));
1533 clear_String(d->pendingSplitUrl); 1572 clear_String(d->pendingSplitUrl);
1573 clear_String(d->pendingSplitOrigin);
1534 } 1574 }
1535 else if (~splitFlags & noEvents_WindowSplit) { 1575 else if (~splitFlags & noEvents_WindowSplit) {
1536 iWidget *docTabs0 = findChild_Widget(w->roots[newRootIndex ^ 1]->widget, "doctabs"); 1576 iWidget *docTabs0 = findChild_Widget(w->roots[newRootIndex ^ 1]->widget, "doctabs");
1537 iWidget *docTabs1 = findChild_Widget(w->roots[newRootIndex]->widget, "doctabs"); 1577 iWidget *docTabs1 = findChild_Widget(w->roots[newRootIndex]->widget, "doctabs");
1538 /* If the old root has multiple tabs, move the current one to the new split. */ 1578 /* If the old root has multiple tabs, move the current one to the new split. */
1539 if (tabCount_Widget(docTabs0) >= 2) { 1579 if (tabCount_Widget(docTabs0) >= 2) {
1540 int movedIndex = tabPageIndex_Widget(docTabs0, moved); 1580 size_t movedIndex = tabPageIndex_Widget(docTabs0, moved);
1541 removeTabPage_Widget(docTabs0, movedIndex); 1581 removeTabPage_Widget(docTabs0, movedIndex);
1542 showTabPage_Widget(docTabs0, tabPage_Widget(docTabs0, iMax(movedIndex - 1, 0))); 1582 showTabPage_Widget(docTabs0, tabPage_Widget(docTabs0, iMax((int) movedIndex - 1, 0)));
1543 iRelease(removeTabPage_Widget(docTabs1, 0)); /* delete the default tab */ 1583 iRelease(removeTabPage_Widget(docTabs1, 0)); /* delete the default tab */
1544 setRoot_Widget(as_Widget(moved), w->roots[newRootIndex]); 1584 setRoot_Widget(as_Widget(moved), w->roots[newRootIndex]);
1545 prependTabPage_Widget(docTabs1, iClob(moved), "", 0, 0); 1585 prependTabPage_Widget(docTabs1, iClob(moved), "", 0, 0);
diff --git a/src/ui/window.h b/src/ui/window.h
index 6c921f09..b4e348d2 100644
--- a/src/ui/window.h
+++ b/src/ui/window.h
@@ -114,8 +114,10 @@ struct Impl_MainWindow {
114 int splitMode; 114 int splitMode;
115 int pendingSplitMode; 115 int pendingSplitMode;
116 iString * pendingSplitUrl; /* URL to open in a newly opened split */ 116 iString * pendingSplitUrl; /* URL to open in a newly opened split */
117 iString * pendingSplitOrigin; /* tab from where split was initiated, if any */
117 SDL_Texture * appIcon; 118 SDL_Texture * appIcon;
118 int keyboardHeight; /* mobile software keyboards */ 119 int keyboardHeight; /* mobile software keyboards */
120 int maxDrawableHeight;
119}; 121};
120 122
121iLocalDef enum iWindowType type_Window(const iAnyWindow *d) { 123iLocalDef enum iWindowType type_Window(const iAnyWindow *d) {
@@ -186,10 +188,10 @@ void setKeyboardHeight_MainWindow (iMainWindow *, int height);
186void setSplitMode_MainWindow (iMainWindow *, int splitMode); 188void setSplitMode_MainWindow (iMainWindow *, int splitMode);
187void checkPendingSplit_MainWindow (iMainWindow *); 189void checkPendingSplit_MainWindow (iMainWindow *);
188void swapRoots_MainWindow (iMainWindow *); 190void swapRoots_MainWindow (iMainWindow *);
189void showToolbars_MainWindow (iMainWindow *, iBool show); 191//void showToolbars_MainWindow (iMainWindow *, iBool show);
190void resize_MainWindow (iMainWindow *, int w, int h); 192void resize_MainWindow (iMainWindow *, int w, int h);
191 193
192iBool processEvent_MainWindow (iMainWindow *, const SDL_Event *); 194//iBool processEvent_MainWindow (iMainWindow *, const SDL_Event *);
193void draw_MainWindow (iMainWindow *); 195void draw_MainWindow (iMainWindow *);
194void drawWhileResizing_MainWindow (iMainWindow *, int w, int h); /* workaround for SDL bug */ 196void drawWhileResizing_MainWindow (iMainWindow *, int w, int h); /* workaround for SDL bug */
195 197
diff --git a/src/visited.c b/src/visited.c
index 4552a053..83e09071 100644
--- a/src/visited.c
+++ b/src/visited.c
@@ -105,7 +105,7 @@ void load_Visited(iVisited *d, const char *dirPath) {
105 char *endp = NULL; 105 char *endp = NULL;
106 const unsigned long long ts = strtoull(line.start, &endp, 10); 106 const unsigned long long ts = strtoull(line.start, &endp, 10);
107 if (ts == 0) break; 107 if (ts == 0) break;
108 const uint32_t flags = strtoul(skipSpace_CStr(endp), &endp, 16); 108 const uint32_t flags = (uint32_t) strtoul(skipSpace_CStr(endp), &endp, 16);
109 const char *urlStart = skipSpace_CStr(endp); 109 const char *urlStart = skipSpace_CStr(endp);
110 iVisitedUrl item; 110 iVisitedUrl item;
111 item.when.ts = (struct timespec){ .tv_sec = ts }; 111 item.when.ts = (struct timespec){ .tv_sec = ts };