summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/app.c115
-rw-r--r--src/app.h2
-rw-r--r--src/audio/player.c21
-rw-r--r--src/audio/player.h3
-rw-r--r--src/defs.h13
-rw-r--r--src/gmdocument.c120
-rw-r--r--src/gmdocument.h27
-rw-r--r--src/gmtypesetter.c25
-rw-r--r--src/gmtypesetter.h41
-rw-r--r--src/gmutil.c4
-rw-r--r--src/history.c183
-rw-r--r--src/history.h25
-rw-r--r--src/ios.h5
-rw-r--r--src/ios.m158
-rw-r--r--src/macos.m2
-rw-r--r--src/media.c35
-rw-r--r--src/media.h3
-rw-r--r--src/prefs.c1
-rw-r--r--src/prefs.h1
-rw-r--r--src/ui/color.c86
-rw-r--r--src/ui/color.h42
-rw-r--r--src/ui/documentwidget.c361
-rw-r--r--src/ui/documentwidget.h8
-rw-r--r--src/ui/indicatorwidget.c2
-rw-r--r--src/ui/inputwidget.c18
-rw-r--r--src/ui/inputwidget.h27
-rw-r--r--src/ui/labelwidget.c57
-rw-r--r--src/ui/labelwidget.h6
-rw-r--r--src/ui/mediaui.c2
-rw-r--r--src/ui/mobile.c20
-rw-r--r--src/ui/root.c141
-rw-r--r--src/ui/root.h3
-rw-r--r--src/ui/sidebarwidget.c91
-rw-r--r--src/ui/sidebarwidget.h4
-rw-r--r--src/ui/text.c46
-rw-r--r--src/ui/text.h3
-rw-r--r--src/ui/touch.c98
-rw-r--r--src/ui/util.c104
-rw-r--r--src/ui/widget.c110
-rw-r--r--src/ui/widget.h17
-rw-r--r--src/ui/window.c8
41 files changed, 1644 insertions, 394 deletions
diff --git a/src/app.c b/src/app.c
index ce0898ba..b25a213e 100644
--- a/src/app.c
+++ b/src/app.c
@@ -212,6 +212,7 @@ static iString *serializePrefs_App_(const iApp *d) {
212 appendFormat_String(str, "smoothscroll arg:%d\n", d->prefs.smoothScrolling); 212 appendFormat_String(str, "smoothscroll arg:%d\n", d->prefs.smoothScrolling);
213 appendFormat_String(str, "imageloadscroll arg:%d\n", d->prefs.loadImageInsteadOfScrolling); 213 appendFormat_String(str, "imageloadscroll arg:%d\n", d->prefs.loadImageInsteadOfScrolling);
214 appendFormat_String(str, "cachesize.set arg:%d\n", d->prefs.maxCacheSize); 214 appendFormat_String(str, "cachesize.set arg:%d\n", d->prefs.maxCacheSize);
215 appendFormat_String(str, "memorysize.set arg:%d\n", d->prefs.maxMemorySize);
215 appendFormat_String(str, "decodeurls arg:%d\n", d->prefs.decodeUserVisibleURLs); 216 appendFormat_String(str, "decodeurls arg:%d\n", d->prefs.decodeUserVisibleURLs);
216 appendFormat_String(str, "linewidth.set arg:%d\n", d->prefs.lineWidth); 217 appendFormat_String(str, "linewidth.set arg:%d\n", d->prefs.lineWidth);
217 /* TODO: Set up an array of booleans in Prefs and do these in a loop. */ 218 /* TODO: Set up an array of booleans in Prefs and do these in a loop. */
@@ -762,6 +763,7 @@ static void init_App_(iApp *d, int argc, char **argv) {
762 setupApplication_iOS(); 763 setupApplication_iOS();
763#endif 764#endif
764 init_Keys(); 765 init_Keys();
766 loadPalette_Color(dataDir_App_());
765 setThemePalette_Color(d->prefs.theme); /* default UI colors */ 767 setThemePalette_Color(d->prefs.theme); /* default UI colors */
766 loadPrefs_App_(d); 768 loadPrefs_App_(d);
767 load_Keys(dataDir_App_()); 769 load_Keys(dataDir_App_());
@@ -920,13 +922,25 @@ const iString *debugInfo_App(void) {
920 extern char **environ; /* The environment variables. */ 922 extern char **environ; /* The environment variables. */
921 iApp *d = &app_; 923 iApp *d = &app_;
922 iString *msg = collectNew_String(); 924 iString *msg = collectNew_String();
925 iObjectList *docs = iClob(listDocuments_App(NULL));
923 format_String(msg, "# Debug information\n"); 926 format_String(msg, "# Debug information\n");
927 appendFormat_String(msg, "## Memory usage\n"); {
928 iMemInfo total = { 0, 0 };
929 iForEach(ObjectList, i, docs) {
930 iDocumentWidget *doc = i.object;
931 iMemInfo usage = memoryUsage_History(history_DocumentWidget(doc));
932 total.cacheSize += usage.cacheSize;
933 total.memorySize += usage.memorySize;
934 }
935 appendFormat_String(msg, "Total cache: %.3f MB\n", total.cacheSize / 1.0e6f);
936 appendFormat_String(msg, "Total memory: %.3f MB\n", total.memorySize / 1.0e6f);
937 }
924 appendFormat_String(msg, "## Documents\n"); 938 appendFormat_String(msg, "## Documents\n");
925 iForEach(ObjectList, k, iClob(listDocuments_App(NULL))) { 939 iForEach(ObjectList, k, docs) {
926 iDocumentWidget *doc = k.object; 940 iDocumentWidget *doc = k.object;
927 appendFormat_String(msg, "### Tab %d.%zu: %s\n", 941 appendFormat_String(msg, "### Tab %d.%zu: %s\n",
928 constAs_Widget(doc)->root == get_Window()->roots[0] ? 0 : 1, 942 constAs_Widget(doc)->root == get_Window()->roots[0] ? 1 : 2,
929 childIndex_Widget(constAs_Widget(doc)->parent, k.object), 943 childIndex_Widget(constAs_Widget(doc)->parent, k.object) + 1,
930 cstr_String(bookmarkTitle_DocumentWidget(doc))); 944 cstr_String(bookmarkTitle_DocumentWidget(doc)));
931 append_String(msg, collect_String(debugInfo_History(history_DocumentWidget(doc)))); 945 append_String(msg, collect_String(debugInfo_History(history_DocumentWidget(doc))));
932 } 946 }
@@ -981,6 +995,33 @@ void trimCache_App(void) {
981 iRelease(docs); 995 iRelease(docs);
982} 996}
983 997
998void trimMemory_App(void) {
999 iApp *d = &app_;
1000 size_t memorySize = 0;
1001 const size_t limit = d->prefs.maxMemorySize * 1000000;
1002 iObjectList *docs = listDocuments_App(NULL);
1003 iForEach(ObjectList, i, docs) {
1004 memorySize += memorySize_History(history_DocumentWidget(i.object));
1005 }
1006 init_ObjectListIterator(&i, docs);
1007 iBool wasPruned = iFalse;
1008 while (memorySize > limit) {
1009 iDocumentWidget *doc = i.object;
1010 const size_t pruned = pruneLeastImportantMemory_History(history_DocumentWidget(doc));
1011 if (pruned) {
1012 memorySize -= pruned;
1013 wasPruned = iTrue;
1014 }
1015 next_ObjectListIterator(&i);
1016 if (!i.value) {
1017 if (!wasPruned) break;
1018 wasPruned = iFalse;
1019 init_ObjectListIterator(&i, docs);
1020 }
1021 }
1022 iRelease(docs);
1023}
1024
984iLocalDef iBool isWaitingAllowed_App_(iApp *d) { 1025iLocalDef iBool isWaitingAllowed_App_(iApp *d) {
985 if (!isEmpty_Periodic(&d->periodic)) { 1026 if (!isEmpty_Periodic(&d->periodic)) {
986 return iFalse; 1027 return iFalse;
@@ -1051,6 +1092,13 @@ void processEvents_App(enum iAppEventMode eventMode) {
1051 postRefresh_App(); 1092 postRefresh_App();
1052 break; 1093 break;
1053 case SDL_APP_WILLENTERBACKGROUND: 1094 case SDL_APP_WILLENTERBACKGROUND:
1095#if defined (iPlatformAppleMobile)
1096 updateNowPlayingInfo_iOS();
1097#endif
1098 setFreezeDraw_Window(d->window, iTrue);
1099 savePrefs_App_(d);
1100 saveState_App_(d);
1101 break;
1054 case SDL_APP_TERMINATING: 1102 case SDL_APP_TERMINATING:
1055 setFreezeDraw_Window(d->window, iTrue); 1103 setFreezeDraw_Window(d->window, iTrue);
1056 savePrefs_App_(d); 1104 savePrefs_App_(d);
@@ -1606,7 +1654,9 @@ static iBool handlePrefsCommands_(iWidget *d, const char *cmd) {
1606 postCommandf_App("searchurl address:%s", 1654 postCommandf_App("searchurl address:%s",
1607 cstrText_InputWidget(findChild_Widget(d, "prefs.searchurl"))); 1655 cstrText_InputWidget(findChild_Widget(d, "prefs.searchurl")));
1608 postCommandf_App("cachesize.set arg:%d", 1656 postCommandf_App("cachesize.set arg:%d",
1609 toInt_String(text_InputWidget(findChild_Widget(d, "prefs.cachesize")))); 1657 toInt_String(text_InputWidget(findChild_Widget(d, "prefs.cachesize"))));
1658 postCommandf_App("memorysize.set arg:%d",
1659 toInt_String(text_InputWidget(findChild_Widget(d, "prefs.memorysize"))));
1610 postCommandf_App("ca.file path:%s", 1660 postCommandf_App("ca.file path:%s",
1611 cstrText_InputWidget(findChild_Widget(d, "prefs.ca.file"))); 1661 cstrText_InputWidget(findChild_Widget(d, "prefs.ca.file")));
1612 postCommandf_App("ca.path path:%s", 1662 postCommandf_App("ca.path path:%s",
@@ -2019,7 +2069,7 @@ iBool handleCommand_App(const char *cmd) {
2019 else if (equal_Command(cmd, "hidetoolbarscroll")) { 2069 else if (equal_Command(cmd, "hidetoolbarscroll")) {
2020 d->prefs.hideToolbarOnScroll = arg_Command(cmd); 2070 d->prefs.hideToolbarOnScroll = arg_Command(cmd);
2021 if (!d->prefs.hideToolbarOnScroll) { 2071 if (!d->prefs.hideToolbarOnScroll) {
2022 showToolbars_Root(get_Root(), iTrue); 2072 showToolbar_Root(get_Root(), iTrue);
2023 } 2073 }
2024 return iTrue; 2074 return iTrue;
2025 } 2075 }
@@ -2168,6 +2218,13 @@ iBool handleCommand_App(const char *cmd) {
2168 } 2218 }
2169 return iTrue; 2219 return iTrue;
2170 } 2220 }
2221 else if (equal_Command(cmd, "memorysize.set")) {
2222 d->prefs.maxMemorySize = arg_Command(cmd);
2223 if (d->prefs.maxMemorySize <= 0) {
2224 d->prefs.maxMemorySize = 0;
2225 }
2226 return iTrue;
2227 }
2171 else if (equal_Command(cmd, "searchurl")) { 2228 else if (equal_Command(cmd, "searchurl")) {
2172 iString *url = &d->prefs.searchUrl; 2229 iString *url = &d->prefs.searchUrl;
2173 setCStr_String(url, suffixPtr_Command(cmd, "address")); 2230 setCStr_String(url, suffixPtr_Command(cmd, "address"));
@@ -2229,7 +2286,8 @@ iBool handleCommand_App(const char *cmd) {
2229 } 2286 }
2230 else if (equal_Command(cmd, "open")) { 2287 else if (equal_Command(cmd, "open")) {
2231 iString *url = collectNewCStr_String(suffixPtr_Command(cmd, "url")); 2288 iString *url = collectNewCStr_String(suffixPtr_Command(cmd, "url"));
2232 const iBool noProxy = argLabel_Command(cmd, "noproxy"); 2289 const iBool noProxy = argLabel_Command(cmd, "noproxy") != 0;
2290 const iBool fromSidebar = argLabel_Command(cmd, "fromsidebar") != 0;
2233 iUrl parts; 2291 iUrl parts;
2234 init_Url(&parts, url); 2292 init_Url(&parts, url);
2235 if (argLabel_Command(cmd, "default") || equalCase_Rangecc(parts.scheme, "mailto") || 2293 if (argLabel_Command(cmd, "default") || equalCase_Rangecc(parts.scheme, "mailto") ||
@@ -2280,7 +2338,9 @@ iBool handleCommand_App(const char *cmd) {
2280 else { 2338 else {
2281 urlEncodePath_String(url); 2339 urlEncodePath_String(url);
2282 } 2340 }
2283 setUrlFromCache_DocumentWidget(doc, url, isHistory); 2341 setUrlFlags_DocumentWidget(doc, url,
2342 (isHistory ? useCachedContentIfAvailable_DocumentWidgetSetUrlFlag : 0) |
2343 (fromSidebar ? openedFromSidebar_DocumentWidgetSetUrlFlag : 0));
2284 /* Optionally, jump to a text in the document. This will only work if the document 2344 /* Optionally, jump to a text in the document. This will only work if the document
2285 is already available, e.g., it's from "about:" or restored from cache. */ 2345 is already available, e.g., it's from "about:" or restored from cache. */
2286 const iRangecc gotoHeading = range_Command(cmd, "gotoheading"); 2346 const iRangecc gotoHeading = range_Command(cmd, "gotoheading");
@@ -2295,6 +2355,36 @@ iBool handleCommand_App(const char *cmd) {
2295 } 2355 }
2296 setCurrent_Root(oldRoot); 2356 setCurrent_Root(oldRoot);
2297 } 2357 }
2358 else if (equal_Command(cmd, "file.open")) {
2359 const char *path = suffixPtr_Command(cmd, "path");
2360 if (path) {
2361 postCommandf_App("open temp:%d url:%s",
2362 argLabel_Command(cmd, "temp"),
2363 makeFileUrl_CStr(path));
2364 return iTrue;
2365 }
2366#if defined (iPlatformAppleMobile)
2367 pickFileForOpening_iOS();
2368#endif
2369 return iTrue;
2370 }
2371 else if (equal_Command(cmd, "file.delete")) {
2372 const char *path = suffixPtr_Command(cmd, "path");
2373 if (argLabel_Command(cmd, "confirm")) {
2374 makeQuestion_Widget(
2375 uiHeading_ColorEscape "${heading.file.delete}",
2376 format_CStr("${dlg.file.delete.confirm}\n%s", path),
2377 (iMenuItem[]){
2378 { "${cancel}", 0, 0, NULL },
2379 { uiTextCaution_ColorEscape "${dlg.file.delete}", 0, 0,
2380 format_CStr("!file.delete path:%s", path) } },
2381 2);
2382 }
2383 else {
2384 remove(path);
2385 }
2386 return iTrue;
2387 }
2298 else if (equal_Command(cmd, "document.request.cancelled")) { 2388 else if (equal_Command(cmd, "document.request.cancelled")) {
2299 /* TODO: How should cancelled requests be treated in the history? */ 2389 /* TODO: How should cancelled requests be treated in the history? */
2300#if 0 2390#if 0
@@ -2446,6 +2536,8 @@ iBool handleCommand_App(const char *cmd) {
2446 iTrue); 2536 iTrue);
2447 setText_InputWidget(findChild_Widget(dlg, "prefs.cachesize"), 2537 setText_InputWidget(findChild_Widget(dlg, "prefs.cachesize"),
2448 collectNewFormat_String("%d", d->prefs.maxCacheSize)); 2538 collectNewFormat_String("%d", d->prefs.maxCacheSize));
2539 setText_InputWidget(findChild_Widget(dlg, "prefs.memorysize"),
2540 collectNewFormat_String("%d", d->prefs.maxMemorySize));
2449 setToggle_Widget(findChild_Widget(dlg, "prefs.decodeurls"), d->prefs.decodeUserVisibleURLs); 2541 setToggle_Widget(findChild_Widget(dlg, "prefs.decodeurls"), d->prefs.decodeUserVisibleURLs);
2450 setText_InputWidget(findChild_Widget(dlg, "prefs.searchurl"), &d->prefs.searchUrl); 2542 setText_InputWidget(findChild_Widget(dlg, "prefs.searchurl"), &d->prefs.searchUrl);
2451 setText_InputWidget(findChild_Widget(dlg, "prefs.ca.file"), &d->prefs.caFile); 2543 setText_InputWidget(findChild_Widget(dlg, "prefs.ca.file"), &d->prefs.caFile);
@@ -2532,11 +2624,16 @@ iBool handleCommand_App(const char *cmd) {
2532 return iTrue; 2624 return iTrue;
2533 } 2625 }
2534 else if (equal_Command(cmd, "feeds.update.started")) { 2626 else if (equal_Command(cmd, "feeds.update.started")) {
2535 showCollapsed_Widget(findWidget_App("feeds.progress"), iTrue); 2627 iAnyObject *prog = findWidget_Root("feeds.progress");
2628 const iWidget *navBar = findWidget_Root("navbar");
2629 updateTextAndResizeWidthCStr_LabelWidget(
2630 prog, flags_Widget(navBar) & tight_WidgetFlag || deviceType_App() == phone_AppDeviceType ?
2631 "\u2605" : "\u2605 ${status.feeds}");
2632 showCollapsed_Widget(prog, iTrue);
2536 return iFalse; 2633 return iFalse;
2537 } 2634 }
2538 else if (equal_Command(cmd, "feeds.update.finished")) { 2635 else if (equal_Command(cmd, "feeds.update.finished")) {
2539 showCollapsed_Widget(findWidget_App("feeds.progress"), iFalse); 2636 showCollapsed_Widget(findWidget_Root("feeds.progress"), iFalse);
2540 refreshFinished_Feeds(); 2637 refreshFinished_Feeds();
2541 postRefresh_App(); 2638 postRefresh_App();
2542 return iFalse; 2639 return iFalse;
diff --git a/src/app.h b/src/app.h
index 918cd396..5d1d42e1 100644
--- a/src/app.h
+++ b/src/app.h
@@ -86,6 +86,7 @@ uint32_t elapsedSinceLastTicker_App (void); /* milliseconds */
86iBool isLandscape_App (void); 86iBool isLandscape_App (void);
87iLocalDef iBool isPortrait_App (void) { return !isLandscape_App(); } 87iLocalDef iBool isPortrait_App (void) { return !isLandscape_App(); }
88enum iAppDeviceType deviceType_App (void); 88enum iAppDeviceType deviceType_App (void);
89iLocalDef iBool isPortraitPhone_App (void) { return isPortrait_App() && deviceType_App() == phone_AppDeviceType; }
89iGmCerts * certs_App (void); 90iGmCerts * certs_App (void);
90iVisited * visited_App (void); 91iVisited * visited_App (void);
91iBookmarks * bookmarks_App (void); 92iBookmarks * bookmarks_App (void);
@@ -96,6 +97,7 @@ iObjectList * listDocuments_App (const iRoot *rootOrNull); /* NULL for a
96iStringSet * listOpenURLs_App (void); /* all tabs */ 97iStringSet * listOpenURLs_App (void); /* all tabs */
97iDocumentWidget * newTab_App (const iDocumentWidget *duplicateOf, iBool switchToNew); 98iDocumentWidget * newTab_App (const iDocumentWidget *duplicateOf, iBool switchToNew);
98void trimCache_App (void); 99void trimCache_App (void);
100void trimMemory_App (void);
99 101
100iDocumentWidget * document_Root (iRoot *); 102iDocumentWidget * document_Root (iRoot *);
101 103
diff --git a/src/audio/player.c b/src/audio/player.c
index 9e026561..94bcd065 100644
--- a/src/audio/player.c
+++ b/src/audio/player.c
@@ -455,6 +455,8 @@ struct Impl_Player {
455 iAVFAudioPlayer * avfPlayer; /* iOS */ 455 iAVFAudioPlayer * avfPlayer; /* iOS */
456}; 456};
457 457
458static iPlayer *activePlayer_;
459
458iDefineTypeConstruction(Player) 460iDefineTypeConstruction(Player)
459 461
460static size_t sampleSize_Player_(const iPlayer *d) { 462static size_t sampleSize_Player_(const iPlayer *d) {
@@ -655,8 +657,14 @@ void deinit_Player(iPlayer *d) {
655#if defined (iPlatformAppleMobile) 657#if defined (iPlatformAppleMobile)
656 if (d->avfPlayer) { 658 if (d->avfPlayer) {
657 delete_AVFAudioPlayer(d->avfPlayer); 659 delete_AVFAudioPlayer(d->avfPlayer);
660 if (activePlayer_ == d) {
661 clearNowPlayingInfo_iOS();
662 }
658 } 663 }
659#endif 664#endif
665 if (activePlayer_ == d) {
666 activePlayer_ = NULL;
667 }
660} 668}
661 669
662iBool isStarted_Player(const iPlayer *d) { 670iBool isStarted_Player(const iPlayer *d) {
@@ -724,6 +732,13 @@ void updateSourceData_Player(iPlayer *d, const iString *mimeType, const iBlock *
724 unlock_Mutex(&input->mtx); 732 unlock_Mutex(&input->mtx);
725} 733}
726 734
735size_t sourceDataSize_Player(const iPlayer *d) {
736 lock_Mutex(&d->data->mtx);
737 const size_t size = size_Block(&d->data->data);
738 unlock_Mutex(&d->data->mtx);
739 return size;
740}
741
727iBool start_Player(iPlayer *d) { 742iBool start_Player(iPlayer *d) {
728 if (isStarted_Player(d)) { 743 if (isStarted_Player(d)) {
729 return iFalse; 744 return iFalse;
@@ -732,6 +747,7 @@ iBool start_Player(iPlayer *d) {
732 if (d->avfPlayer) { 747 if (d->avfPlayer) {
733 play_AVFAudioPlayer(d->avfPlayer); 748 play_AVFAudioPlayer(d->avfPlayer);
734 setNotIdle_Player(d); 749 setNotIdle_Player(d);
750 activePlayer_ = d;
735 return iTrue; 751 return iTrue;
736 } 752 }
737#endif 753#endif
@@ -749,6 +765,7 @@ iBool start_Player(iPlayer *d) {
749 d->decoder->gain = d->volume; 765 d->decoder->gain = d->volume;
750 SDL_PauseAudioDevice(d->device, SDL_FALSE); 766 SDL_PauseAudioDevice(d->device, SDL_FALSE);
751 setNotIdle_Player(d); 767 setNotIdle_Player(d);
768 activePlayer_ = d;
752 return iTrue; 769 return iTrue;
753} 770}
754 771
@@ -882,3 +899,7 @@ iString *metadataLabel_Player(const iPlayer *d) {
882 } 899 }
883 return meta; 900 return meta;
884} 901}
902
903iPlayer *active_Player(void) {
904 return activePlayer_;
905}
diff --git a/src/audio/player.h b/src/audio/player.h
index 8753d811..ca307dc4 100644
--- a/src/audio/player.h
+++ b/src/audio/player.h
@@ -48,6 +48,7 @@ enum iPlayerTag {
48 48
49void updateSourceData_Player (iPlayer *, const iString *mimeType, const iBlock *data, 49void updateSourceData_Player (iPlayer *, const iString *mimeType, const iBlock *data,
50 enum iPlayerUpdate update); 50 enum iPlayerUpdate update);
51size_t sourceDataSize_Player (const iPlayer *);
51 52
52iBool start_Player (iPlayer *); 53iBool start_Player (iPlayer *);
53void stop_Player (iPlayer *); 54void stop_Player (iPlayer *);
@@ -67,3 +68,5 @@ float streamProgress_Player (const iPlayer *); /* normalized 0...1 */
67 68
68uint32_t idleTimeMs_Player (const iPlayer *); 69uint32_t idleTimeMs_Player (const iPlayer *);
69iString * metadataLabel_Player (const iPlayer *); 70iString * metadataLabel_Player (const iPlayer *);
71
72iPlayer * active_Player (void);
diff --git a/src/defs.h b/src/defs.h
index d16c56f5..3ea28e95 100644
--- a/src/defs.h
+++ b/src/defs.h
@@ -24,18 +24,26 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
24 24
25#include "lang.h" 25#include "lang.h"
26 26
27enum iSourceFormat {
28 undefined_SourceFormat = -1,
29 gemini_SourceFormat = 0,
30 plainText_SourceFormat,
31};
32
27enum iFileVersion { 33enum iFileVersion {
28 initial_FileVersion = 0, 34 initial_FileVersion = 0,
29 addedResponseTimestamps_FileVersion = 1, 35 addedResponseTimestamps_FileVersion = 1,
30 multipleRoots_FileVersion = 2, 36 multipleRoots_FileVersion = 2,
31 serializedSidebarState_FileVersion = 3, 37 serializedSidebarState_FileVersion = 3,
38 addedRecentUrlFlags_FileVersion = 4,
32 /* meta */ 39 /* meta */
33 idents_FileVersion = 1, /* version used by GmCerts/idents.lgr */ 40 idents_FileVersion = 1, /* version used by GmCerts/idents.lgr */
34 latest_FileVersion = 3, 41 latest_FileVersion = 4,
35}; 42};
36 43
37/* Icons */ 44/* Icons */
38 45
46#define menu_Icon "\U0001d362"
39#define rightArrowhead_Icon "\u27a4" 47#define rightArrowhead_Icon "\u27a4"
40#define leftArrowhead_Icon "\u27a4" 48#define leftArrowhead_Icon "\u27a4"
41#define warning_Icon "\u26a0" 49#define warning_Icon "\u26a0"
@@ -104,6 +112,9 @@ enum iFileVersion {
104# define shiftReturn_Icon shift_Icon " " return_Icon 112# define shiftReturn_Icon shift_Icon " " return_Icon
105#endif 113#endif
106 114
115#if defined (iPlatformAppleDesktop)
116# define iHaveNativeMenus
117#endif
107 118
108/* UI labels that depend on the platform */ 119/* UI labels that depend on the platform */
109 120
diff --git a/src/gmdocument.c b/src/gmdocument.c
index 002494b6..f15d9d1d 100644
--- a/src/gmdocument.c
+++ b/src/gmdocument.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 "gmdocument.h" 23#include "gmdocument.h"
24#include "gmtypesetter.h"
24#include "gmutil.h" 25#include "gmutil.h"
25#include "lang.h" 26#include "lang.h"
26#include "ui/color.h" 27#include "ui/color.h"
@@ -48,7 +49,7 @@ iBool isDark_GmDocumentTheme(enum iGmDocumentTheme d) {
48iDeclareType(GmLink) 49iDeclareType(GmLink)
49 50
50struct Impl_GmLink { 51struct Impl_GmLink {
51 iString url; 52 iString url; /* resolved */
52 iRangecc urlRange; /* URL in the source */ 53 iRangecc urlRange; /* URL in the source */
53 iRangecc labelRange; /* label in the source */ 54 iRangecc labelRange; /* label in the source */
54 iRangecc labelIcon; /* special icon defined in the label text */ 55 iRangecc labelIcon; /* special icon defined in the label text */
@@ -74,9 +75,10 @@ iDefineTypeConstruction(GmLink)
74 75
75struct Impl_GmDocument { 76struct Impl_GmDocument {
76 iObject object; 77 iObject object;
77 enum iGmDocumentFormat format; 78 enum iSourceFormat format;
78 iString source; 79 iString unormSource; /* unnormalized source */
79 iString url; /* for resolving relative links */ 80 iString source; /* normalized source */
81 iString url; /* for resolving relative links */
80 iString localHost; 82 iString localHost;
81 iInt2 size; 83 iInt2 size;
82 iArray layout; /* contents of source, laid out in document space */ 84 iArray layout; /* contents of source, laid out in document space */
@@ -90,12 +92,14 @@ struct Impl_GmDocument {
90 iChar siteIcon; 92 iChar siteIcon;
91 iMedia * media; 93 iMedia * media;
92 iStringSet *openURLs; /* currently open URLs for highlighting links */ 94 iStringSet *openURLs; /* currently open URLs for highlighting links */
95 iBool isPaletteValid;
96 iColor palette[tmMax_ColorId]; /* copy of the color palette */
93}; 97};
94 98
95iDefineObjectConstruction(GmDocument) 99iDefineObjectConstruction(GmDocument)
96 100
97static enum iGmLineType lineType_GmDocument_(const iGmDocument *d, const iRangecc line) { 101static enum iGmLineType lineType_GmDocument_(const iGmDocument *d, const iRangecc line) {
98 if (d->format == plainText_GmDocumentFormat) { 102 if (d->format == plainText_SourceFormat) {
99 return text_GmLineType; 103 return text_GmLineType;
100 } 104 }
101 return lineType_Rangecc(line); 105 return lineType_Rangecc(line);
@@ -314,7 +318,7 @@ static void linkContentWasLaidOut_GmDocument_(iGmDocument *d, const iGmMediaInfo
314 318
315static iBool isNormalized_GmDocument_(const iGmDocument *d) { 319static iBool isNormalized_GmDocument_(const iGmDocument *d) {
316 const iPrefs *prefs = prefs_App(); 320 const iPrefs *prefs = prefs_App();
317 if (d->format == plainText_GmDocumentFormat) { 321 if (d->format == plainText_SourceFormat) {
318 return iTrue; /* tabs are always normalized in plain text */ 322 return iTrue; /* tabs are always normalized in plain text */
319 } 323 }
320 if (startsWithCase_String(&d->url, "gemini:") && prefs->monospaceGemini) { 324 if (startsWithCase_String(&d->url, "gemini:") && prefs->monospaceGemini) {
@@ -444,12 +448,15 @@ static void doLayout_GmDocument_(iGmDocument *d) {
444 enum iGmLineType prevType = text_GmLineType; 448 enum iGmLineType prevType = text_GmLineType;
445 enum iGmLineType prevNonBlankType = text_GmLineType; 449 enum iGmLineType prevNonBlankType = text_GmLineType;
446 iBool followsBlank = iFalse; 450 iBool followsBlank = iFalse;
447 if (d->format == plainText_GmDocumentFormat) { 451 if (d->format == plainText_SourceFormat) {
448 isPreformat = iTrue; 452 isPreformat = iTrue;
449 isFirstText = iFalse; 453 isFirstText = iFalse;
450 } 454 }
451 while (nextSplit_Rangecc(content, "\n", &contentLine)) { 455 while (nextSplit_Rangecc(content, "\n", &contentLine)) {
452 iRangecc line = contentLine; /* `line` will be trimmed later; would confuse nextSplit */ 456 iRangecc line = contentLine; /* `line` will be trimmed; modifying would confuse `nextSplit_Rangecc` */
457 if (*line.end == '\r') {
458 line.end--; /* trim CR always */
459 }
453 iGmRun run = { .color = white_ColorId }; 460 iGmRun run = { .color = white_ColorId };
454 enum iGmLineType type; 461 enum iGmLineType type;
455 float indent = 0.0f; 462 float indent = 0.0f;
@@ -510,14 +517,14 @@ static void doLayout_GmDocument_(iGmDocument *d) {
510 if (contentLine.start == content.start) { 517 if (contentLine.start == content.start) {
511 prevType = type; 518 prevType = type;
512 } 519 }
513 if (d->format == gemini_GmDocumentFormat && 520 if (d->format == gemini_SourceFormat &&
514 startsWithSc_Rangecc(line, "```", &iCaseSensitive)) { 521 startsWithSc_Rangecc(line, "```", &iCaseSensitive)) {
515 isPreformat = iFalse; 522 isPreformat = iFalse;
516 addSiteBanner = iFalse; /* overrides the banner */ 523 addSiteBanner = iFalse; /* overrides the banner */
517 continue; 524 continue;
518 } 525 }
519 run.preId = preId; 526 run.preId = preId;
520 run.font = (d->format == plainText_GmDocumentFormat ? regularMonospace_FontId : preFont); 527 run.font = (d->format == plainText_SourceFormat ? regularMonospace_FontId : preFont);
521 indent = indents[type]; 528 indent = indents[type];
522 } 529 }
523 if (addSiteBanner) { 530 if (addSiteBanner) {
@@ -590,7 +597,7 @@ static void doLayout_GmDocument_(iGmDocument *d) {
590 } 597 }
591 } 598 }
592 /* Folded blocks are represented by a single run with the alt text. */ 599 /* Folded blocks are represented by a single run with the alt text. */
593 if (isPreformat && d->format != plainText_GmDocumentFormat) { 600 if (isPreformat && d->format != plainText_SourceFormat) {
594 const iGmPreMeta *meta = constAt_Array(&d->preMeta, preId - 1); 601 const iGmPreMeta *meta = constAt_Array(&d->preMeta, preId - 1);
595 if (meta->flags & folded_GmPreMetaFlag) { 602 if (meta->flags & folded_GmPreMetaFlag) {
596 const iBool isBlank = isEmpty_Range(&meta->altText); 603 const iBool isBlank = isEmpty_Range(&meta->altText);
@@ -686,7 +693,7 @@ static void doLayout_GmDocument_(iGmDocument *d) {
686 pushBack_Array(&d->layout, &icon); 693 pushBack_Array(&d->layout, &icon);
687 } 694 }
688 run.color = colors[type]; 695 run.color = colors[type];
689 if (d->format == plainText_GmDocumentFormat) { 696 if (d->format == plainText_SourceFormat) {
690 run.color = colors[text_GmLineType]; 697 run.color = colors[text_GmLineType];
691 } 698 }
692 /* Special formatting for the first paragraph (e.g., subtitle, introduction, or lede). */ 699 /* Special formatting for the first paragraph (e.g., subtitle, introduction, or lede). */
@@ -715,8 +722,8 @@ static void doLayout_GmDocument_(iGmDocument *d) {
715 type == quote_GmLineType ? 4 : 0); 722 type == quote_GmLineType ? 4 : 0);
716 } 723 }
717 const iBool isWordWrapped = 724 const iBool isWordWrapped =
718 (d->format == plainText_GmDocumentFormat ? prefs->plainTextWrap : !isPreformat); 725 (d->format == plainText_SourceFormat ? prefs->plainTextWrap : !isPreformat);
719 if (isPreformat && d->format != plainText_GmDocumentFormat) { 726 if (isPreformat && d->format != plainText_SourceFormat) {
720 /* Remember the top left coordinates of the block (first line of block). */ 727 /* Remember the top left coordinates of the block (first line of block). */
721 iGmPreMeta *meta = at_Array(&d->preMeta, preId - 1); 728 iGmPreMeta *meta = at_Array(&d->preMeta, preId - 1);
722 if (~meta->flags & topLeft_GmPreMetaFlag) { 729 if (~meta->flags & topLeft_GmPreMetaFlag) {
@@ -869,10 +876,13 @@ static void doLayout_GmDocument_(iGmDocument *d) {
869 } 876 }
870 } 877 }
871 } 878 }
879 printf("[GmDocument] layout size: %zu runs (%zu bytes)\n",
880 size_Array(&d->layout), size_Array(&d->layout) * sizeof(iGmRun));
872} 881}
873 882
874void init_GmDocument(iGmDocument *d) { 883void init_GmDocument(iGmDocument *d) {
875 d->format = gemini_GmDocumentFormat; 884 d->format = gemini_SourceFormat;
885 init_String(&d->unormSource);
876 init_String(&d->source); 886 init_String(&d->source);
877 init_String(&d->url); 887 init_String(&d->url);
878 init_String(&d->localHost); 888 init_String(&d->localHost);
@@ -888,6 +898,8 @@ void init_GmDocument(iGmDocument *d) {
888 d->siteIcon = 0; 898 d->siteIcon = 0;
889 d->media = new_Media(); 899 d->media = new_Media();
890 d->openURLs = NULL; 900 d->openURLs = NULL;
901 d->isPaletteValid = iFalse;
902 iZap(d->palette);
891} 903}
892 904
893void deinit_GmDocument(iGmDocument *d) { 905void deinit_GmDocument(iGmDocument *d) {
@@ -903,6 +915,7 @@ void deinit_GmDocument(iGmDocument *d) {
903 deinit_String(&d->localHost); 915 deinit_String(&d->localHost);
904 deinit_String(&d->url); 916 deinit_String(&d->url);
905 deinit_String(&d->source); 917 deinit_String(&d->source);
918 deinit_String(&d->unormSource);
906} 919}
907 920
908iMedia *media_GmDocument(iGmDocument *d) { 921iMedia *media_GmDocument(iGmDocument *d) {
@@ -913,6 +926,11 @@ const iMedia *constMedia_GmDocument(const iGmDocument *d) {
913 return d->media; 926 return d->media;
914} 927}
915 928
929const iString *url_GmDocument(const iGmDocument *d) {
930 return &d->url;
931}
932
933#if 0
916void reset_GmDocument(iGmDocument *d) { 934void reset_GmDocument(iGmDocument *d) {
917 clear_Media(d->media); 935 clear_Media(d->media);
918 clearLinks_GmDocument_(d); 936 clearLinks_GmDocument_(d);
@@ -921,8 +939,11 @@ void reset_GmDocument(iGmDocument *d) {
921 clear_Array(&d->preMeta); 939 clear_Array(&d->preMeta);
922 clear_String(&d->url); 940 clear_String(&d->url);
923 clear_String(&d->localHost); 941 clear_String(&d->localHost);
942 clear_String(&d->source);
943 clear_String(&d->unormSource);
924 d->themeSeed = 0; 944 d->themeSeed = 0;
925} 945}
946#endif
926 947
927static void setDerivedThemeColors_(enum iGmDocumentTheme theme) { 948static void setDerivedThemeColors_(enum iGmDocumentTheme theme) {
928 set_Color(tmQuoteIcon_ColorId, 949 set_Color(tmQuoteIcon_ColorId,
@@ -1395,9 +1416,23 @@ void setThemeSeed_GmDocument(iGmDocument *d, const iBlock *seed) {
1395 } 1416 }
1396 printf("---\n"); 1417 printf("---\n");
1397#endif 1418#endif
1419 /* Color functions operate on the global palette for convenience, but we may need to switch
1420 palettes on the fly if more than one GmDocument is being displayed simultaneously. */
1421 memcpy(d->palette, get_Root()->tmPalette, sizeof(d->palette));
1422 d->isPaletteValid = iTrue;
1398} 1423}
1399 1424
1400void setFormat_GmDocument(iGmDocument *d, enum iGmDocumentFormat format) { 1425void makePaletteGlobal_GmDocument(const iGmDocument *d) {
1426 if (d->isPaletteValid) {
1427 memcpy(get_Root()->tmPalette, d->palette, sizeof(d->palette));
1428 }
1429}
1430
1431void invalidatePalette_GmDocument(iGmDocument *d) {
1432 d->isPaletteValid = iFalse;
1433}
1434
1435void setFormat_GmDocument(iGmDocument *d, enum iSourceFormat format) {
1401 d->format = format; 1436 d->format = format;
1402} 1437}
1403 1438
@@ -1442,10 +1477,12 @@ static void normalize_GmDocument(iGmDocument *d) {
1442 iRangecc src = range_String(&d->source); 1477 iRangecc src = range_String(&d->source);
1443 iRangecc line = iNullRange; 1478 iRangecc line = iNullRange;
1444 iBool isPreformat = iFalse; 1479 iBool isPreformat = iFalse;
1445 if (d->format == plainText_GmDocumentFormat) { 1480 if (d->format == plainText_SourceFormat) {
1446 isPreformat = iTrue; /* Cannot be turned off. */ 1481 isPreformat = iTrue; /* Cannot be turned off. */
1447 } 1482 }
1448 const int preTabWidth = 4; /* TODO: user-configurable parameter */ 1483 const int preTabWidth = 4; /* TODO: user-configurable parameter */
1484 iBool wasNormalized = iFalse;
1485 iBool hasTabs = iFalse;
1449 while (nextSplit_Rangecc(src, "\n", &line)) { 1486 while (nextSplit_Rangecc(src, "\n", &line)) {
1450 if (isPreformat) { 1487 if (isPreformat) {
1451 /* Replace any tab characters with spaces for visualization. */ 1488 /* Replace any tab characters with spaces for visualization. */
@@ -1456,13 +1493,19 @@ static void normalize_GmDocument(iGmDocument *d) {
1456 while (numSpaces-- > 0) { 1493 while (numSpaces-- > 0) {
1457 appendCStrN_String(normalized, " ", 1); 1494 appendCStrN_String(normalized, " ", 1);
1458 } 1495 }
1496 hasTabs = iTrue;
1497 wasNormalized = iTrue;
1459 } 1498 }
1460 else if (*ch != '\r') { 1499 else if (*ch != '\v') {
1461 appendCStrN_String(normalized, ch, 1); 1500 appendCStrN_String(normalized, ch, 1);
1462 } 1501 }
1502 else {
1503 hasTabs = iTrue;
1504 wasNormalized = iTrue;
1505 }
1463 } 1506 }
1464 appendCStr_String(normalized, "\n"); 1507 appendCStr_String(normalized, "\n");
1465 if (d->format == gemini_GmDocumentFormat && 1508 if (d->format == gemini_SourceFormat &&
1466 lineType_GmDocument_(d, line) == preformatted_GmLineType) { 1509 lineType_GmDocument_(d, line) == preformatted_GmLineType) {
1467 isPreformat = iFalse; 1510 isPreformat = iFalse;
1468 } 1511 }
@@ -1478,7 +1521,10 @@ static void normalize_GmDocument(iGmDocument *d) {
1478 int spaceCount = 0; 1521 int spaceCount = 0;
1479 for (const char *ch = line.start; ch != line.end; ch++) { 1522 for (const char *ch = line.start; ch != line.end; ch++) {
1480 char c = *ch; 1523 char c = *ch;
1481 if (c == '\r') continue; 1524 if (c == '\v') {
1525 wasNormalized = iTrue;
1526 continue;
1527 }
1482 if (isNormalizableSpace_(c)) { 1528 if (isNormalizableSpace_(c)) {
1483 if (isPrevSpace) { 1529 if (isPrevSpace) {
1484 if (++spaceCount == 8) { 1530 if (++spaceCount == 8) {
@@ -1487,9 +1533,13 @@ static void normalize_GmDocument(iGmDocument *d) {
1487 popBack_Block(&normalized->chars); 1533 popBack_Block(&normalized->chars);
1488 pushBack_Block(&normalized->chars, '\t'); 1534 pushBack_Block(&normalized->chars, '\t');
1489 } 1535 }
1536 wasNormalized = iTrue;
1490 continue; /* skip repeated spaces */ 1537 continue; /* skip repeated spaces */
1491 } 1538 }
1492 c = ' '; 1539 if (c != ' ') {
1540 c = ' ';
1541 wasNormalized = iTrue;
1542 }
1493 isPrevSpace = iTrue; 1543 isPrevSpace = iTrue;
1494 } 1544 }
1495 else { 1545 else {
@@ -1500,8 +1550,14 @@ static void normalize_GmDocument(iGmDocument *d) {
1500 } 1550 }
1501 appendCStr_String(normalized, "\n"); 1551 appendCStr_String(normalized, "\n");
1502 } 1552 }
1553 printf("hasTabs: %d\n", hasTabs);
1554 printf("wasNormalized: %d\n", wasNormalized);
1555 fflush(stdout);
1503 set_String(&d->source, collect_String(normalized)); 1556 set_String(&d->source, collect_String(normalized));
1504 normalize_String(&d->source); /* NFC */ 1557 normalize_String(&d->source); /* NFC */
1558 printf("orig:%zu norm:%zu\n", size_String(&d->unormSource), size_String(&d->source));
1559 /* normalized source has an extra newline at the end */
1560// iAssert(wasNormalized || equal_String(&d->unormSource, &d->source));
1505} 1561}
1506 1562
1507void setUrl_GmDocument(iGmDocument *d, const iString *url) { 1563void setUrl_GmDocument(iGmDocument *d, const iString *url) {
@@ -1512,8 +1568,18 @@ void setUrl_GmDocument(iGmDocument *d, const iString *url) {
1512 updateIconBasedOnUrl_GmDocument_(d); 1568 updateIconBasedOnUrl_GmDocument_(d);
1513} 1569}
1514 1570
1515void setSource_GmDocument(iGmDocument *d, const iString *source, int width) { 1571void setSource_GmDocument(iGmDocument *d, const iString *source, int width,
1516 set_String(&d->source, source); 1572 enum iGmDocumentUpdate updateType) {
1573 printf("[GmDocument] source update (%zu bytes), width:%d, final:%d\n",
1574 size_String(source), width, updateType == final_GmDocumentUpdate);
1575 if (size_String(source) == size_String(&d->unormSource)) {
1576 iAssert(equal_String(source, &d->unormSource));
1577 printf("[GmDocument] source is unchanged!\n");
1578 return; /* Nothing to do. */
1579 }
1580 set_String(&d->unormSource, source);
1581 /* Normalize. */
1582 set_String(&d->source, &d->unormSource);
1517 if (isNormalized_GmDocument_(d)) { 1583 if (isNormalized_GmDocument_(d)) {
1518 normalize_GmDocument(d); 1584 normalize_GmDocument(d);
1519 } 1585 }
@@ -1615,6 +1681,14 @@ const iString *source_GmDocument(const iGmDocument *d) {
1615 return &d->source; 1681 return &d->source;
1616} 1682}
1617 1683
1684size_t memorySize_GmDocument(const iGmDocument *d) {
1685 return size_String(&d->unormSource) +
1686 size_String(&d->source) +
1687 size_Array(&d->layout) * sizeof(iGmRun) +
1688 size_Array(&d->links) * sizeof(iGmLink) +
1689 memorySize_Media(d->media);
1690}
1691
1618iRangecc findText_GmDocument(const iGmDocument *d, const iString *text, const char *start) { 1692iRangecc findText_GmDocument(const iGmDocument *d, const iString *text, const char *start) {
1619 const char * src = constBegin_String(&d->source); 1693 const char * src = constBegin_String(&d->source);
1620 const size_t startPos = (start ? start - src : 0); 1694 const size_t startPos = (start ? start - src : 0);
diff --git a/src/gmdocument.h b/src/gmdocument.h
index fcbb7e59..831459d8 100644
--- a/src/gmdocument.h
+++ b/src/gmdocument.h
@@ -22,6 +22,7 @@ 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 "gmutil.h" 26#include "gmutil.h"
26#include "media.h" 27#include "media.h"
27 28
@@ -125,12 +126,12 @@ enum iGmRunMediaType {
125 126
126struct Impl_GmRun { 127struct Impl_GmRun {
127 iRangecc text; 128 iRangecc text;
129 iRect bounds; /* used for hit testing, may extend to edges */
130 iRect visBounds; /* actual visual bounds */
128 uint8_t font; 131 uint8_t font;
129 uint8_t color; 132 uint8_t color;
130 uint8_t flags; 133 uint8_t flags;
131 uint8_t mediaType; 134 uint8_t mediaType;
132 iRect bounds; /* used for hit testing, may extend to edges */
133 iRect visBounds; /* actual visual bounds */
134 uint16_t preId; /* preformatted block ID (sequential) */ 135 uint16_t preId; /* preformatted block ID (sequential) */
135 iGmLinkId linkId; /* zero for non-links */ 136 iGmLinkId linkId; /* zero for non-links */
136 uint16_t mediaId; /* zero if not an image */ 137 uint16_t mediaId; /* zero if not an image */
@@ -148,34 +149,37 @@ iRangecc findLoc_GmRun (const iGmRun *, iInt2 pos);
148iDeclareClass(GmDocument) 149iDeclareClass(GmDocument)
149iDeclareObjectConstruction(GmDocument) 150iDeclareObjectConstruction(GmDocument)
150 151
151enum iGmDocumentFormat {
152 undefined_GmDocumentFormat = -1,
153 gemini_GmDocumentFormat = 0,
154 plainText_GmDocumentFormat,
155};
156
157enum iGmDocumentBanner { 152enum iGmDocumentBanner {
158 none_GmDocumentBanner, 153 none_GmDocumentBanner,
159 siteDomain_GmDocumentBanner, 154 siteDomain_GmDocumentBanner,
160 certificateWarning_GmDocumentBanner, 155 certificateWarning_GmDocumentBanner,
161}; 156};
162 157
158enum iGmDocumentUpdate {
159 partial_GmDocumentUpdate, /* appending more content */
160 final_GmDocumentUpdate, /* process all lines, including the last one if not terminated */
161};
162
163void setThemeSeed_GmDocument (iGmDocument *, const iBlock *seed); 163void setThemeSeed_GmDocument (iGmDocument *, const iBlock *seed);
164void setFormat_GmDocument (iGmDocument *, enum iGmDocumentFormat format); 164void setFormat_GmDocument (iGmDocument *, enum iSourceFormat format);
165void setBanner_GmDocument (iGmDocument *, enum iGmDocumentBanner type); 165void setBanner_GmDocument (iGmDocument *, enum iGmDocumentBanner type);
166void setWidth_GmDocument (iGmDocument *, int width); 166void setWidth_GmDocument (iGmDocument *, int width);
167void redoLayout_GmDocument (iGmDocument *); 167void redoLayout_GmDocument (iGmDocument *);
168iBool updateOpenURLs_GmDocument(iGmDocument *); 168iBool updateOpenURLs_GmDocument(iGmDocument *);
169void setUrl_GmDocument (iGmDocument *, const iString *url); 169void setUrl_GmDocument (iGmDocument *, const iString *url);
170void setSource_GmDocument (iGmDocument *, const iString *source, int width); 170void setSource_GmDocument (iGmDocument *, const iString *source, int width,
171 enum iGmDocumentUpdate updateType);
171void foldPre_GmDocument (iGmDocument *, uint16_t preId); 172void foldPre_GmDocument (iGmDocument *, uint16_t preId);
173void invalidatePalette_GmDocument(iGmDocument *);
174void makePaletteGlobal_GmDocument(const iGmDocument *); /* copies document colors to the global palette */
172 175
173void reset_GmDocument (iGmDocument *); /* free images */ 176//void reset_GmDocument (iGmDocument *); /* free images */
174 177
175typedef void (*iGmDocumentRenderFunc)(void *, const iGmRun *); 178typedef void (*iGmDocumentRenderFunc)(void *, const iGmRun *);
176 179
177iMedia * media_GmDocument (iGmDocument *); 180iMedia * media_GmDocument (iGmDocument *);
178const iMedia * constMedia_GmDocument (const iGmDocument *); 181const iMedia * constMedia_GmDocument (const iGmDocument *);
182const iString * url_GmDocument (const iGmDocument *);
179 183
180void render_GmDocument (const iGmDocument *, iRangei visRangeY, 184void render_GmDocument (const iGmDocument *, iRangei visRangeY,
181 iGmDocumentRenderFunc render, void *); /* includes partial overlaps */ 185 iGmDocumentRenderFunc render, void *); /* includes partial overlaps */
@@ -190,6 +194,7 @@ enum iGmDocumentBanner bannerType_GmDocument(const iGmDocument *);
190const iString * bannerText_GmDocument (const iGmDocument *); 194const iString * bannerText_GmDocument (const iGmDocument *);
191const iArray * headings_GmDocument (const iGmDocument *); /* array of GmHeadings */ 195const iArray * headings_GmDocument (const iGmDocument *); /* array of GmHeadings */
192const iString * source_GmDocument (const iGmDocument *); 196const iString * source_GmDocument (const iGmDocument *);
197size_t memorySize_GmDocument (const iGmDocument *); /* bytes */
193 198
194iRangecc findText_GmDocument (const iGmDocument *, const iString *text, const char *start); 199iRangecc findText_GmDocument (const iGmDocument *, const iString *text, const char *start);
195iRangecc findTextBefore_GmDocument (const iGmDocument *, const iString *text, const char *before); 200iRangecc findTextBefore_GmDocument (const iGmDocument *, const iString *text, const char *before);
diff --git a/src/gmtypesetter.c b/src/gmtypesetter.c
new file mode 100644
index 00000000..29a1bd93
--- /dev/null
+++ b/src/gmtypesetter.c
@@ -0,0 +1,25 @@
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 "gmtypesetter.h"
24#include "gmdocument.h"
25
diff --git a/src/gmtypesetter.h b/src/gmtypesetter.h
new file mode 100644
index 00000000..aba351dd
--- /dev/null
+++ b/src/gmtypesetter.h
@@ -0,0 +1,41 @@
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 "defs.h"
26
27#include <the_Foundation/array.h>
28#include <the_Foundation/string.h>
29#include <the_Foundation/vec2.h>
30
31/* GmTypesetter has two jobs: it normalizes incoming source text, and typesets it as a
32 sequence of GmRuns. New data can be appended progressively. */
33
34iDeclareType(GmTypesetter)
35iDeclareTypeConstruction(GmTypesetter)
36
37void reset_GmTypesetter (iGmTypesetter *, enum iSourceFormat format);
38void setWidth_GmTypesetter (iGmTypesetter *, int width);
39void addInput_GmTypesetter (iGmTypesetter *, const iString *source);
40iBool getRuns_GmTypesetter (iGmTypesetter *, iArray *runs_out); /* returns false when no output generated */
41void skip_GmTypesetter (iGmTypesetter *, int ySkip);
diff --git a/src/gmutil.c b/src/gmutil.c
index 3ca93901..7a1ae938 100644
--- a/src/gmutil.c
+++ b/src/gmutil.c
@@ -556,7 +556,7 @@ const iString *feedEntryOpenCommand_String(const iString *url, int newTab) {
556 iString *head = newRange_String( 556 iString *head = newRange_String(
557 (iRangecc){ constBegin_String(url) + fragPos + 1, constEnd_String(url) }); 557 (iRangecc){ constBegin_String(url) + fragPos + 1, constEnd_String(url) });
558 format_String(cmd, 558 format_String(cmd,
559 "open newtab:%d gotourlheading:%s url:%s", 559 "open fromsidebar:1 newtab:%d gotourlheading:%s url:%s",
560 newTab, 560 newTab,
561 cstr_String(head), 561 cstr_String(head),
562 cstr_Rangecc((iRangecc){ constBegin_String(url), 562 cstr_Rangecc((iRangecc){ constBegin_String(url),
@@ -564,7 +564,7 @@ const iString *feedEntryOpenCommand_String(const iString *url, int newTab) {
564 delete_String(head); 564 delete_String(head);
565 } 565 }
566 else { 566 else {
567 format_String(cmd, "open newtab:%d url:%s", newTab, cstr_String(url)); 567 format_String(cmd, "open fromsidebar:1 newtab:%d url:%s", newTab, cstr_String(url));
568 } 568 }
569 return cmd; 569 return cmd;
570 } 570 }
diff --git a/src/history.c b/src/history.c
index ed8e6725..2cea393d 100644
--- a/src/history.c
+++ b/src/history.c
@@ -34,11 +34,14 @@ static const size_t maxStack_History_ = 50; /* back/forward navigable items */
34 34
35void init_RecentUrl(iRecentUrl *d) { 35void init_RecentUrl(iRecentUrl *d) {
36 init_String(&d->url); 36 init_String(&d->url);
37 d->normScrollY = 0; 37 d->normScrollY = 0;
38 d->cachedResponse = NULL; 38 d->cachedResponse = NULL;
39 d->cachedDoc = NULL;
40 d->flags.openedFromSidebar = iFalse;
39} 41}
40 42
41void deinit_RecentUrl(iRecentUrl *d) { 43void deinit_RecentUrl(iRecentUrl *d) {
44 iRelease(d->cachedDoc);
42 deinit_String(&d->url); 45 deinit_String(&d->url);
43 delete_GmResponse(d->cachedResponse); 46 delete_GmResponse(d->cachedResponse);
44} 47}
@@ -48,11 +51,30 @@ iDefineTypeConstruction(RecentUrl)
48iRecentUrl *copy_RecentUrl(const iRecentUrl *d) { 51iRecentUrl *copy_RecentUrl(const iRecentUrl *d) {
49 iRecentUrl *copy = new_RecentUrl(); 52 iRecentUrl *copy = new_RecentUrl();
50 set_String(&copy->url, &d->url); 53 set_String(&copy->url, &d->url);
51 copy->normScrollY = d->normScrollY; 54 copy->normScrollY = d->normScrollY;
52 copy->cachedResponse = d->cachedResponse ? copy_GmResponse(d->cachedResponse) : NULL; 55 copy->cachedResponse = d->cachedResponse ? copy_GmResponse(d->cachedResponse) : NULL;
56 copy->cachedDoc = ref_Object(d->cachedDoc);
57 copy->flags = d->flags;
53 return copy; 58 return copy;
54} 59}
55 60
61size_t cacheSize_RecentUrl(const iRecentUrl *d) {
62 size_t size = 0;
63 if (d->cachedResponse) {
64 size += size_String(&d->cachedResponse->meta);
65 size += size_Block(&d->cachedResponse->body);
66 }
67 return size;
68}
69
70size_t memorySize_RecentUrl(const iRecentUrl *d) {
71 size_t size = cacheSize_RecentUrl(d);
72 if (d->cachedDoc) {
73 size += memorySize_GmDocument(d->cachedDoc);
74 }
75 return size;
76}
77
56/*----------------------------------------------------------------------------------------------*/ 78/*----------------------------------------------------------------------------------------------*/
57 79
58struct Impl_History { 80struct Impl_History {
@@ -88,24 +110,45 @@ iHistory *copy_History(const iHistory *d) {
88 return copy; 110 return copy;
89} 111}
90 112
113iMemInfo memoryUsage_History(const iHistory *d) {
114 iMemInfo mem = { 0, 0 };
115 iConstForEach(Array, i, &d->recent) {
116 const iRecentUrl *item = i.value;
117 mem.cacheSize += cacheSize_RecentUrl(item);
118 mem.memorySize += memorySize_RecentUrl(item);
119 }
120 return mem;
121}
122
91iString *debugInfo_History(const iHistory *d) { 123iString *debugInfo_History(const iHistory *d) {
92 iString *str = new_String(); 124 iString *str = new_String();
93 format_String(str, 125 format_String(str,
94 "```\n" 126 "```\n"
95 "Idx | Size | SP%% | URL\n" 127 "Idx | Cache | Memory | SP%% | URL\n"
96 "----+---------+-----+-----\n"); 128 "----+---------+----------+-----+-----\n");
97 size_t totalSize = 0; 129 size_t totalCache = 0;
130 size_t totalMemory = 0;
98 iConstForEach(Array, i, &d->recent) { 131 iConstForEach(Array, i, &d->recent) {
99 const iRecentUrl *item = i.value; 132 const iRecentUrl *item = i.value;
100 appendFormat_String( 133 appendFormat_String(
101 str, " %2zu | ", size_Array(&d->recent) - index_ArrayConstIterator(&i) - 1); 134 str, " %2zu | ", size_Array(&d->recent) - index_ArrayConstIterator(&i) - 1);
102 if (item->cachedResponse) { 135 const size_t cacheSize = cacheSize_RecentUrl(item);
103 appendFormat_String(str, "%7zu", size_Block(&item->cachedResponse->body)); 136 const size_t memSize = memorySize_RecentUrl(item);
104 totalSize += size_Block(&item->cachedResponse->body); 137 if (cacheSize) {
138 appendFormat_String(str, "%7zu", cacheSize);
139 totalCache += cacheSize;
105 } 140 }
106 else { 141 else {
107 appendFormat_String(str, " --"); 142 appendFormat_String(str, " --");
108 } 143 }
144 appendCStr_String(str, " | ");
145 if (memSize) {
146 appendFormat_String(str, "%8zu", memSize);
147 totalMemory += memSize;
148 }
149 else {
150 appendFormat_String(str, " --");
151 }
109 appendFormat_String(str, 152 appendFormat_String(str,
110 " | %3d | %s\n", 153 " | %3d | %s\n",
111 iRound(100.0f * item->normScrollY), 154 iRound(100.0f * item->normScrollY),
@@ -114,8 +157,10 @@ iString *debugInfo_History(const iHistory *d) {
114 appendFormat_String(str, "\n```\n"); 157 appendFormat_String(str, "\n```\n");
115 appendFormat_String(str, 158 appendFormat_String(str,
116 "Total cached data: %.3f MB\n" 159 "Total cached data: %.3f MB\n"
160 "Total memory usage: %.3f MB\n"
117 "Navigation position: %zu\n\n", 161 "Navigation position: %zu\n\n",
118 totalSize / 1.0e6f, 162 totalCache / 1.0e6f,
163 totalMemory / 1.0e6f,
119 d->recentPos); 164 d->recentPos);
120 return str; 165 return str;
121} 166}
@@ -128,6 +173,7 @@ void serialize_History(const iHistory *d, iStream *outs) {
128 const iRecentUrl *item = i.value; 173 const iRecentUrl *item = i.value;
129 serialize_String(&item->url, outs); 174 serialize_String(&item->url, outs);
130 write32_Stream(outs, item->normScrollY * 1.0e6f); 175 write32_Stream(outs, item->normScrollY * 1.0e6f);
176 writeU16_Stream(outs, item->flags.openedFromSidebar ? iBit(1) : 0);
131 if (item->cachedResponse) { 177 if (item->cachedResponse) {
132 write8_Stream(outs, 1); 178 write8_Stream(outs, 1);
133 serialize_GmResponse(item->cachedResponse, outs); 179 serialize_GmResponse(item->cachedResponse, outs);
@@ -149,6 +195,12 @@ void deserialize_History(iHistory *d, iStream *ins) {
149 init_RecentUrl(&item); 195 init_RecentUrl(&item);
150 deserialize_String(&item.url, ins); 196 deserialize_String(&item.url, ins);
151 item.normScrollY = (float) read32_Stream(ins) / 1.0e6f; 197 item.normScrollY = (float) read32_Stream(ins) / 1.0e6f;
198 if (version_Stream(ins) >= addedRecentUrlFlags_FileVersion) {
199 uint16_t flags = readU16_Stream(ins);
200 if (flags & iBit(1)) {
201 item.flags.openedFromSidebar = iTrue;
202 }
203 }
152 if (read8_Stream(ins)) { 204 if (read8_Stream(ins)) {
153 item.cachedResponse = new_GmResponse(); 205 item.cachedResponse = new_GmResponse();
154 deserialize_GmResponse(item.cachedResponse, ins); 206 deserialize_GmResponse(item.cachedResponse, ins);
@@ -241,6 +293,39 @@ void add_History(iHistory *d, const iString *url ){
241 unlock_Mutex(d->mtx); 293 unlock_Mutex(d->mtx);
242} 294}
243 295
296iBool preceding_History(iHistory *d, iRecentUrl *recent_out) {
297 iBool ok = iFalse;
298 lock_Mutex(d->mtx);
299 if (!isEmpty_Array(&d->recent) && d->recentPos < size_Array(&d->recent) - 1) {
300 const iRecentUrl *recent = constAt_Array(&d->recent, size_Array(&d->recent) - 1 -
301 (d->recentPos + 1));
302 set_String(&recent_out->url, &recent->url);
303 recent_out->normScrollY = recent->normScrollY;
304 iChangeRef(recent_out->cachedDoc, recent->cachedDoc);
305 /* Cached response is not returned, would involve a deep copy. */
306 ok = iTrue;
307 }
308 unlock_Mutex(d->mtx);
309 return ok;
310}
311
312#if 0
313iBool following_History(iHistory *d, iRecentUrl *recent_out) {
314 iBool ok = iFalse;
315 lock_Mutex(d->mtx);
316 if (!isEmpty_Array(&d->recent) && d->recentPos > 0) {
317 const iRecentUrl *recent = constAt_Array(&d->recent, d->recentPos - 1);
318 set_String(&recent_out->url, &recent->url);
319 recent_out->normScrollY = recent->normScrollY;
320 recent_out->cachedDoc = ref_Object(recent->cachedDoc);
321 /* Cached response is not returned, would involve a deep copy. */
322 ok = iTrue;
323 }
324 unlock_Mutex(d->mtx);
325 return ok;
326}
327#endif
328
244iBool goBack_History(iHistory *d) { 329iBool goBack_History(iHistory *d) {
245 lock_Mutex(d->mtx); 330 lock_Mutex(d->mtx);
246 if (!isEmpty_Array(&d->recent) && d->recentPos < size_Array(&d->recent) - 1) { 331 if (!isEmpty_Array(&d->recent) && d->recentPos < size_Array(&d->recent) - 1) {
@@ -302,19 +387,42 @@ void setCachedResponse_History(iHistory *d, const iGmResponse *response) {
302 unlock_Mutex(d->mtx); 387 unlock_Mutex(d->mtx);
303} 388}
304 389
390void setCachedDocument_History(iHistory *d, iGmDocument *doc, iBool openedFromSidebar) {
391 lock_Mutex(d->mtx);
392 iRecentUrl *item = mostRecentUrl_History(d);
393 if (item) {
394 iAssert(equal_String(url_GmDocument(doc), &item->url));
395 item->flags.openedFromSidebar = openedFromSidebar;
396 if (item->cachedDoc != doc) {
397 iRelease(item->cachedDoc);
398 item->cachedDoc = ref_Object(doc);
399 }
400 }
401 unlock_Mutex(d->mtx);
402}
403
305size_t cacheSize_History(const iHistory *d) { 404size_t cacheSize_History(const iHistory *d) {
306 size_t cached = 0; 405 size_t cached = 0;
307 lock_Mutex(d->mtx); 406 lock_Mutex(d->mtx);
308 iConstForEach(Array, i, &d->recent) { 407 iConstForEach(Array, i, &d->recent) {
309 const iRecentUrl *url = i.value; 408 const iRecentUrl *url = i.value;
310 if (url->cachedResponse) { 409 cached += cacheSize_RecentUrl(url);
311 cached += size_Block(&url->cachedResponse->body);
312 }
313 } 410 }
314 unlock_Mutex(d->mtx); 411 unlock_Mutex(d->mtx);
315 return cached; 412 return cached;
316} 413}
317 414
415size_t memorySize_History(const iHistory *d) {
416 size_t bytes = 0;
417 lock_Mutex(d->mtx);
418 iConstForEach(Array, i, &d->recent) {
419 const iRecentUrl *url = i.value;
420 bytes += memorySize_RecentUrl(url);
421 }
422 unlock_Mutex(d->mtx);
423 return bytes;
424}
425
318void clearCache_History(iHistory *d) { 426void clearCache_History(iHistory *d) {
319 lock_Mutex(d->mtx); 427 lock_Mutex(d->mtx);
320 iForEach(Array, i, &d->recent) { 428 iForEach(Array, i, &d->recent) {
@@ -323,6 +431,7 @@ void clearCache_History(iHistory *d) {
323 delete_GmResponse(url->cachedResponse); 431 delete_GmResponse(url->cachedResponse);
324 url->cachedResponse = NULL; 432 url->cachedResponse = NULL;
325 } 433 }
434 iReleasePtr(&url->cachedDoc); /* release all cached documents and media as well */
326 } 435 }
327 unlock_Mutex(d->mtx); 436 unlock_Mutex(d->mtx);
328} 437}
@@ -338,7 +447,7 @@ size_t pruneLeastImportant_History(iHistory *d) {
338 const iRecentUrl *url = i.value; 447 const iRecentUrl *url = i.value;
339 if (url->cachedResponse) { 448 if (url->cachedResponse) {
340 const double urlScore = 449 const double urlScore =
341 size_Block(&url->cachedResponse->body) * 450 cacheSize_RecentUrl(url) *
342 pow(secondsSince_Time(&now, &url->cachedResponse->when) / 60.0, 1.25); 451 pow(secondsSince_Time(&now, &url->cachedResponse->when) / 60.0, 1.25);
343 if (urlScore > score) { 452 if (urlScore > score) {
344 chosen = index_ArrayConstIterator(&i); 453 chosen = index_ArrayConstIterator(&i);
@@ -348,14 +457,60 @@ size_t pruneLeastImportant_History(iHistory *d) {
348 } 457 }
349 if (chosen != iInvalidPos) { 458 if (chosen != iInvalidPos) {
350 iRecentUrl *url = at_Array(&d->recent, chosen); 459 iRecentUrl *url = at_Array(&d->recent, chosen);
351 delta = size_Block(&url->cachedResponse->body); 460 delta = cacheSize_RecentUrl(url);
352 delete_GmResponse(url->cachedResponse); 461 delete_GmResponse(url->cachedResponse);
353 url->cachedResponse = NULL; 462 url->cachedResponse = NULL;
463 iReleasePtr(&url->cachedDoc);
354 } 464 }
355 unlock_Mutex(d->mtx); 465 unlock_Mutex(d->mtx);
356 return delta; 466 return delta;
357} 467}
358 468
469size_t pruneLeastImportantMemory_History(iHistory *d) {
470 size_t delta = 0;
471 size_t chosen = iInvalidPos;
472 double score = 0.0f;
473 iTime now;
474 initCurrent_Time(&now);
475 lock_Mutex(d->mtx);
476 iConstForEach(Array, i, &d->recent) {
477 const iRecentUrl *url = i.value;
478 if (d->recentPos == size_Array(&d->recent) - index_ArrayConstIterator(&i) - 1) {
479 continue; /* Not the current navigation position. */
480 }
481 if (url->cachedDoc) {
482 const double urlScore =
483 memorySize_RecentUrl(url) *
484 (url->cachedResponse
485 ? pow(secondsSince_Time(&now, &url->cachedResponse->when) / 60.0, 1.25)
486 : 1.0);
487 if (urlScore > score) {
488 chosen = index_ArrayConstIterator(&i);
489 score = urlScore;
490 }
491 }
492 }
493 if (chosen != iInvalidPos) {
494 iRecentUrl *url = at_Array(&d->recent, chosen);
495 const size_t before = memorySize_RecentUrl(url);
496 iReleasePtr(&url->cachedDoc);
497 delta = before - memorySize_RecentUrl(url);
498 }
499 unlock_Mutex(d->mtx);
500 return delta;
501}
502
503void invalidateTheme_History(iHistory *d) {
504 lock_Mutex(d->mtx);
505 iForEach(Array, i, &d->recent) {
506 iRecentUrl *r = i.value;
507 if (r->cachedDoc) {
508 invalidatePalette_GmDocument(r->cachedDoc);
509 }
510 }
511 unlock_Mutex(d->mtx);
512}
513
359const iStringArray *searchContents_History(const iHistory *d, const iRegExp *pattern) { 514const iStringArray *searchContents_History(const iHistory *d, const iRegExp *pattern) {
360 iStringArray *urls = iClob(new_StringArray()); 515 iStringArray *urls = iClob(new_StringArray());
361 lock_Mutex(d->mtx); 516 lock_Mutex(d->mtx);
diff --git a/src/history.h b/src/history.h
index 164a61d6..7dad72df 100644
--- a/src/history.h
+++ b/src/history.h
@@ -22,6 +22,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22 22
23#pragma once 23#pragma once
24 24
25#include "gmdocument.h"
25#include "gmrequest.h" 26#include "gmrequest.h"
26 27
27#include <the_Foundation/ptrarray.h> 28#include <the_Foundation/ptrarray.h>
@@ -37,6 +38,17 @@ struct Impl_RecentUrl {
37 iString url; 38 iString url;
38 float normScrollY; /* normalized to document height */ 39 float normScrollY; /* normalized to document height */
39 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) */
42 struct {
43 uint8_t openedFromSidebar : 1;
44 } flags;
45};
46
47iDeclareType(MemInfo)
48
49struct Impl_MemInfo {
50 size_t cacheSize; /* number of bytes stored persistently */
51 size_t memorySize; /* number of bytes stored in RAM */
40}; 52};
41 53
42/*----------------------------------------------------------------------------------------------*/ 54/*----------------------------------------------------------------------------------------------*/
@@ -51,13 +63,19 @@ void clear_History (iHistory *);
51void add_History (iHistory *, const iString *url); 63void add_History (iHistory *, const iString *url);
52void replace_History (iHistory *, const iString *url); 64void replace_History (iHistory *, const iString *url);
53void setCachedResponse_History (iHistory *, const iGmResponse *response); 65void setCachedResponse_History (iHistory *, const iGmResponse *response);
66void setCachedDocument_History (iHistory *, iGmDocument *doc, iBool openedFromSidebar);
54iBool goBack_History (iHistory *); 67iBool goBack_History (iHistory *);
55iBool goForward_History (iHistory *); 68iBool goForward_History (iHistory *);
69iBool preceding_History (iHistory *d, iRecentUrl *recent_out);
70//iBool following_History (iHistory *d, iRecentUrl *recent_out);
56iRecentUrl *recentUrl_History (iHistory *, size_t pos); 71iRecentUrl *recentUrl_History (iHistory *, size_t pos);
57iRecentUrl *mostRecentUrl_History (iHistory *); 72iRecentUrl *mostRecentUrl_History (iHistory *);
58iRecentUrl *findUrl_History (iHistory *, const iString *url); 73iRecentUrl *findUrl_History (iHistory *, const iString *url);
59void clearCache_History (iHistory *); 74
60size_t pruneLeastImportant_History (iHistory *); 75void clearCache_History (iHistory *);
76size_t pruneLeastImportant_History (iHistory *);
77size_t pruneLeastImportantMemory_History (iHistory *);
78void invalidateTheme_History (iHistory *); /* theme has changed, cached contents need updating */
61 79
62iBool atLatest_History (const iHistory *); 80iBool atLatest_History (const iHistory *);
63iBool atOldest_History (const iHistory *); 81iBool atOldest_History (const iHistory *);
@@ -73,6 +91,7 @@ const iRecentUrl *
73const iGmResponse * 91const iGmResponse *
74 cachedResponse_History (const iHistory *); 92 cachedResponse_History (const iHistory *);
75size_t cacheSize_History (const iHistory *); 93size_t cacheSize_History (const iHistory *);
94size_t memorySize_History (const iHistory *);
76 95
77iString * debugInfo_History (const iHistory *); 96iString * debugInfo_History (const iHistory *);
78 97iMemInfo memoryUsage_History (const iHistory *);
diff --git a/src/ios.h b/src/ios.h
index 578c85fe..70b889bf 100644
--- a/src/ios.h
+++ b/src/ios.h
@@ -28,6 +28,7 @@ iDeclareType(Window)
28 28
29enum iHapticEffect { 29enum iHapticEffect {
30 tap_HapticEffect, 30 tap_HapticEffect,
31 gentleTap_HapticEffect,
31}; 32};
32 33
33void setupApplication_iOS (void); 34void setupApplication_iOS (void);
@@ -35,6 +36,7 @@ void setupWindow_iOS (iWindow *window);
35iBool processEvent_iOS (const SDL_Event *); 36iBool processEvent_iOS (const SDL_Event *);
36void playHapticEffect_iOS (enum iHapticEffect effect); 37void playHapticEffect_iOS (enum iHapticEffect effect);
37void exportDownloadedFile_iOS(const iString *path); 38void exportDownloadedFile_iOS(const iString *path);
39void pickFileForOpening_iOS (void);
38 40
39iBool isPhone_iOS (void); 41iBool isPhone_iOS (void);
40void safeAreaInsets_iOS (float *left, float *top, float *right, float *bottom); 42void safeAreaInsets_iOS (float *left, float *top, float *right, float *bottom);
@@ -55,3 +57,6 @@ double currentTime_AVFAudioPlayer (const iAVFAudioPlayer *);
55double duration_AVFAudioPlayer (const iAVFAudioPlayer *); 57double duration_AVFAudioPlayer (const iAVFAudioPlayer *);
56iBool isStarted_AVFAudioPlayer (const iAVFAudioPlayer *); 58iBool isStarted_AVFAudioPlayer (const iAVFAudioPlayer *);
57iBool isPaused_AVFAudioPlayer (const iAVFAudioPlayer *); 59iBool isPaused_AVFAudioPlayer (const iAVFAudioPlayer *);
60
61void clearNowPlayingInfo_iOS (void);
62void updateNowPlayingInfo_iOS (void);
diff --git a/src/ios.m b/src/ios.m
index a1654df2..b82d54a7 100644
--- a/src/ios.m
+++ b/src/ios.m
@@ -22,6 +22,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22 22
23#include "ios.h" 23#include "ios.h"
24#include "app.h" 24#include "app.h"
25#include "audio/player.h"
25#include "ui/command.h" 26#include "ui/command.h"
26#include "ui/window.h" 27#include "ui/window.h"
27 28
@@ -32,9 +33,10 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
32#include <SDL_syswm.h> 33#include <SDL_syswm.h>
33#include <SDL_timer.h> 34#include <SDL_timer.h>
34 35
35#import <UIKit/UIKit.h>
36#import <CoreHaptics/CoreHaptics.h>
37#import <AVFAudio/AVFAudio.h> 36#import <AVFAudio/AVFAudio.h>
37#import <CoreHaptics/CoreHaptics.h>
38#import <UIKit/UIKit.h>
39#import <MediaPlayer/MediaPlayer.h>
38 40
39static iBool isSystemDarkMode_ = iFalse; 41static iBool isSystemDarkMode_ = iFalse;
40static iBool isPhone_ = iFalse; 42static iBool isPhone_ = iFalse;
@@ -64,6 +66,7 @@ API_AVAILABLE(ios(13.0))
64@interface HapticState : NSObject 66@interface HapticState : NSObject
65@property (nonatomic, strong) CHHapticEngine *engine; 67@property (nonatomic, strong) CHHapticEngine *engine;
66@property (nonatomic, strong) NSDictionary *tapDef; 68@property (nonatomic, strong) NSDictionary *tapDef;
69@property (nonatomic, strong) NSDictionary *gentleTapDef;
67@end 70@end
68 71
69@implementation HapticState 72@implementation HapticState
@@ -105,26 +108,47 @@ API_AVAILABLE(ios(13.0))
105 CHHapticPatternKeyEvent: @{ 108 CHHapticPatternKeyEvent: @{
106 CHHapticPatternKeyEventType: CHHapticEventTypeHapticTransient, 109 CHHapticPatternKeyEventType: CHHapticEventTypeHapticTransient,
107 CHHapticPatternKeyTime: @0.0, 110 CHHapticPatternKeyTime: @0.0,
108 CHHapticPatternKeyEventDuration:@0.1 111 CHHapticPatternKeyEventDuration:@0.1,
112 CHHapticPatternKeyEventParameters: @[
113 @{
114 CHHapticPatternKeyParameterID: CHHapticEventParameterIDHapticIntensity,
115 CHHapticPatternKeyParameterValue: @1.0
116 }
117 ]
118 },
119 },
120 ]
121 };
122 self.gentleTapDef = @{
123 CHHapticPatternKeyPattern:
124 @[
125 @{
126 CHHapticPatternKeyEvent: @{
127 CHHapticPatternKeyEventType: CHHapticEventTypeHapticTransient,
128 CHHapticPatternKeyTime: @0.0,
129 CHHapticPatternKeyEventDuration:@0.1,
130 CHHapticPatternKeyEventParameters: @[
131 @{
132 CHHapticPatternKeyParameterID: CHHapticEventParameterIDHapticIntensity,
133 CHHapticPatternKeyParameterValue: @0.33
134 }
135 ]
109 }, 136 },
110 }, 137 },
111 ], 138 ]
112 }; 139 };
113} 140}
114 141
115-(void)playTapEffect { 142
143-(void)playHapticEffect:(NSDictionary *)def {
116 NSError *error = nil; 144 NSError *error = nil;
117 CHHapticPattern *pattern = [[CHHapticPattern alloc] initWithDictionary:self.tapDef 145 CHHapticPattern *pattern = [[CHHapticPattern alloc] initWithDictionary:def
118 error:&error]; 146 error:&error];
119 // TODO: Check the error. 147 // TODO: Check the error.
120 id<CHHapticPatternPlayer> player = [self.engine createPlayerWithPattern:pattern error:&error]; 148 id<CHHapticPatternPlayer> player = [self.engine createPlayerWithPattern:pattern error:&error];
121 // TODO: Check the error. 149 // TODO: Check the error.
122 [self.engine startWithCompletionHandler:^(NSError *err){ 150 [self.engine startWithCompletionHandler:^(NSError *err){
123 if (err == nil) { 151 if (err == nil) {
124 /* Just keep it running. */
125// [self.engine notifyWhenPlayersFinished:^(NSError * _Nullable error) {
126// return CHHapticEngineFinishedActionStopEngine;
127// }];
128 NSError *startError = nil; 152 NSError *startError = nil;
129 [player startAtTime:0.0 error:&startError]; 153 [player startAtTime:0.0 error:&startError];
130 } 154 }
@@ -181,11 +205,22 @@ static AppState *appState_;
181 205
182- (void)documentPicker:(UIDocumentPickerViewController *)controller 206- (void)documentPicker:(UIDocumentPickerViewController *)controller
183didPickDocumentsAtURLs:(NSArray<NSURL *> *)urls { 207didPickDocumentsAtURLs:(NSArray<NSURL *> *)urls {
184 [self removeSavedFile]; 208 if (fileBeingSaved) {
209 [self removeSavedFile];
210 }
211 else {
212 /* A file is being opened. */
213 NSURL *url = [urls firstObject];
214 iString *path = localFilePathFromUrl_String(collectNewCStr_String([[url absoluteString]
215 UTF8String]));
216 postCommandf_App("file.open temp:1 path:%s", cstrCollect_String(path));
217 }
185} 218}
186 219
187- (void)documentPickerWasCancelled:(UIDocumentPickerViewController *)controller { 220- (void)documentPickerWasCancelled:(UIDocumentPickerViewController *)controller {
188 [self removeSavedFile]; 221 if (fileBeingSaved) {
222 [self removeSavedFile];
223 }
189} 224}
190 225
191-(void)keyboardOnScreen:(NSNotification *)notification { 226-(void)keyboardOnScreen:(NSNotification *)notification {
@@ -230,6 +265,55 @@ void setupApplication_iOS(void) {
230 name:UIKeyboardWillHideNotification 265 name:UIKeyboardWillHideNotification
231 object:nil]; 266 object:nil];
232 [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:nil]; 267 [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:nil];
268 /* Media player remote controls. */
269 MPRemoteCommandCenter *commandCenter = [MPRemoteCommandCenter sharedCommandCenter];
270 [[commandCenter pauseCommand] addTargetWithHandler:^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent * _Nonnull event) {
271 iPlayer *player = active_Player();
272 if (player) {
273 setPaused_Player(player, iTrue);
274 return MPRemoteCommandHandlerStatusSuccess;
275 }
276 return MPRemoteCommandHandlerStatusCommandFailed;
277 }];
278 [[commandCenter playCommand] addTargetWithHandler:^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent * _Nonnull event) {
279 iPlayer *player = active_Player();
280 if (player) {
281 if (isPaused_Player(player)) {
282 setPaused_Player(player, iFalse);
283 }
284 else {
285 start_Player(player);
286 }
287 return MPRemoteCommandHandlerStatusSuccess;
288 }
289 return MPRemoteCommandHandlerStatusCommandFailed;
290 }];
291 [[commandCenter stopCommand] addTargetWithHandler:^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent * _Nonnull event) {
292 iPlayer *player = active_Player();
293 if (player) {
294 stop_Player(player);
295 return MPRemoteCommandHandlerStatusSuccess;
296 }
297 return MPRemoteCommandHandlerStatusCommandFailed;
298 }];
299 [[commandCenter togglePlayPauseCommand] addTargetWithHandler:^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent * _Nonnull event) {
300 iPlayer *player = active_Player();
301 if (player) {
302 setPaused_Player(player, !isPaused_Player(player));
303 return MPRemoteCommandHandlerStatusSuccess;
304 }
305 return MPRemoteCommandHandlerStatusCommandFailed;
306 }];
307 [[commandCenter nextTrackCommand] setEnabled:NO];
308 [[commandCenter previousTrackCommand] setEnabled:NO];
309 [[commandCenter changeRepeatModeCommand] setEnabled:NO];
310 [[commandCenter changeShuffleModeCommand] setEnabled:NO];
311 [[commandCenter changePlaybackRateCommand] setEnabled:NO];
312 [[commandCenter seekForwardCommand] setEnabled:NO];
313 [[commandCenter seekBackwardCommand] setEnabled:NO];
314 [[commandCenter skipForwardCommand] setEnabled:NO];
315 [[commandCenter skipBackwardCommand] setEnabled:NO];
316 [[commandCenter changePlaybackPositionCommand] setEnabled:NO];
233} 317}
234 318
235static iBool isDarkMode_(iWindow *window) { 319static iBool isDarkMode_(iWindow *window) {
@@ -265,7 +349,7 @@ iBool isPhone_iOS(void) {
265} 349}
266 350
267int displayRefreshRate_iOS(void) { 351int displayRefreshRate_iOS(void) {
268 return uiWindow_(get_Window()).screen.maximumFramesPerSecond; 352 return (int) uiWindow_(get_Window()).screen.maximumFramesPerSecond;
269} 353}
270 354
271void setupWindow_iOS(iWindow *window) { 355void setupWindow_iOS(iWindow *window) {
@@ -279,7 +363,10 @@ void playHapticEffect_iOS(enum iHapticEffect effect) {
279 HapticState *hs = (HapticState *) appState_.haptic; 363 HapticState *hs = (HapticState *) appState_.haptic;
280 switch(effect) { 364 switch(effect) {
281 case tap_HapticEffect: 365 case tap_HapticEffect:
282 [hs playTapEffect]; 366 [hs playHapticEffect:hs.tapDef];
367 break;
368 case gentleTap_HapticEffect:
369 [hs playHapticEffect:hs.gentleTapDef];
283 break; 370 break;
284 } 371 }
285 } 372 }
@@ -324,6 +411,38 @@ iBool processEvent_iOS(const SDL_Event *ev) {
324 return iFalse; /* allow normal processing */ 411 return iFalse; /* allow normal processing */
325} 412}
326 413
414void updateNowPlayingInfo_iOS(void) {
415 const iPlayer *player = active_Player();
416 if (!player) {
417 clearNowPlayingInfo_iOS();
418 return;
419 }
420 NSMutableDictionary<NSString *, id> *info = [[NSMutableDictionary<NSString *, id> alloc] init];
421 [info setObject:[NSNumber numberWithDouble:time_Player(player)]
422 forKey:MPNowPlayingInfoPropertyElapsedPlaybackTime];
423 [info setObject:[NSNumber numberWithInt:MPNowPlayingInfoMediaTypeAudio]
424 forKey:MPNowPlayingInfoPropertyMediaType];
425 [info setObject:[NSNumber numberWithDouble:duration_Player(player)]
426 forKey:MPMediaItemPropertyPlaybackDuration];
427 const iString *title = tag_Player(player, title_PlayerTag);
428 const iString *artist = tag_Player(player, artist_PlayerTag);
429 if (isEmpty_String(title)) {
430 title = collectNewCStr_String("Audio"); /* TODO: Use link label or URL file name */
431 }
432 if (isEmpty_String(artist)) {
433 artist = collectNewCStr_String("Lagrange"); /* TODO: Use domain or base URL */
434 }
435 [info setObject:[NSString stringWithUTF8String:cstr_String(title)]
436 forKey:MPMediaItemPropertyTitle];
437 [info setObject:[NSString stringWithUTF8String:cstr_String(artist)]
438 forKey:MPMediaItemPropertyArtist];
439 [[MPNowPlayingInfoCenter defaultCenter] setNowPlayingInfo:info];
440}
441
442void clearNowPlayingInfo_iOS(void) {
443 [[MPNowPlayingInfoCenter defaultCenter] setNowPlayingInfo:nil];
444}
445
327void exportDownloadedFile_iOS(const iString *path) { 446void exportDownloadedFile_iOS(const iString *path) {
328 NSURL *url = [NSURL fileURLWithPath:[[NSString alloc] initWithCString:cstr_String(path) 447 NSURL *url = [NSURL fileURLWithPath:[[NSString alloc] initWithCString:cstr_String(path)
329 encoding:NSUTF8StringEncoding]]; 448 encoding:NSUTF8StringEncoding]];
@@ -335,6 +454,17 @@ void exportDownloadedFile_iOS(const iString *path) {
335 [viewController_(get_Window()) presentViewController:picker animated:YES completion:nil]; 454 [viewController_(get_Window()) presentViewController:picker animated:YES completion:nil];
336} 455}
337 456
457void pickFileForOpening_iOS(void) {
458 UIDocumentPickerViewController *picker = [[UIDocumentPickerViewController alloc]
459 initWithDocumentTypes:@[@"fi.skyjake.lagrange.gemini",
460 @"public.text",
461 @"public.image",
462 @"public.audio"]
463 inMode:UIDocumentPickerModeImport];
464 picker.delegate = appState_;
465 [viewController_(get_Window()) presentViewController:picker animated:YES completion:nil];
466}
467
338/*----------------------------------------------------------------------------------------------*/ 468/*----------------------------------------------------------------------------------------------*/
339 469
340enum iAVFAudioPlayerState { 470enum iAVFAudioPlayerState {
diff --git a/src/macos.m b/src/macos.m
index bbef94c6..6bacfdd1 100644
--- a/src/macos.m
+++ b/src/macos.m
@@ -480,7 +480,7 @@ void insertMenuItems_MacOS(const char *menuLabel, int atIndex, const iMenuItem *
480 [menu setAutoenablesItems:NO]; 480 [menu setAutoenablesItems:NO];
481 for (size_t i = 0; i < count; ++i) { 481 for (size_t i = 0; i < count; ++i) {
482 const char *label = translateCStr_Lang(items[i].label); 482 const char *label = translateCStr_Lang(items[i].label);
483 if (label[0] == '\r') { 483 if (label[0] == '\v') {
484 /* Skip the formatting escape. */ 484 /* Skip the formatting escape. */
485 label += 2; 485 label += 2;
486 } 486 }
diff --git a/src/media.c b/src/media.c
index 1313b7da..eb4a8311 100644
--- a/src/media.c
+++ b/src/media.c
@@ -24,6 +24,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
24#include "gmdocument.h" 24#include "gmdocument.h"
25#include "gmrequest.h" 25#include "gmrequest.h"
26#include "ui/window.h" 26#include "ui/window.h"
27#include "ui/paint.h" /* size_SDLTexture */
27#include "audio/player.h" 28#include "audio/player.h"
28#include "app.h" 29#include "app.h"
29#include "stb_image.h" 30#include "stb_image.h"
@@ -261,6 +262,31 @@ void clear_Media(iMedia *d) {
261 clear_PtrArray(&d->downloads); 262 clear_PtrArray(&d->downloads);
262} 263}
263 264
265size_t memorySize_Media(const iMedia *d) {
266 size_t memSize = 0;
267 iConstForEach(PtrArray, i, &d->images) {
268 const iGmImage *img = i.ptr;
269 if (img->texture) {
270 const iInt2 texSize = size_SDLTexture(img->texture);
271 memSize += 4 * texSize.x * texSize.y; /* RGBA */
272 }
273 else {
274 memSize += size_Block(&img->partialData);
275 }
276 }
277 iConstForEach(PtrArray, a, &d->audio) {
278 const iGmAudio *audio = a.ptr;
279 if (audio->player) {
280 memSize += sourceDataSize_Player(audio->player);
281 }
282 }
283 iConstForEach(PtrArray, n, &d->downloads) {
284 const iGmDownload *down = n.ptr;
285 memSize += down->numBytes;
286 }
287 return memSize;
288}
289
264iBool setDownloadUrl_Media(iMedia *d, iGmLinkId linkId, const iString *url) { 290iBool setDownloadUrl_Media(iMedia *d, iGmLinkId linkId, const iString *url) {
265 iGmDownload *dl = NULL; 291 iGmDownload *dl = NULL;
266 iMediaId existing = findLinkDownload_Media(d, linkId); 292 iMediaId existing = findLinkDownload_Media(d, linkId);
@@ -464,6 +490,15 @@ iPlayer *audioPlayer_Media(const iMedia *d, iMediaId audioId) {
464 return NULL; 490 return NULL;
465} 491}
466 492
493void pauseAllPlayers_Media(const iMedia *d, iBool setPaused) {
494 for (size_t i = 0; i < size_PtrArray(&d->audio); ++i) {
495 const iGmAudio *audio = constAt_PtrArray(&d->audio, i);
496 if (audio->player) {
497 setPaused_Player(audio->player, setPaused);
498 }
499 }
500}
501
467iBool downloadInfo_Media(const iMedia *d, iMediaId downloadId, iGmMediaInfo *info_out) { 502iBool downloadInfo_Media(const iMedia *d, iMediaId downloadId, iGmMediaInfo *info_out) {
468 if (downloadId > 0 && downloadId <= size_PtrArray(&d->downloads)) { 503 if (downloadId > 0 && downloadId <= size_PtrArray(&d->downloads)) {
469 const iGmDownload *dl = constAt_PtrArray(&d->downloads, downloadId - 1); 504 const iGmDownload *dl = constAt_PtrArray(&d->downloads, downloadId - 1);
diff --git a/src/media.h b/src/media.h
index 7cc941d0..f7ad6efd 100644
--- a/src/media.h
+++ b/src/media.h
@@ -50,6 +50,8 @@ void clear_Media (iMedia *);
50iBool setDownloadUrl_Media (iMedia *, uint16_t linkId, const iString *url); 50iBool setDownloadUrl_Media (iMedia *, uint16_t linkId, const iString *url);
51iBool setData_Media (iMedia *, uint16_t linkId, const iString *mime, const iBlock *data, int flags); 51iBool setData_Media (iMedia *, uint16_t linkId, const iString *mime, const iBlock *data, int flags);
52 52
53size_t memorySize_Media (const iMedia *);
54
53iMediaId findLinkImage_Media (const iMedia *, uint16_t linkId); 55iMediaId findLinkImage_Media (const iMedia *, uint16_t linkId);
54iBool imageInfo_Media (const iMedia *, iMediaId imageId, iGmMediaInfo *info_out); 56iBool imageInfo_Media (const iMedia *, iMediaId imageId, iGmMediaInfo *info_out);
55iInt2 imageSize_Media (const iMedia *, iMediaId imageId); 57iInt2 imageSize_Media (const iMedia *, iMediaId imageId);
@@ -59,6 +61,7 @@ size_t numAudio_Media (const iMedia *);
59iMediaId findLinkAudio_Media (const iMedia *, uint16_t linkId); 61iMediaId findLinkAudio_Media (const iMedia *, uint16_t linkId);
60iBool audioInfo_Media (const iMedia *, iMediaId audioId, iGmMediaInfo *info_out); 62iBool audioInfo_Media (const iMedia *, iMediaId audioId, iGmMediaInfo *info_out);
61iPlayer * audioPlayer_Media (const iMedia *, iMediaId audioId); 63iPlayer * audioPlayer_Media (const iMedia *, iMediaId audioId);
64void pauseAllPlayers_Media(const iMedia *, iBool setPaused);
62 65
63iMediaId findLinkDownload_Media (const iMedia *, uint16_t linkId); 66iMediaId findLinkDownload_Media (const iMedia *, uint16_t linkId);
64iBool downloadInfo_Media (const iMedia *, iMediaId downloadId, iGmMediaInfo *info_out); 67iBool downloadInfo_Media (const iMedia *, iMediaId downloadId, iGmMediaInfo *info_out);
diff --git a/src/prefs.c b/src/prefs.c
index 385dee78..f1842e9a 100644
--- a/src/prefs.c
+++ b/src/prefs.c
@@ -46,6 +46,7 @@ void init_Prefs(iPrefs *d) {
46 d->openArchiveIndexPages = iTrue; 46 d->openArchiveIndexPages = iTrue;
47 d->decodeUserVisibleURLs = iTrue; 47 d->decodeUserVisibleURLs = iTrue;
48 d->maxCacheSize = 10; 48 d->maxCacheSize = 10;
49 d->maxMemorySize = 200;
49 d->font = nunito_TextFont; 50 d->font = nunito_TextFont;
50 d->headingFont = nunito_TextFont; 51 d->headingFont = nunito_TextFont;
51 d->monospaceGemini = iFalse; 52 d->monospaceGemini = iFalse;
diff --git a/src/prefs.h b/src/prefs.h
index 7185c8f9..655ec949 100644
--- a/src/prefs.h
+++ b/src/prefs.h
@@ -66,6 +66,7 @@ struct Impl_Prefs {
66 iString caPath; 66 iString caPath;
67 iBool decodeUserVisibleURLs; 67 iBool decodeUserVisibleURLs;
68 int maxCacheSize; /* MB */ 68 int maxCacheSize; /* MB */
69 int maxMemorySize; /* MB */
69 iString geminiProxy; 70 iString geminiProxy;
70 iString gopherProxy; 71 iString gopherProxy;
71 iString httpProxy; 72 iString httpProxy;
diff --git a/src/ui/color.c b/src/ui/color.c
index a6ba18e4..05ec1f6f 100644
--- a/src/ui/color.c
+++ b/src/ui/color.c
@@ -24,11 +24,13 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
24#include "root.h" 24#include "root.h"
25#include "app.h" 25#include "app.h"
26 26
27#include <the_Foundation/file.h>
28#include <the_Foundation/path.h>
27#include <the_Foundation/string.h> 29#include <the_Foundation/string.h>
28 30
29static const iColor transparent_; 31static const iColor transparent_;
30 32
31static const iColor darkPalette_[] = { 33static iColor darkPalette_[] = {
32 { 0, 0, 0, 255 }, 34 { 0, 0, 0, 255 },
33 { 40, 40, 40, 255 }, 35 { 40, 40, 40, 255 },
34 { 80, 80, 80, 255 }, 36 { 80, 80, 80, 255 },
@@ -47,7 +49,7 @@ static const iColor darkPalette_[] = {
47 { 0, 200, 0, 255 }, 49 { 0, 200, 0, 255 },
48}; 50};
49 51
50static const iColor lightPalette_[] = { 52static iColor lightPalette_[] = {
51 { 0, 0, 0, 255 }, 53 { 0, 0, 0, 255 },
52 { 75, 75, 75, 255 }, 54 { 75, 75, 75, 255 },
53 { 150, 150, 150, 255 }, 55 { 150, 150, 150, 255 },
@@ -468,12 +470,12 @@ const char *escape_Color(int color) {
468 return esc[color]; 470 return esc[color];
469 } 471 }
470 /* TODO: Conflict with format strings! "%" (37) may be used as the color value. */ 472 /* TODO: Conflict with format strings! "%" (37) may be used as the color value. */
471 /* Double-\r is used for range extension. */ 473 /* Double-\v is used for range extension. */
472 if (color + asciiBase_ColorEscape > 127) { 474 if (color + asciiBase_ColorEscape > 127) {
473 iAssert(color - asciiExtended_ColorEscape + asciiBase_ColorEscape <= 127); 475 iAssert(color - asciiExtended_ColorEscape + asciiBase_ColorEscape <= 127);
474 return format_CStr("\r\r%c", color - asciiExtended_ColorEscape + asciiBase_ColorEscape); 476 return format_CStr("\v\v%c", color - asciiExtended_ColorEscape + asciiBase_ColorEscape);
475 } 477 }
476 return format_CStr("\r%c", color + asciiBase_ColorEscape); 478 return format_CStr("\v%c", color + asciiBase_ColorEscape);
477} 479}
478 480
479iHSLColor setSat_HSLColor(iHSLColor d, float sat) { 481iHSLColor setSat_HSLColor(iHSLColor d, float sat) {
@@ -804,3 +806,77 @@ iColor ansiForeground_Color(iRangecc escapeSequence, int fallback) {
804 } 806 }
805 return clr; 807 return clr;
806} 808}
809
810iBool loadPalette_Color(const char *path) {
811 iBool wasLoaded = iFalse;
812 iFile *f = newCStr_File(concatPath_CStr(path, "palette.txt"));
813 if (open_File(f, text_FileMode | readOnly_FileMode)) {
814 iColor *dstPal = darkPalette_;
815 iRangecc srcLine = iNullRange;
816 const iBlock *src = collect_Block(readAll_File(f));
817 while (nextSplit_Rangecc(range_Block(src), "\n", &srcLine)) {
818 iRangecc line = srcLine;
819 trim_Rangecc(&line);
820 if (isEmpty_Range(&line)) {
821 continue;
822 }
823 if (*line.start == '#') {
824 /* Control directive. */
825 line.start++;
826 trim_Rangecc(&line);
827 if (equalCase_Rangecc(line, "dark")) {
828 dstPal = darkPalette_;
829 }
830 else if (equalCase_Rangecc(line, "light")) {
831 dstPal = lightPalette_;
832 }
833 continue;
834 }
835 static const struct {
836 const char *label;
837 int paletteIndex;
838 } colors_[] = {
839 { "black:", 0 }, { "gray25:", 1 }, { "gray50:", 2 }, { "gray75:", 3 },
840 { "white:", 4 }, { "brown:", 5 }, { "orange:", 6 }, { "teal:", 7 },
841 { "cyan:", 8 }, { "yellow:", 9 }, { "red:", 10 }, { "magenta:", 11 },
842 { "blue:", 12 }, { "green:", 13 },
843 };
844 iForIndices(i, colors_) {
845 if (startsWithCase_Rangecc(line, colors_[i].label)) {
846 iColor *dst = &dstPal[colors_[i].paletteIndex];
847 line.start += strlen(colors_[i].label);
848 trim_Rangecc(&line);
849 if (!isEmpty_Range(&line)) {
850 if (*line.start == '#') {
851 /* Hexadecimal color. */
852 line.start++;
853 if (size_Range(&line) == 6) {
854 iBlock *vals = hexDecode_Rangecc(line);
855 iAssert(size_Block(vals) == 3);
856 const uint8_t *rgb = constData_Block(vals);
857 *dst = (iColor){ rgb[0], rgb[1], rgb[2], 255 };
858 delete_Block(vals);
859 }
860 else {
861 fprintf(stderr, "[Color] invalid custom color: %s\n",
862 cstr_Rangecc(line));
863 }
864 }
865 else {
866 unsigned int red = 0, green = 0, blue = 0;
867 sscanf(line.start, "%u %u %u", &red, &green, &blue);
868 if (red > 255 || green > 255 || blue > 255) {
869 fprintf(stderr, "[Color] RGB value(s) out of range: %s\n",
870 cstr_Rangecc(line));
871 }
872 *dst = (iColor){ red, green, blue, 255 };
873 }
874 }
875 }
876 }
877 }
878 wasLoaded = iTrue;
879 }
880 iRelease(f);
881 return wasLoaded;
882}
diff --git a/src/ui/color.h b/src/ui/color.h
index d2fa3c00..37ec49eb 100644
--- a/src/ui/color.h
+++ b/src/ui/color.h
@@ -187,26 +187,26 @@ iLocalDef iBool isRegularText_ColorId(enum iColorId d) {
187#define asciiBase_ColorEscape 33 187#define asciiBase_ColorEscape 33
188#define asciiExtended_ColorEscape (128 - asciiBase_ColorEscape) 188#define asciiExtended_ColorEscape (128 - asciiBase_ColorEscape)
189 189
190#define restore_ColorEscape "\r\x24" /* ASCII Cancel */ 190#define restore_ColorEscape "\v\x24" /* ASCII Cancel */
191#define black_ColorEscape "\r!" 191#define black_ColorEscape "\v!"
192#define gray25_ColorEscape "\r\"" 192#define gray25_ColorEscape "\v\""
193#define gray50_ColorEscape "\r#" 193#define gray50_ColorEscape "\v#"
194#define gray75_ColorEscape "\r$" 194#define gray75_ColorEscape "\v$"
195#define white_ColorEscape "\r%" 195#define white_ColorEscape "\v%"
196#define brown_ColorEscape "\r&" 196#define brown_ColorEscape "\v&"
197#define orange_ColorEscape "\r'" 197#define orange_ColorEscape "\v'"
198#define teal_ColorEscape "\r(" 198#define teal_ColorEscape "\v("
199#define cyan_ColorEscape "\r)" 199#define cyan_ColorEscape "\v)"
200#define yellow_ColorEscape "\r*" 200#define yellow_ColorEscape "\v*"
201#define red_ColorEscape "\r+" 201#define red_ColorEscape "\v+"
202#define magenta_ColorEscape "\r," 202#define magenta_ColorEscape "\v,"
203#define blue_ColorEscape "\r-" 203#define blue_ColorEscape "\v-"
204#define green_ColorEscape "\r." 204#define green_ColorEscape "\v."
205#define uiText_ColorEscape "\r4" 205#define uiText_ColorEscape "\v4"
206#define uiTextAction_ColorEscape "\r<" 206#define uiTextAction_ColorEscape "\v<"
207#define uiTextCaution_ColorEscape "\r=" 207#define uiTextCaution_ColorEscape "\v="
208#define uiTextStrong_ColorEscape "\r:" 208#define uiTextStrong_ColorEscape "\v:"
209#define uiHeading_ColorEscape "\rR" 209#define uiHeading_ColorEscape "\vR"
210 210
211iDeclareType(Color) 211iDeclareType(Color)
212iDeclareType(HSLColor) 212iDeclareType(HSLColor)
@@ -244,7 +244,9 @@ iLocalDef void setHsl_Color(int color, iHSLColor hsl) {
244 set_Color(color, rgb_HSLColor(hsl)); 244 set_Color(color, rgb_HSLColor(hsl));
245} 245}
246 246
247iBool loadPalette_Color (const char *path);
247void setThemePalette_Color (enum iColorTheme theme); 248void setThemePalette_Color (enum iColorTheme theme);
248 249
249iColor ansiForeground_Color (iRangecc escapeSequence, int fallback); 250iColor ansiForeground_Color (iRangecc escapeSequence, int fallback);
250const char * escape_Color (int color); 251const char * escape_Color (int color);
252
diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c
index 6dc80a07..cb1fde28 100644
--- a/src/ui/documentwidget.c
+++ b/src/ui/documentwidget.c
@@ -130,7 +130,8 @@ void deinit_PersistentDocumentState(iPersistentDocumentState *d) {
130 130
131void serialize_PersistentDocumentState(const iPersistentDocumentState *d, iStream *outs) { 131void serialize_PersistentDocumentState(const iPersistentDocumentState *d, iStream *outs) {
132 serialize_String(d->url, outs); 132 serialize_String(d->url, outs);
133 writeU16_Stream(outs, d->reloadInterval & 7); 133 uint16_t params = d->reloadInterval & 7;
134 writeU16_Stream(outs, params);
134 serialize_History(d->history, outs); 135 serialize_History(d->history, outs);
135} 136}
136 137
@@ -223,6 +224,7 @@ enum iDocumentWidgetFlag {
223 movingSelectMarkEnd_DocumentWidgetFlag = iBit(11), 224 movingSelectMarkEnd_DocumentWidgetFlag = iBit(11),
224 otherRootByDefault_DocumentWidgetFlag = iBit(12), /* links open to other root by default */ 225 otherRootByDefault_DocumentWidgetFlag = iBit(12), /* links open to other root by default */
225 urlChanged_DocumentWidgetFlag = iBit(13), 226 urlChanged_DocumentWidgetFlag = iBit(13),
227 openedFromSidebar_DocumentWidgetFlag = iBit(14),
226}; 228};
227 229
228enum iDocumentLinkOrdinalMode { 230enum iDocumentLinkOrdinalMode {
@@ -315,6 +317,10 @@ void init_DocumentWidget(iDocumentWidget *d) {
315 init_Widget(w); 317 init_Widget(w);
316 setId_Widget(w, format_CStr("document%03d", ++docEnum_)); 318 setId_Widget(w, format_CStr("document%03d", ++docEnum_));
317 setFlags_Widget(w, hover_WidgetFlag | noBackground_WidgetFlag, iTrue); 319 setFlags_Widget(w, hover_WidgetFlag | noBackground_WidgetFlag, iTrue);
320 if (deviceType_App() != desktop_AppDeviceType) {
321 setFlags_Widget(w, leftEdgeDraggable_WidgetFlag | rightEdgeDraggable_WidgetFlag |
322 horizontalOffset_WidgetFlag, iTrue);
323 }
318 init_PersistentDocumentState(&d->mod); 324 init_PersistentDocumentState(&d->mod);
319 d->flags = 0; 325 d->flags = 0;
320 d->phoneToolbar = NULL; 326 d->phoneToolbar = NULL;
@@ -392,6 +398,7 @@ void init_DocumentWidget(iDocumentWidget *d) {
392} 398}
393 399
394void deinit_DocumentWidget(iDocumentWidget *d) { 400void deinit_DocumentWidget(iDocumentWidget *d) {
401 pauseAllPlayers_Media(media_GmDocument(d->doc), iTrue);
395 removeTicker_App(animate_DocumentWidget_, d); 402 removeTicker_App(animate_DocumentWidget_, d);
396 removeTicker_App(prerender_DocumentWidget_, d); 403 removeTicker_App(prerender_DocumentWidget_, d);
397 remove_Periodic(periodic_App(), d); 404 remove_Periodic(periodic_App(), d);
@@ -976,6 +983,9 @@ static void updateTimestampBuf_DocumentWidget_(const iDocumentWidget *d) {
976} 983}
977 984
978static void invalidate_DocumentWidget_(iDocumentWidget *d) { 985static void invalidate_DocumentWidget_(iDocumentWidget *d) {
986 if (flags_Widget(as_Widget(d)) & destroyPending_WidgetFlag) {
987 return;
988 }
979 invalidate_VisBuf(d->visBuf); 989 invalidate_VisBuf(d->visBuf);
980 clear_PtrSet(d->invalidRuns); 990 clear_PtrSet(d->invalidRuns);
981} 991}
@@ -1016,9 +1026,7 @@ static void showOrHidePinningIndicator_DocumentWidget_(iDocumentWidget *d) {
1016 isPinned_DocumentWidget_(d)); 1026 isPinned_DocumentWidget_(d));
1017} 1027}
1018 1028
1019void setSource_DocumentWidget(iDocumentWidget *d, const iString *source) { 1029static void documentWasChanged_DocumentWidget_(iDocumentWidget *d) {
1020 setUrl_GmDocument(d->doc, d->mod.url);
1021 setSource_GmDocument(d->doc, source, documentWidth_DocumentWidget_(d));
1022 documentRunsInvalidated_DocumentWidget_(d); 1030 documentRunsInvalidated_DocumentWidget_(d);
1023 updateWindowTitle_DocumentWidget_(d); 1031 updateWindowTitle_DocumentWidget_(d);
1024 updateVisible_DocumentWidget_(d); 1032 updateVisible_DocumentWidget_(d);
@@ -1035,6 +1043,26 @@ void setSource_DocumentWidget(iDocumentWidget *d, const iString *source) {
1035 } 1043 }
1036 } 1044 }
1037 showOrHidePinningIndicator_DocumentWidget_(d); 1045 showOrHidePinningIndicator_DocumentWidget_(d);
1046 setCachedDocument_History(d->mod.history,
1047 d->doc, /* keeps a ref */
1048 (d->flags & openedFromSidebar_DocumentWidgetFlag) != 0);
1049}
1050
1051void setSource_DocumentWidget(iDocumentWidget *d, const iString *source) {
1052 setUrl_GmDocument(d->doc, d->mod.url);
1053 setSource_GmDocument(d->doc,
1054 source,
1055 documentWidth_DocumentWidget_(d),
1056 isFinished_GmRequest(d->request) ? final_GmDocumentUpdate
1057 : partial_GmDocumentUpdate);
1058 documentWasChanged_DocumentWidget_(d);
1059}
1060
1061static void replaceDocument_DocumentWidget_(iDocumentWidget *d, iGmDocument *newDoc) {
1062 pauseAllPlayers_Media(media_GmDocument(d->doc), iTrue);
1063 iRelease(d->doc);
1064 d->doc = ref_Object(newDoc);
1065 documentWasChanged_DocumentWidget_(d);
1038} 1066}
1039 1067
1040static void updateTheme_DocumentWidget_(iDocumentWidget *d) { 1068static void updateTheme_DocumentWidget_(iDocumentWidget *d) {
@@ -1143,16 +1171,22 @@ static void showErrorPage_DocumentWidget_(iDocumentWidget *d, enum iGmStatusCode
1143 { person_Icon " ${menu.identity.new}", newIdentity_KeyShortcut, "ident.new" } }, 1171 { person_Icon " ${menu.identity.new}", newIdentity_KeyShortcut, "ident.new" } },
1144 2); 1172 2);
1145 } 1173 }
1146 setBanner_GmDocument(d->doc, useBanner ? bannerType_DocumentWidget_(d) : none_GmDocumentBanner); 1174 /* Make a new document for the error page.*/ {
1147 setFormat_GmDocument(d->doc, gemini_GmDocumentFormat); 1175 iGmDocument *errorDoc = new_GmDocument();
1176 setUrl_GmDocument(errorDoc, d->mod.url);
1177 setBanner_GmDocument(errorDoc, useBanner ? bannerType_DocumentWidget_(d) : none_GmDocumentBanner);
1178 setFormat_GmDocument(errorDoc, gemini_SourceFormat);
1179 replaceDocument_DocumentWidget_(d, errorDoc);
1180 iRelease(errorDoc);
1181 }
1148 translate_Lang(src); 1182 translate_Lang(src);
1183 d->state = ready_RequestState;
1149 setSource_DocumentWidget(d, src); 1184 setSource_DocumentWidget(d, src);
1150 updateTheme_DocumentWidget_(d); 1185 updateTheme_DocumentWidget_(d);
1151 reset_SmoothScroll(&d->scrollY); 1186 reset_SmoothScroll(&d->scrollY);
1152 init_Anim(&d->sideOpacity, 0); 1187 init_Anim(&d->sideOpacity, 0);
1153 init_Anim(&d->altTextOpacity, 0); 1188 init_Anim(&d->altTextOpacity, 0);
1154 resetWideRuns_DocumentWidget_(d); 1189 resetWideRuns_DocumentWidget_(d);
1155 d->state = ready_RequestState;
1156} 1190}
1157 1191
1158static void updateFetchProgress_DocumentWidget_(iDocumentWidget *d) { 1192static void updateFetchProgress_DocumentWidget_(iDocumentWidget *d) {
@@ -1264,9 +1298,9 @@ static void postProcessRequestContent_DocumentWidget_(iDocumentWidget *d, iBool
1264 2); 1298 2);
1265 } 1299 }
1266 if (preloadCoverImage_Gempub(d->sourceGempub, d->doc)) { 1300 if (preloadCoverImage_Gempub(d->sourceGempub, d->doc)) {
1267 redoLayout_GmDocument(d->doc); 1301 redoLayout_GmDocument(d->doc);
1268 updateVisible_DocumentWidget_(d); 1302 updateVisible_DocumentWidget_(d);
1269 invalidate_DocumentWidget_(d); 1303 invalidate_DocumentWidget_(d);
1270 } 1304 }
1271 } 1305 }
1272 else if (equal_String(d->mod.url, indexPageUrl_Gempub(d->sourceGempub))) { 1306 else if (equal_String(d->mod.url, indexPageUrl_Gempub(d->sourceGempub))) {
@@ -1348,7 +1382,9 @@ static void postProcessRequestContent_DocumentWidget_(iDocumentWidget *d, iBool
1348 } 1382 }
1349} 1383}
1350 1384
1351static void updateDocument_DocumentWidget_(iDocumentWidget *d, const iGmResponse *response, 1385static void updateDocument_DocumentWidget_(iDocumentWidget *d,
1386 const iGmResponse *response,
1387 iGmDocument *cachedDoc,
1352 const iBool isInitialUpdate) { 1388 const iBool isInitialUpdate) {
1353 if (d->state == ready_RequestState) { 1389 if (d->state == ready_RequestState) {
1354 return; 1390 return;
@@ -1371,7 +1407,7 @@ static void updateDocument_DocumentWidget_(iDocumentWidget *d, const iGmResponse
1371 if (isSuccess_GmStatusCode(statusCode)) { 1407 if (isSuccess_GmStatusCode(statusCode)) {
1372 /* Check the MIME type. */ 1408 /* Check the MIME type. */
1373 iRangecc charset = range_CStr("utf-8"); 1409 iRangecc charset = range_CStr("utf-8");
1374 enum iGmDocumentFormat docFormat = undefined_GmDocumentFormat; 1410 enum iSourceFormat docFormat = undefined_SourceFormat;
1375 const iString *mimeStr = collect_String(lower_String(&response->meta)); /* for convenience */ 1411 const iString *mimeStr = collect_String(lower_String(&response->meta)); /* for convenience */
1376 set_String(&d->sourceMime, mimeStr); 1412 set_String(&d->sourceMime, mimeStr);
1377 iRangecc mime = range_String(mimeStr); 1413 iRangecc mime = range_String(mimeStr);
@@ -1380,20 +1416,20 @@ static void updateDocument_DocumentWidget_(iDocumentWidget *d, const iGmResponse
1380 iRangecc param = seg; 1416 iRangecc param = seg;
1381 trim_Rangecc(&param); 1417 trim_Rangecc(&param);
1382 if (equal_Rangecc(param, "text/gemini")) { 1418 if (equal_Rangecc(param, "text/gemini")) {
1383 docFormat = gemini_GmDocumentFormat; 1419 docFormat = gemini_SourceFormat;
1384 setRange_String(&d->sourceMime, param); 1420 setRange_String(&d->sourceMime, param);
1385 } 1421 }
1386 else if (startsWith_Rangecc(param, "text/") || 1422 else if (startsWith_Rangecc(param, "text/") ||
1387 equal_Rangecc(param, "application/json") || 1423 equal_Rangecc(param, "application/json") ||
1388 equal_Rangecc(param, "application/x-pem-file") || 1424 equal_Rangecc(param, "application/x-pem-file") ||
1389 equal_Rangecc(param, "application/pem-certificate-chain")) { 1425 equal_Rangecc(param, "application/pem-certificate-chain")) {
1390 docFormat = plainText_GmDocumentFormat; 1426 docFormat = plainText_SourceFormat;
1391 setRange_String(&d->sourceMime, param); 1427 setRange_String(&d->sourceMime, param);
1392 } 1428 }
1393 else if (equal_Rangecc(param, "application/zip") || 1429 else if (equal_Rangecc(param, "application/zip") ||
1394 (startsWith_Rangecc(param, "application/") && 1430 (startsWith_Rangecc(param, "application/") &&
1395 endsWithCase_Rangecc(param, "+zip"))) { 1431 endsWithCase_Rangecc(param, "+zip"))) {
1396 docFormat = gemini_GmDocumentFormat; 1432 docFormat = gemini_SourceFormat;
1397 setRange_String(&d->sourceMime, param); 1433 setRange_String(&d->sourceMime, param);
1398 iString *key = collectNew_String(); 1434 iString *key = collectNew_String();
1399 toString_Sym(SDLK_s, KMOD_PRIMARY, key); 1435 toString_Sym(SDLK_s, KMOD_PRIMARY, key);
@@ -1420,9 +1456,10 @@ static void updateDocument_DocumentWidget_(iDocumentWidget *d, const iGmResponse
1420 startsWith_Rangecc(param, "audio/")) { 1456 startsWith_Rangecc(param, "audio/")) {
1421 const iBool isAudio = startsWith_Rangecc(param, "audio/"); 1457 const iBool isAudio = startsWith_Rangecc(param, "audio/");
1422 /* Make a simple document with an image or audio player. */ 1458 /* Make a simple document with an image or audio player. */
1423 docFormat = gemini_GmDocumentFormat; 1459 docFormat = gemini_SourceFormat;
1424 setRange_String(&d->sourceMime, param); 1460 setRange_String(&d->sourceMime, param);
1425 const iGmLinkId imgLinkId = 1; /* there's only the one link */ 1461 const iGmLinkId imgLinkId = 1; /* there's only the one link */
1462 /* TODO: Do the image loading in `postProcessRequestContent_DocumentWidget_()` */
1426 if ((isAudio && isInitialUpdate) || (!isAudio && isRequestFinished)) { 1463 if ((isAudio && isInitialUpdate) || (!isAudio && isRequestFinished)) {
1427 const char *linkTitle = 1464 const char *linkTitle =
1428 startsWith_String(mimeStr, "image/") ? "Image" : "Audio"; 1465 startsWith_String(mimeStr, "image/") ? "Image" : "Audio";
@@ -1466,7 +1503,7 @@ static void updateDocument_DocumentWidget_(iDocumentWidget *d, const iGmResponse
1466 } 1503 }
1467 } 1504 }
1468 } 1505 }
1469 if (docFormat == undefined_GmDocumentFormat) { 1506 if (docFormat == undefined_SourceFormat) {
1470 showErrorPage_DocumentWidget_(d, unsupportedMimeType_GmStatusCode, &response->meta); 1507 showErrorPage_DocumentWidget_(d, unsupportedMimeType_GmStatusCode, &response->meta);
1471 deinit_String(&str); 1508 deinit_String(&str);
1472 return; 1509 return;
@@ -1478,7 +1515,10 @@ static void updateDocument_DocumentWidget_(iDocumentWidget *d, const iGmResponse
1478 collect_String(decode_Block(&str.chars, cstr_Rangecc(charset)))); 1515 collect_String(decode_Block(&str.chars, cstr_Rangecc(charset))));
1479 } 1516 }
1480 } 1517 }
1481 if (setSource) { 1518 if (cachedDoc) {
1519 replaceDocument_DocumentWidget_(d, cachedDoc);
1520 }
1521 else if (setSource) {
1482 setSource_DocumentWidget(d, &str); 1522 setSource_DocumentWidget(d, &str);
1483 } 1523 }
1484 deinit_String(&str); 1524 deinit_String(&str);
@@ -1558,14 +1598,16 @@ static void cacheDocumentGlyphs_DocumentWidget_(const iDocumentWidget *d) {
1558} 1598}
1559 1599
1560static void updateFromCachedResponse_DocumentWidget_(iDocumentWidget *d, float normScrollY, 1600static void updateFromCachedResponse_DocumentWidget_(iDocumentWidget *d, float normScrollY,
1561 const iGmResponse *resp) { 1601 const iGmResponse *resp, iGmDocument *cachedDoc) {
1562 setLinkNumberMode_DocumentWidget_(d, iFalse); 1602 setLinkNumberMode_DocumentWidget_(d, iFalse);
1563 clear_ObjectList(d->media); 1603 clear_ObjectList(d->media);
1564 delete_Gempub(d->sourceGempub); 1604 delete_Gempub(d->sourceGempub);
1565 d->sourceGempub = NULL; 1605 d->sourceGempub = NULL;
1566 reset_GmDocument(d->doc); 1606 pauseAllPlayers_Media(media_GmDocument(d->doc), iTrue);
1607 iRelease(d->doc);
1567 destroy_Widget(d->footerButtons); 1608 destroy_Widget(d->footerButtons);
1568 d->footerButtons = NULL; 1609 d->footerButtons = NULL;
1610 d->doc = new_GmDocument();
1569 resetWideRuns_DocumentWidget_(d); 1611 resetWideRuns_DocumentWidget_(d);
1570 d->state = fetching_RequestState; 1612 d->state = fetching_RequestState;
1571 /* Do the fetch. */ { 1613 /* Do the fetch. */ {
@@ -1576,10 +1618,12 @@ static void updateFromCachedResponse_DocumentWidget_(iDocumentWidget *d, float n
1576 d->sourceStatus = success_GmStatusCode; 1618 d->sourceStatus = success_GmStatusCode;
1577 format_String(&d->sourceHeader, cstr_Lang("pageinfo.header.cached")); 1619 format_String(&d->sourceHeader, cstr_Lang("pageinfo.header.cached"));
1578 set_Block(&d->sourceContent, &resp->body); 1620 set_Block(&d->sourceContent, &resp->body);
1579 updateDocument_DocumentWidget_(d, resp, iTrue); 1621 updateDocument_DocumentWidget_(d, resp, cachedDoc, iTrue);
1580 postProcessRequestContent_DocumentWidget_(d, iTrue); 1622// setCachedDocument_History(d->mod.history, d->doc,
1623// (d->flags & openedFromSidebar_DocumentWidgetFlag) != 0);
1581 } 1624 }
1582 d->state = ready_RequestState; 1625 d->state = ready_RequestState;
1626 postProcessRequestContent_DocumentWidget_(d, iTrue);
1583 init_Anim(&d->altTextOpacity, 0); 1627 init_Anim(&d->altTextOpacity, 0);
1584 reset_SmoothScroll(&d->scrollY); 1628 reset_SmoothScroll(&d->scrollY);
1585 init_Anim(&d->scrollY.pos, d->initNormScrollY * size_GmDocument(d->doc).y); 1629 init_Anim(&d->scrollY.pos, d->initNormScrollY * size_GmDocument(d->doc).y);
@@ -1589,13 +1633,18 @@ static void updateFromCachedResponse_DocumentWidget_(iDocumentWidget *d, float n
1589 cacheDocumentGlyphs_DocumentWidget_(d); 1633 cacheDocumentGlyphs_DocumentWidget_(d);
1590 d->drawBufs->flags |= updateTimestampBuf_DrawBufsFlag | updateSideBuf_DrawBufsFlag; 1634 d->drawBufs->flags |= updateTimestampBuf_DrawBufsFlag | updateSideBuf_DrawBufsFlag;
1591 d->flags &= ~urlChanged_DocumentWidgetFlag; 1635 d->flags &= ~urlChanged_DocumentWidgetFlag;
1592 postCommandf_Root(as_Widget(d)->root, "document.changed doc:%p url:%s", d, cstr_String(d->mod.url)); 1636 postCommandf_Root(
1637 as_Widget(d)->root, "document.changed doc:%p url:%s", d, cstr_String(d->mod.url));
1593} 1638}
1594 1639
1595static iBool updateFromHistory_DocumentWidget_(iDocumentWidget *d) { 1640static iBool updateFromHistory_DocumentWidget_(iDocumentWidget *d) {
1596 const iRecentUrl *recent = findUrl_History(d->mod.history, withSpacesEncoded_String(d->mod.url)); 1641 const iRecentUrl *recent = findUrl_History(d->mod.history, withSpacesEncoded_String(d->mod.url));
1597 if (recent && recent->cachedResponse) { 1642 if (recent && recent->cachedResponse) {
1598 updateFromCachedResponse_DocumentWidget_(d, recent->normScrollY, recent->cachedResponse); 1643 iChangeFlags(d->flags,
1644 openedFromSidebar_DocumentWidgetFlag,
1645 recent->flags.openedFromSidebar);
1646 updateFromCachedResponse_DocumentWidget_(
1647 d, recent->normScrollY, recent->cachedResponse, recent->cachedDoc);
1599 return iTrue; 1648 return iTrue;
1600 } 1649 }
1601 else if (!isEmpty_String(d->mod.url)) { 1650 else if (!isEmpty_String(d->mod.url)) {
@@ -1636,7 +1685,7 @@ static void scrollBegan_DocumentWidget_(iAnyObject *any, int offset, uint32_t du
1636 if (deviceType_App() == phone_AppDeviceType) { 1685 if (deviceType_App() == phone_AppDeviceType) {
1637 const float normPos = normScrollPos_DocumentWidget_(d); 1686 const float normPos = normScrollPos_DocumentWidget_(d);
1638 if (prefs_App()->hideToolbarOnScroll && iAbs(offset) > 5 && normPos >= 0) { 1687 if (prefs_App()->hideToolbarOnScroll && iAbs(offset) > 5 && normPos >= 0) {
1639 showToolbars_Root(as_Widget(d)->root, offset < 0); 1688 showToolbar_Root(as_Widget(d)->root, offset < 0);
1640 } 1689 }
1641 } 1690 }
1642 updateVisible_DocumentWidget_(d); 1691 updateVisible_DocumentWidget_(d);
@@ -1845,13 +1894,15 @@ static void checkResponse_DocumentWidget_(iDocumentWidget *d) {
1845 /* Keep scroll position when reloading the same page. */ 1894 /* Keep scroll position when reloading the same page. */
1846 reset_SmoothScroll(&d->scrollY); 1895 reset_SmoothScroll(&d->scrollY);
1847 } 1896 }
1848 reset_GmDocument(d->doc); /* new content incoming */ 1897 pauseAllPlayers_Media(media_GmDocument(d->doc), iTrue);
1898 iRelease(d->doc); /* new content incoming */
1899 d->doc = new_GmDocument();
1849 delete_Gempub(d->sourceGempub); 1900 delete_Gempub(d->sourceGempub);
1850 d->sourceGempub = NULL; 1901 d->sourceGempub = NULL;
1851 destroy_Widget(d->footerButtons); 1902 destroy_Widget(d->footerButtons);
1852 d->footerButtons = NULL; 1903 d->footerButtons = NULL;
1853 resetWideRuns_DocumentWidget_(d); 1904 resetWideRuns_DocumentWidget_(d);
1854 updateDocument_DocumentWidget_(d, resp, iTrue); 1905 updateDocument_DocumentWidget_(d, resp, NULL, iTrue);
1855 break; 1906 break;
1856 case categoryRedirect_GmStatusCode: 1907 case categoryRedirect_GmStatusCode:
1857 if (isEmpty_String(&resp->meta)) { 1908 if (isEmpty_String(&resp->meta)) {
@@ -1902,7 +1953,7 @@ static void checkResponse_DocumentWidget_(iDocumentWidget *d) {
1902 switch (category_GmStatusCode(statusCode)) { 1953 switch (category_GmStatusCode(statusCode)) {
1903 case categorySuccess_GmStatusCode: 1954 case categorySuccess_GmStatusCode:
1904 /* More content available. */ 1955 /* More content available. */
1905 updateDocument_DocumentWidget_(d, resp, iFalse); 1956 updateDocument_DocumentWidget_(d, resp, NULL, iFalse);
1906 break; 1957 break;
1907 default: 1958 default:
1908 break; 1959 break;
@@ -2087,16 +2138,16 @@ static const iString *saveToDownloads_(const iString *url, const iString *mime,
2087 exportDownloadedFile_iOS(savePath); 2138 exportDownloadedFile_iOS(savePath);
2088#else 2139#else
2089 if (showDialog) { 2140 if (showDialog) {
2090 const iMenuItem items[2] = { 2141 const iMenuItem items[2] = {
2091 { "${dlg.save.opendownload}", 0, 0, 2142 { "${dlg.save.opendownload}", 0, 0,
2092 format_CStr("!open url:%s", cstrCollect_String(makeFileUrl_String(savePath))) }, 2143 format_CStr("!open url:%s", cstrCollect_String(makeFileUrl_String(savePath))) },
2093 { "${dlg.message.ok}", 0, 0, "message.ok" }, 2144 { "${dlg.message.ok}", 0, 0, "message.ok" },
2094 }; 2145 };
2095 makeMessage_Widget(uiHeading_ColorEscape "${heading.save}", 2146 makeMessage_Widget(uiHeading_ColorEscape "${heading.save}",
2096 format_CStr("%s\n${dlg.save.size} %.3f %s", 2147 format_CStr("%s\n${dlg.save.size} %.3f %s",
2097 cstr_String(path_File(f)), 2148 cstr_String(path_File(f)),
2098 isMega ? size / 1.0e6f : (size / 1.0e3f), 2149 isMega ? size / 1.0e6f : (size / 1.0e3f),
2099 isMega ? "${mb}" : "${kb}"), 2150 isMega ? "${mb}" : "${kb}"),
2100 items, 2151 items,
2101 iElemCount(items)); 2152 iElemCount(items));
2102 } 2153 }
@@ -2210,6 +2261,147 @@ static iBool handlePinch_DocumentWidget_(iDocumentWidget *d, const char *cmd) {
2210 return iTrue; 2261 return iTrue;
2211} 2262}
2212 2263
2264static void swap_DocumentWidget_(iDocumentWidget *d, iGmDocument *doc,
2265 iDocumentWidget *swapBuffersWith) {
2266 if (doc) {
2267 iAssert(isInstance_Object(doc, &Class_GmDocument));
2268 iGmDocument *copy = ref_Object(doc);
2269 iRelease(d->doc);
2270 d->doc = copy;
2271 d->scrollY = swapBuffersWith->scrollY;
2272 updateVisible_DocumentWidget_(d);
2273 iSwap(iVisBuf *, d->visBuf, swapBuffersWith->visBuf);
2274 iSwap(iVisBufMeta *, d->visBufMeta, swapBuffersWith->visBufMeta);
2275 iSwap(iDrawBufs *, d->drawBufs, swapBuffersWith->drawBufs);
2276 invalidate_DocumentWidget_(swapBuffersWith);
2277 }
2278}
2279
2280static iWidget *swipeParent_DocumentWidget_(iDocumentWidget *d) {
2281 return findChild_Widget(as_Widget(d)->root->widget, "doctabs");
2282}
2283
2284static iBool handleSwipe_DocumentWidget_(iDocumentWidget *d, const char *cmd) {
2285 iWidget *w = as_Widget(d);
2286 /* Swipe animations are rather complex and utilize both cached GmDocument content
2287 and temporary DocumentWidgets. Depending on the swipe direction, this DocumentWidget
2288 may wait until the finger is released to actually perform the navigation action. */
2289 if (equal_Command(cmd, "edgeswipe.moved")) {
2290 //printf("[%p] responds to edgeswipe.moved\n", d);
2291 as_Widget(d)->offsetRef = NULL;
2292 const int side = argLabel_Command(cmd, "side");
2293 const int offset = arg_Command(cmd);
2294 if (side == 1) { /* left edge */
2295 if (atOldest_History(d->mod.history)) {
2296 return iTrue;
2297 }
2298 iWidget *swipeParent = swipeParent_DocumentWidget_(d);
2299 /* The temporary "swipeIn" will display the previous page until the finger is lifted. */
2300 iDocumentWidget *swipeIn = findChild_Widget(swipeParent, "swipein");
2301 if (!swipeIn) {
2302 const iBool sidebarSwipe = (isPortraitPhone_App() &&
2303 d->flags & openedFromSidebar_DocumentWidgetFlag &&
2304 !isVisible_Widget(findWidget_App("sidebar")));
2305 swipeIn = new_DocumentWidget();
2306 setId_Widget(as_Widget(swipeIn), "swipein");
2307 setFlags_Widget(as_Widget(swipeIn),
2308 disabled_WidgetFlag | refChildrenOffset_WidgetFlag |
2309 fixedPosition_WidgetFlag | fixedSize_WidgetFlag, iTrue);
2310 swipeIn->widget.rect.pos = windowToInner_Widget(swipeParent, localToWindow_Widget(w, w->rect.pos));
2311 swipeIn->widget.rect.size = d->widget.rect.size;
2312 swipeIn->widget.offsetRef = parent_Widget(w);
2313 if (!sidebarSwipe) {
2314 iRecentUrl *recent = new_RecentUrl();
2315 preceding_History(d->mod.history, recent);
2316 if (recent->cachedDoc) {
2317 iChangeRef(swipeIn->doc, recent->cachedDoc);
2318 updateScrollMax_DocumentWidget_(d);
2319 setValue_Anim(&swipeIn->scrollY.pos, size_GmDocument(swipeIn->doc).y * recent->normScrollY, 0);
2320 updateVisible_DocumentWidget_(swipeIn);
2321 swipeIn->drawBufs->flags |= updateTimestampBuf_DrawBufsFlag | updateSideBuf_DrawBufsFlag;
2322 }
2323 delete_RecentUrl(recent);
2324 }
2325 addChildPos_Widget(swipeParent, iClob(swipeIn), front_WidgetAddPos);
2326 }
2327 }
2328 if (side == 2) { /* right edge */
2329 if (offset < -get_Window()->pixelRatio * 10) {
2330 int animSpan = 10;
2331 if (!atLatest_History(d->mod.history) &&
2332 ~flags_Widget(w) & dragged_WidgetFlag) {
2333 animSpan = 0;
2334 postCommand_Widget(d, "navigate.forward");
2335 setFlags_Widget(w, dragged_WidgetFlag, iTrue);
2336 /* Set up the swipe dummy. */
2337 iWidget *swipeParent = swipeParent_DocumentWidget_(d);
2338 iDocumentWidget *target = new_DocumentWidget();
2339 setId_Widget(as_Widget(target), "swipeout");
2340 /* The target takes the old document and jumps on top. */
2341 target->widget.rect.pos = windowToInner_Widget(swipeParent, localToWindow_Widget(w, w->rect.pos));
2342 target->widget.rect.size = d->widget.rect.size;
2343 setFlags_Widget(as_Widget(target), fixedPosition_WidgetFlag | fixedSize_WidgetFlag, iTrue);
2344 swap_DocumentWidget_(target, d->doc, d);
2345 addChildPos_Widget(swipeParent, iClob(target), front_WidgetAddPos);
2346 setFlags_Widget(as_Widget(target), refChildrenOffset_WidgetFlag, iTrue);
2347 as_Widget(target)->offsetRef = parent_Widget(w);
2348 destroy_Widget(as_Widget(target)); /* will be actually deleted after animation finishes */
2349 }
2350 if (flags_Widget(w) & dragged_WidgetFlag) {
2351 setVisualOffset_Widget(w, width_Widget(w) +
2352 width_Widget(d) * offset / size_Root(w->root).x,
2353 animSpan, 0);
2354 }
2355 else {
2356 setVisualOffset_Widget(w, offset / 4, animSpan, 0);
2357 }
2358 }
2359 return iTrue;
2360 }
2361 }
2362 if (equal_Command(cmd, "edgeswipe.ended") && argLabel_Command(cmd, "side") == 2) {
2363 if (argLabel_Command(cmd, "abort") && flags_Widget(w) & dragged_WidgetFlag) {
2364 postCommand_Widget(d, "navigate.back");
2365 }
2366 setFlags_Widget(w, dragged_WidgetFlag, iFalse);
2367 setVisualOffset_Widget(w, 0, 100, 0);
2368 return iTrue;
2369 }
2370 if (equal_Command(cmd, "edgeswipe.ended") && argLabel_Command(cmd, "side") == 1) {
2371 iWidget *swipeParent = swipeParent_DocumentWidget_(d);
2372 iWidget *swipeIn = findChild_Widget(swipeParent, "swipein");
2373 if (swipeIn) {
2374 swipeIn->offsetRef = NULL;
2375 destroy_Widget(swipeIn);
2376 }
2377 }
2378 if (equal_Command(cmd, "swipe.back")) {
2379 if (atOldest_History(d->mod.history)) {
2380 setVisualOffset_Widget(w, 0, 100, 0);
2381 return iTrue;
2382 }
2383 iWidget *swipeParent = swipeParent_DocumentWidget_(d);
2384 iDocumentWidget *target = new_DocumentWidget();
2385 setId_Widget(as_Widget(target), "swipeout");
2386 /* The target takes the old document and jumps on top. */
2387 target->widget.rect.pos = windowToInner_Widget(swipeParent, innerToWindow_Widget(w, zero_I2()));
2388 /* Note: `innerToWindow_Widget` does not apply visual offset. */
2389 target->widget.rect.size = w->rect.size;
2390 setFlags_Widget(as_Widget(target), fixedPosition_WidgetFlag | fixedSize_WidgetFlag, iTrue);
2391 swap_DocumentWidget_(target, d->doc, d);
2392 addChildPos_Widget(swipeParent, iClob(target), back_WidgetAddPos);
2393 setFlags_Widget(as_Widget(d), refChildrenOffset_WidgetFlag, iTrue);
2394 as_Widget(d)->offsetRef = swipeParent;
2395 setVisualOffset_Widget(as_Widget(target), value_Anim(&w->visualOffset), 0, 0);
2396 setVisualOffset_Widget(as_Widget(target), width_Widget(target), 150, 0);
2397 destroy_Widget(as_Widget(target)); /* will be actually deleted after animation finishes */
2398 setVisualOffset_Widget(w, 0, 0, 0);
2399 postCommand_Widget(d, "navigate.back");
2400 return iTrue;
2401 }
2402 return iFalse;
2403}
2404
2213static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) { 2405static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) {
2214 iWidget *w = as_Widget(d); 2406 iWidget *w = as_Widget(d);
2215 if (equal_Command(cmd, "document.openurls.changed")) { 2407 if (equal_Command(cmd, "document.openurls.changed")) {
@@ -2258,6 +2450,7 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
2258 return iFalse; 2450 return iFalse;
2259 } 2451 }
2260 else if (equal_Command(cmd, "theme.changed") && document_App() == d) { 2452 else if (equal_Command(cmd, "theme.changed") && document_App() == d) {
2453// invalidateTheme_History(d->mod.history); /* cached colors */
2261 updateTheme_DocumentWidget_(d); 2454 updateTheme_DocumentWidget_(d);
2262 updateVisible_DocumentWidget_(d); 2455 updateVisible_DocumentWidget_(d);
2263 updateTrust_DocumentWidget_(d, NULL); 2456 updateTrust_DocumentWidget_(d, NULL);
@@ -2347,6 +2540,7 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
2347 msg, "%s\n", formatCStrs_Lang("num.bytes.n", size_Block(&d->sourceContent))); 2540 msg, "%s\n", formatCStrs_Lang("num.bytes.n", size_Block(&d->sourceContent)));
2348 } 2541 }
2349 } 2542 }
2543 /* TODO: On mobile, omit the CA status. */
2350 appendFormat_String( 2544 appendFormat_String(
2351 msg, 2545 msg,
2352 "\n%s${pageinfo.cert.status}\n" 2546 "\n%s${pageinfo.cert.status}\n"
@@ -2651,6 +2845,18 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
2651 return iTrue; 2845 return iTrue;
2652 } 2846 }
2653 else if (equal_Command(cmd, "navigate.back") && document_App() == d) { 2847 else if (equal_Command(cmd, "navigate.back") && document_App() == d) {
2848 if (isPortraitPhone_App()) {
2849 if (d->flags & openedFromSidebar_DocumentWidgetFlag &&
2850 !isVisible_Widget(findWidget_App("sidebar"))) {
2851 postCommand_App("sidebar.toggle");
2852 showToolbar_Root(get_Root(), iTrue);
2853#if defined (iPlatformAppleMobile)
2854 playHapticEffect_iOS(gentleTap_HapticEffect);
2855#endif
2856 return iTrue;
2857 }
2858 d->flags &= ~openedFromSidebar_DocumentWidgetFlag;
2859 }
2654 if (d->request) { 2860 if (d->request) {
2655 postCommandf_Root(w->root, 2861 postCommandf_Root(w->root,
2656 "document.request.cancelled doc:%p url:%s", d, cstr_String(d->mod.url)); 2862 "document.request.cancelled doc:%p url:%s", d, cstr_String(d->mod.url));
@@ -2813,7 +3019,7 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
2813 uiHeading_ColorEscape "${heading.import.bookmarks}", 3019 uiHeading_ColorEscape "${heading.import.bookmarks}",
2814 formatCStrs_Lang("dlg.import.found.n", count), 3020 formatCStrs_Lang("dlg.import.found.n", count),
2815 (iMenuItem[]){ { "${cancel}", 0, 0, NULL }, 3021 (iMenuItem[]){ { "${cancel}", 0, 0, NULL },
2816 { format_CStr(cstrCount_Lang("dlg.import.add.n", count), 3022 { format_CStr(cstrCount_Lang("dlg.import.add.n", (int) count),
2817 uiTextAction_ColorEscape, 3023 uiTextAction_ColorEscape,
2818 count), 3024 count),
2819 0, 3025 0,
@@ -2872,6 +3078,10 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
2872 else if (startsWith_CStr(cmd, "pinch.") && document_Command(cmd) == d) { 3078 else if (startsWith_CStr(cmd, "pinch.") && document_Command(cmd) == d) {
2873 return handlePinch_DocumentWidget_(d, cmd); 3079 return handlePinch_DocumentWidget_(d, cmd);
2874 } 3080 }
3081 else if ((startsWith_CStr(cmd, "edgeswipe.") || startsWith_CStr(cmd, "swipe.")) &&
3082 document_App() == d) {
3083 return handleSwipe_DocumentWidget_(d, cmd);
3084 }
2875 return iFalse; 3085 return iFalse;
2876} 3086}
2877 3087
@@ -3320,6 +3530,18 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
3320 d->contextLink->linkId) }); 3530 d->contextLink->linkId) });
3321 } 3531 }
3322 } 3532 }
3533 if (equalCase_Rangecc(scheme, "file")) {
3534 /* Local files may be deleted. */
3535 pushBack_Array(
3536 &items,
3537 &(iMenuItem){ delete_Icon " " uiTextCaution_ColorEscape
3538 "${link.file.delete}",
3539 0,
3540 0,
3541 format_CStr("!file.delete confirm:1 path:%s",
3542 cstrCollect_String(
3543 localFilePathFromUrl_String(linkUrl))) });
3544 }
3323 } 3545 }
3324 else if (deviceType_App() == desktop_AppDeviceType) { 3546 else if (deviceType_App() == desktop_AppDeviceType) {
3325 if (!isEmpty_Range(&d->selectMark)) { 3547 if (!isEmpty_Range(&d->selectMark)) {
@@ -3877,9 +4099,9 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) {
3877 int bg = tmBackgroundOpenLink_ColorId; 4099 int bg = tmBackgroundOpenLink_ColorId;
3878 const int frame = tmFrameOpenLink_ColorId; 4100 const int frame = tmFrameOpenLink_ColorId;
3879 iRect wideRect = { init_I2(left_Rect(d->widgetBounds), visPos.y), 4101 iRect wideRect = { init_I2(left_Rect(d->widgetBounds), visPos.y),
3880 init_I2(width_Rect(d->widgetBounds) + 4102 init_I2(width_Rect(d->widgetBounds) +
3881 width_Widget(d->widget->scroll), 4103 width_Widget(d->widget->scroll),
3882 height_Rect(run->visBounds)) }; 4104 height_Rect(run->visBounds)) };
3883 /* The first line is composed of two runs that may be drawn in either order, so 4105 /* The first line is composed of two runs that may be drawn in either order, so
3884 only draw half of the background. */ 4106 only draw half of the background. */
3885 if (run->flags & decoration_GmRunFlag) { 4107 if (run->flags & decoration_GmRunFlag) {
@@ -3943,7 +4165,7 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) {
3943 drawCentered_Text(defaultContentSmall_FontId, 4165 drawCentered_Text(defaultContentSmall_FontId,
3944 circleArea, 4166 circleArea,
3945 iTrue, 4167 iTrue,
3946 tmQuote_ColorId, 4168 tmQuote_ColorId,
3947 "%lc", 4169 "%lc",
3948 (int) ordChar); 4170 (int) ordChar);
3949 goto runDrawn; 4171 goto runDrawn;
@@ -4181,7 +4403,7 @@ static void drawSideElements_DocumentWidget_(const iDocumentWidget *d) {
4181 iDrawBufs * dbuf = d->drawBufs; 4403 iDrawBufs * dbuf = d->drawBufs;
4182 iPaint p; 4404 iPaint p;
4183 init_Paint(&p); 4405 init_Paint(&p);
4184 setClip_Paint(&p, bounds); 4406 setClip_Paint(&p, boundsWithoutVisualOffset_Widget(w));
4185 /* Side icon and current heading. */ 4407 /* Side icon and current heading. */
4186 if (prefs_App()->sideIcon && opacity > 0 && dbuf->sideIconBuf) { 4408 if (prefs_App()->sideIcon && opacity > 0 && dbuf->sideIconBuf) {
4187 const iInt2 texSize = size_SDLTexture(dbuf->sideIconBuf); 4409 const iInt2 texSize = size_SDLTexture(dbuf->sideIconBuf);
@@ -4266,7 +4488,8 @@ static iBool render_DocumentWidget_(const iDocumentWidget *d, iDrawContext *ctx,
4266 /* Swap buffers around to have room available both before and after the visible region. */ 4488 /* Swap buffers around to have room available both before and after the visible region. */
4267 allocVisBuffer_DocumentWidget_(d); 4489 allocVisBuffer_DocumentWidget_(d);
4268 reposition_VisBuf(visBuf, vis); 4490 reposition_VisBuf(visBuf, vis);
4269 /* Redraw the invalid ranges. */ { 4491 /* Redraw the invalid ranges. */
4492 if (~flags_Widget(constAs_Widget(d)) & destroyPending_WidgetFlag) {
4270 iPaint *p = &ctx->paint; 4493 iPaint *p = &ctx->paint;
4271 init_Paint(p); 4494 init_Paint(p);
4272 iForIndices(i, visBuf->buffers) { 4495 iForIndices(i, visBuf->buffers) {
@@ -4430,12 +4653,17 @@ static void prerender_DocumentWidget_(iAny *context) {
4430} 4653}
4431 4654
4432static void draw_DocumentWidget_(const iDocumentWidget *d) { 4655static void draw_DocumentWidget_(const iDocumentWidget *d) {
4433 const iWidget *w = constAs_Widget(d); 4656 const iWidget *w = constAs_Widget(d);
4434 const iRect bounds = bounds_Widget(w); 4657 const iRect bounds = bounds_Widget(w);
4658 const iRect boundsWithoutVisOff = boundsWithoutVisualOffset_Widget(w);
4659 const iRect clipBounds = intersect_Rect(bounds, boundsWithoutVisOff);
4435 if (width_Rect(bounds) <= 0) { 4660 if (width_Rect(bounds) <= 0) {
4436 return; 4661 return;
4437 } 4662 }
4438// draw_Widget(w); 4663 /* TODO: Come up with a better palette caching system.
4664 It should be able to recompute cached colors in `History` when the theme has changed.
4665 Cache the theme seed in `GmDocument`? */
4666// makePaletteGlobal_GmDocument(d->doc);
4439 if (d->drawBufs->flags & updateTimestampBuf_DrawBufsFlag) { 4667 if (d->drawBufs->flags & updateTimestampBuf_DrawBufsFlag) {
4440 updateTimestampBuf_DocumentWidget_(d); 4668 updateTimestampBuf_DocumentWidget_(d);
4441 } 4669 }
@@ -4450,14 +4678,15 @@ static void draw_DocumentWidget_(const iDocumentWidget *d) {
4450 .vis = vis, 4678 .vis = vis,
4451 .showLinkNumbers = (d->flags & showLinkNumbers_DocumentWidgetFlag) != 0, 4679 .showLinkNumbers = (d->flags & showLinkNumbers_DocumentWidgetFlag) != 0,
4452 }; 4680 };
4681 init_Paint(&ctx.paint);
4453 render_DocumentWidget_(d, &ctx, iFalse /* just the mandatory parts */); 4682 render_DocumentWidget_(d, &ctx, iFalse /* just the mandatory parts */);
4454 setClip_Paint(&ctx.paint, bounds); 4683 setClip_Paint(&ctx.paint, clipBounds);
4455 int yTop = docBounds.pos.y - pos_SmoothScroll(&d->scrollY); 4684 int yTop = docBounds.pos.y - pos_SmoothScroll(&d->scrollY);
4456 draw_VisBuf(d->visBuf, init_I2(bounds.pos.x, yTop), ySpan_Rect(bounds)); 4685 draw_VisBuf(d->visBuf, init_I2(bounds.pos.x, yTop), ySpan_Rect(bounds));
4457 /* Text markers. */ 4686 /* Text markers. */
4458 const iBool isTouchSelecting = (flags_Widget(w) & touchDrag_WidgetFlag) != 0; 4687 const iBool isTouchSelecting = (flags_Widget(w) & touchDrag_WidgetFlag) != 0;
4459 if (!isEmpty_Range(&d->foundMark) || !isEmpty_Range(&d->selectMark)) { 4688 if (!isEmpty_Range(&d->foundMark) || !isEmpty_Range(&d->selectMark)) {
4460 SDL_Renderer *render = renderer_Window(get_Window()); 4689 SDL_Renderer *render = renderer_Window(get_Window());
4461 ctx.firstMarkRect = zero_Rect(); 4690 ctx.firstMarkRect = zero_Rect();
4462 ctx.lastMarkRect = zero_Rect(); 4691 ctx.lastMarkRect = zero_Rect();
4463 SDL_SetRenderDrawBlendMode(render, 4692 SDL_SetRenderDrawBlendMode(render,
@@ -4486,7 +4715,6 @@ static void draw_DocumentWidget_(const iDocumentWidget *d) {
4486 } 4715 }
4487 } 4716 }
4488 drawMedia_DocumentWidget_(d, &ctx.paint); 4717 drawMedia_DocumentWidget_(d, &ctx.paint);
4489 unsetClip_Paint(&ctx.paint);
4490 /* Fill the top and bottom, in case the document is short. */ 4718 /* Fill the top and bottom, in case the document is short. */
4491 if (yTop > top_Rect(bounds)) { 4719 if (yTop > top_Rect(bounds)) {
4492 fillRect_Paint(&ctx.paint, 4720 fillRect_Paint(&ctx.paint,
@@ -4500,6 +4728,7 @@ static void draw_DocumentWidget_(const iDocumentWidget *d) {
4500 init_Rect(bounds.pos.x, yBottom, bounds.size.x, bottom_Rect(bounds) - yBottom), 4728 init_Rect(bounds.pos.x, yBottom, bounds.size.x, bottom_Rect(bounds) - yBottom),
4501 tmBackground_ColorId); 4729 tmBackground_ColorId);
4502 } 4730 }
4731 unsetClip_Paint(&ctx.paint);
4503 drawSideElements_DocumentWidget_(d); 4732 drawSideElements_DocumentWidget_(d);
4504 if (prefs_App()->hoverLink && d->hoverLink) { 4733 if (prefs_App()->hoverLink && d->hoverLink) {
4505 const int font = uiLabel_FontId; 4734 const int font = uiLabel_FontId;
@@ -4561,6 +4790,23 @@ static void draw_DocumentWidget_(const iDocumentWidget *d) {
4561 drawCentered_Text(uiLabelBold_FontId, rect, iFalse, uiBackground_ColorId, "%zu bytes selected", 4790 drawCentered_Text(uiLabelBold_FontId, rect, iFalse, uiBackground_ColorId, "%zu bytes selected",
4562 size_Range(&mark)); 4791 size_Range(&mark));
4563 } 4792 }
4793 if (w->offsetRef) {
4794 const int offX = visualOffsetByReference_Widget(w);
4795 if (offX) {
4796 setClip_Paint(&ctx.paint, clipBounds);
4797 SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_BLEND);
4798 ctx.paint.alpha = iAbs(offX) / (float) get_Window()->size.x * 300;
4799 fillRect_Paint(&ctx.paint, bounds, backgroundFadeColor_Widget());
4800 SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_NONE);
4801 unsetClip_Paint(&ctx.paint);
4802 }
4803 else {
4804 /* TODO: Should have a better place to do this; drawing is supposed to be immutable. */
4805 iWidget *mut = iConstCast(iWidget *, w);
4806 mut->offsetRef = NULL;
4807 mut->flags &= ~refChildrenOffset_WidgetFlag;
4808 }
4809 }
4564} 4810}
4565 4811
4566/*----------------------------------------------------------------------------------------------*/ 4812/*----------------------------------------------------------------------------------------------*/
@@ -4628,9 +4874,12 @@ static void setUrl_DocumentWidget_(iDocumentWidget *d, const iString *url) {
4628 d->flags |= urlChanged_DocumentWidgetFlag; 4874 d->flags |= urlChanged_DocumentWidgetFlag;
4629 set_String(d->mod.url, url); 4875 set_String(d->mod.url, url);
4630 } 4876 }
4631} 4877}
4632 4878
4633void setUrlFromCache_DocumentWidget(iDocumentWidget *d, const iString *url, iBool isFromCache) { 4879void setUrlFlags_DocumentWidget(iDocumentWidget *d, const iString *url, int setUrlFlags) {
4880 iChangeFlags(d->flags, openedFromSidebar_DocumentWidgetFlag,
4881 (setUrlFlags & openedFromSidebar_DocumentWidgetSetUrlFlag) != 0);
4882 const iBool isFromCache = (setUrlFlags & useCachedContentIfAvailable_DocumentWidgetSetUrlFlag) != 0;
4634 setLinkNumberMode_DocumentWidget_(d, iFalse); 4883 setLinkNumberMode_DocumentWidget_(d, iFalse);
4635 setUrl_DocumentWidget_(d, urlFragmentStripped_String(url)); 4884 setUrl_DocumentWidget_(d, urlFragmentStripped_String(url));
4636 /* See if there a username in the URL. */ 4885 /* See if there a username in the URL. */
@@ -4642,6 +4891,7 @@ void setUrlFromCache_DocumentWidget(iDocumentWidget *d, const iString *url, iBoo
4642 4891
4643void setUrlAndSource_DocumentWidget(iDocumentWidget *d, const iString *url, const iString *mime, 4892void setUrlAndSource_DocumentWidget(iDocumentWidget *d, const iString *url, const iString *mime,
4644 const iBlock *source) { 4893 const iBlock *source) {
4894 d->flags &= ~openedFromSidebar_DocumentWidgetFlag;
4645 setLinkNumberMode_DocumentWidget_(d, iFalse); 4895 setLinkNumberMode_DocumentWidget_(d, iFalse);
4646 setUrl_DocumentWidget_(d, url); 4896 setUrl_DocumentWidget_(d, url);
4647 parseUser_DocumentWidget_(d); 4897 parseUser_DocumentWidget_(d);
@@ -4650,7 +4900,7 @@ void setUrlAndSource_DocumentWidget(iDocumentWidget *d, const iString *url, cons
4650 initCurrent_Time(&resp->when); 4900 initCurrent_Time(&resp->when);
4651 set_String(&resp->meta, mime); 4901 set_String(&resp->meta, mime);
4652 set_Block(&resp->body, source); 4902 set_Block(&resp->body, source);
4653 updateFromCachedResponse_DocumentWidget_(d, 0, resp); 4903 updateFromCachedResponse_DocumentWidget_(d, 0, resp, NULL);
4654 delete_GmResponse(resp); 4904 delete_GmResponse(resp);
4655} 4905}
4656 4906
@@ -4659,12 +4909,12 @@ iDocumentWidget *duplicate_DocumentWidget(const iDocumentWidget *orig) {
4659 delete_History(d->mod.history); 4909 delete_History(d->mod.history);
4660 d->initNormScrollY = normScrollPos_DocumentWidget_(d); 4910 d->initNormScrollY = normScrollPos_DocumentWidget_(d);
4661 d->mod.history = copy_History(orig->mod.history); 4911 d->mod.history = copy_History(orig->mod.history);
4662 setUrlFromCache_DocumentWidget(d, orig->mod.url, iTrue); 4912 setUrlFlags_DocumentWidget(d, orig->mod.url, useCachedContentIfAvailable_DocumentWidgetSetUrlFlag);
4663 return d; 4913 return d;
4664} 4914}
4665 4915
4666void setUrl_DocumentWidget(iDocumentWidget *d, const iString *url) { 4916void setUrl_DocumentWidget(iDocumentWidget *d, const iString *url) {
4667 setUrlFromCache_DocumentWidget(d, url, iFalse); 4917 setUrlFlags_DocumentWidget(d, url, 0);
4668} 4918}
4669 4919
4670void setInitialScroll_DocumentWidget(iDocumentWidget *d, float normScrollY) { 4920void setInitialScroll_DocumentWidget(iDocumentWidget *d, float normScrollY) {
@@ -4675,6 +4925,11 @@ void setRedirectCount_DocumentWidget(iDocumentWidget *d, int count) {
4675 d->redirectCount = count; 4925 d->redirectCount = count;
4676} 4926}
4677 4927
4928void setOpenedFromSidebar_DocumentWidget(iDocumentWidget *d, iBool fromSidebar) {
4929 iChangeFlags(d->flags, openedFromSidebar_DocumentWidgetFlag, fromSidebar);
4930// setCachedDocument_History(d->mod.history, d->doc, fromSidebar);
4931}
4932
4678iBool isRequestOngoing_DocumentWidget(const iDocumentWidget *d) { 4933iBool isRequestOngoing_DocumentWidget(const iDocumentWidget *d) {
4679 return d->request != NULL; 4934 return d->request != NULL;
4680} 4935}
diff --git a/src/ui/documentwidget.h b/src/ui/documentwidget.h
index c038f981..1921b25a 100644
--- a/src/ui/documentwidget.h
+++ b/src/ui/documentwidget.h
@@ -45,11 +45,17 @@ const iString * bookmarkTitle_DocumentWidget (const iDocumentWidget *);
45const iString * feedTitle_DocumentWidget (const iDocumentWidget *); 45const iString * feedTitle_DocumentWidget (const iDocumentWidget *);
46int documentWidth_DocumentWidget (const iDocumentWidget *); 46int documentWidth_DocumentWidget (const iDocumentWidget *);
47 47
48enum iDocumentWidgetSetUrlFlags {
49 useCachedContentIfAvailable_DocumentWidgetSetUrlFlag = iBit(1),
50 openedFromSidebar_DocumentWidgetSetUrlFlag = iBit(2),
51};
52
48void setUrl_DocumentWidget (iDocumentWidget *, const iString *url); 53void setUrl_DocumentWidget (iDocumentWidget *, const iString *url);
49void setUrlFromCache_DocumentWidget (iDocumentWidget *, const iString *url, iBool isFromCache); 54void setUrlFlags_DocumentWidget (iDocumentWidget *, const iString *url, int setUrlFlags);
50void setUrlAndSource_DocumentWidget (iDocumentWidget *, const iString *url, const iString *mime, const iBlock *source); 55void setUrlAndSource_DocumentWidget (iDocumentWidget *, const iString *url, const iString *mime, const iBlock *source);
51void setInitialScroll_DocumentWidget (iDocumentWidget *, float normScrollY); /* set after content received */ 56void setInitialScroll_DocumentWidget (iDocumentWidget *, float normScrollY); /* set after content received */
52void setRedirectCount_DocumentWidget (iDocumentWidget *, int count); 57void setRedirectCount_DocumentWidget (iDocumentWidget *, int count);
53void setSource_DocumentWidget (iDocumentWidget *, const iString *sourceText); 58void setSource_DocumentWidget (iDocumentWidget *, const iString *sourceText);
59void setOpenedFromSidebar_DocumentWidget(iDocumentWidget *, iBool fromSidebar);
54 60
55void updateSize_DocumentWidget (iDocumentWidget *); 61void updateSize_DocumentWidget (iDocumentWidget *);
diff --git a/src/ui/indicatorwidget.c b/src/ui/indicatorwidget.c
index 4a829ae3..bc0bd0fa 100644
--- a/src/ui/indicatorwidget.c
+++ b/src/ui/indicatorwidget.c
@@ -109,7 +109,7 @@ void draw_IndicatorWidget_(const iIndicatorWidget *d) {
109 colors[0] = black_ColorId; 109 colors[0] = black_ColorId;
110 } 110 }
111 fillRect_Paint(&p, 111 fillRect_Paint(&p,
112 (iRect){ topLeft_Rect(rect), init_I2(pos * width_Rect(rect), gap_UI / 4)}, 112 (iRect){ topLeft_Rect(rect), init_I2(pos * width_Rect(rect), gap_UI / 3)},
113 colors[isCompleted_IndicatorWidget_(d) ? 1 : 0]); 113 colors[isCompleted_IndicatorWidget_(d) ? 1 : 0]);
114 } 114 }
115} 115}
diff --git a/src/ui/inputwidget.c b/src/ui/inputwidget.c
index 39da1c3b..b108ee17 100644
--- a/src/ui/inputwidget.c
+++ b/src/ui/inputwidget.c
@@ -80,6 +80,7 @@ enum iInputWidgetFlag {
80 markWords_InputWidgetFlag = iBit(8), 80 markWords_InputWidgetFlag = iBit(8),
81 needUpdateBuffer_InputWidgetFlag = iBit(9), 81 needUpdateBuffer_InputWidgetFlag = iBit(9),
82 enterKeyEnabled_InputWidgetFlag = iBit(10), 82 enterKeyEnabled_InputWidgetFlag = iBit(10),
83 enterKeyInsertsLineFeed_InputWidgetFlag = iBit(11),
83}; 84};
84 85
85/*----------------------------------------------------------------------------------------------*/ 86/*----------------------------------------------------------------------------------------------*/
@@ -347,8 +348,11 @@ void init_InputWidget(iInputWidget *d, size_t maxLen) {
347 d->lastCursor = 0; 348 d->lastCursor = 0;
348 d->cursorLine = 0; 349 d->cursorLine = 0;
349 d->lastUpdateWidth = 0; 350 d->lastUpdateWidth = 0;
350 d->verticalMoveX = -1; /* TODO: Use this. */ 351 d->verticalMoveX = -1; /* TODO: Use this. */
351 d->inFlags = eatEscape_InputWidgetFlag | enterKeyEnabled_InputWidgetFlag; 352 d->inFlags = eatEscape_InputWidgetFlag | enterKeyEnabled_InputWidgetFlag;
353 if (deviceType_App() != desktop_AppDeviceType) {
354 d->inFlags |= enterKeyInsertsLineFeed_InputWidgetFlag;
355 }
352 iZap(d->mark); 356 iZap(d->mark);
353 setMaxLen_InputWidget(d, maxLen); 357 setMaxLen_InputWidget(d, maxLen);
354 d->maxLayoutLines = iInvalidSize; 358 d->maxLayoutLines = iInvalidSize;
@@ -464,6 +468,10 @@ void setValidator_InputWidget(iInputWidget *d, iInputWidgetValidatorFunc validat
464 d->validatorContext = context; 468 d->validatorContext = context;
465} 469}
466 470
471void setEnterInsertsLF_InputWidget(iInputWidget *d, iBool enterInsertsLF) {
472 iChangeFlags(d->inFlags, enterKeyInsertsLineFeed_InputWidgetFlag, enterInsertsLF);
473}
474
467void setEnterKeyEnabled_InputWidget(iInputWidget *d, iBool enterKeyEnabled) { 475void setEnterKeyEnabled_InputWidget(iInputWidget *d, iBool enterKeyEnabled) {
468 iChangeFlags(d->inFlags, enterKeyEnabled_InputWidgetFlag, enterKeyEnabled); 476 iChangeFlags(d->inFlags, enterKeyEnabled_InputWidgetFlag, enterKeyEnabled);
469} 477}
@@ -714,12 +722,12 @@ iLocalDef iBool isLastLine_InputWidget_(const iInputWidget *d, const iInputLine
714} 722}
715 723
716static size_t indexForRelativeX_InputWidget_(const iInputWidget *d, int x, const iInputLine *line) { 724static size_t indexForRelativeX_InputWidget_(const iInputWidget *d, int x, const iInputLine *line) {
725 size_t index = line->offset;
717 if (x <= 0) { 726 if (x <= 0) {
718 return line->offset; 727 return index;
719 } 728 }
720 const char *endPos; 729 const char *endPos;
721 tryAdvanceNoWrap_Text(d->font, range_String(&line->text), x, &endPos); 730 tryAdvanceNoWrap_Text(d->font, range_String(&line->text), x, &endPos);
722 size_t index = line->offset;
723 if (endPos == constEnd_String(&line->text)) { 731 if (endPos == constEnd_String(&line->text)) {
724 index += line->len; 732 index += line->len;
725 } 733 }
@@ -1166,7 +1174,7 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) {
1166 case SDLK_KP_ENTER: 1174 case SDLK_KP_ENTER:
1167 if (mods == KMOD_SHIFT || (d->maxLen == 0 && 1175 if (mods == KMOD_SHIFT || (d->maxLen == 0 &&
1168 ~d->inFlags & isUrl_InputWidgetFlag && 1176 ~d->inFlags & isUrl_InputWidgetFlag &&
1169 deviceType_App() != desktop_AppDeviceType)) { 1177 d->inFlags & enterKeyInsertsLineFeed_InputWidgetFlag)) {
1170 pushUndo_InputWidget_(d); 1178 pushUndo_InputWidget_(d);
1171 deleteMarked_InputWidget_(d); 1179 deleteMarked_InputWidget_(d);
1172 insertChar_InputWidget_(d, '\n'); 1180 insertChar_InputWidget_(d, '\n');
diff --git a/src/ui/inputwidget.h b/src/ui/inputwidget.h
index 5c39aae0..c70d9ad6 100644
--- a/src/ui/inputwidget.h
+++ b/src/ui/inputwidget.h
@@ -41,20 +41,21 @@ struct Impl_InputWidgetContentPadding {
41 41
42typedef void (*iInputWidgetValidatorFunc)(iInputWidget *, void *context); 42typedef void (*iInputWidgetValidatorFunc)(iInputWidget *, void *context);
43 43
44void setHint_InputWidget (iInputWidget *, const char *hintText); 44void setHint_InputWidget (iInputWidget *, const char *hintText);
45void setMode_InputWidget (iInputWidget *, enum iInputMode mode); 45void setMode_InputWidget (iInputWidget *, enum iInputMode mode);
46void setMaxLen_InputWidget (iInputWidget *, size_t maxLen); 46void setMaxLen_InputWidget (iInputWidget *, size_t maxLen);
47void setText_InputWidget (iInputWidget *, const iString *text); 47void setText_InputWidget (iInputWidget *, const iString *text);
48void setTextCStr_InputWidget (iInputWidget *, const char *cstr); 48void setTextCStr_InputWidget (iInputWidget *, const char *cstr);
49void setFont_InputWidget (iInputWidget *, int fontId); 49void setFont_InputWidget (iInputWidget *, int fontId);
50void setCursor_InputWidget (iInputWidget *, size_t pos); 50void setCursor_InputWidget (iInputWidget *, size_t pos);
51void setContentPadding_InputWidget (iInputWidget *, int left, int right); /* only affects the text entry */ 51void setContentPadding_InputWidget (iInputWidget *, int left, int right); /* only affects the text entry */
52void setMaxLayoutLines_InputWidget (iInputWidget *, size_t maxLayoutLines); 52void setMaxLayoutLines_InputWidget (iInputWidget *, size_t maxLayoutLines);
53void setValidator_InputWidget (iInputWidget *, iInputWidgetValidatorFunc validator, void *context); 53void setValidator_InputWidget (iInputWidget *, iInputWidgetValidatorFunc validator, void *context);
54void setEnterInsertsLF_InputWidget (iInputWidget *, iBool enterInsertsLF);
54void setEnterKeyEnabled_InputWidget (iInputWidget *, iBool enterKeyEnabled); 55void setEnterKeyEnabled_InputWidget (iInputWidget *, iBool enterKeyEnabled);
55void begin_InputWidget (iInputWidget *); 56void begin_InputWidget (iInputWidget *);
56void end_InputWidget (iInputWidget *, iBool accept); 57void end_InputWidget (iInputWidget *, iBool accept);
57void selectAll_InputWidget (iInputWidget *); 58void selectAll_InputWidget (iInputWidget *);
58 59
59void setSelectAllOnFocus_InputWidget (iInputWidget *, iBool selectAllOnFocus); 60void setSelectAllOnFocus_InputWidget (iInputWidget *, iBool selectAllOnFocus);
60void setSensitiveContent_InputWidget (iInputWidget *, iBool isSensitive); 61void setSensitiveContent_InputWidget (iInputWidget *, iBool isSensitive);
@@ -62,15 +63,13 @@ void setUrlContent_InputWidget (iInputWidget *, iBool isUrl);
62void setNotifyEdits_InputWidget (iInputWidget *, iBool notifyEdits); 63void setNotifyEdits_InputWidget (iInputWidget *, iBool notifyEdits);
63void setEatEscape_InputWidget (iInputWidget *, iBool eatEscape); 64void setEatEscape_InputWidget (iInputWidget *, iBool eatEscape);
64 65
65const iString * text_InputWidget (const iInputWidget *); 66iInputWidgetContentPadding contentPadding_InputWidget (const iInputWidget *);
67const iString * text_InputWidget (const iInputWidget *);
66 68
67iLocalDef const char *cstrText_InputWidget(const iInputWidget *d) { 69iLocalDef const char *cstrText_InputWidget(const iInputWidget *d) {
68 return cstr_String(text_InputWidget(d)); 70 return cstr_String(text_InputWidget(d));
69} 71}
70 72
71iInputWidgetContentPadding
72 contentPadding_InputWidget (const iInputWidget *);
73
74iLocalDef iInputWidget *newHint_InputWidget(size_t maxLen, const char *hint) { 73iLocalDef iInputWidget *newHint_InputWidget(size_t maxLen, const char *hint) {
75 iInputWidget *d = new_InputWidget(maxLen); 74 iInputWidget *d = new_InputWidget(maxLen);
76 setHint_InputWidget(d, hint); 75 setHint_InputWidget(d, hint);
diff --git a/src/ui/labelwidget.c b/src/ui/labelwidget.c
index 95f281be..b68ab793 100644
--- a/src/ui/labelwidget.c
+++ b/src/ui/labelwidget.c
@@ -44,6 +44,9 @@ struct Impl_LabelWidget {
44 struct { 44 struct {
45 uint8_t alignVisual : 1; /* align according to visible bounds, not font metrics */ 45 uint8_t alignVisual : 1; /* align according to visible bounds, not font metrics */
46 uint8_t noAutoMinHeight : 1; /* minimum height is not set automatically */ 46 uint8_t noAutoMinHeight : 1; /* minimum height is not set automatically */
47 uint8_t drawAsOutline : 1; /* draw as outline, filled with background color */
48 uint8_t noTopFrame : 1;
49 uint8_t wrap : 1;
47 } flags; 50 } flags;
48}; 51};
49 52
@@ -206,7 +209,7 @@ static void getColors_LabelWidget_(const iLabelWidget *d, int *bg, int *fg, int
206 } 209 }
207 } 210 }
208 int colorEscape = none_ColorId; 211 int colorEscape = none_ColorId;
209 if (startsWith_String(&d->label, "\r")) { 212 if (startsWith_String(&d->label, "\v")) {
210 colorEscape = cstr_String(&d->label)[1] - asciiBase_ColorEscape; /* TODO: can be two bytes long */ 213 colorEscape = cstr_String(&d->label)[1] - asciiBase_ColorEscape; /* TODO: can be two bytes long */
211 } 214 }
212 if (isHover_LabelWidget_(d)) { 215 if (isHover_LabelWidget_(d)) {
@@ -289,7 +292,7 @@ static void draw_LabelWidget_(const iLabelWidget *d) {
289 }; 292 };
290 drawLines_Paint(&p, points + 2, 3, frame2); 293 drawLines_Paint(&p, points + 2, 3, frame2);
291 drawLines_Paint( 294 drawLines_Paint(
292 &p, points, !isHover && flags & noTopFrame_WidgetFlag ? 2 : 3, frame); 295 &p, points, !isHover && flags & d->flags.noTopFrame ? 2 : 3, frame);
293 } 296 }
294 } 297 }
295 setClip_Paint(&p, rect); 298 setClip_Paint(&p, rect);
@@ -315,7 +318,7 @@ static void draw_LabelWidget_(const iLabelWidget *d) {
315 cstr_String(&str)); 318 cstr_String(&str));
316 deinit_String(&str); 319 deinit_String(&str);
317 } 320 }
318 if (flags & wrapText_WidgetFlag) { 321 if (d->flags.wrap) {
319 const iRect inner = adjusted_Rect(innerBounds_Widget(w), init_I2(iconPad, 0), zero_I2()); 322 const iRect inner = adjusted_Rect(innerBounds_Widget(w), init_I2(iconPad, 0), zero_I2());
320 const int wrap = inner.size.x; 323 const int wrap = inner.size.x;
321 drawWrapRange_Text(d->font, topLeft_Rect(inner), wrap, fg, range_String(&d->label)); 324 drawWrapRange_Text(d->font, topLeft_Rect(inner), wrap, fg, range_String(&d->label));
@@ -348,14 +351,14 @@ static void draw_LabelWidget_(const iLabelWidget *d) {
348 cstr_String(&d->label)); 351 cstr_String(&d->label));
349 } 352 }
350 else { 353 else {
351 drawCentered_Text(d->font, 354 drawCenteredOutline_Text(
352 adjusted_Rect(bounds, 355 d->font,
353 add_I2(zero_I2(), init_I2(iconPad, 0)), 356 adjusted_Rect(bounds, add_I2(zero_I2(), init_I2(iconPad, 0)), neg_I2(zero_I2())),
354 neg_I2(zero_I2())), 357 d->flags.alignVisual,
355 d->flags.alignVisual, 358 d->flags.drawAsOutline ? fg : none_ColorId,
356 fg, 359 d->flags.drawAsOutline ? d->widget.bgColor : fg,
357 "%s", 360 "%s",
358 cstr_String(&d->label)); 361 cstr_String(&d->label));
359 } 362 }
360 if (flags & chevron_WidgetFlag) { 363 if (flags & chevron_WidgetFlag) {
361 const iRect chRect = rect; 364 const iRect chRect = rect;
@@ -370,7 +373,7 @@ static void draw_LabelWidget_(const iLabelWidget *d) {
370 373
371static void sizeChanged_LabelWidget_(iLabelWidget *d) { 374static void sizeChanged_LabelWidget_(iLabelWidget *d) {
372 iWidget *w = as_Widget(d); 375 iWidget *w = as_Widget(d);
373 if (flags_Widget(w) & wrapText_WidgetFlag) { 376 if (d->flags.wrap) {
374 if (flags_Widget(w) & fixedHeight_WidgetFlag) { 377 if (flags_Widget(w) & fixedHeight_WidgetFlag) {
375 /* Calculate a new height based on the wrapping. */ 378 /* Calculate a new height based on the wrapping. */
376 w->rect.size.y = advanceWrapRange_Text( 379 w->rect.size.y = advanceWrapRange_Text(
@@ -408,7 +411,7 @@ void updateSize_LabelWidget(iLabelWidget *d) {
408 w->minSize.y = size.y; /* vertically text must remain visible */ 411 w->minSize.y = size.y; /* vertically text must remain visible */
409 } 412 }
410 /* Wrapped text implies that width must be defined by arrangement. */ 413 /* Wrapped text implies that width must be defined by arrangement. */
411 if (!(flags & (fixedWidth_WidgetFlag | wrapText_WidgetFlag))) { 414 if (~flags & fixedWidth_WidgetFlag && !d->flags.wrap) {
412 w->rect.size.x = size.x; 415 w->rect.size.x = size.x;
413 } 416 }
414 if (~flags & fixedHeight_WidgetFlag) { 417 if (~flags & fixedHeight_WidgetFlag) {
@@ -440,8 +443,11 @@ void init_LabelWidget(iLabelWidget *d, const char *label, const char *cmd) {
440 d->kmods = 0; 443 d->kmods = 0;
441 init_Click(&d->click, d, !isEmpty_String(&d->command) ? SDL_BUTTON_LEFT : 0); 444 init_Click(&d->click, d, !isEmpty_String(&d->command) ? SDL_BUTTON_LEFT : 0);
442 setFlags_Widget(w, hover_WidgetFlag, d->click.button != 0); 445 setFlags_Widget(w, hover_WidgetFlag, d->click.button != 0);
443 d->flags.alignVisual = iFalse; 446 d->flags.alignVisual = iFalse;
444 d->flags.noAutoMinHeight = iFalse; 447 d->flags.noAutoMinHeight = iFalse;
448 d->flags.drawAsOutline = iFalse;
449 d->flags.noTopFrame = iFalse;
450 d->flags.wrap = iFalse;
445 updateSize_LabelWidget(d); 451 updateSize_LabelWidget(d);
446 updateKey_LabelWidget_(d); /* could be bound to another key */ 452 updateKey_LabelWidget_(d); /* could be bound to another key */
447} 453}
@@ -481,6 +487,20 @@ void setNoAutoMinHeight_LabelWidget(iLabelWidget *d, iBool noAutoMinHeight) {
481 } 487 }
482} 488}
483 489
490void setNoTopFrame_LabelWidget(iLabelWidget *d, iBool noTopFrame) {
491 d->flags.noTopFrame = noTopFrame;
492}
493
494void setWrap_LabelWidget(iLabelWidget *d, iBool wrap) {
495 d->flags.wrap = wrap;
496}
497
498void setOutline_LabelWidget(iLabelWidget *d, iBool drawAsOutline) {
499 if (d) {
500 d->flags.drawAsOutline = drawAsOutline;
501 }
502}
503
484void updateText_LabelWidget(iLabelWidget *d, const iString *text) { 504void updateText_LabelWidget(iLabelWidget *d, const iString *text) {
485 set_String(&d->label, text); 505 set_String(&d->label, text);
486 set_String(&d->srcLabel, text); 506 set_String(&d->srcLabel, text);
@@ -495,6 +515,11 @@ void updateTextCStr_LabelWidget(iLabelWidget *d, const char *text) {
495 refresh_Widget(&d->widget); 515 refresh_Widget(&d->widget);
496} 516}
497 517
518void updateTextAndResizeWidthCStr_LabelWidget(iLabelWidget *d, const char *text) {
519 updateTextCStr_LabelWidget(d, text);
520 d->widget.rect.size.x = defaultSize_LabelWidget(d).x;
521}
522
498void setTextCStr_LabelWidget(iLabelWidget *d, const char *text) { 523void setTextCStr_LabelWidget(iLabelWidget *d, const char *text) {
499 setCStr_String(&d->label, text); 524 setCStr_String(&d->label, text);
500 set_String(&d->srcLabel, &d->label); 525 set_String(&d->srcLabel, &d->label);
@@ -537,6 +562,10 @@ iChar icon_LabelWidget(const iLabelWidget *d) {
537 return d->icon; 562 return d->icon;
538} 563}
539 564
565iBool isWrapped_LabelWidget(const iLabelWidget *d) {
566 return d->flags.wrap;
567}
568
540const iString *text_LabelWidget(const iLabelWidget *d) { 569const iString *text_LabelWidget(const iLabelWidget *d) {
541 if (!d) return collectNew_String(); 570 if (!d) return collectNew_String();
542 return &d->label; 571 return &d->label;
diff --git a/src/ui/labelwidget.h b/src/ui/labelwidget.h
index e38a1dc8..b8b6fd87 100644
--- a/src/ui/labelwidget.h
+++ b/src/ui/labelwidget.h
@@ -31,6 +31,9 @@ iDeclareObjectConstructionArgs(LabelWidget, const char *label, const char *comma
31 31
32void setAlignVisually_LabelWidget(iLabelWidget *, iBool alignVisual); 32void setAlignVisually_LabelWidget(iLabelWidget *, iBool alignVisual);
33void setNoAutoMinHeight_LabelWidget(iLabelWidget *, iBool noAutoMinHeight); 33void setNoAutoMinHeight_LabelWidget(iLabelWidget *, iBool noAutoMinHeight);
34void setNoTopFrame_LabelWidget (iLabelWidget *, iBool noTopFrame);
35void setWrap_LabelWidget (iLabelWidget *, iBool wrap);
36void setOutline_LabelWidget (iLabelWidget *, iBool drawAsOutline);
34void setFont_LabelWidget (iLabelWidget *, int fontId); 37void setFont_LabelWidget (iLabelWidget *, int fontId);
35void setTextColor_LabelWidget (iLabelWidget *, int color); 38void setTextColor_LabelWidget (iLabelWidget *, int color);
36void setText_LabelWidget (iLabelWidget *, const iString *text); /* resizes widget */ 39void setText_LabelWidget (iLabelWidget *, const iString *text); /* resizes widget */
@@ -43,12 +46,15 @@ void updateSize_LabelWidget (iLabelWidget *);
43void updateText_LabelWidget (iLabelWidget *, const iString *text); /* not resized */ 46void updateText_LabelWidget (iLabelWidget *, const iString *text); /* not resized */
44void updateTextCStr_LabelWidget (iLabelWidget *, const char *text); /* not resized */ 47void updateTextCStr_LabelWidget (iLabelWidget *, const char *text); /* not resized */
45 48
49void updateTextAndResizeWidthCStr_LabelWidget (iLabelWidget *, const char *text);
50
46iInt2 defaultSize_LabelWidget (const iLabelWidget *); 51iInt2 defaultSize_LabelWidget (const iLabelWidget *);
47int font_LabelWidget (const iLabelWidget *); 52int font_LabelWidget (const iLabelWidget *);
48const iString * text_LabelWidget (const iLabelWidget *); 53const iString * text_LabelWidget (const iLabelWidget *);
49const iString * sourceText_LabelWidget (const iLabelWidget *); /* untranslated */ 54const iString * sourceText_LabelWidget (const iLabelWidget *); /* untranslated */
50const iString * command_LabelWidget (const iLabelWidget *); 55const iString * command_LabelWidget (const iLabelWidget *);
51iChar icon_LabelWidget (const iLabelWidget *); 56iChar icon_LabelWidget (const iLabelWidget *);
57iBool isWrapped_LabelWidget (const iLabelWidget *);
52 58
53iLabelWidget *newKeyMods_LabelWidget(const char *label, int key, int kmods, const char *command); 59iLabelWidget *newKeyMods_LabelWidget(const char *label, int key, int kmods, const char *command);
54iLabelWidget *newColor_LabelWidget (const char *text, int color); 60iLabelWidget *newColor_LabelWidget (const char *text, int color);
diff --git a/src/ui/mediaui.c b/src/ui/mediaui.c
index bc417fc3..fa09b214 100644
--- a/src/ui/mediaui.c
+++ b/src/ui/mediaui.c
@@ -118,7 +118,7 @@ void draw_PlayerUI(iPlayerUI *d, iPaint *p) {
118 isPaused_Player(d->player) ? "\U0001f782" : "\u23f8", 118 isPaused_Player(d->player) ? "\U0001f782" : "\u23f8",
119 uiContent_FontId); 119 uiContent_FontId);
120 drawPlayerButton_(p, d->rewindRect, "\u23ee", uiContent_FontId); 120 drawPlayerButton_(p, d->rewindRect, "\u23ee", uiContent_FontId);
121 drawPlayerButton_(p, d->menuRect, "\U0001d362", uiContent_FontId); 121 drawPlayerButton_(p, d->menuRect, menu_Icon, uiContent_FontId);
122 if (!isAdjusting) { 122 if (!isAdjusting) {
123 drawPlayerButton_( 123 drawPlayerButton_(
124 p, d->volumeRect, volumeChar_(volume_Player(d->player)), uiContentSymbols_FontId); 124 p, d->volumeRect, volumeChar_(volume_Player(d->player)), uiContentSymbols_FontId);
diff --git a/src/ui/mobile.c b/src/ui/mobile.c
index 263fc141..0ff3fe85 100644
--- a/src/ui/mobile.c
+++ b/src/ui/mobile.c
@@ -120,7 +120,7 @@ static iBool mainDetailSplitHandler_(iWidget *mainDetailSplit, const char *cmd)
120 } 120 }
121 iForEach(ObjectList, i, children_Widget(detailStack)) { 121 iForEach(ObjectList, i, children_Widget(detailStack)) {
122 iWidget *panel = i.object; 122 iWidget *panel = i.object;
123 setFlags_Widget(panel, edgeDraggable_WidgetFlag, !isSideBySide); 123 setFlags_Widget(panel, leftEdgeDraggable_WidgetFlag, !isSideBySide);
124 if (isSideBySide) { 124 if (isSideBySide) {
125 setVisualOffset_Widget(panel, 0, 0, 0); 125 setVisualOffset_Widget(panel, 0, 0, 0);
126 } 126 }
@@ -150,8 +150,7 @@ static iBool topPanelHandler_(iWidget *topPanel, const char *cmd) {
150 setFlags_Widget(button, selected_WidgetFlag, iTrue); 150 setFlags_Widget(button, selected_WidgetFlag, iTrue);
151 return iTrue; 151 return iTrue;
152 } 152 }
153 if (equal_Command(cmd, "mouse.clicked") && arg_Command(cmd) && 153 if (equal_Command(cmd, "swipe.back")) {
154 argLabel_Command(cmd, "button") == SDL_BUTTON_X1) {
155 postCommand_App("panel.close"); 154 postCommand_App("panel.close");
156 return iTrue; 155 return iTrue;
157 } 156 }
@@ -201,6 +200,8 @@ static iBool isTwoColumnPage_(iWidget *d) {
201 200
202static iBool isOmittedPref_(const iString *id) { 201static iBool isOmittedPref_(const iString *id) {
203 static const char *omittedPrefs[] = { 202 static const char *omittedPrefs[] = {
203 "prefs.userfont",
204 "prefs.animate",
204 "prefs.smoothscroll", 205 "prefs.smoothscroll",
205 "prefs.imageloadscroll", 206 "prefs.imageloadscroll",
206 "prefs.pinsplit", 207 "prefs.pinsplit",
@@ -413,7 +414,7 @@ void finalizeSheet_Mobile(iWidget *sheet) {
413 setFlags_Widget(sheet, 414 setFlags_Widget(sheet,
414 frameless_WidgetFlag | 415 frameless_WidgetFlag |
415 //resizeWidthOfChildren_WidgetFlag | 416 //resizeWidthOfChildren_WidgetFlag |
416 edgeDraggable_WidgetFlag | 417 leftEdgeDraggable_WidgetFlag |
417 commandOnClick_WidgetFlag, 418 commandOnClick_WidgetFlag,
418 iTrue); 419 iTrue);
419 iPtrArray * contents = collect_PtrArray(new_PtrArray()); /* two-column pages */ 420 iPtrArray * contents = collect_PtrArray(new_PtrArray()); /* two-column pages */
@@ -446,7 +447,7 @@ void finalizeSheet_Mobile(iWidget *sheet) {
446 } 447 }
447 addChild_Widget(topPanel, iClob(makePadding_Widget(lineHeight_Text(labelFont_())))); 448 addChild_Widget(topPanel, iClob(makePadding_Widget(lineHeight_Text(labelFont_()))));
448 /* Slide top panel with detail panels. */ { 449 /* Slide top panel with detail panels. */ {
449 setFlags_Widget(topPanel, refChildrenOffset_WidgetFlag, iTrue); 450 setFlags_Widget(topPanel, refChildrenOffset_WidgetFlag, iTrue);
450 topPanel->offsetRef = detailStack; 451 topPanel->offsetRef = detailStack;
451 } 452 }
452 if (prefsTabs) { 453 if (prefsTabs) {
@@ -472,7 +473,8 @@ void finalizeSheet_Mobile(iWidget *sheet) {
472 0x02699, /* gear */ 473 0x02699, /* gear */
473 0x1f4f1, /* mobile phone */ 474 0x1f4f1, /* mobile phone */
474 0x1f3a8, /* palette */ 475 0x1f3a8, /* palette */
475 0x1f523, 476 0x1f5da, /* aA */
477 0x1f660, /* pointing bud */
476 0x1f5a7, /* computer network */ 478 0x1f5a7, /* computer network */
477 }; 479 };
478 setIcon_LabelWidget(panelButton, icons[i]); 480 setIcon_LabelWidget(panelButton, icons[i]);
@@ -621,6 +623,12 @@ void finalizeSheet_Mobile(iWidget *sheet) {
621 /* Additional elements for preferences. */ 623 /* Additional elements for preferences. */
622 if (isPrefs) { 624 if (isPrefs) {
623 addChild_Widget(topPanel, iClob(makePadding_Widget(lineHeight_Text(labelFont_())))); 625 addChild_Widget(topPanel, iClob(makePadding_Widget(lineHeight_Text(labelFont_()))));
626 /* Management. */ {
627 iLabelWidget *idManButton = addChildFlags_Widget(topPanel,
628 iClob(makePanelButton_(person_Icon " ${sidebar.identities}", "panel.open")),
629 chevron_WidgetFlag | borderTop_WidgetFlag);
630 }
631 addChild_Widget(topPanel, iClob(makePadding_Widget(lineHeight_Text(labelFont_()))));
624 iLabelWidget *aboutButton = addChildFlags_Widget(topPanel, 632 iLabelWidget *aboutButton = addChildFlags_Widget(topPanel,
625 iClob(makePanelButton_(planet_Icon " ${menu.about}", "panel.open")), 633 iClob(makePanelButton_(planet_Icon " ${menu.about}", "panel.open")),
626 chevron_WidgetFlag | borderTop_WidgetFlag); 634 chevron_WidgetFlag | borderTop_WidgetFlag);
diff --git a/src/ui/root.c b/src/ui/root.c
index 15548e74..5266978b 100644
--- a/src/ui/root.c
+++ b/src/ui/root.c
@@ -52,10 +52,6 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
52 52
53#include <SDL_timer.h> 53#include <SDL_timer.h>
54 54
55#if defined (iPlatformAppleDesktop)
56# define iHaveNativeMenus
57#endif
58
59#if defined (iPlatformPcDesktop) 55#if defined (iPlatformPcDesktop)
60/* TODO: Submenus wouldn't hurt here. */ 56/* TODO: Submenus wouldn't hurt here. */
61static const iMenuItem navMenuItems_[] = { 57static const iMenuItem navMenuItems_[] = {
@@ -90,6 +86,7 @@ static const iMenuItem navMenuItems_[] = {
90#if defined (iPlatformAppleMobile) 86#if defined (iPlatformAppleMobile)
91/* Tablet menu. */ 87/* Tablet menu. */
92static const iMenuItem tabletNavMenuItems_[] = { 88static const iMenuItem tabletNavMenuItems_[] = {
89 { folder_Icon " ${menu.openfile}", SDLK_o, KMOD_PRIMARY, "file.open" },
93 { add_Icon " ${menu.newtab}", 't', KMOD_PRIMARY, "tabs.new" }, 90 { add_Icon " ${menu.newtab}", 't', KMOD_PRIMARY, "tabs.new" },
94 { close_Icon " ${menu.closetab}", 'w', KMOD_PRIMARY, "tabs.close" }, 91 { close_Icon " ${menu.closetab}", 'w', KMOD_PRIMARY, "tabs.close" },
95 { "---", 0, 0, NULL }, 92 { "---", 0, 0, NULL },
@@ -110,6 +107,7 @@ static const iMenuItem tabletNavMenuItems_[] = {
110 107
111/* Phone menu. */ 108/* Phone menu. */
112static const iMenuItem phoneNavMenuItems_[] = { 109static const iMenuItem phoneNavMenuItems_[] = {
110 { folder_Icon " ${menu.openfile}", SDLK_o, KMOD_PRIMARY, "file.open" },
113 { add_Icon " ${menu.newtab}", 't', KMOD_PRIMARY, "tabs.new" }, 111 { add_Icon " ${menu.newtab}", 't', KMOD_PRIMARY, "tabs.new" },
114 { close_Icon " ${menu.closetab}", 'w', KMOD_PRIMARY, "tabs.close" }, 112 { close_Icon " ${menu.closetab}", 'w', KMOD_PRIMARY, "tabs.close" },
115 { "---", 0, 0, NULL }, 113 { "---", 0, 0, NULL },
@@ -273,7 +271,8 @@ void destroyPending_Root(iRoot *d) {
273 setCurrent_Root(d); 271 setCurrent_Root(d);
274 iForEach(PtrSet, i, d->pendingDestruction) { 272 iForEach(PtrSet, i, d->pendingDestruction) {
275 iWidget *widget = *i.value; 273 iWidget *widget = *i.value;
276 if (!isFinished_Anim(&widget->visualOffset)) { 274 if (!isFinished_Anim(&widget->visualOffset) ||
275 isBeingVisuallyOffsetByReference_Widget(widget)) {
277 continue; 276 continue;
278 } 277 }
279 if (widget->flags & keepOnTop_WidgetFlag) { 278 if (widget->flags & keepOnTop_WidgetFlag) {
@@ -282,7 +281,7 @@ void destroyPending_Root(iRoot *d) {
282 if (widget->parent) { 281 if (widget->parent) {
283 removeChild_Widget(widget->parent, widget); 282 removeChild_Widget(widget->parent, widget);
284 } 283 }
285 iAssert(widget->parent == NULL); 284 iAssert(widget->parent == NULL);
286 iRelease(widget); 285 iRelease(widget);
287 remove_PtrSetIterator(&i); 286 remove_PtrSetIterator(&i);
288 } 287 }
@@ -433,9 +432,9 @@ static void updateNavBarIdentity_(iWidget *navBar) {
433 const iGmIdentity *ident = 432 const iGmIdentity *ident =
434 identityForUrl_GmCerts(certs_App(), url_DocumentWidget(document_App())); 433 identityForUrl_GmCerts(certs_App(), url_DocumentWidget(document_App()));
435 iWidget *button = findChild_Widget(navBar, "navbar.ident"); 434 iWidget *button = findChild_Widget(navBar, "navbar.ident");
436 iWidget *tool = findWidget_App("toolbar.ident"); 435 iLabelWidget *toolButton = findWidget_App("toolbar.ident");
437 setFlags_Widget(button, selected_WidgetFlag, ident != NULL); 436 setFlags_Widget(button, selected_WidgetFlag, ident != NULL);
438 setFlags_Widget(tool, selected_WidgetFlag, ident != NULL); 437 setOutline_LabelWidget(toolButton, ident == NULL);
439 /* Update menu. */ 438 /* Update menu. */
440 iLabelWidget *idItem = child_Widget(findChild_Widget(button, "menu"), 0); 439 iLabelWidget *idItem = child_Widget(findChild_Widget(button, "menu"), 0);
441 const iString *subjectName = ident ? name_GmIdentity(ident) : NULL; 440 const iString *subjectName = ident ? name_GmIdentity(ident) : NULL;
@@ -444,6 +443,12 @@ static void updateNavBarIdentity_(iWidget *navBar) {
444 subjectName ? format_CStr(uiTextAction_ColorEscape "%s", cstr_String(subjectName)) 443 subjectName ? format_CStr(uiTextAction_ColorEscape "%s", cstr_String(subjectName))
445 : "${menu.identity.notactive}"); 444 : "${menu.identity.notactive}");
446 setFlags_Widget(as_Widget(idItem), disabled_WidgetFlag, !ident); 445 setFlags_Widget(as_Widget(idItem), disabled_WidgetFlag, !ident);
446 iLabelWidget *toolName = findWidget_App("toolbar.name");
447 if (toolName) {
448 updateTextCStr_LabelWidget(toolName, subjectName ? cstr_String(subjectName) : "");
449 setFont_LabelWidget(toolButton, subjectName ? defaultMedium_FontId : uiLabelLarge_FontId);
450 arrange_Widget(parent_Widget(toolButton));
451 }
447} 452}
448 453
449static void updateNavDirButtons_(iWidget *navBar) { 454static void updateNavDirButtons_(iWidget *navBar) {
@@ -515,6 +520,26 @@ void updatePadding_Root(iRoot *d) {
515#endif 520#endif
516} 521}
517 522
523void updateToolbarColors_Root(iRoot *d) {
524#if defined (iPlatformMobile)
525 iWidget *toolBar = findChild_Widget(d->widget, "toolbar");
526 if (toolBar) {
527 const iBool isSidebarVisible = isVisible_Widget(findChild_Widget(d->widget, "sidebar"));
528 const int bg = isSidebarVisible ? uiBackgroundSidebar_ColorId :
529 tmBannerBackground_ColorId;
530 setBackgroundColor_Widget(toolBar, bg);
531 iForEach(ObjectList, i, children_Widget(toolBar)) {
532 iLabelWidget *btn = i.object;
533 setTextColor_LabelWidget(i.object, isSidebarVisible ? uiTextDim_ColorId :
534 tmBannerIcon_ColorId);
535 setBackgroundColor_Widget(i.object, bg); /* using noBackground, but ident has outline */
536 }
537 }
538#else
539 iUnused(d);
540#endif
541}
542
518void dismissPortraitPhoneSidebars_Root(iRoot *d) { 543void dismissPortraitPhoneSidebars_Root(iRoot *d) {
519 if (deviceType_App() == phone_AppDeviceType && isPortrait_App()) { 544 if (deviceType_App() == phone_AppDeviceType && isPortrait_App()) {
520 iWidget *sidebar = findChild_Widget(d->widget, "sidebar"); 545 iWidget *sidebar = findChild_Widget(d->widget, "sidebar");
@@ -551,9 +576,16 @@ static void updateUrlInputContentPadding_(iWidget *navBar) {
551} 576}
552 577
553static void showSearchQueryIndicator_(iBool show) { 578static void showSearchQueryIndicator_(iBool show) {
579 iWidget *navBar = findWidget_Root("navbar");
554 iWidget *indicator = findWidget_App("input.indicator.search"); 580 iWidget *indicator = findWidget_App("input.indicator.search");
581 updateTextCStr_LabelWidget((iLabelWidget *) indicator,
582 (deviceType_App() == phone_AppDeviceType ||
583 flags_Widget(navBar) & tight_WidgetFlag)
584 ? "${status.query.tight} " return_Icon
585 : "${status.query} " return_Icon);
586 indicator->rect.size.x = defaultSize_LabelWidget((iLabelWidget *) indicator).x; /* don't touch height */
555 showCollapsed_Widget(indicator, show); 587 showCollapsed_Widget(indicator, show);
556 updateUrlInputContentPadding_(findWidget_Root("navbar")); 588 updateUrlInputContentPadding_(navBar);
557} 589}
558 590
559static int navBarAvailableSpace_(iWidget *navBar) { 591static int navBarAvailableSpace_(iWidget *navBar) {
@@ -692,6 +724,7 @@ static iBool handleNavBarCommands_(iWidget *navBar, const char *cmd) {
692 iInputWidget *url = findWidget_Root("url"); 724 iInputWidget *url = findWidget_Root("url");
693 const iString *urlStr = collect_String(suffix_Command(cmd, "url")); 725 const iString *urlStr = collect_String(suffix_Command(cmd, "url"));
694 trimCache_App(); 726 trimCache_App();
727 trimMemory_App();
695 visitUrl_Visited(visited_App(), withSpacesEncoded_String(urlStr), 0); /* TODO: internal URI normalization */ 728 visitUrl_Visited(visited_App(), withSpacesEncoded_String(urlStr), 0); /* TODO: internal URI normalization */
696 postCommand_App("visited.changed"); /* sidebar will update */ 729 postCommand_App("visited.changed"); /* sidebar will update */
697 setText_InputWidget(url, urlStr); 730 setText_InputWidget(url, urlStr);
@@ -827,20 +860,20 @@ static iBool handleToolBarCommands_(iWidget *toolBar, const char *cmd) {
827 const int viewHeight = size_Root(get_Root()).y; 860 const int viewHeight = size_Root(get_Root()).y;
828 if (arg_Command(cmd) >= 0) { 861 if (arg_Command(cmd) >= 0) {
829 postCommandf_App("sidebar.mode arg:%d show:1", arg_Command(cmd)); 862 postCommandf_App("sidebar.mode arg:%d show:1", arg_Command(cmd));
830 if (!isVisible) { 863// if (!isVisible) {
831 setVisualOffset_Widget(sidebar, viewHeight, 0, 0); 864// setVisualOffset_Widget(sidebar, viewHeight, 0, 0);
832 setVisualOffset_Widget(sidebar, 0, 400, easeOut_AnimFlag | softer_AnimFlag); 865// setVisualOffset_Widget(sidebar, 0, 400, easeOut_AnimFlag | softer_AnimFlag);
833 } 866// }
834 } 867 }
835 else { 868 else {
836 postCommandf_App("sidebar.toggle"); 869 postCommandf_App("sidebar.toggle");
837 if (isVisible) { 870// if (isVisible) {
838 setVisualOffset_Widget(sidebar, height_Widget(sidebar), 250, easeIn_AnimFlag); 871// setVisualOffset_Widget(sidebar, height_Widget(sidebar), 250, easeIn_AnimFlag);
839 } 872// }
840 else { 873// else {
841 setVisualOffset_Widget(sidebar, viewHeight, 0, 0); 874// setVisualOffset_Widget(sidebar, viewHeight, 0, 0);
842 setVisualOffset_Widget(sidebar, 0, 400, easeOut_AnimFlag | softer_AnimFlag); 875// setVisualOffset_Widget(sidebar, 0, 400, easeOut_AnimFlag | softer_AnimFlag);
843 } 876// }
844 } 877 }
845 return iTrue; 878 return iTrue;
846 } 879 }
@@ -848,7 +881,10 @@ static iBool handleToolBarCommands_(iWidget *toolBar, const char *cmd) {
848 /* TODO: Clean this up. */ 881 /* TODO: Clean this up. */
849 iWidget *sidebar = findWidget_App("sidebar"); 882 iWidget *sidebar = findWidget_App("sidebar");
850 iWidget *sidebar2 = findWidget_App("sidebar2"); 883 iWidget *sidebar2 = findWidget_App("sidebar2");
851 dismissSidebar_(sidebar, "toolbar.view"); 884 //dismissSidebar_(sidebar, "toolbar.view");
885 if (isVisible_Widget(sidebar)) {
886 postCommandf_App("sidebar.toggle");
887 }
852 const iBool isVisible = isVisible_Widget(sidebar2); 888 const iBool isVisible = isVisible_Widget(sidebar2);
853 // setFlags_Widget(findChild_Widget(toolBar, "toolbar.ident"), noBackground_WidgetFlag, 889 // setFlags_Widget(findChild_Widget(toolBar, "toolbar.ident"), noBackground_WidgetFlag,
854 // isVisible); 890 // isVisible);
@@ -921,6 +957,23 @@ void updateMetrics_Root(iRoot *d) {
921 updatePadding_Root(d); 957 updatePadding_Root(d);
922 arrange_Widget(d->widget); 958 arrange_Widget(d->widget);
923 updateUrlInputContentPadding_(navBar); 959 updateUrlInputContentPadding_(navBar);
960 /* Position the toolbar identity name label manually. */ {
961 iLabelWidget *idName = findChild_Widget(d->widget, "toolbar.name");
962 if (idName) {
963 const iWidget *toolBar = findChild_Widget(d->widget, "toolbar");
964 const iWidget *viewButton = findChild_Widget(d->widget, "toolbar.view");
965 const iWidget *idButton = findChild_Widget(toolBar, "toolbar.ident");
966 const int font = defaultSmall_FontId;
967 setFont_LabelWidget(idName, font);
968 setPos_Widget(as_Widget(idName),
969 windowToLocal_Widget(as_Widget(idName),
970 init_I2(left_Rect(bounds_Widget(idButton)),
971 bottom_Rect(bounds_Widget(viewButton)) -
972 lineHeight_Text(font) - gap_UI / 2)));
973 setFixedSize_Widget(as_Widget(idName), init_I2(width_Widget(idButton),
974 lineHeight_Text(font)));
975 }
976 }
924 postRefresh_App(); 977 postRefresh_App();
925} 978}
926 979
@@ -1048,9 +1101,9 @@ void createUserInterface_Root(iRoot *d) {
1048 resizeHeightOfChildren_WidgetFlag | 1101 resizeHeightOfChildren_WidgetFlag |
1049 moveToParentRightEdge_WidgetFlag); 1102 moveToParentRightEdge_WidgetFlag);
1050 /* Feeds refresh indicator is inside the input field. */ { 1103 /* Feeds refresh indicator is inside the input field. */ {
1051 iLabelWidget *queryInd = 1104 iLabelWidget *queryInd = new_LabelWidget("${status.query} " return_Icon, NULL);
1052 new_LabelWidget(uiTextAction_ColorEscape "${status.query} " return_Icon, NULL);
1053 setId_Widget(as_Widget(queryInd), "input.indicator.search"); 1105 setId_Widget(as_Widget(queryInd), "input.indicator.search");
1106 setTextColor_LabelWidget(queryInd, uiTextAction_ColorId);
1054 setBackgroundColor_Widget(as_Widget(queryInd), uiBackground_ColorId); 1107 setBackgroundColor_Widget(as_Widget(queryInd), uiBackground_ColorId);
1055 setFrameColor_Widget(as_Widget(queryInd), uiTextAction_ColorId); 1108 setFrameColor_Widget(as_Widget(queryInd), uiTextAction_ColorId);
1056 setAlignVisually_LabelWidget(queryInd, iTrue); 1109 setAlignVisually_LabelWidget(queryInd, iTrue);
@@ -1060,9 +1113,9 @@ void createUserInterface_Root(iRoot *d) {
1060 collapse_WidgetFlag | hidden_WidgetFlag); 1113 collapse_WidgetFlag | hidden_WidgetFlag);
1061 } 1114 }
1062 /* Feeds refresh indicator is inside the input field. */ { 1115 /* Feeds refresh indicator is inside the input field. */ {
1063 iLabelWidget *fprog = new_LabelWidget(uiTextCaution_ColorEscape 1116 iLabelWidget *fprog = new_LabelWidget("", NULL);
1064 "\u2605 ${status.feeds}", NULL);
1065 setId_Widget(as_Widget(fprog), "feeds.progress"); 1117 setId_Widget(as_Widget(fprog), "feeds.progress");
1118 setTextColor_LabelWidget(fprog, uiTextCaution_ColorId);
1066 setBackgroundColor_Widget(as_Widget(fprog), uiBackground_ColorId); 1119 setBackgroundColor_Widget(as_Widget(fprog), uiBackground_ColorId);
1067 setAlignVisually_LabelWidget(fprog, iTrue); 1120 setAlignVisually_LabelWidget(fprog, iTrue);
1068 setNoAutoMinHeight_LabelWidget(fprog, iTrue); 1121 setNoAutoMinHeight_LabelWidget(fprog, iTrue);
@@ -1116,7 +1169,7 @@ void createUserInterface_Root(iRoot *d) {
1116 setId_Widget(as_Widget(pageMenuButton), "pagemenubutton"); 1169 setId_Widget(as_Widget(pageMenuButton), "pagemenubutton");
1117 setFont_LabelWidget(pageMenuButton, uiContentBold_FontId); 1170 setFont_LabelWidget(pageMenuButton, uiContentBold_FontId);
1118 setAlignVisually_LabelWidget(pageMenuButton, iTrue); 1171 setAlignVisually_LabelWidget(pageMenuButton, iTrue);
1119 addChildFlags_Widget(urlButtons, iClob(pageMenuButton), embedFlags); 1172 addChildFlags_Widget(urlButtons, iClob(pageMenuButton), embedFlags | tight_WidgetFlag);
1120 updateSize_LabelWidget(pageMenuButton); 1173 updateSize_LabelWidget(pageMenuButton);
1121 } 1174 }
1122 /* Reload button. */ { 1175 /* Reload button. */ {
@@ -1146,11 +1199,11 @@ void createUserInterface_Root(iRoot *d) {
1146#if !defined (iHaveNativeMenus) 1199#if !defined (iHaveNativeMenus)
1147# if defined (iPlatformAppleMobile) 1200# if defined (iPlatformAppleMobile)
1148 iLabelWidget *navMenu = 1201 iLabelWidget *navMenu =
1149 makeMenuButton_LabelWidget("\U0001d362", isPhone ? phoneNavMenuItems_ : tabletNavMenuItems_, 1202 makeMenuButton_LabelWidget(menu_Icon, isPhone ? phoneNavMenuItems_ : tabletNavMenuItems_,
1150 isPhone ? iElemCount(phoneNavMenuItems_) : iElemCount(tabletNavMenuItems_)); 1203 isPhone ? iElemCount(phoneNavMenuItems_) : iElemCount(tabletNavMenuItems_));
1151# else 1204# else
1152 iLabelWidget *navMenu = 1205 iLabelWidget *navMenu =
1153 makeMenuButton_LabelWidget("\U0001d362", navMenuItems_, iElemCount(navMenuItems_)); 1206 makeMenuButton_LabelWidget(menu_Icon, navMenuItems_, iElemCount(navMenuItems_));
1154# endif 1207# endif
1155 setAlignVisually_LabelWidget(navMenu, iTrue); 1208 setAlignVisually_LabelWidget(navMenu, iTrue);
1156 setId_Widget(addChildFlags_Widget(navBar, iClob(navMenu), collapse_WidgetFlag), "navbar.menu"); 1209 setId_Widget(addChildFlags_Widget(navBar, iClob(navMenu), collapse_WidgetFlag), "navbar.menu");
@@ -1161,11 +1214,11 @@ void createUserInterface_Root(iRoot *d) {
1161 setId_Widget(mainStack, "stack"); 1214 setId_Widget(mainStack, "stack");
1162 addChildFlags_Widget(div, iClob(mainStack), resizeChildren_WidgetFlag | expand_WidgetFlag | 1215 addChildFlags_Widget(div, iClob(mainStack), resizeChildren_WidgetFlag | expand_WidgetFlag |
1163 unhittable_WidgetFlag); 1216 unhittable_WidgetFlag);
1164 iWidget *tabBar = makeTabs_Widget(mainStack); 1217 iWidget *docTabs = makeTabs_Widget(mainStack);
1165 setId_Widget(tabBar, "doctabs"); 1218 setId_Widget(docTabs, "doctabs");
1166 setBackgroundColor_Widget(tabBar, uiBackground_ColorId); 1219 setBackgroundColor_Widget(docTabs, uiBackground_ColorId);
1167 appendTabPage_Widget(tabBar, iClob(new_DocumentWidget()), "Document", 0, 0); 1220 appendTabPage_Widget(docTabs, iClob(new_DocumentWidget()), "Document", 0, 0);
1168 iWidget *buttons = findChild_Widget(tabBar, "tabs.buttons"); 1221 iWidget *buttons = findChild_Widget(docTabs, "tabs.buttons");
1169 setFlags_Widget(buttons, collapse_WidgetFlag | hidden_WidgetFlag | 1222 setFlags_Widget(buttons, collapse_WidgetFlag | hidden_WidgetFlag |
1170 drawBackgroundToHorizontalSafeArea_WidgetFlag, iTrue); 1223 drawBackgroundToHorizontalSafeArea_WidgetFlag, iTrue);
1171 if (deviceType_App() == phone_AppDeviceType) { 1224 if (deviceType_App() == phone_AppDeviceType) {
@@ -1177,9 +1230,9 @@ void createUserInterface_Root(iRoot *d) {
1177 } 1230 }
1178 /* Sidebars. */ { 1231 /* Sidebars. */ {
1179 iWidget *content = findChild_Widget(root, "tabs.content"); 1232 iWidget *content = findChild_Widget(root, "tabs.content");
1180 iSidebarWidget *sidebar1 = new_SidebarWidget(left_SideBarSide); 1233 iSidebarWidget *sidebar1 = new_SidebarWidget(left_SidebarSide);
1181 addChildPos_Widget(content, iClob(sidebar1), front_WidgetAddPos); 1234 addChildPos_Widget(content, iClob(sidebar1), front_WidgetAddPos);
1182 iSidebarWidget *sidebar2 = new_SidebarWidget(right_SideBarSide); 1235 iSidebarWidget *sidebar2 = new_SidebarWidget(right_SidebarSide);
1183 if (deviceType_App() != phone_AppDeviceType) { 1236 if (deviceType_App() != phone_AppDeviceType) {
1184 addChildPos_Widget(content, iClob(sidebar2), back_WidgetAddPos); 1237 addChildPos_Widget(content, iClob(sidebar2), back_WidgetAddPos);
1185 } 1238 }
@@ -1218,6 +1271,7 @@ void createUserInterface_Root(iRoot *d) {
1218 setHint_InputWidget(input, "${hint.findtext}"); 1271 setHint_InputWidget(input, "${hint.findtext}");
1219 setSelectAllOnFocus_InputWidget(input, iTrue); 1272 setSelectAllOnFocus_InputWidget(input, iTrue);
1220 setEatEscape_InputWidget(input, iFalse); /* unfocus and close with one keypress */ 1273 setEatEscape_InputWidget(input, iFalse); /* unfocus and close with one keypress */
1274 setEnterInsertsLF_InputWidget(input, iFalse);
1221 setId_Widget(addChildFlags_Widget(searchBar, iClob(input), expand_WidgetFlag), 1275 setId_Widget(addChildFlags_Widget(searchBar, iClob(input), expand_WidgetFlag),
1222 "find.input"); 1276 "find.input");
1223 addChild_Widget(searchBar, iClob(newIcon_LabelWidget(" \u2b9f ", 'g', KMOD_PRIMARY, "find.next"))); 1277 addChild_Widget(searchBar, iClob(newIcon_LabelWidget(" \u2b9f ", 'g', KMOD_PRIMARY, "find.next")));
@@ -1237,7 +1291,6 @@ void createUserInterface_Root(iRoot *d) {
1237 arrangeHeight_WidgetFlag | arrangeHorizontal_WidgetFlag | 1291 arrangeHeight_WidgetFlag | arrangeHorizontal_WidgetFlag |
1238 commandOnClick_WidgetFlag | 1292 commandOnClick_WidgetFlag |
1239 drawBackgroundToBottom_WidgetFlag, iTrue); 1293 drawBackgroundToBottom_WidgetFlag, iTrue);
1240 setBackgroundColor_Widget(toolBar, tmBannerBackground_ColorId);
1241 setId_Widget(addChildFlags_Widget(toolBar, 1294 setId_Widget(addChildFlags_Widget(toolBar,
1242 iClob(newLargeIcon_LabelWidget("\U0001f870", "navigate.back")), 1295 iClob(newLargeIcon_LabelWidget("\U0001f870", "navigate.back")),
1243 frameless_WidgetFlag), 1296 frameless_WidgetFlag),
@@ -1254,7 +1307,16 @@ void createUserInterface_Root(iRoot *d) {
1254 iClob(newLargeIcon_LabelWidget(book_Icon, "toolbar.showview arg:-1")), 1307 iClob(newLargeIcon_LabelWidget(book_Icon, "toolbar.showview arg:-1")),
1255 frameless_WidgetFlag | commandOnClick_WidgetFlag), 1308 frameless_WidgetFlag | commandOnClick_WidgetFlag),
1256 "toolbar.view"); 1309 "toolbar.view");
1257 iLabelWidget *menuButton = makeMenuButton_LabelWidget("\U0001d362", phoneNavMenuItems_, 1310 setId_Widget(addChildFlags_Widget(toolBar,
1311 iClob(new_LabelWidget("", "toolbar.showident")),
1312 frameless_WidgetFlag |
1313 noBackground_WidgetFlag |
1314 fixedPosition_WidgetFlag |
1315 fixedSize_WidgetFlag |
1316 ignoreForParentWidth_WidgetFlag |
1317 ignoreForParentHeight_WidgetFlag),
1318 "toolbar.name");
1319 iLabelWidget *menuButton = makeMenuButton_LabelWidget(menu_Icon, phoneNavMenuItems_,
1258 iElemCount(phoneNavMenuItems_)); 1320 iElemCount(phoneNavMenuItems_));
1259 setFont_LabelWidget(menuButton, uiLabelLarge_FontId); 1321 setFont_LabelWidget(menuButton, uiLabelLarge_FontId);
1260 setId_Widget(as_Widget(menuButton), "toolbar.navmenu"); 1322 setId_Widget(as_Widget(menuButton), "toolbar.navmenu");
@@ -1262,9 +1324,8 @@ void createUserInterface_Root(iRoot *d) {
1262 iForEach(ObjectList, i, children_Widget(toolBar)) { 1324 iForEach(ObjectList, i, children_Widget(toolBar)) {
1263 iLabelWidget *btn = i.object; 1325 iLabelWidget *btn = i.object;
1264 setFlags_Widget(i.object, noBackground_WidgetFlag, iTrue); 1326 setFlags_Widget(i.object, noBackground_WidgetFlag, iTrue);
1265 setTextColor_LabelWidget(i.object, tmBannerIcon_ColorId);
1266 // setBackgroundColor_Widget(i.object, tmBannerSideTitle_ColorId);
1267 } 1327 }
1328 updateToolbarColors_Root(d);
1268 const iMenuItem items[] = { 1329 const iMenuItem items[] = {
1269 { book_Icon " ${sidebar.bookmarks}", 0, 0, "toolbar.showview arg:0" }, 1330 { book_Icon " ${sidebar.bookmarks}", 0, 0, "toolbar.showview arg:0" },
1270 { star_Icon " ${sidebar.feeds}", 0, 0, "toolbar.showview arg:1" }, 1331 { star_Icon " ${sidebar.feeds}", 0, 0, "toolbar.showview arg:1" },
@@ -1346,7 +1407,7 @@ void createUserInterface_Root(iRoot *d) {
1346 } 1407 }
1347} 1408}
1348 1409
1349void showToolbars_Root(iRoot *d, iBool show) { 1410void showToolbar_Root(iRoot *d, iBool show) {
1350 /* The toolbar is only used on phone portrait layout. */ 1411 /* The toolbar is only used on phone portrait layout. */
1351 if (isLandscape_App()) return; 1412 if (isLandscape_App()) return;
1352 iWidget *toolBar = findChild_Widget(d->widget, "toolbar"); 1413 iWidget *toolBar = findChild_Widget(d->widget, "toolbar");
diff --git a/src/ui/root.h b/src/ui/root.h
index 96864a15..740e97c9 100644
--- a/src/ui/root.h
+++ b/src/ui/root.h
@@ -34,7 +34,8 @@ void postArrange_Root (iRoot *);
34void updateMetrics_Root (iRoot *); 34void updateMetrics_Root (iRoot *);
35void updatePadding_Root (iRoot *); /* TODO: is part of metrics? */ 35void updatePadding_Root (iRoot *); /* TODO: is part of metrics? */
36void dismissPortraitPhoneSidebars_Root (iRoot *); 36void dismissPortraitPhoneSidebars_Root (iRoot *);
37void showToolbars_Root (iRoot *, iBool show); 37void showToolbar_Root (iRoot *, iBool show);
38void updateToolbarColors_Root (iRoot *);
38 39
39iInt2 size_Root (const iRoot *); 40iInt2 size_Root (const iRoot *);
40iRect rect_Root (const iRoot *); 41iRect rect_Root (const iRoot *);
diff --git a/src/ui/sidebarwidget.c b/src/ui/sidebarwidget.c
index 27646b22..c0a22e99 100644
--- a/src/ui/sidebarwidget.c
+++ b/src/ui/sidebarwidget.c
@@ -523,15 +523,17 @@ static void updateItems_SidebarWidget_(iSidebarWidget *d) {
523 addChild_Widget(div, iClob(makePadding_Widget(gap_UI))); 523 addChild_Widget(div, iClob(makePadding_Widget(gap_UI)));
524 addChild_Widget(div, iClob(new_LabelWidget("${menu.identity.import}", "ident.import"))); 524 addChild_Widget(div, iClob(new_LabelWidget("${menu.identity.import}", "ident.import")));
525 addChildFlags_Widget(div, iClob(new_Widget()), expand_WidgetFlag); /* pad */ 525 addChildFlags_Widget(div, iClob(new_Widget()), expand_WidgetFlag); /* pad */
526 iLabelWidget *linkLabel;
526 setBackgroundColor_Widget( 527 setBackgroundColor_Widget(
527 addChildFlags_Widget( 528 addChildFlags_Widget(
528 div, 529 div,
529 iClob(new_LabelWidget(format_CStr(cstr_Lang("ident.gotohelp"), 530 iClob(linkLabel = new_LabelWidget(format_CStr(cstr_Lang("ident.gotohelp"),
530 uiTextStrong_ColorEscape, 531 uiTextStrong_ColorEscape,
531 restore_ColorEscape), 532 restore_ColorEscape),
532 "!open newtab:1 gotoheading:1.6 url:about:help")), 533 "!open newtab:1 gotoheading:1.6 url:about:help")),
533 frameless_WidgetFlag | fixedHeight_WidgetFlag | wrapText_WidgetFlag), 534 frameless_WidgetFlag | fixedHeight_WidgetFlag),
534 uiBackgroundSidebar_ColorId); 535 uiBackgroundSidebar_ColorId);
536 setWrap_LabelWidget(linkLabel, iTrue);
535 addChild_Widget(d->blank, iClob(div)); 537 addChild_Widget(d->blank, iClob(div));
536 } 538 }
537// arrange_Widget(d->blank); 539// arrange_Widget(d->blank);
@@ -625,13 +627,14 @@ static void updateMetrics_SidebarWidget_(iSidebarWidget *d) {
625void init_SidebarWidget(iSidebarWidget *d, enum iSidebarSide side) { 627void init_SidebarWidget(iSidebarWidget *d, enum iSidebarSide side) {
626 iWidget *w = as_Widget(d); 628 iWidget *w = as_Widget(d);
627 init_Widget(w); 629 init_Widget(w);
628 setId_Widget(w, side == left_SideBarSide ? "sidebar" : "sidebar2"); 630 setId_Widget(w, side == left_SidebarSide ? "sidebar" : "sidebar2");
629 initCopy_String(&d->cmdPrefix, id_Widget(w)); 631 initCopy_String(&d->cmdPrefix, id_Widget(w));
630 appendChar_String(&d->cmdPrefix, '.'); 632 appendChar_String(&d->cmdPrefix, '.');
631 setBackgroundColor_Widget(w, none_ColorId); 633 setBackgroundColor_Widget(w, none_ColorId);
632 setFlags_Widget(w, 634 setFlags_Widget(w,
633 collapse_WidgetFlag | hidden_WidgetFlag | arrangeHorizontal_WidgetFlag | 635 collapse_WidgetFlag | hidden_WidgetFlag | arrangeHorizontal_WidgetFlag |
634 resizeWidthOfChildren_WidgetFlag, 636 resizeWidthOfChildren_WidgetFlag | noFadeBackground_WidgetFlag |
637 noShadowBorder_WidgetFlag,
635 iTrue); 638 iTrue);
636 iZap(d->modeScroll); 639 iZap(d->modeScroll);
637 d->side = side; 640 d->side = side;
@@ -659,7 +662,7 @@ void init_SidebarWidget(iSidebarWidget *d, enum iSidebarSide side) {
659 d->actions = NULL; 662 d->actions = NULL;
660 /* On a phone, the right sidebar is used exclusively for Identities. */ 663 /* On a phone, the right sidebar is used exclusively for Identities. */
661 const iBool isPhone = deviceType_App() == phone_AppDeviceType; 664 const iBool isPhone = deviceType_App() == phone_AppDeviceType;
662 if (!isPhone || d->side == left_SideBarSide) { 665 if (!isPhone || d->side == left_SidebarSide) {
663 iWidget *buttons = new_Widget(); 666 iWidget *buttons = new_Widget();
664 setId_Widget(buttons, "buttons"); 667 setId_Widget(buttons, "buttons");
665 for (int i = 0; i < max_SidebarMode; i++) { 668 for (int i = 0; i < max_SidebarMode; i++) {
@@ -678,8 +681,8 @@ void init_SidebarWidget(iSidebarWidget *d, enum iSidebarSide side) {
678 iClob(buttons), 681 iClob(buttons),
679 arrangeHorizontal_WidgetFlag | 682 arrangeHorizontal_WidgetFlag |
680 resizeWidthOfChildren_WidgetFlag | 683 resizeWidthOfChildren_WidgetFlag |
681 arrangeHeight_WidgetFlag | resizeToParentWidth_WidgetFlag | 684 arrangeHeight_WidgetFlag | resizeToParentWidth_WidgetFlag); // |
682 drawBackgroundToHorizontalSafeArea_WidgetFlag); 685// drawBackgroundToHorizontalSafeArea_WidgetFlag);
683 setBackgroundColor_Widget(buttons, uiBackgroundSidebar_ColorId); 686 setBackgroundColor_Widget(buttons, uiBackgroundSidebar_ColorId);
684 } 687 }
685 else { 688 else {
@@ -700,13 +703,13 @@ void init_SidebarWidget(iSidebarWidget *d, enum iSidebarSide side) {
700 setPadding_Widget(as_Widget(d->list), 0, gap_UI, 0, gap_UI); 703 setPadding_Widget(as_Widget(d->list), 0, gap_UI, 0, gap_UI);
701 addChildFlags_Widget(listAndActions, 704 addChildFlags_Widget(listAndActions,
702 iClob(d->list), 705 iClob(d->list),
703 expand_WidgetFlag | drawBackgroundToHorizontalSafeArea_WidgetFlag); 706 expand_WidgetFlag); // | drawBackgroundToHorizontalSafeArea_WidgetFlag);
704 setId_Widget(addChildPosFlags_Widget(listAndActions, 707 setId_Widget(addChildPosFlags_Widget(listAndActions,
705 iClob(d->actions = new_Widget()), 708 iClob(d->actions = new_Widget()),
706 isPhone ? front_WidgetAddPos : back_WidgetAddPos, 709 isPhone ? front_WidgetAddPos : back_WidgetAddPos,
707 arrangeHorizontal_WidgetFlag | arrangeHeight_WidgetFlag | 710 arrangeHorizontal_WidgetFlag | arrangeHeight_WidgetFlag |
708 resizeWidthOfChildren_WidgetFlag | 711 resizeWidthOfChildren_WidgetFlag), // |
709 drawBackgroundToHorizontalSafeArea_WidgetFlag), 712// drawBackgroundToHorizontalSafeArea_WidgetFlag),
710 "actions"); 713 "actions");
711 setBackgroundColor_Widget(d->actions, uiBackgroundSidebar_ColorId); 714 setBackgroundColor_Widget(d->actions, uiBackgroundSidebar_ColorId);
712 d->contextItem = NULL; 715 d->contextItem = NULL;
@@ -715,24 +718,24 @@ void init_SidebarWidget(iSidebarWidget *d, enum iSidebarSide side) {
715 addChildFlags_Widget(content, iClob(d->blank), resizeChildren_WidgetFlag); 718 addChildFlags_Widget(content, iClob(d->blank), resizeChildren_WidgetFlag);
716 addChildFlags_Widget(vdiv, iClob(content), expand_WidgetFlag); 719 addChildFlags_Widget(vdiv, iClob(content), expand_WidgetFlag);
717 setMode_SidebarWidget(d, 720 setMode_SidebarWidget(d,
718 deviceType_App() == phone_AppDeviceType && d->side == right_SideBarSide ? 721 deviceType_App() == phone_AppDeviceType && d->side == right_SidebarSide ?
719 identities_SidebarMode : bookmarks_SidebarMode); 722 identities_SidebarMode : bookmarks_SidebarMode);
720 d->resizer = 723 d->resizer =
721 addChildFlags_Widget(w, 724 addChildFlags_Widget(w,
722 iClob(new_Widget()), 725 iClob(new_Widget()),
723 hover_WidgetFlag | commandOnClick_WidgetFlag | fixedWidth_WidgetFlag | 726 hover_WidgetFlag | commandOnClick_WidgetFlag | fixedWidth_WidgetFlag |
724 resizeToParentHeight_WidgetFlag | 727 resizeToParentHeight_WidgetFlag |
725 (side == left_SideBarSide ? moveToParentRightEdge_WidgetFlag 728 (side == left_SidebarSide ? moveToParentRightEdge_WidgetFlag
726 : moveToParentLeftEdge_WidgetFlag)); 729 : moveToParentLeftEdge_WidgetFlag));
727 if (deviceType_App() == phone_AppDeviceType) { 730 if (deviceType_App() == phone_AppDeviceType) {
728 setFlags_Widget(d->resizer, hidden_WidgetFlag | disabled_WidgetFlag, iTrue); 731 setFlags_Widget(d->resizer, hidden_WidgetFlag | disabled_WidgetFlag, iTrue);
729 } 732 }
730 setId_Widget(d->resizer, side == left_SideBarSide ? "sidebar.grab" : "sidebar2.grab"); 733 setId_Widget(d->resizer, side == left_SidebarSide ? "sidebar.grab" : "sidebar2.grab");
731 setBackgroundColor_Widget(d->resizer, none_ColorId); 734 setBackgroundColor_Widget(d->resizer, none_ColorId);
732 d->menu = NULL; 735 d->menu = NULL;
733 addAction_Widget(w, SDLK_r, KMOD_PRIMARY | KMOD_SHIFT, "feeds.refresh"); 736 addAction_Widget(w, SDLK_r, KMOD_PRIMARY | KMOD_SHIFT, "feeds.refresh");
734 updateMetrics_SidebarWidget_(d); 737 updateMetrics_SidebarWidget_(d);
735 if (side == left_SideBarSide) { 738 if (side == left_SidebarSide) {
736 postCommand_App("~sidebar.update"); /* unread count */ 739 postCommand_App("~sidebar.update"); /* unread count */
737 } 740 }
738} 741}
@@ -773,6 +776,7 @@ static void itemClicked_SidebarWidget_(iSidebarWidget *d, iSidebarItem *item, si
773 const iGmHeading *head = constAt_Array(headings_GmDocument(doc), item->id); 776 const iGmHeading *head = constAt_Array(headings_GmDocument(doc), item->id);
774 postCommandf_App("document.goto loc:%p", head->text.start); 777 postCommandf_App("document.goto loc:%p", head->text.start);
775 dismissPortraitPhoneSidebars_Root(as_Widget(d)->root); 778 dismissPortraitPhoneSidebars_Root(as_Widget(d)->root);
779 setOpenedFromSidebar_DocumentWidget(document_App(), iTrue);
776 break; 780 break;
777 } 781 }
778 case feeds_SidebarMode: { 782 case feeds_SidebarMode: {
@@ -783,7 +787,7 @@ static void itemClicked_SidebarWidget_(iSidebarWidget *d, iSidebarItem *item, si
783 case bookmarks_SidebarMode: 787 case bookmarks_SidebarMode:
784 case history_SidebarMode: { 788 case history_SidebarMode: {
785 if (!isEmpty_String(&item->url)) { 789 if (!isEmpty_String(&item->url)) {
786 postCommandf_Root(get_Root(), "open newtab:%d url:%s", 790 postCommandf_Root(get_Root(), "open fromsidebar:1 newtab:%d url:%s",
787 openTabMode_Sym(modState_Keys()), 791 openTabMode_Sym(modState_Keys()),
788 cstr_String(&item->url)); 792 cstr_String(&item->url));
789 } 793 }
@@ -799,7 +803,7 @@ static void itemClicked_SidebarWidget_(iSidebarWidget *d, iSidebarItem *item, si
799 updateContextMenu_SidebarWidget_(d); 803 updateContextMenu_SidebarWidget_(d);
800 arrange_Widget(d->menu); 804 arrange_Widget(d->menu);
801 openMenu_Widget(d->menu, 805 openMenu_Widget(d->menu,
802 d->side == left_SideBarSide 806 d->side == left_SidebarSide
803 ? topRight_Rect(itemRect_ListWidget(d->list, itemIndex)) 807 ? topRight_Rect(itemRect_ListWidget(d->list, itemIndex))
804 : addX_I2(topLeft_Rect(itemRect_ListWidget(d->list, itemIndex)), 808 : addX_I2(topLeft_Rect(itemRect_ListWidget(d->list, itemIndex)),
805 -width_Widget(d->menu))); 809 -width_Widget(d->menu)));
@@ -857,7 +861,7 @@ void setWidth_SidebarWidget(iSidebarWidget *d, float widthAsGaps) {
857 if (!isFixedWidth) { 861 if (!isFixedWidth) {
858 /* Even less space if the other sidebar is visible, too. */ 862 /* Even less space if the other sidebar is visible, too. */
859 const int otherWidth = 863 const int otherWidth =
860 width_Widget(findWidget_App(d->side == left_SideBarSide ? "sidebar2" : "sidebar")); 864 width_Widget(findWidget_App(d->side == left_SidebarSide ? "sidebar2" : "sidebar"));
861 width = iClamp(width, 30 * gap_UI, size_Root(w->root).x - 50 * gap_UI - otherWidth); 865 width = iClamp(width, 30 * gap_UI, size_Root(w->root).x - 50 * gap_UI - otherWidth);
862 } 866 }
863 d->widthAsGaps = (float) width / (float) gap_UI; 867 d->widthAsGaps = (float) width / (float) gap_UI;
@@ -938,33 +942,39 @@ static iBool handleSidebarCommand_SidebarWidget_(iSidebarWidget *d, const char *
938 } 942 }
939 const iBool isAnimated = prefs_App()->uiAnimations && 943 const iBool isAnimated = prefs_App()->uiAnimations &&
940 argLabel_Command(cmd, "noanim") == 0 && 944 argLabel_Command(cmd, "noanim") == 0 &&
941 (deviceType_App() != phone_AppDeviceType); 945 (d->side == left_SidebarSide || deviceType_App() != phone_AppDeviceType);
942 int visX = 0; 946 int visX = 0;
943 if (isVisible_Widget(w)) { 947 if (isVisible_Widget(w)) {
944 visX = left_Rect(bounds_Widget(w)) - left_Rect(w->root->widget->rect); 948 visX = left_Rect(bounds_Widget(w)) - left_Rect(w->root->widget->rect);
945 } 949 }
946 setFlags_Widget(w, hidden_WidgetFlag, isVisible_Widget(w)); 950 setFlags_Widget(w, hidden_WidgetFlag, isVisible_Widget(w));
951 /* Safe area inset for mobile. */
952 const int safePad = (d->side == left_SidebarSide ? left_Rect(safeRect_Root(w->root)) : 0);
947 if (isVisible_Widget(w)) { 953 if (isVisible_Widget(w)) {
948 setFlags_Widget(w, keepOnTop_WidgetFlag, iFalse); 954 setFlags_Widget(w, keepOnTop_WidgetFlag, iFalse);
949 w->rect.size.x = d->widthAsGaps * gap_UI; 955 w->rect.size.x = d->widthAsGaps * gap_UI;
950 invalidate_ListWidget(d->list); 956 invalidate_ListWidget(d->list);
951 if (isAnimated) { 957 if (isAnimated) {
952 setFlags_Widget(w, horizontalOffset_WidgetFlag, iTrue); 958 setFlags_Widget(w, horizontalOffset_WidgetFlag, iTrue);
953 setVisualOffset_Widget(w, (d->side == left_SideBarSide ? -1 : 1) * w->rect.size.x, 0, 0); 959 setVisualOffset_Widget(
960 w, (d->side == left_SidebarSide ? -1 : 1) * (w->rect.size.x + safePad), 0, 0);
954 setVisualOffset_Widget(w, 0, 300, easeOut_AnimFlag | softer_AnimFlag); 961 setVisualOffset_Widget(w, 0, 300, easeOut_AnimFlag | softer_AnimFlag);
955 } 962 }
956 } 963 }
957 else if (isAnimated) { 964 else if (isAnimated) {
958 setFlags_Widget(w, horizontalOffset_WidgetFlag, iTrue); 965 setFlags_Widget(w, horizontalOffset_WidgetFlag, iTrue);
959 if (d->side == right_SideBarSide) { 966 if (d->side == right_SidebarSide) {
960 setVisualOffset_Widget(w, visX, 0, 0); 967 setVisualOffset_Widget(w, visX, 0, 0);
961 setVisualOffset_Widget(w, visX + w->rect.size.x, 300, easeOut_AnimFlag | softer_AnimFlag); 968 setVisualOffset_Widget(
969 w, visX + w->rect.size.x + safePad, 300, easeOut_AnimFlag | softer_AnimFlag);
962 } 970 }
963 else { 971 else {
964 setFlags_Widget(w, keepOnTop_WidgetFlag, iTrue); 972 setFlags_Widget(w, keepOnTop_WidgetFlag, iTrue);
965 setVisualOffset_Widget(w, -w->rect.size.x, 300, easeOut_AnimFlag | softer_AnimFlag); 973 setVisualOffset_Widget(
974 w, -w->rect.size.x - safePad, 300, easeOut_AnimFlag | softer_AnimFlag);
966 } 975 }
967 } 976 }
977 updateToolbarColors_Root(w->root);
968 arrange_Widget(w->parent); 978 arrange_Widget(w->parent);
969 /* BUG: Rearranging because the arrange above didn't fully resolve the height. */ 979 /* BUG: Rearranging because the arrange above didn't fully resolve the height. */
970 arrange_Widget(w); 980 arrange_Widget(w);
@@ -984,11 +994,21 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev)
984 /* Handle commands. */ 994 /* Handle commands. */
985 if (isResize_UserEvent(ev)) { 995 if (isResize_UserEvent(ev)) {
986 checkModeButtonLayout_SidebarWidget_(d); 996 checkModeButtonLayout_SidebarWidget_(d);
987 if (deviceType_App() == phone_AppDeviceType && d->side == left_SideBarSide) { 997 if (deviceType_App() == phone_AppDeviceType && d->side == left_SidebarSide) {
998 setFlags_Widget(w, rightEdgeDraggable_WidgetFlag, isPortrait_App());
988 /* In landscape, visibility of the toolbar is controlled separately. */ 999 /* In landscape, visibility of the toolbar is controlled separately. */
989 if (isVisible_Widget(w)) { 1000 if (isVisible_Widget(w)) {
990 postCommand_Widget(w, "sidebar.toggle"); 1001 postCommand_Widget(w, "sidebar.toggle");
991 } 1002 }
1003 setFlags_Widget(findChild_Widget(w, "buttons"),
1004 drawBackgroundToHorizontalSafeArea_WidgetFlag,
1005 isLandscape_App());
1006 setFlags_Widget(findChild_Widget(w, "actions"),
1007 drawBackgroundToHorizontalSafeArea_WidgetFlag,
1008 isLandscape_App());
1009 setFlags_Widget(as_Widget(d->list),
1010 drawBackgroundToHorizontalSafeArea_WidgetFlag,
1011 isLandscape_App());
992 return iFalse; 1012 return iFalse;
993 } 1013 }
994 } 1014 }
@@ -1029,6 +1049,11 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev)
1029 postCommandf_App("sidebar.mode arg:%d toggle:1", identities_SidebarMode); 1049 postCommandf_App("sidebar.mode arg:%d toggle:1", identities_SidebarMode);
1030 return iTrue; 1050 return iTrue;
1031 } 1051 }
1052 else if (isPortraitPhone_App() && isVisible_Widget(w) && d->side == left_SidebarSide &&
1053 equal_Command(cmd, "swipe.forward")) {
1054 postCommand_App("sidebar.toggle");
1055 return iTrue;
1056 }
1032 else if (startsWith_CStr(cmd, cstr_String(&d->cmdPrefix))) { 1057 else if (startsWith_CStr(cmd, cstr_String(&d->cmdPrefix))) {
1033 if (handleSidebarCommand_SidebarWidget_(d, cmd + size_String(&d->cmdPrefix))) { 1058 if (handleSidebarCommand_SidebarWidget_(d, cmd + size_String(&d->cmdPrefix))) {
1034 return iTrue; 1059 return iTrue;
@@ -1059,7 +1084,7 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev)
1059 const int resMid = d->resizer->rect.size.x / 2; 1084 const int resMid = d->resizer->rect.size.x / 2;
1060 setWidth_SidebarWidget( 1085 setWidth_SidebarWidget(
1061 d, 1086 d,
1062 ((d->side == left_SideBarSide 1087 ((d->side == left_SidebarSide
1063 ? inner.x 1088 ? inner.x
1064 : (right_Rect(rect_Root(w->root)) - coord_Command(cmd).x)) + 1089 : (right_Rect(rect_Root(w->root)) - coord_Command(cmd).x)) +
1065 resMid) / (float) gap_UI); 1090 resMid) / (float) gap_UI);
@@ -1528,15 +1553,17 @@ static void draw_SidebarWidget_(const iSidebarWidget *d) {
1528 const iRect bounds = bounds_Widget(w); 1553 const iRect bounds = bounds_Widget(w);
1529 iPaint p; 1554 iPaint p;
1530 init_Paint(&p); 1555 init_Paint(&p);
1531 if (flags_Widget(w) & visualOffset_WidgetFlag && 1556 if (deviceType_App() != phone_AppDeviceType) {
1532 flags_Widget(w) & horizontalOffset_WidgetFlag && isVisible_Widget(w)) { 1557 if (flags_Widget(w) & visualOffset_WidgetFlag &&
1533 fillRect_Paint(&p, boundsWithoutVisualOffset_Widget(w), tmBackground_ColorId); 1558 flags_Widget(w) & horizontalOffset_WidgetFlag && isVisible_Widget(w)) {
1559 fillRect_Paint(&p, boundsWithoutVisualOffset_Widget(w), tmBackground_ColorId);
1560 }
1534 } 1561 }
1535 draw_Widget(w); 1562 draw_Widget(w);
1536 if (isVisible_Widget(w)) { 1563 if (isVisible_Widget(w)) {
1537 drawVLine_Paint( 1564 drawVLine_Paint(
1538 &p, 1565 &p,
1539 addX_I2(d->side == left_SideBarSide ? topRight_Rect(bounds) : topLeft_Rect(bounds), -1), 1566 addX_I2(d->side == left_SidebarSide ? topRight_Rect(bounds) : topLeft_Rect(bounds), -1),
1540 height_Rect(bounds), 1567 height_Rect(bounds),
1541 uiSeparator_ColorId); 1568 uiSeparator_ColorId);
1542 } 1569 }
@@ -1782,13 +1809,7 @@ static void draw_SidebarItem_(const iSidebarItem *d, iPaint *p, iRect itemRect,
1782 : uiTextFramelessHover_ColorId) 1809 : uiTextFramelessHover_ColorId)
1783 : uiTextDim_ColorId; 1810 : uiTextDim_ColorId;
1784 if (!d->listItem.isSelected && !isUsedOnDomain) { 1811 if (!d->listItem.isSelected && !isUsedOnDomain) {
1785 /* Draw an outline of the icon. */ 1812 drawOutline_Text(font, cPos, metaFg, none_ColorId, range_String(&icon));
1786 for (int off = 0; off < 4; ++off) {
1787 drawRange_Text(font,
1788 add_I2(cPos, init_I2(off % 2 == 0 ? -1 : 1, off / 2 == 0 ? -1 : 1)),
1789 metaFg,
1790 range_String(&icon));
1791 }
1792 } 1813 }
1793 drawRange_Text(font, 1814 drawRange_Text(font,
1794 cPos, 1815 cPos,
diff --git a/src/ui/sidebarwidget.h b/src/ui/sidebarwidget.h
index 2e418aa4..130242ab 100644
--- a/src/ui/sidebarwidget.h
+++ b/src/ui/sidebarwidget.h
@@ -36,8 +36,8 @@ enum iSidebarMode {
36const char * icon_SidebarMode (enum iSidebarMode mode); 36const char * icon_SidebarMode (enum iSidebarMode mode);
37 37
38enum iSidebarSide { 38enum iSidebarSide {
39 left_SideBarSide, 39 left_SidebarSide,
40 right_SideBarSide, 40 right_SidebarSide,
41}; 41};
42 42
43enum iFeedsMode { 43enum iFeedsMode {
diff --git a/src/ui/text.c b/src/ui/text.c
index 51865654..edbc6583 100644
--- a/src/ui/text.c
+++ b/src/ui/text.c
@@ -316,6 +316,7 @@ static void initFonts_Text_(iText *d) {
316 { &fontSourceSans3Regular_Embedded, uiSize * 1.125f, 1.0f, uiMedium_FontSize }, 316 { &fontSourceSans3Regular_Embedded, uiSize * 1.125f, 1.0f, uiMedium_FontSize },
317 { &fontSourceSans3Regular_Embedded, uiSize * 1.333f, 1.0f, uiBig_FontSize }, 317 { &fontSourceSans3Regular_Embedded, uiSize * 1.333f, 1.0f, uiBig_FontSize },
318 { &fontSourceSans3Regular_Embedded, uiSize * 1.666f, 1.0f, uiLarge_FontSize }, 318 { &fontSourceSans3Regular_Embedded, uiSize * 1.666f, 1.0f, uiLarge_FontSize },
319 { &fontSourceSans3Semibold_Embedded, uiSize * 0.8f, 1.0f, uiNormal_FontSize },
319 /* UI fonts: bold weight */ 320 /* UI fonts: bold weight */
320 { &fontSourceSans3Bold_Embedded, uiSize, 1.0f, uiNormal_FontSize }, 321 { &fontSourceSans3Bold_Embedded, uiSize, 1.0f, uiNormal_FontSize },
321 { &fontSourceSans3Bold_Embedded, uiSize * 1.125f, 1.0f, uiMedium_FontSize }, 322 { &fontSourceSans3Bold_Embedded, uiSize * 1.125f, 1.0f, uiMedium_FontSize },
@@ -399,7 +400,9 @@ static void initCache_Text_(iText *d) {
399 d->cacheRowAllocStep = iMax(2, textSize / 6); 400 d->cacheRowAllocStep = iMax(2, textSize / 6);
400 /* Allocate initial (empty) rows. These will be assigned actual locations in the cache 401 /* Allocate initial (empty) rows. These will be assigned actual locations in the cache
401 once at least one glyph is stored. */ 402 once at least one glyph is stored. */
402 for (int h = d->cacheRowAllocStep; h <= 2 * textSize + d->cacheRowAllocStep; h += d->cacheRowAllocStep) { 403 for (int h = d->cacheRowAllocStep;
404 h <= 2.5 * textSize + d->cacheRowAllocStep;
405 h += d->cacheRowAllocStep) {
403 pushBack_Array(&d->cacheRows, &(iCacheRow){ .height = 0 }); 406 pushBack_Array(&d->cacheRows, &(iCacheRow){ .height = 0 });
404 } 407 }
405 d->cacheBottom = 0; 408 d->cacheBottom = 0;
@@ -1020,10 +1023,10 @@ static iRect run_Font_(iFont *d, const iRunArgs *args) {
1020 prevCh = 0; 1023 prevCh = 0;
1021 continue; 1024 continue;
1022 } 1025 }
1023 if (ch == '\r') { /* color change */ 1026 if (ch == '\v') { /* color change */
1024 iChar esc = nextChar_(&chPos, args->text.end); 1027 iChar esc = nextChar_(&chPos, args->text.end);
1025 int colorNum = args->color; 1028 int colorNum = args->color;
1026 if (esc == '\r') { /* Extended range. */ 1029 if (esc == '\v') { /* Extended range. */
1027 esc = nextChar_(&chPos, args->text.end) + asciiExtended_ColorEscape; 1030 esc = nextChar_(&chPos, args->text.end) + asciiExtended_ColorEscape;
1028 colorNum = esc - asciiBase_ColorEscape; 1031 colorNum = esc - asciiBase_ColorEscape;
1029 } 1032 }
@@ -1335,6 +1338,18 @@ void drawRangeN_Text(int fontId, iInt2 pos, int color, iRangecc text, size_t max
1335 drawBoundedN_Text_(fontId, pos, 0, color, text, maxChars); 1338 drawBoundedN_Text_(fontId, pos, 0, color, text, maxChars);
1336} 1339}
1337 1340
1341void drawOutline_Text(int fontId, iInt2 pos, int outlineColor, int fillColor, iRangecc text) {
1342 for (int off = 0; off < 4; ++off) {
1343 drawRange_Text(fontId,
1344 add_I2(pos, init_I2(off % 2 == 0 ? -1 : 1, off / 2 == 0 ? -1 : 1)),
1345 outlineColor,
1346 text);
1347 }
1348 if (fillColor != none_ColorId) {
1349 drawRange_Text(fontId, pos, fillColor, text);
1350 }
1351}
1352
1338iInt2 advanceWrapRange_Text(int fontId, int maxWidth, iRangecc text) { 1353iInt2 advanceWrapRange_Text(int fontId, int maxWidth, iRangecc text) {
1339 iInt2 size = zero_I2(); 1354 iInt2 size = zero_I2();
1340 const char *endp; 1355 const char *endp;
@@ -1376,6 +1391,31 @@ void drawCentered_Text(int fontId, iRect rect, iBool alignVisual, int color, con
1376 deinit_Block(&chars); 1391 deinit_Block(&chars);
1377} 1392}
1378 1393
1394void drawCenteredOutline_Text(int fontId, iRect rect, iBool alignVisual, int outlineColor,
1395 int fillColor, const char *format, ...) {
1396 iBlock chars;
1397 init_Block(&chars, 0); {
1398 va_list args;
1399 va_start(args, format);
1400 vprintf_Block(&chars, format, args);
1401 va_end(args);
1402 }
1403 if (outlineColor != none_ColorId) {
1404 for (int off = 0; off < 4; ++off) {
1405 drawCenteredRange_Text(
1406 fontId,
1407 moved_Rect(rect, init_I2(off % 2 == 0 ? -1 : 1, off / 2 == 0 ? -1 : 1)),
1408 alignVisual,
1409 outlineColor,
1410 range_Block(&chars));
1411 }
1412 }
1413 if (fillColor != none_ColorId) {
1414 drawCenteredRange_Text(fontId, rect, alignVisual, fillColor, range_Block(&chars));
1415 }
1416 deinit_Block(&chars);
1417}
1418
1379void drawCenteredRange_Text(int fontId, iRect rect, iBool alignVisual, int color, iRangecc text) { 1419void drawCenteredRange_Text(int fontId, iRect rect, iBool alignVisual, int color, iRangecc text) {
1380 iRect textBounds = alignVisual ? visualBounds_Text(fontId, text) 1420 iRect textBounds = alignVisual ? visualBounds_Text(fontId, text)
1381 : (iRect){ zero_I2(), advanceRange_Text(fontId, text) }; 1421 : (iRect){ zero_I2(), advanceRange_Text(fontId, text) };
diff --git a/src/ui/text.h b/src/ui/text.h
index 768713ee..5a099142 100644
--- a/src/ui/text.h
+++ b/src/ui/text.h
@@ -50,6 +50,7 @@ enum iFontId {
50 defaultMedium_FontId, 50 defaultMedium_FontId,
51 defaultBig_FontId, 51 defaultBig_FontId,
52 defaultLarge_FontId, 52 defaultLarge_FontId,
53 defaultSmall_FontId,
53 /* UI fonts: bold weight */ 54 /* UI fonts: bold weight */
54 defaultBold_FontId, 55 defaultBold_FontId,
55 defaultMediumBold_FontId, 56 defaultMediumBold_FontId,
@@ -163,9 +164,11 @@ void draw_Text (int fontId, iInt2 pos, int color, const char *t
163void drawAlign_Text (int fontId, iInt2 pos, int color, enum iAlignment align, const char *text, ...); 164void drawAlign_Text (int fontId, iInt2 pos, int color, enum iAlignment align, const char *text, ...);
164void drawCentered_Text (int fontId, iRect rect, iBool alignVisual, int color, const char *text, ...); 165void drawCentered_Text (int fontId, iRect rect, iBool alignVisual, int color, const char *text, ...);
165void drawCenteredRange_Text (int fontId, iRect rect, iBool alignVisual, int color, iRangecc text); 166void drawCenteredRange_Text (int fontId, iRect rect, iBool alignVisual, int color, iRangecc text);
167void drawCenteredOutline_Text(int fontId, iRect rect, iBool alignVisual, int outlineColor, int fillColor, const char *text, ...);
166void drawString_Text (int fontId, iInt2 pos, int color, const iString *text); 168void drawString_Text (int fontId, iInt2 pos, int color, const iString *text);
167void drawRange_Text (int fontId, iInt2 pos, int color, iRangecc text); 169void drawRange_Text (int fontId, iInt2 pos, int color, iRangecc text);
168void drawRangeN_Text (int fontId, iInt2 pos, int color, iRangecc text, size_t maxLen); 170void drawRangeN_Text (int fontId, iInt2 pos, int color, iRangecc text, size_t maxLen);
171void drawOutline_Text (int fontId, iInt2 pos, int outlineColor, int fillColor, iRangecc text);
169void drawBoundRange_Text (int fontId, iInt2 pos, int boundWidth, int color, iRangecc text); /* bound does not wrap */ 172void drawBoundRange_Text (int fontId, iInt2 pos, int boundWidth, int color, iRangecc text); /* bound does not wrap */
170int drawWrapRange_Text (int fontId, iInt2 pos, int maxWidth, int color, iRangecc text); /* returns new Y */ 173int drawWrapRange_Text (int fontId, iInt2 pos, int maxWidth, int color, iRangecc text); /* returns new Y */
171 174
diff --git a/src/ui/touch.c b/src/ui/touch.c
index 74a22baf..dac1152e 100644
--- a/src/ui/touch.c
+++ b/src/ui/touch.c
@@ -40,6 +40,10 @@ iDeclareType(TouchState)
40#define numHistory_Touch_ 5 40#define numHistory_Touch_ 5
41#define lastIndex_Touch_ (numHistory_Touch_ - 1) 41#define lastIndex_Touch_ (numHistory_Touch_ - 1)
42 42
43static const uint32_t longPressSpanMs_ = 500;
44static const uint32_t shortPressSpanMs_ = 250;
45static const int tapRadiusPt_ = 10;
46
43enum iTouchEdge { 47enum iTouchEdge {
44 none_TouchEdge, 48 none_TouchEdge,
45 left_TouchEdge, 49 left_TouchEdge,
@@ -55,12 +59,13 @@ enum iTouchAxis {
55struct Impl_Touch { 59struct Impl_Touch {
56 SDL_FingerID id; 60 SDL_FingerID id;
57 iWidget *affinity; /* widget on which the touch started */ 61 iWidget *affinity; /* widget on which the touch started */
58 iWidget *edgeDragging; 62// iWidget *edgeDragging;
59 iBool hasMoved; 63 iBool hasMoved;
60 iBool isTapBegun; 64 iBool isTapBegun;
61 iBool isLeftDown; 65 iBool isLeftDown;
62 iBool isTouchDrag; 66 iBool isTouchDrag;
63 iBool isTapAndHold; 67 iBool isTapAndHold;
68 iBool didPostEdgeMove;
64 iBool didBeginOnTouchDrag; 69 iBool didBeginOnTouchDrag;
65 int pinchId; 70 int pinchId;
66 enum iTouchEdge edge; 71 enum iTouchEdge edge;
@@ -132,9 +137,6 @@ static iTouch *find_TouchState_(iTouchState *d, SDL_FingerID id) {
132 return NULL; 137 return NULL;
133} 138}
134 139
135static const uint32_t longPressSpanMs_ = 500;
136static const int tapRadiusPt_ = 10;
137
138iLocalDef float distance_Touch_(const iTouch *d) { 140iLocalDef float distance_Touch_(const iTouch *d) {
139 return length_F3(sub_F3(d->pos[0], d->startPos)); 141 return length_F3(sub_F3(d->pos[0], d->startPos));
140} 142}
@@ -246,6 +248,11 @@ iLocalDef double accurateTicks_(void) {
246 return 1000.0 * (double) count / (double) freq; 248 return 1000.0 * (double) count / (double) freq;
247} 249}
248 250
251static iFloat3 gestureVector_Touch_(const iTouch *d) {
252 const size_t lastIndex = iMin(d->posCount - 1, lastIndex_Touch_);
253 return sub_F3(d->pos[0], d->pos[lastIndex]);
254}
255
249static void update_TouchState_(void *ptr) { 256static void update_TouchState_(void *ptr) {
250 iTouchState *d = ptr; 257 iTouchState *d = ptr;
251 /* Check for long presses to simulate right clicks. */ 258 /* Check for long presses to simulate right clicks. */
@@ -255,6 +262,24 @@ static void update_TouchState_(void *ptr) {
255 if (touch->pinchId || touch->isTouchDrag) { 262 if (touch->pinchId || touch->isTouchDrag) {
256 continue; 263 continue;
257 } 264 }
265 if (touch->edge) {
266 const iFloat3 pos = touch->pos[0];
267 /* Cancel the swipe if the finger doesn't move or moves mostly vertically. */
268 const iFloat3 gestureVector = gestureVector_Touch_(touch);
269 if (fabsf(2 * x_F3(gestureVector)) < fabsf(y_F3(gestureVector)) ||
270 (isStationary_Touch_(touch) && nowTime - touch->startTime > shortPressSpanMs_)) {
271 //const int swipeDir = x_F3(gestureVector) > 0 ? +1 : -1;
272 //dispatchClick_Touch_(touch,
273// touch->edge == left_TouchEdge && swipeDir > 0 ? SDL_BUTTON_X1 :
274// touch->edge == right_TouchEdge && swipeDir < 0 ? SDL_BUTTON_X2 : 0);
275// setHover_Widget(NULL);
276 postCommandf_App("edgeswipe.ended abort:1 side:%d id:%llu", touch->edge, touch->id);
277 touch->edge = none_TouchEdge;
278 /* May be a regular drag along the edge so don't remove. */
279 //remove_ArrayIterator(&i);
280 }
281 continue;
282 }
258 /* Holding a touch will reset previous momentum for this widget. */ 283 /* Holding a touch will reset previous momentum for this widget. */
259 if (isStationary_Touch_(touch)) { 284 if (isStationary_Touch_(touch)) {
260 const int elapsed = nowTime - touch->startTime; 285 const int elapsed = nowTime - touch->startTime;
@@ -340,6 +365,7 @@ static void update_TouchState_(void *ptr) {
340 } 365 }
341} 366}
342 367
368#if 0
343static iWidget *findSlidePanel_Widget_(iWidget *d) { 369static iWidget *findSlidePanel_Widget_(iWidget *d) {
344 for (iWidget *w = d; w; w = parent_Widget(w)) { 370 for (iWidget *w = d; w; w = parent_Widget(w)) {
345 if (isVisible_Widget(w) && flags_Widget(w) & edgeDraggable_WidgetFlag) { 371 if (isVisible_Widget(w) && flags_Widget(w) & edgeDraggable_WidgetFlag) {
@@ -348,6 +374,7 @@ static iWidget *findSlidePanel_Widget_(iWidget *d) {
348 } 374 }
349 return NULL; 375 return NULL;
350} 376}
377#endif
351 378
352static void checkNewPinch_TouchState_(iTouchState *d, iTouch *newTouch) { 379static void checkNewPinch_TouchState_(iTouchState *d, iTouch *newTouch) {
353 iWidget *affinity = newTouch->affinity; 380 iWidget *affinity = newTouch->affinity;
@@ -365,6 +392,12 @@ static void checkNewPinch_TouchState_(iTouchState *d, iTouch *newTouch) {
365 pinch.touchIds[1] = other->id; 392 pinch.touchIds[1] = other->id;
366 newTouch->pinchId = other->pinchId = pinch.id; 393 newTouch->pinchId = other->pinchId = pinch.id;
367 clearWidgetMomentum_TouchState_(d, affinity); 394 clearWidgetMomentum_TouchState_(d, affinity);
395 if (other->edge && other->didPostEdgeMove) {
396 postCommandf_App("edgeswipe.ended abort:1 side:%d id:%llu", other->edge, other->id);
397 other->didPostEdgeMove = iFalse;
398 }
399 other->edge = none_TouchEdge;
400 newTouch->edge = none_TouchEdge;
368 /* Remember current positions to determine pinch amount. */ 401 /* Remember current positions to determine pinch amount. */
369 newTouch->startPos = newTouch->pos[0]; 402 newTouch->startPos = newTouch->pos[0];
370 other->startPos = other->pos[0]; 403 other->startPos = other->pos[0];
@@ -452,6 +485,7 @@ iBool processEvent_Touch(const SDL_Event *ev) {
452 edge = right_TouchEdge; 485 edge = right_TouchEdge;
453 } 486 }
454 iWidget *aff = hitChild_Window(window, init_I2(iRound(x), iRound(y_F3(pos)))); 487 iWidget *aff = hitChild_Window(window, init_I2(iRound(x), iRound(y_F3(pos))));
488#if 0
455 if (edge == left_TouchEdge) { 489 if (edge == left_TouchEdge) {
456 dragging = findSlidePanel_Widget_(aff); 490 dragging = findSlidePanel_Widget_(aff);
457 if (dragging) { 491 if (dragging) {
@@ -460,6 +494,7 @@ iBool processEvent_Touch(const SDL_Event *ev) {
460 setFlags_Widget(dragging, dragged_WidgetFlag, iTrue); 494 setFlags_Widget(dragging, dragged_WidgetFlag, iTrue);
461 } 495 }
462 } 496 }
497#endif
463 /* TODO: We must retain a reference to the affinity widget, or otherwise it might 498 /* TODO: We must retain a reference to the affinity widget, or otherwise it might
464 be destroyed during the gesture. */ 499 be destroyed during the gesture. */
465// printf("aff:[%p] %s:'%s'\n", aff, aff ? class_Widget(aff)->name : "-", 500// printf("aff:[%p] %s:'%s'\n", aff, aff ? class_Widget(aff)->name : "-",
@@ -469,7 +504,7 @@ iBool processEvent_Touch(const SDL_Event *ev) {
469 iTouch newTouch = { 504 iTouch newTouch = {
470 .id = fing->fingerId, 505 .id = fing->fingerId,
471 .affinity = aff, 506 .affinity = aff,
472 .edgeDragging = dragging, 507// .edgeDragging = dragging,
473 .didBeginOnTouchDrag = (flags_Widget(aff) & touchDrag_WidgetFlag) != 0, 508 .didBeginOnTouchDrag = (flags_Widget(aff) & touchDrag_WidgetFlag) != 0,
474 .edge = edge, 509 .edge = edge,
475 .startTime = nowTime, 510 .startTime = nowTime,
@@ -487,6 +522,16 @@ iBool processEvent_Touch(const SDL_Event *ev) {
487 } 522 }
488 else if (ev->type == SDL_FINGERMOTION) { 523 else if (ev->type == SDL_FINGERMOTION) {
489 iTouch *touch = find_TouchState_(d, fing->fingerId); 524 iTouch *touch = find_TouchState_(d, fing->fingerId);
525 if (touch && touch->edge) {
526 clear_Array(d->moms);
527 pushPos_Touch_(touch, pos, nowTime);
528 postCommandf_App("edgeswipe.moved arg:%d side:%d id:%llu",
529 (int) (x_F3(pos) - x_F3(touch->startPos)),
530 touch->edge,
531 touch->id);
532 touch->didPostEdgeMove = iTrue;
533 return iTrue;
534 }
490 if (touch && touch->affinity) { 535 if (touch && touch->affinity) {
491 if (touch->isTouchDrag) { 536 if (touch->isTouchDrag) {
492 dispatchMotion_Touch_(pos, SDL_BUTTON_LMASK); 537 dispatchMotion_Touch_(pos, SDL_BUTTON_LMASK);
@@ -556,36 +601,18 @@ iBool processEvent_Touch(const SDL_Event *ev) {
556 touch->axis = y_TouchAxis; 601 touch->axis = y_TouchAxis;
557 } 602 }
558 } 603 }
559 /* Edge swipe aborted? */ 604 iAssert(touch->edge == none_TouchEdge);
560 if (touch->edge == left_TouchEdge) {
561 if (fing->dx < 0 && x_F3(touch->pos[0]) < tapRadiusPt_ * window->pixelRatio) {
562 touch->edge = none_TouchEdge;
563 if (touch->edgeDragging) {
564 setFlags_Widget(touch->edgeDragging, dragged_WidgetFlag, iFalse);
565 setVisualOffset_Widget(touch->edgeDragging, 0, 200, easeOut_AnimFlag);
566 touch->edgeDragging = NULL;
567 }
568 }
569 else if (touch->edgeDragging) {
570 setVisualOffset_Widget(touch->edgeDragging, x_F3(pos) - x_F3(touch->startPos), 10, 0);
571 }
572 }
573 if (touch->edge == right_TouchEdge && fing->dx > 0) {
574 touch->edge = none_TouchEdge;
575 }
576 if (touch->edge) {
577 pixels.y = 0;
578 }
579 if (touch->axis == x_TouchAxis) { 605 if (touch->axis == x_TouchAxis) {
580 pixels.y = 0; 606 pixels.y = 0;
581 } 607 }
582 if (touch->axis == y_TouchAxis) { 608 if (touch->axis == y_TouchAxis) {
583 pixels.x = 0; 609 pixels.x = 0;
584 } 610 }
585// printf("%p (%s) py: %i wy: %f acc: %f\n", 611// printf("%p (%s) py: %i wy: %f acc: %f edge: %d\n",
586// touch->affinity, 612// touch->affinity,
587// class_Widget(touch->affinity)->name, 613// class_Widget(touch->affinity)->name,
588// pixels.y, y_F3(amount), y_F3(touch->accum)); 614// pixels.y, y_F3(amount), y_F3(touch->accum),
615// touch->edge);
589 if (pixels.x || pixels.y) { 616 if (pixels.x || pixels.y) {
590 setFocus_Widget(NULL); 617 setFocus_Widget(NULL);
591 dispatchMotion_Touch_(touch->pos[0], 0); 618 dispatchMotion_Touch_(touch->pos[0], 0);
@@ -612,9 +639,21 @@ iBool processEvent_Touch(const SDL_Event *ev) {
612 endPinch_TouchState_(d, touch->pinchId); 639 endPinch_TouchState_(d, touch->pinchId);
613 break; 640 break;
614 } 641 }
642#if 0
615 if (touch->edgeDragging) { 643 if (touch->edgeDragging) {
616 setFlags_Widget(touch->edgeDragging, dragged_WidgetFlag, iFalse); 644 setFlags_Widget(touch->edgeDragging, dragged_WidgetFlag, iFalse);
617 } 645 }
646#endif
647 if (touch->edge && !isStationary_Touch_(touch)) {
648 const iFloat3 gesture = gestureVector_Touch_(touch);
649 const float pixel = window->pixelRatio;
650 const int moveDir = x_F3(gesture) < -pixel ? -1 : x_F3(gesture) > pixel ? +1 : 0;
651 const int didAbort = (touch->edge == left_TouchEdge && moveDir < 0) ||
652 (touch->edge == right_TouchEdge && moveDir > 0);
653 postCommandf_App("edgeswipe.ended abort:%d side:%d id:%llu", didAbort, touch->edge, touch->id);
654 remove_ArrayIterator(&i);
655 continue;
656 }
618 if (flags_Widget(touch->affinity) & touchDrag_WidgetFlag) { 657 if (flags_Widget(touch->affinity) & touchDrag_WidgetFlag) {
619 if (!touch->isLeftDown && !touch->isTapAndHold) { 658 if (!touch->isLeftDown && !touch->isTapAndHold) {
620 /* This will be a click on a touchDrag widget. */ 659 /* This will be a click on a touchDrag widget. */
@@ -638,6 +677,7 @@ iBool processEvent_Touch(const SDL_Event *ev) {
638 const uint32_t duration = nowTime - touch->startTime; 677 const uint32_t duration = nowTime - touch->startTime;
639 const iFloat3 gestureVector = sub_F3(pos, touch->pos[lastIndex]); 678 const iFloat3 gestureVector = sub_F3(pos, touch->pos[lastIndex]);
640 iFloat3 velocity = zero_F3(); 679 iFloat3 velocity = zero_F3();
680#if 0
641 if (touch->edge && fabsf(2 * x_F3(gestureVector)) > fabsf(y_F3(gestureVector)) && 681 if (touch->edge && fabsf(2 * x_F3(gestureVector)) > fabsf(y_F3(gestureVector)) &&
642 !isStationary_Touch_(touch)) { 682 !isStationary_Touch_(touch)) {
643 const int swipeDir = x_F3(gestureVector) > 0 ? +1 : -1; 683 const int swipeDir = x_F3(gestureVector) > 0 ? +1 : -1;
@@ -646,7 +686,9 @@ iBool processEvent_Touch(const SDL_Event *ev) {
646 touch->edge == right_TouchEdge && swipeDir < 0 ? SDL_BUTTON_X2 : 0); 686 touch->edge == right_TouchEdge && swipeDir < 0 ? SDL_BUTTON_X2 : 0);
647 setHover_Widget(NULL); 687 setHover_Widget(NULL);
648 } 688 }
649 else { 689 else
690#endif
691 {
650 const uint32_t elapsed = fing->timestamp - touch->posTime[lastIndex]; 692 const uint32_t elapsed = fing->timestamp - touch->posTime[lastIndex];
651 const float minVelocity = 400.0f; 693 const float minVelocity = 400.0f;
652 if (elapsed < 150) { 694 if (elapsed < 150) {
diff --git a/src/ui/util.c b/src/ui/util.c
index c4fb8886..e0b05a44 100644
--- a/src/ui/util.c
+++ b/src/ui/util.c
@@ -710,7 +710,8 @@ iWidget *makeMenu_Widget(iWidget *parent, const iMenuItem *items, size_t n) {
710 menu, 710 menu,
711 iClob(newKeyMods_LabelWidget(labelText, item->key, item->kmods, item->command)), 711 iClob(newKeyMods_LabelWidget(labelText, item->key, item->kmods, item->command)),
712 noBackground_WidgetFlag | frameless_WidgetFlag | alignLeft_WidgetFlag | 712 noBackground_WidgetFlag | frameless_WidgetFlag | alignLeft_WidgetFlag |
713 drawKey_WidgetFlag | (isInfo ? wrapText_WidgetFlag : 0) | itemFlags); 713 drawKey_WidgetFlag | itemFlags);
714 setWrap_LabelWidget(label, isInfo);
714 haveIcons |= checkIcon_LabelWidget(label); 715 haveIcons |= checkIcon_LabelWidget(label);
715 updateSize_LabelWidget(label); /* drawKey was set */ 716 updateSize_LabelWidget(label); /* drawKey was set */
716 if (isInfo) { 717 if (isInfo) {
@@ -776,7 +777,7 @@ void openMenuFlags_Widget(iWidget *d, iInt2 windowCoord, iBool postCommands) {
776 if (isInstance_Object(i.object, &Class_LabelWidget)) { 777 if (isInstance_Object(i.object, &Class_LabelWidget)) {
777 iLabelWidget *label = i.object; 778 iLabelWidget *label = i.object;
778 const iBool isCaution = startsWith_String(text_LabelWidget(label), uiTextCaution_ColorEscape); 779 const iBool isCaution = startsWith_String(text_LabelWidget(label), uiTextCaution_ColorEscape);
779 if (flags_Widget(as_Widget(label)) & wrapText_WidgetFlag) { 780 if (isWrapped_LabelWidget(label)) {
780 continue; 781 continue;
781 } 782 }
782 if (deviceType_App() == desktop_AppDeviceType) { 783 if (deviceType_App() == desktop_AppDeviceType) {
@@ -979,8 +980,8 @@ static void addTabPage_Widget_(iWidget *tabs, enum iWidgetAddPos addPos, iWidget
979 iClob(newKeyMods_LabelWidget(label, key, kmods, format_CStr("tabs.switch page:%p", page))), 980 iClob(newKeyMods_LabelWidget(label, key, kmods, format_CStr("tabs.switch page:%p", page))),
980 addPos); 981 addPos);
981 setFlags_Widget(button, selected_WidgetFlag, isSel); 982 setFlags_Widget(button, selected_WidgetFlag, isSel);
982 setFlags_Widget( 983 setFlags_Widget(button, commandOnClick_WidgetFlag | expand_WidgetFlag, iTrue);
983 button, noTopFrame_WidgetFlag | commandOnClick_WidgetFlag | expand_WidgetFlag, iTrue); 984 setNoTopFrame_LabelWidget((iLabelWidget *) button, iTrue);
984 addChildPos_Widget(pages, page, addPos); 985 addChildPos_Widget(pages, page, addPos);
985 if (tabCount_Widget(tabs) > 1) { 986 if (tabCount_Widget(tabs) > 1) {
986 setFlags_Widget(buttons, hidden_WidgetFlag, iFalse); 987 setFlags_Widget(buttons, hidden_WidgetFlag, iFalse);
@@ -1317,7 +1318,7 @@ void updateValueInput_Widget(iWidget *d, const char *title, const char *prompt)
1317 1318
1318static iBool messageHandler_(iWidget *msg, const char *cmd) { 1319static iBool messageHandler_(iWidget *msg, const char *cmd) {
1319 /* Almost any command dismisses the sheet. */ 1320 /* Almost any command dismisses the sheet. */
1320 /* TODO: Use a "notification" prefix (like `) to ignore all types of commands line this? */ 1321 /* TODO: Add a "notification" type of user events to separate them from user actions. */
1321 if (!(equal_Command(cmd, "media.updated") || 1322 if (!(equal_Command(cmd, "media.updated") ||
1322 equal_Command(cmd, "media.player.update") || 1323 equal_Command(cmd, "media.player.update") ||
1323 equal_Command(cmd, "bookmarks.request.finished") || 1324 equal_Command(cmd, "bookmarks.request.finished") ||
@@ -1326,6 +1327,7 @@ static iBool messageHandler_(iWidget *msg, const char *cmd) {
1326 equal_Command(cmd, "document.request.updated") || 1327 equal_Command(cmd, "document.request.updated") ||
1327 equal_Command(cmd, "scrollbar.fade") || 1328 equal_Command(cmd, "scrollbar.fade") ||
1328 equal_Command(cmd, "widget.overflow") || 1329 equal_Command(cmd, "widget.overflow") ||
1330 equal_Command(cmd, "edgeswipe.ended") ||
1329 startsWith_CStr(cmd, "window."))) { 1331 startsWith_CStr(cmd, "window."))) {
1330 setupSheetTransition_Mobile(msg, iFalse); 1332 setupSheetTransition_Mobile(msg, iFalse);
1331 destroy_Widget(msg); 1333 destroy_Widget(msg);
@@ -1599,8 +1601,6 @@ iWidget *makePreferences_Widget(void) {
1599 /* General preferences. */ { 1601 /* General preferences. */ {
1600 appendTwoColumnPage_(tabs, "${heading.prefs.general}", '1', &headings, &values); 1602 appendTwoColumnPage_(tabs, "${heading.prefs.general}", '1', &headings, &values);
1601#if defined (LAGRANGE_ENABLE_DOWNLOAD_EDIT) 1603#if defined (LAGRANGE_ENABLE_DOWNLOAD_EDIT)
1602 //addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.downloads}")));
1603 //setId_Widget(addChild_Widget(values, iClob(new_InputWidget(0))), "prefs.downloads");
1604 addPrefsInputWithHeading_(headings, values, "prefs.downloads", iClob(new_InputWidget(0))); 1604 addPrefsInputWithHeading_(headings, values, "prefs.downloads", iClob(new_InputWidget(0)));
1605#endif 1605#endif
1606 iInputWidget *searchUrl; 1606 iInputWidget *searchUrl;
@@ -1608,12 +1608,6 @@ iWidget *makePreferences_Widget(void) {
1608 setUrlContent_InputWidget(searchUrl, iTrue); 1608 setUrlContent_InputWidget(searchUrl, iTrue);
1609 addChild_Widget(headings, iClob(makePadding_Widget(bigGap))); 1609 addChild_Widget(headings, iClob(makePadding_Widget(bigGap)));
1610 addChild_Widget(values, iClob(makePadding_Widget(bigGap))); 1610 addChild_Widget(values, iClob(makePadding_Widget(bigGap)));
1611 addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.collapsepreonload}")));
1612 addChild_Widget(values, iClob(makeToggle_Widget("prefs.collapsepreonload")));
1613 addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.plaintext.wrap}")));
1614 addChild_Widget(values, iClob(makeToggle_Widget("prefs.plaintext.wrap")));
1615 addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.centershort}")));
1616 addChild_Widget(values, iClob(makeToggle_Widget("prefs.centershort")));
1617 addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.hoverlink}"))); 1611 addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.hoverlink}")));
1618 addChild_Widget(values, iClob(makeToggle_Widget("prefs.hoverlink"))); 1612 addChild_Widget(values, iClob(makeToggle_Widget("prefs.hoverlink")));
1619 addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.archive.openindex}"))); 1613 addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.archive.openindex}")));
@@ -1674,26 +1668,6 @@ iWidget *makePreferences_Widget(void) {
1674 } 1668 }
1675 /* User Interface. */ { 1669 /* User Interface. */ {
1676 appendTwoColumnPage_(tabs, "${heading.prefs.interface}", '2', &headings, &values); 1670 appendTwoColumnPage_(tabs, "${heading.prefs.interface}", '2', &headings, &values);
1677#if defined (iPlatformApple) || defined (iPlatformMSys)
1678 addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.ostheme}")));
1679 addChild_Widget(values, iClob(makeToggle_Widget("prefs.ostheme")));
1680#endif
1681 addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.theme}")));
1682 iWidget *themes = new_Widget();
1683 /* Themes. */ {
1684 setId_Widget(addChild_Widget(themes, iClob(new_LabelWidget("${prefs.theme.black}", "theme.set arg:0"))), "prefs.theme.0");
1685 setId_Widget(addChild_Widget(themes, iClob(new_LabelWidget("${prefs.theme.dark}", "theme.set arg:1"))), "prefs.theme.1");
1686 setId_Widget(addChild_Widget(themes, iClob(new_LabelWidget("${prefs.theme.light}", "theme.set arg:2"))), "prefs.theme.2");
1687 setId_Widget(addChild_Widget(themes, iClob(new_LabelWidget("${prefs.theme.white}", "theme.set arg:3"))), "prefs.theme.3");
1688 }
1689 addChildFlags_Widget(values, iClob(themes), arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag);
1690 /* Accents. */
1691 iWidget *accent = new_Widget(); {
1692 setId_Widget(addChild_Widget(accent, iClob(new_LabelWidget("${prefs.accent.teal}", "accent.set arg:0"))), "prefs.accent.0");
1693 setId_Widget(addChild_Widget(accent, iClob(new_LabelWidget("${prefs.accent.orange}", "accent.set arg:1"))), "prefs.accent.1");
1694 }
1695 addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.accent}")));
1696 addChildFlags_Widget(values, iClob(accent), arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag);
1697#if defined (LAGRANGE_ENABLE_CUSTOM_FRAME) 1671#if defined (LAGRANGE_ENABLE_CUSTOM_FRAME)
1698 addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.customframe}"))); 1672 addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.customframe}")));
1699 addChild_Widget(values, iClob(makeToggle_Widget("prefs.customframe"))); 1673 addChild_Widget(values, iClob(makeToggle_Widget("prefs.customframe")));
@@ -1718,6 +1692,27 @@ iWidget *makePreferences_Widget(void) {
1718 } 1692 }
1719 /* Colors. */ { 1693 /* Colors. */ {
1720 appendTwoColumnPage_(tabs, "${heading.prefs.colors}", '3', &headings, &values); 1694 appendTwoColumnPage_(tabs, "${heading.prefs.colors}", '3', &headings, &values);
1695 makeTwoColumnHeading_("${heading.prefs.uitheme}", headings, values);
1696#if defined (iPlatformApple) || defined (iPlatformMSys)
1697 addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.ostheme}")));
1698 addChild_Widget(values, iClob(makeToggle_Widget("prefs.ostheme")));
1699#endif
1700 addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.theme}")));
1701 iWidget *themes = new_Widget();
1702 /* Themes. */ {
1703 setId_Widget(addChild_Widget(themes, iClob(new_LabelWidget("${prefs.theme.black}", "theme.set arg:0"))), "prefs.theme.0");
1704 setId_Widget(addChild_Widget(themes, iClob(new_LabelWidget("${prefs.theme.dark}", "theme.set arg:1"))), "prefs.theme.1");
1705 setId_Widget(addChild_Widget(themes, iClob(new_LabelWidget("${prefs.theme.light}", "theme.set arg:2"))), "prefs.theme.2");
1706 setId_Widget(addChild_Widget(themes, iClob(new_LabelWidget("${prefs.theme.white}", "theme.set arg:3"))), "prefs.theme.3");
1707 }
1708 addChildFlags_Widget(values, iClob(themes), arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag);
1709 /* Accents. */
1710 iWidget *accent = new_Widget(); {
1711 setId_Widget(addChild_Widget(accent, iClob(new_LabelWidget("${prefs.accent.teal}", "accent.set arg:0"))), "prefs.accent.0");
1712 setId_Widget(addChild_Widget(accent, iClob(new_LabelWidget("${prefs.accent.orange}", "accent.set arg:1"))), "prefs.accent.1");
1713 }
1714 addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.accent}")));
1715 addChildFlags_Widget(values, iClob(accent), arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag);
1721 makeTwoColumnHeading_("${heading.prefs.pagecontent}", headings, values); 1716 makeTwoColumnHeading_("${heading.prefs.pagecontent}", headings, values);
1722 for (int i = 0; i < 2; ++i) { 1717 for (int i = 0; i < 2; ++i) {
1723 const iBool isDark = (i == 0); 1718 const iBool isDark = (i == 0);
@@ -1750,14 +1745,15 @@ iWidget *makePreferences_Widget(void) {
1750 } 1745 }
1751 addChildFlags_Widget(values, iClob(sats), arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag); 1746 addChildFlags_Widget(values, iClob(sats), arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag);
1752 } 1747 }
1753 /* Layout. */ { 1748 /* Fonts. */ {
1754 setId_Widget(appendTwoColumnPage_(tabs, "${heading.prefs.style}", '4', &headings, &values), "prefs.page.style"); 1749 setId_Widget(appendTwoColumnPage_(tabs, "${heading.prefs.fonts}", '4', &headings, &values), "prefs.page.fonts");
1755 makeTwoColumnHeading_("${heading.prefs.fonts}", headings, values);
1756 /* Fonts. */ { 1750 /* Fonts. */ {
1757 addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.headingfont}"))); 1751 addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.headingfont}")));
1758 addFontButtons_(values, "headingfont"); 1752 addFontButtons_(values, "headingfont");
1759 addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.font}"))); 1753 addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.font}")));
1760 addFontButtons_(values, "font"); 1754 addFontButtons_(values, "font");
1755 addChild_Widget(headings, iClob(makePadding_Widget(bigGap)));
1756 addChild_Widget(values, iClob(makePadding_Widget(bigGap)));
1761 addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.mono}"))); 1757 addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.mono}")));
1762 iWidget *mono = new_Widget(); { 1758 iWidget *mono = new_Widget(); {
1763 iWidget *tog; 1759 iWidget *tog;
@@ -1789,9 +1785,18 @@ iWidget *makePreferences_Widget(void) {
1789 updateSize_LabelWidget((iLabelWidget *) tog); 1785 updateSize_LabelWidget((iLabelWidget *) tog);
1790 } 1786 }
1791 addChildFlags_Widget(values, iClob(boldLink), arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag); 1787 addChildFlags_Widget(values, iClob(boldLink), arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag);
1792 addPrefsInputWithHeading_(headings, values, "prefs.userfont", iClob(new_InputWidget(0))); 1788 addChild_Widget(headings, iClob(makePadding_Widget(bigGap)));
1793 } 1789 addChild_Widget(values, iClob(makePadding_Widget(bigGap)));
1794 makeTwoColumnHeading_("${heading.prefs.paragraph}", headings, values); 1790 /* Custom font. */ {
1791 iInputWidget *customFont = new_InputWidget(0);
1792 setHint_InputWidget(customFont, "${hint.prefs.userfont}");
1793 addPrefsInputWithHeading_(headings, values, "prefs.userfont", iClob(customFont));
1794 }
1795 }
1796 }
1797 /* Style. */ {
1798 setId_Widget(appendTwoColumnPage_(tabs, "${heading.prefs.style}", '5', &headings, &values), "prefs.page.style");
1799// makeTwoColumnHeading_("${heading.prefs.paragraph}", headings, values);
1795 addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.linewidth}"))); 1800 addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.linewidth}")));
1796 iWidget *widths = new_Widget(); 1801 iWidget *widths = new_Widget();
1797 /* Line widths. */ { 1802 /* Line widths. */ {
@@ -1811,14 +1816,20 @@ iWidget *makePreferences_Widget(void) {
1811 addChildFlags_Widget(values, iClob(quote), arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag); 1816 addChildFlags_Widget(values, iClob(quote), arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag);
1812 addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.biglede}"))); 1817 addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.biglede}")));
1813 addChild_Widget(values, iClob(makeToggle_Widget("prefs.biglede"))); 1818 addChild_Widget(values, iClob(makeToggle_Widget("prefs.biglede")));
1819 addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.plaintext.wrap}")));
1820 addChild_Widget(values, iClob(makeToggle_Widget("prefs.plaintext.wrap")));
1821 addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.collapsepreonload}")));
1822 addChild_Widget(values, iClob(makeToggle_Widget("prefs.collapsepreonload")));
1814// makeTwoColumnHeading_("${heading.prefs.widelayout}", headings, values); 1823// makeTwoColumnHeading_("${heading.prefs.widelayout}", headings, values);
1815 addChild_Widget(headings, iClob(makePadding_Widget(bigGap))); 1824 addChild_Widget(headings, iClob(makePadding_Widget(bigGap)));
1816 addChild_Widget(values, iClob(makePadding_Widget(bigGap))); 1825 addChild_Widget(values, iClob(makePadding_Widget(bigGap)));
1817 addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.sideicon}"))); 1826 addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.sideicon}")));
1818 addChild_Widget(values, iClob(makeToggle_Widget("prefs.sideicon"))); 1827 addChild_Widget(values, iClob(makeToggle_Widget("prefs.sideicon")));
1828 addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.centershort}")));
1829 addChild_Widget(values, iClob(makeToggle_Widget("prefs.centershort")));
1819 } 1830 }
1820 /* Network. */ { 1831 /* Network. */ {
1821 appendTwoColumnPage_(tabs, "${heading.prefs.network}", '5', &headings, &values); 1832 appendTwoColumnPage_(tabs, "${heading.prefs.network}", '6', &headings, &values);
1822 addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.decodeurls}"))); 1833 addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.decodeurls}")));
1823 addChild_Widget(values, iClob(makeToggle_Widget("prefs.decodeurls"))); 1834 addChild_Widget(values, iClob(makeToggle_Widget("prefs.decodeurls")));
1824 /* Cache size. */ { 1835 /* Cache size. */ {
@@ -1832,6 +1843,17 @@ iWidget *makePreferences_Widget(void) {
1832 resizeToParentHeight_WidgetFlag); 1843 resizeToParentHeight_WidgetFlag);
1833 setContentPadding_InputWidget(cache, 0, width_Widget(unit) - 4 * gap_UI); 1844 setContentPadding_InputWidget(cache, 0, width_Widget(unit) - 4 * gap_UI);
1834 } 1845 }
1846 /* Memory size. */ {
1847 iInputWidget *mem = new_InputWidget(4);
1848 setSelectAllOnFocus_InputWidget(mem, iTrue);
1849 addPrefsInputWithHeading_(headings, values, "prefs.memorysize", iClob(mem));
1850 iWidget *unit =
1851 addChildFlags_Widget(as_Widget(mem),
1852 iClob(new_LabelWidget("${mb}", NULL)),
1853 frameless_WidgetFlag | moveToParentRightEdge_WidgetFlag |
1854 resizeToParentHeight_WidgetFlag);
1855 setContentPadding_InputWidget(mem, 0, width_Widget(unit) - 4 * gap_UI);
1856 }
1835 makeTwoColumnHeading_("${heading.prefs.certs}", headings, values); 1857 makeTwoColumnHeading_("${heading.prefs.certs}", headings, values);
1836 addPrefsInputWithHeading_(headings, values, "prefs.ca.file", iClob(new_InputWidget(0))); 1858 addPrefsInputWithHeading_(headings, values, "prefs.ca.file", iClob(new_InputWidget(0)));
1837 addPrefsInputWithHeading_(headings, values, "prefs.ca.path", iClob(new_InputWidget(0))); 1859 addPrefsInputWithHeading_(headings, values, "prefs.ca.path", iClob(new_InputWidget(0)));
@@ -1843,7 +1865,7 @@ iWidget *makePreferences_Widget(void) {
1843 /* Keybindings. */ 1865 /* Keybindings. */
1844 if (deviceType_App() == desktop_AppDeviceType) { 1866 if (deviceType_App() == desktop_AppDeviceType) {
1845 iBindingsWidget *bind = new_BindingsWidget(); 1867 iBindingsWidget *bind = new_BindingsWidget();
1846 appendFramelessTabPage_(tabs, iClob(bind), "${heading.prefs.keys}", '6', KMOD_PRIMARY); 1868 appendFramelessTabPage_(tabs, iClob(bind), "${heading.prefs.keys}", '7', KMOD_PRIMARY);
1847 } 1869 }
1848 addChild_Widget(dlg, iClob(makePadding_Widget(gap_UI))); 1870 addChild_Widget(dlg, iClob(makePadding_Widget(gap_UI)));
1849 updatePreferencesLayout_Widget(dlg); 1871 updatePreferencesLayout_Widget(dlg);
diff --git a/src/ui/widget.c b/src/ui/widget.c
index 543b8bc9..992f115d 100644
--- a/src/ui/widget.c
+++ b/src/ui/widget.c
@@ -82,10 +82,10 @@ static void visualOffsetAnimation_Widget_(void *ptr) {
82 82
83void deinit_Widget(iWidget *d) { 83void deinit_Widget(iWidget *d) {
84 releaseChildren_Widget(d); 84 releaseChildren_Widget(d);
85//#if !defined (NDEBUG) 85#if 0 && !defined (NDEBUG)
86// printf("widget %p (%s) deleted (on top:%d)\n", d, cstr_String(&d->id), 86 printf("widget %p (%s) deleted (on top:%d)\n", d, cstr_String(&d->id),
87// d->flags & keepOnTop_WidgetFlag ? 1 : 0); 87 d->flags & keepOnTop_WidgetFlag ? 1 : 0);
88//#endif 88#endif
89 deinit_String(&d->id); 89 deinit_String(&d->id);
90 if (d->flags & keepOnTop_WidgetFlag) { 90 if (d->flags & keepOnTop_WidgetFlag) {
91 removeAll_PtrArray(onTop_Root(d->root), d); 91 removeAll_PtrArray(onTop_Root(d->root), d);
@@ -321,7 +321,6 @@ static iBool setWidth_Widget_(iWidget *d, int width) {
321 d->rect.size.x = width; 321 d->rect.size.x = width;
322 TRACE(d, "width has changed to %d", width); 322 TRACE(d, "width has changed to %d", width);
323 if (class_Widget(d)->sizeChanged) { 323 if (class_Widget(d)->sizeChanged) {
324 const int oldHeight = d->rect.size.y;
325 class_Widget(d)->sizeChanged(d); 324 class_Widget(d)->sizeChanged(d);
326 } 325 }
327 return iTrue; 326 return iTrue;
@@ -407,6 +406,11 @@ static void boundsOfChildren_Widget_(const iWidget *d, iRect *bounds_out) {
407 iRect childRect = child->rect; 406 iRect childRect = child->rect;
408 if (child->flags & ignoreForParentWidth_WidgetFlag) { 407 if (child->flags & ignoreForParentWidth_WidgetFlag) {
409 childRect.size.x = 0; 408 childRect.size.x = 0;
409 childRect.pos.x = bounds_out->pos.x;
410 }
411 if (child->flags & ignoreForParentHeight_WidgetFlag) {
412 childRect.size.y = 0;
413 childRect.pos.y = bounds_out->pos.y;
410 } 414 }
411 if (isEmpty_Rect(*bounds_out)) { 415 if (isEmpty_Rect(*bounds_out)) {
412 *bounds_out = childRect; 416 *bounds_out = childRect;
@@ -760,6 +764,27 @@ void arrange_Widget(iWidget *d) {
760 } 764 }
761} 765}
762 766
767iBool isBeingVisuallyOffsetByReference_Widget(const iWidget *d) {
768 return visualOffsetByReference_Widget(d) != 0;
769}
770
771int visualOffsetByReference_Widget(const iWidget *d) {
772 if (d->offsetRef && d->flags & refChildrenOffset_WidgetFlag) {
773 int offX = 0;
774 iConstForEach(ObjectList, i, children_Widget(d->offsetRef)) {
775 const iWidget *child = i.object;
776 if (child == d) continue;
777 if (child->flags & (visualOffset_WidgetFlag | dragged_WidgetFlag)) {
778// const float factor = width_Widget(d) / (float) size_Root(d->root).x;
779 const int invOff = width_Widget(d) - iRound(value_Anim(&child->visualOffset));
780 offX -= invOff / 4;
781 }
782 }
783 return offX;
784 }
785 return 0;
786}
787
763static void applyVisualOffset_Widget_(const iWidget *d, iInt2 *pos) { 788static void applyVisualOffset_Widget_(const iWidget *d, iInt2 *pos) {
764 if (d->flags & (visualOffset_WidgetFlag | dragged_WidgetFlag)) { 789 if (d->flags & (visualOffset_WidgetFlag | dragged_WidgetFlag)) {
765 const int off = iRound(value_Anim(&d->visualOffset)); 790 const int off = iRound(value_Anim(&d->visualOffset));
@@ -774,14 +799,7 @@ static void applyVisualOffset_Widget_(const iWidget *d, iInt2 *pos) {
774 pos->y -= value_Anim(d->animOffsetRef); 799 pos->y -= value_Anim(d->animOffsetRef);
775 } 800 }
776 if (d->flags & refChildrenOffset_WidgetFlag) { 801 if (d->flags & refChildrenOffset_WidgetFlag) {
777 iConstForEach(ObjectList, i, children_Widget(d->offsetRef)) { 802 pos->x += visualOffsetByReference_Widget(d);
778 const iWidget *child = i.object;
779 if (child == d) continue;
780 if (child->flags & (visualOffset_WidgetFlag | dragged_WidgetFlag)) {
781 const int invOff = size_Root(d->root).x - iRound(value_Anim(&child->visualOffset));
782 pos->x -= invOff / 4;
783 }
784 }
785 } 803 }
786} 804}
787 805
@@ -1057,10 +1075,38 @@ iBool processEvent_Widget(iWidget *d, const SDL_Event *ev) {
1057 isCommand_UserEvent(ev, "widget.overflow")) { 1075 isCommand_UserEvent(ev, "widget.overflow")) {
1058 scrollOverflow_Widget(d, 0); /* check bounds */ 1076 scrollOverflow_Widget(d, 0); /* check bounds */
1059 } 1077 }
1060 if (ev->user.code == command_UserEventCode && d->commandHandler && 1078 if (ev->user.code == command_UserEventCode) {
1061 d->commandHandler(d, ev->user.data1)) { 1079 const char *cmd = command_UserEvent(ev);
1062 iAssert(get_Root() == d->root); 1080 if (d->flags & (leftEdgeDraggable_WidgetFlag | rightEdgeDraggable_WidgetFlag) &&
1063 return iTrue; 1081 isVisible_Widget(d) && ~d->flags & disabled_WidgetFlag &&
1082 equal_Command(cmd, "edgeswipe.moved")) {
1083 /* Check the side. */
1084 const int side = argLabel_Command(cmd, "side");
1085 if ((side == 1 && d->flags & leftEdgeDraggable_WidgetFlag) ||
1086 (side == 2 && d->flags & rightEdgeDraggable_WidgetFlag)) {
1087 if (~d->flags & dragged_WidgetFlag) {
1088 setFlags_Widget(d, dragged_WidgetFlag, iTrue);
1089 }
1090 setVisualOffset_Widget(d, arg_Command(command_UserEvent(ev)) *
1091 width_Widget(d) / size_Root(d->root).x,
1092 10, 0);
1093 return iTrue;
1094 }
1095 }
1096 if (d->flags & dragged_WidgetFlag && equal_Command(cmd, "edgeswipe.ended")) {
1097 if (argLabel_Command(cmd, "abort")) {
1098 setVisualOffset_Widget(d, 0, 200, easeOut_AnimFlag);
1099 }
1100 else {
1101 postCommand_Widget(
1102 d, argLabel_Command(cmd, "side") == 1 ? "swipe.back" : "swipe.forward");
1103 }
1104 setFlags_Widget(d, dragged_WidgetFlag, iFalse);
1105 }
1106 if (d->commandHandler && d->commandHandler(d, ev->user.data1)) {
1107 iAssert(get_Root() == d->root);
1108 return iTrue;
1109 }
1064 } 1110 }
1065 break; 1111 break;
1066 } 1112 }
@@ -1091,6 +1137,17 @@ iBool processEvent_Widget(iWidget *d, const SDL_Event *ev) {
1091 return iFalse; 1137 return iFalse;
1092} 1138}
1093 1139
1140int backgroundFadeColor_Widget(void) {
1141 switch (colorTheme_App()) {
1142 case light_ColorTheme:
1143 return gray25_ColorId;
1144 case pureWhite_ColorTheme:
1145 return gray50_ColorId;
1146 default:
1147 return black_ColorId;
1148 }
1149}
1150
1094void drawBackground_Widget(const iWidget *d) { 1151void drawBackground_Widget(const iWidget *d) {
1095 if (d->flags & noBackground_WidgetFlag) { 1152 if (d->flags & noBackground_WidgetFlag) {
1096 return; 1153 return;
@@ -1107,14 +1164,13 @@ void drawBackground_Widget(const iWidget *d) {
1107 shadowBorder = iFalse; 1164 shadowBorder = iFalse;
1108 } 1165 }
1109 } 1166 }
1110 if (shadowBorder) { 1167 if (shadowBorder && ~d->flags & noShadowBorder_WidgetFlag) {
1111 iPaint p; 1168 iPaint p;
1112 init_Paint(&p); 1169 init_Paint(&p);
1113 drawSoftShadow_Paint(&p, bounds_Widget(d), 12 * gap_UI, black_ColorId, 30); 1170 drawSoftShadow_Paint(&p, bounds_Widget(d), 12 * gap_UI, black_ColorId, 30);
1114 } 1171 }
1115 const iBool isFaded = fadeBackground && 1172 const iBool isFaded = fadeBackground &&
1116 ~d->flags & noFadeBackground_WidgetFlag;/* && 1173 ~d->flags & noFadeBackground_WidgetFlag;
1117 ~d->flags & destroyPending_WidgetFlag;*/
1118 if (isFaded) { 1174 if (isFaded) {
1119 iPaint p; 1175 iPaint p;
1120 init_Paint(&p); 1176 init_Paint(&p);
@@ -1125,19 +1181,7 @@ void drawBackground_Widget(const iWidget *d) {
1125 p.alpha *= (area > 0 ? visibleArea / area : 0.0f); 1181 p.alpha *= (area > 0 ? visibleArea / area : 0.0f);
1126 } 1182 }
1127 SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_BLEND); 1183 SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_BLEND);
1128 int fadeColor; 1184 fillRect_Paint(&p, rect_Root(d->root), backgroundFadeColor_Widget());
1129 switch (colorTheme_App()) {
1130 default:
1131 fadeColor = black_ColorId;
1132 break;
1133 case light_ColorTheme:
1134 fadeColor = gray25_ColorId;
1135 break;
1136 case pureWhite_ColorTheme:
1137 fadeColor = gray50_ColorId;
1138 break;
1139 }
1140 fillRect_Paint(&p, rect_Root(d->root), fadeColor);
1141 SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_NONE); 1185 SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_NONE);
1142 } 1186 }
1143 if (d->bgColor >= 0 || d->frameColor >= 0) { 1187 if (d->bgColor >= 0 || d->frameColor >= 0) {
diff --git a/src/ui/widget.h b/src/ui/widget.h
index 8de62b7a..41784b99 100644
--- a/src/ui/widget.h
+++ b/src/ui/widget.h
@@ -87,12 +87,12 @@ enum iWidgetFlag {
87}; 87};
88 88
89/* 64-bit extended flags */ 89/* 64-bit extended flags */
90//#define wasCollapsed_WidgetFlag iBit64(32) 90#define rightEdgeDraggable_WidgetFlag iBit64(31)
91#define disabledWhenHidden_WidgetFlag iBit64(32) 91#define disabledWhenHidden_WidgetFlag iBit64(32)
92#define centerHorizontal_WidgetFlag iBit64(33) 92#define centerHorizontal_WidgetFlag iBit64(33)
93#define moveToParentLeftEdge_WidgetFlag iBit64(34) 93#define moveToParentLeftEdge_WidgetFlag iBit64(34)
94#define moveToParentRightEdge_WidgetFlag iBit64(35) 94#define moveToParentRightEdge_WidgetFlag iBit64(35)
95#define wrapText_WidgetFlag iBit64(36) 95#define noShadowBorder_WidgetFlag iBit64(36)
96#define borderTop_WidgetFlag iBit64(37) 96#define borderTop_WidgetFlag iBit64(37)
97#define overflowScrollable_WidgetFlag iBit64(38) 97#define overflowScrollable_WidgetFlag iBit64(38)
98#define focusRoot_WidgetFlag iBit64(39) 98#define focusRoot_WidgetFlag iBit64(39)
@@ -103,7 +103,7 @@ enum iWidgetFlag {
103#define drawBackgroundToVerticalSafeArea_WidgetFlag iBit64(44) 103#define drawBackgroundToVerticalSafeArea_WidgetFlag iBit64(44)
104#define visualOffset_WidgetFlag iBit64(45) 104#define visualOffset_WidgetFlag iBit64(45)
105#define parentCannotResize_WidgetFlag iBit64(46) 105#define parentCannotResize_WidgetFlag iBit64(46)
106#define noTopFrame_WidgetFlag iBit64(47) 106#define ignoreForParentHeight_WidgetFlag iBit64(47)
107#define unpadded_WidgetFlag iBit64(48) /* ignore parent's padding */ 107#define unpadded_WidgetFlag iBit64(48) /* ignore parent's padding */
108#define extraPadding_WidgetFlag iBit64(49) 108#define extraPadding_WidgetFlag iBit64(49)
109#define borderBottom_WidgetFlag iBit64(50) 109#define borderBottom_WidgetFlag iBit64(50)
@@ -117,8 +117,8 @@ enum iWidgetFlag {
117#define parentCannotResizeHeight_WidgetFlag iBit64(58) 117#define parentCannotResizeHeight_WidgetFlag iBit64(58)
118#define ignoreForParentWidth_WidgetFlag iBit64(59) 118#define ignoreForParentWidth_WidgetFlag iBit64(59)
119#define noFadeBackground_WidgetFlag iBit64(60) 119#define noFadeBackground_WidgetFlag iBit64(60)
120#define destroyPending_WidgetFlag iBit64(61) /* TODO: needed? */ 120#define destroyPending_WidgetFlag iBit64(61)
121#define edgeDraggable_WidgetFlag iBit64(62) 121#define leftEdgeDraggable_WidgetFlag iBit64(62)
122#define refChildrenOffset_WidgetFlag iBit64(63) /* visual offset determined by the offset of referenced children */ 122#define refChildrenOffset_WidgetFlag iBit64(63) /* visual offset determined by the offset of referenced children */
123 123
124enum iWidgetAddPos { 124enum iWidgetAddPos {
@@ -242,8 +242,9 @@ iBool isSelected_Widget (const iAnyObject *);
242iBool isUnderKeyRoot_Widget (const iAnyObject *); 242iBool isUnderKeyRoot_Widget (const iAnyObject *);
243iBool isCommand_Widget (const iWidget *d, const SDL_Event *ev, const char *cmd); 243iBool isCommand_Widget (const iWidget *d, const SDL_Event *ev, const char *cmd);
244iBool hasParent_Widget (const iWidget *d, const iWidget *someParent); 244iBool hasParent_Widget (const iWidget *d, const iWidget *someParent);
245iBool isAffectedByVisualOffset_Widget 245iBool isAffectedByVisualOffset_Widget (const iWidget *);
246 (const iWidget *); 246iBool isBeingVisuallyOffsetByReference_Widget (const iWidget *);
247int visualOffsetByReference_Widget (const iWidget *);
247void setId_Widget (iWidget *, const char *id); 248void setId_Widget (iWidget *, const char *id);
248void setFlags_Widget (iWidget *, int64_t flags, iBool set); 249void setFlags_Widget (iWidget *, int64_t flags, iBool set);
249void setPos_Widget (iWidget *, iInt2 pos); 250void setPos_Widget (iWidget *, iInt2 pos);
@@ -276,6 +277,8 @@ void refresh_Widget (const iAnyObject *);
276 277
277iBool equalWidget_Command (const char *cmd, const iWidget *widget, const char *checkCommand); 278iBool equalWidget_Command (const char *cmd, const iWidget *widget, const char *checkCommand);
278 279
280int backgroundFadeColor_Widget (void);
281
279void setFocus_Widget (iWidget *); 282void setFocus_Widget (iWidget *);
280iWidget * focus_Widget (void); 283iWidget * focus_Widget (void);
281void setHover_Widget (iWidget *); 284void setHover_Widget (iWidget *);
diff --git a/src/ui/window.c b/src/ui/window.c
index 96a22fee..f71d8102 100644
--- a/src/ui/window.c
+++ b/src/ui/window.c
@@ -69,10 +69,6 @@ iDefineTypeConstructionArgs(Window, (iRect rect), rect)
69 69
70/* TODO: Define menus per platform. */ 70/* TODO: Define menus per platform. */
71 71
72#if defined (iPlatformAppleDesktop)
73# define iHaveNativeMenus
74#endif
75
76#if defined (iHaveNativeMenus) 72#if defined (iHaveNativeMenus)
77/* Using native menus. */ 73/* Using native menus. */
78static const iMenuItem fileMenuItems_[] = { 74static const iMenuItem fileMenuItems_[] = {
@@ -202,7 +198,7 @@ static void windowSizeChanged_Window_(iWindow *d) {
202} 198}
203 199
204static void setupUserInterface_Window(iWindow *d) { 200static void setupUserInterface_Window(iWindow *d) {
205#if defined (iPlatformAppleDesktop) 201#if defined (iHaveNativeMenus)
206 insertMacMenus_(); 202 insertMacMenus_();
207#endif 203#endif
208 /* One root is created by default. */ 204 /* One root is created by default. */
@@ -913,7 +909,7 @@ iBool processEvent_Window(iWindow *d, const SDL_Event *ev) {
913 } 909 }
914 } 910 }
915 if (isCommand_UserEvent(&event, "lang.changed")) { 911 if (isCommand_UserEvent(&event, "lang.changed")) {
916#if defined (iPlatformAppleDesktop) 912#if defined (iHaveNativeMenus)
917 /* Retranslate the menus. */ 913 /* Retranslate the menus. */
918 removeMacMenus_(); 914 removeMacMenus_();
919 insertMacMenus_(); 915 insertMacMenus_();