summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CMakeLists.txt4
-rw-r--r--src/defs.h1
-rw-r--r--src/ui/documentwidget.c202
-rw-r--r--src/ui/scrollwidget.c11
-rw-r--r--src/ui/scrollwidget.h1
-rw-r--r--src/ui/touch.c27
-rw-r--r--src/ui/util.c26
-rw-r--r--src/ui/util.h4
-rw-r--r--src/ui/widget.c5
-rw-r--r--src/ui/widget.h1
-rw-r--r--src/ui/window.c45
11 files changed, 253 insertions, 74 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index b6093888..bb937fa0 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -18,11 +18,11 @@
18cmake_minimum_required (VERSION 3.9) 18cmake_minimum_required (VERSION 3.9)
19 19
20project (Lagrange 20project (Lagrange
21 VERSION 1.3.2 21 VERSION 1.4.0
22 DESCRIPTION "A Beautiful Gemini Client" 22 DESCRIPTION "A Beautiful Gemini Client"
23 LANGUAGES C 23 LANGUAGES C
24) 24)
25set (IOS_BUNDLE_VERSION 9) 25set (IOS_BUNDLE_VERSION 1)
26set (COPYRIGHT_YEAR 2021) 26set (COPYRIGHT_YEAR 2021)
27 27
28# Build configuration. 28# Build configuration.
diff --git a/src/defs.h b/src/defs.h
index 6b76ed71..e8043e1e 100644
--- a/src/defs.h
+++ b/src/defs.h
@@ -84,6 +84,7 @@ enum iFileVersion {
84#define unhappy_Icon "\U0001f641" 84#define unhappy_Icon "\U0001f641"
85#define globe_Icon "\U0001f310" 85#define globe_Icon "\U0001f310"
86#define magnifyingGlass_Icon "\U0001f50d" 86#define magnifyingGlass_Icon "\U0001f50d"
87#define midEllipsis_Icon "\u22ef"
87 88
88/* UI labels that depend on the platform */ 89/* UI labels that depend on the platform */
89 90
diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c
index e18d5283..105a7158 100644
--- a/src/ui/documentwidget.c
+++ b/src/ui/documentwidget.c
@@ -200,6 +200,8 @@ enum iDocumentWidgetFlag {
200 selectWords_DocumentWidgetFlag = iBit(7), 200 selectWords_DocumentWidgetFlag = iBit(7),
201 selectLines_DocumentWidgetFlag = iBit(8), 201 selectLines_DocumentWidgetFlag = iBit(8),
202 pinchZoom_DocumentWidgetFlag = iBit(9), 202 pinchZoom_DocumentWidgetFlag = iBit(9),
203 movingSelectMarkStart_DocumentWidgetFlag = iBit(10),
204 movingSelectMarkEnd_DocumentWidgetFlag = iBit(11),
203}; 205};
204 206
205enum iDocumentLinkOrdinalMode { 207enum iDocumentLinkOrdinalMode {
@@ -251,6 +253,7 @@ struct Impl_DocumentWidget {
251 const iGmRun * firstVisibleRun; 253 const iGmRun * firstVisibleRun;
252 const iGmRun * lastVisibleRun; 254 const iGmRun * lastVisibleRun;
253 iClick click; 255 iClick click;
256 iInt2 contextPos; /* coordinates of latest right click */
254 iString pendingGotoHeading; 257 iString pendingGotoHeading;
255 float initNormScrollY; 258 float initNormScrollY;
256 iAnim scrollY; 259 iAnim scrollY;
@@ -259,6 +262,7 @@ struct Impl_DocumentWidget {
259 iScrollWidget *scroll; 262 iScrollWidget *scroll;
260 iWidget * menu; 263 iWidget * menu;
261 iWidget * playerMenu; 264 iWidget * playerMenu;
265 iWidget * copyMenu;
262 iVisBuf * visBuf; 266 iVisBuf * visBuf;
263 iPtrSet * invalidRuns; 267 iPtrSet * invalidRuns;
264 iDrawBufs * drawBufs; /* dynamic state for drawing */ 268 iDrawBufs * drawBufs; /* dynamic state for drawing */
@@ -324,6 +328,7 @@ void init_DocumentWidget(iDocumentWidget *d) {
324 addChild_Widget(w, iClob(d->scroll = new_ScrollWidget())); 328 addChild_Widget(w, iClob(d->scroll = new_ScrollWidget()));
325 d->menu = NULL; /* created when clicking */ 329 d->menu = NULL; /* created when clicking */
326 d->playerMenu = NULL; 330 d->playerMenu = NULL;
331 d->copyMenu = NULL;
327 d->drawBufs = new_DrawBufs(); 332 d->drawBufs = new_DrawBufs();
328 d->translation = NULL; 333 d->translation = NULL;
329 addChildFlags_Widget(w, 334 addChildFlags_Widget(w,
@@ -367,6 +372,15 @@ void deinit_DocumentWidget(iDocumentWidget *d) {
367 deinit_PersistentDocumentState(&d->mod); 372 deinit_PersistentDocumentState(&d->mod);
368} 373}
369 374
375static iRangecc selectMark_DocumentWidget_(const iDocumentWidget *d) {
376 /* Normalize so start < end. */
377 iRangecc norm = d->selectMark;
378 if (norm.start > norm.end) {
379 iSwap(const char *, norm.start, norm.end);
380 }
381 return norm;
382}
383
370static void enableActions_DocumentWidget_(iDocumentWidget *d, iBool enable) { 384static void enableActions_DocumentWidget_(iDocumentWidget *d, iBool enable) {
371 /* Actions are invisible child widgets of the DocumentWidget. */ 385 /* Actions are invisible child widgets of the DocumentWidget. */
372 iForEach(ObjectList, i, children_Widget(d)) { 386 iForEach(ObjectList, i, children_Widget(d)) {
@@ -1747,6 +1761,26 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
1747 updateWindowTitle_DocumentWidget_(d); 1761 updateWindowTitle_DocumentWidget_(d);
1748 return iFalse; 1762 return iFalse;
1749 } 1763 }
1764 else if (equal_Command(cmd, "document.select") && d == document_App()) {
1765 /* Touch selection mode. */
1766 if (!arg_Command(cmd)) {
1767 d->selectMark = iNullRange;
1768 setFlags_Widget(w, touchDrag_WidgetFlag, iFalse);
1769 setFadeEnabled_ScrollWidget(d->scroll, iTrue);
1770 }
1771 else {
1772 setFlags_Widget(w, touchDrag_WidgetFlag, iTrue);
1773 d->flags |= movingSelectMarkEnd_DocumentWidgetFlag |
1774 selectWords_DocumentWidgetFlag; /* finger-based selection is imprecise */
1775 d->flags &= ~selectLines_DocumentWidgetFlag;
1776 setFadeEnabled_ScrollWidget(d->scroll, iFalse);
1777 d->selectMark = sourceLoc_DocumentWidget_(d, d->contextPos);
1778 extendRange_Rangecc(&d->selectMark, range_String(source_GmDocument(d->doc)),
1779 word_RangeExtension | bothStartAndEnd_RangeExtension);
1780 d->initialSelectMark = d->selectMark;
1781 }
1782 return iTrue;
1783 }
1750 else if (equal_Command(cmd, "document.info") && d == document_App()) { 1784 else if (equal_Command(cmd, "document.info") && d == document_App()) {
1751 const char *unchecked = red_ColorEscape "\u2610"; 1785 const char *unchecked = red_ColorEscape "\u2610";
1752 const char *checked = green_ColorEscape "\u2611"; 1786 const char *checked = green_ColorEscape "\u2611";
@@ -1870,6 +1904,9 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
1870 } 1904 }
1871 SDL_SetClipboardText(cstr_String(copied)); 1905 SDL_SetClipboardText(cstr_String(copied));
1872 delete_String(copied); 1906 delete_String(copied);
1907 if (flags_Widget(w) & touchDrag_WidgetFlag) {
1908 postCommand_App("document.select arg:0");
1909 }
1873 return iTrue; 1910 return iTrue;
1874 } 1911 }
1875 else if (equal_Command(cmd, "document.copylink") && document_App() == d) { 1912 else if (equal_Command(cmd, "document.copylink") && document_App() == d) {
@@ -1960,7 +1997,7 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
1960 cacheDocumentGlyphs_DocumentWidget_(d); 1997 cacheDocumentGlyphs_DocumentWidget_(d);
1961 return iFalse; 1998 return iFalse;
1962 } 1999 }
1963 else if (equalWidget_Command(cmd, w, "document.translate")) { 2000 else if (equal_Command(cmd, "document.translate") && d == document_App()) {
1964 if (!d->translation) { 2001 if (!d->translation) {
1965 d->translation = new_Translation(d); 2002 d->translation = new_Translation(d);
1966 } 2003 }
@@ -2623,10 +2660,12 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
2623 } 2660 }
2624 if (ev->button.button == SDL_BUTTON_RIGHT && 2661 if (ev->button.button == SDL_BUTTON_RIGHT &&
2625 contains_Widget(w, init_I2(ev->button.x, ev->button.y))) { 2662 contains_Widget(w, init_I2(ev->button.x, ev->button.y))) {
2626 if (!d->menu || !isVisible_Widget(d->menu)) { 2663 if (!isVisible_Widget(d->menu)) {
2627 d->contextLink = d->hoverLink; 2664 d->contextLink = d->hoverLink;
2665 d->contextPos = init_I2(ev->button.x, ev->button.y);
2628 if (d->menu) { 2666 if (d->menu) {
2629 destroy_Widget(d->menu); 2667 destroy_Widget(d->menu);
2668 d->menu = NULL;
2630 } 2669 }
2631 setFocus_Widget(NULL); 2670 setFocus_Widget(NULL);
2632 iArray items; 2671 iArray items;
@@ -2637,6 +2676,12 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
2637 const iRangecc scheme = urlScheme_String(linkUrl); 2676 const iRangecc scheme = urlScheme_String(linkUrl);
2638 const iBool isGemini = equalCase_Rangecc(scheme, "gemini"); 2677 const iBool isGemini = equalCase_Rangecc(scheme, "gemini");
2639 iBool isNative = iFalse; 2678 iBool isNative = iFalse;
2679 if (deviceType_App() != desktop_AppDeviceType) {
2680 /* Show the link as the first, non-interactive item. */
2681 pushBack_Array(&items, &(iMenuItem){
2682 format_CStr("```%s", cstr_String(linkUrl)),
2683 0, 0, NULL });
2684 }
2640 if (willUseProxy_App(scheme) || isGemini || 2685 if (willUseProxy_App(scheme) || isGemini ||
2641 equalCase_Rangecc(scheme, "finger") || 2686 equalCase_Rangecc(scheme, "finger") ||
2642 equalCase_Rangecc(scheme, "gopher")) { 2687 equalCase_Rangecc(scheme, "gopher")) {
@@ -2707,24 +2752,18 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
2707 } 2752 }
2708 } 2753 }
2709 } 2754 }
2710 else { 2755 else if (deviceType_App() == desktop_AppDeviceType) {
2711 if (!isEmpty_Range(&d->selectMark)) { 2756 if (!isEmpty_Range(&d->selectMark)) {
2712 pushBackN_Array( 2757 pushBackN_Array(&items,
2713 &items, 2758 (iMenuItem[]){ { "${menu.copy}", 0, 0, "copy" },
2714 (iMenuItem[]){ { "${menu.copy}", 0, 0, "copy" }, { "---", 0, 0, NULL } }, 2759 { "---", 0, 0, NULL } },
2715 2); 2760 2);
2716 }
2717 if (deviceType_App() == desktop_AppDeviceType) {
2718 pushBackN_Array(
2719 &items,
2720 (iMenuItem[]){
2721 { "${menu.back}", navigateBack_KeyShortcut, "navigate.back" },
2722 { "${menu.forward}", navigateForward_KeyShortcut, "navigate.forward" } },
2723 2);
2724 } 2761 }
2725 pushBackN_Array( 2762 pushBackN_Array(
2726 &items, 2763 &items,
2727 (iMenuItem[]){ 2764 (iMenuItem[]){
2765 { "${menu.back}", navigateBack_KeyShortcut, "navigate.back" },
2766 { "${menu.forward}", navigateForward_KeyShortcut, "navigate.forward" },
2728 { upArrow_Icon " ${menu.parent}", navigateParent_KeyShortcut, "navigate.parent" }, 2767 { upArrow_Icon " ${menu.parent}", navigateParent_KeyShortcut, "navigate.parent" },
2729 { upArrowBar_Icon " ${menu.root}", navigateRoot_KeyShortcut, "navigate.root" }, 2768 { upArrowBar_Icon " ${menu.root}", navigateRoot_KeyShortcut, "navigate.root" },
2730 { "---", 0, 0, NULL }, 2769 { "---", 0, 0, NULL },
@@ -2738,7 +2777,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
2738 { globe_Icon " ${menu.page.translate}", 0, 0, "document.translate" }, 2777 { globe_Icon " ${menu.page.translate}", 0, 0, "document.translate" },
2739 { "---", 0, 0, NULL }, 2778 { "---", 0, 0, NULL },
2740 { "${menu.page.copyurl}", 0, 0, "document.copylink" } }, 2779 { "${menu.page.copyurl}", 0, 0, "document.copylink" } },
2741 12); 2780 15);
2742 if (isEmpty_Range(&d->selectMark)) { 2781 if (isEmpty_Range(&d->selectMark)) {
2743 pushBackN_Array( 2782 pushBackN_Array(
2744 &items, 2783 &items,
@@ -2748,6 +2787,21 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
2748 2); 2787 2);
2749 } 2788 }
2750 } 2789 }
2790 else {
2791 /* Mobile text selection menu. */
2792#if 0
2793 pushBackN_Array(
2794 &items,
2795 (iMenuItem[]){
2796 { "${menu.select}", 0, 0, "document.select arg:1" },
2797 { "${menu.select.word}", 0, 0, "document.select arg:2" },
2798 { "${menu.select.par}", 0, 0, "document.select arg:3" },
2799 },
2800 3);
2801#endif
2802 postCommand_App("document.select arg:1");
2803 return iTrue;
2804 }
2751 d->menu = makeMenu_Widget(w, data_Array(&items), size_Array(&items)); 2805 d->menu = makeMenu_Widget(w, data_Array(&items), size_Array(&items));
2752 deinit_Array(&items); 2806 deinit_Array(&items);
2753 } 2807 }
@@ -2763,26 +2817,29 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
2763 if (d->grabbedPlayer) { 2817 if (d->grabbedPlayer) {
2764 return iTrue; 2818 return iTrue;
2765 } 2819 }
2820 /* Enable hover state now that scrolling has surely finished. */
2766 if (d->flags & noHoverWhileScrolling_DocumentWidgetFlag) { 2821 if (d->flags & noHoverWhileScrolling_DocumentWidgetFlag) {
2767 d->flags &= ~noHoverWhileScrolling_DocumentWidgetFlag; 2822 d->flags &= ~noHoverWhileScrolling_DocumentWidgetFlag;
2768 updateHover_DocumentWidget_(d, mouseCoord_Window(get_Window())); 2823 updateHover_DocumentWidget_(d, mouseCoord_Window(get_Window()));
2769 } 2824 }
2770 iChangeFlags(d->flags, selecting_DocumentWidgetFlag, iFalse); 2825 if (~flags_Widget(w) & touchDrag_WidgetFlag) {
2771 iChangeFlags(d->flags, selectWords_DocumentWidgetFlag, d->click.count == 2); 2826 iChangeFlags(d->flags, selecting_DocumentWidgetFlag, iFalse);
2772 iChangeFlags(d->flags, selectLines_DocumentWidgetFlag, d->click.count >= 3); 2827 iChangeFlags(d->flags, selectWords_DocumentWidgetFlag, d->click.count == 2);
2773 /* Double/triple clicks marks the selection immediately. */ 2828 iChangeFlags(d->flags, selectLines_DocumentWidgetFlag, d->click.count >= 3);
2774 if (d->click.count >= 2) { 2829 /* Double/triple clicks marks the selection immediately. */
2775 beginMarkingSelection_DocumentWidget_(d, d->click.startPos); 2830 if (d->click.count >= 2) {
2776 extendRange_Rangecc( 2831 beginMarkingSelection_DocumentWidget_(d, d->click.startPos);
2777 &d->selectMark, 2832 extendRange_Rangecc(
2778 range_String(source_GmDocument(d->doc)), 2833 &d->selectMark,
2779 bothStartAndEnd_RangeExtension | 2834 range_String(source_GmDocument(d->doc)),
2780 (d->click.count == 2 ? word_RangeExtension : line_RangeExtension)); 2835 bothStartAndEnd_RangeExtension |
2781 d->initialSelectMark = d->selectMark; 2836 (d->click.count == 2 ? word_RangeExtension : line_RangeExtension));
2782 refresh_Widget(w); 2837 d->initialSelectMark = d->selectMark;
2783 } 2838 refresh_Widget(w);
2784 else { 2839 }
2785 d->initialSelectMark = iNullRange; 2840 else {
2841 d->initialSelectMark = iNullRange;
2842 }
2786 } 2843 }
2787 return iTrue; 2844 return iTrue;
2788 case drag_ClickResult: { 2845 case drag_ClickResult: {
@@ -2796,29 +2853,59 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
2796 refresh_Widget(w); 2853 refresh_Widget(w);
2797 return iTrue; 2854 return iTrue;
2798 } 2855 }
2799 /* Begin selecting a range of text. */ 2856 /* Fold/unfold a preformatted block. */
2800 if (~d->flags & selecting_DocumentWidgetFlag && d->hoverPre && 2857 if (~d->flags & selecting_DocumentWidgetFlag && d->hoverPre &&
2801 preIsFolded_GmDocument(d->doc, d->hoverPre->preId)) { 2858 preIsFolded_GmDocument(d->doc, d->hoverPre->preId)) {
2802 return iTrue; 2859 return iTrue;
2803 } 2860 }
2861 /* Begin selecting a range of text. */
2804 if (~d->flags & selecting_DocumentWidgetFlag) { 2862 if (~d->flags & selecting_DocumentWidgetFlag) {
2805 beginMarkingSelection_DocumentWidget_(d, d->click.startPos); 2863 beginMarkingSelection_DocumentWidget_(d, d->click.startPos);
2806 } 2864 }
2807 iRangecc loc = sourceLoc_DocumentWidget_(d, pos_Click(&d->click)); 2865 iRangecc loc = sourceLoc_DocumentWidget_(d, pos_Click(&d->click));
2808 if (!d->selectMark.start) { 2866 if (d->selectMark.start == NULL) {
2809 d->selectMark = loc; 2867 d->selectMark = loc;
2810 } 2868 }
2811 else if (loc.end) { 2869 else if (loc.end) {
2812 d->selectMark.end = (d->selectMark.end > d->selectMark.start ? loc.end : loc.start); 2870 if (flags_Widget(w) & touchDrag_WidgetFlag) {
2871 /* Choose which end to move. */
2872 if (!(d->flags & (movingSelectMarkStart_DocumentWidgetFlag |
2873 movingSelectMarkEnd_DocumentWidgetFlag))) {
2874 const iRangecc mark = selectMark_DocumentWidget_(d);
2875 const char * midMark = mark.start + size_Range(&mark) / 2;
2876 const iRangecc loc = sourceLoc_DocumentWidget_(d, pos_Click(&d->click));
2877 const iBool isCloserToStart = d->selectMark.start > d->selectMark.end ?
2878 (loc.start > midMark) : (loc.start < midMark);
2879 iChangeFlags(d->flags, movingSelectMarkStart_DocumentWidgetFlag, isCloserToStart);
2880 iChangeFlags(d->flags, movingSelectMarkEnd_DocumentWidgetFlag, !isCloserToStart);
2881 }
2882 /* Move the start or the end depending on which is nearer. */
2883 if (d->flags & movingSelectMarkStart_DocumentWidgetFlag) {
2884 d->selectMark.start = loc.start;
2885 }
2886 else {
2887 d->selectMark.end = (d->selectMark.end > d->selectMark.start ? loc.end : loc.start);
2888 }
2889 }
2890 else {
2891 d->selectMark.end = (d->selectMark.end > d->selectMark.start ? loc.end : loc.start);
2892 }
2813 } 2893 }
2814 iAssert((!d->selectMark.start && !d->selectMark.end) || 2894 iAssert((!d->selectMark.start && !d->selectMark.end) ||
2815 ( d->selectMark.start && d->selectMark.end)); 2895 ( d->selectMark.start && d->selectMark.end));
2816 /* Extend the selection when double/triple clicking. */ 2896 /* Extend to full words/paragraphs. */
2817 if (d->flags & (selectWords_DocumentWidgetFlag | selectLines_DocumentWidgetFlag)) { 2897 if (d->flags & (selectWords_DocumentWidgetFlag | selectLines_DocumentWidgetFlag)) {
2818 extendRange_Rangecc( 2898 extendRange_Rangecc(
2819 &d->selectMark, 2899 &d->selectMark,
2820 range_String(source_GmDocument(d->doc)), 2900 range_String(source_GmDocument(d->doc)),
2821 d->click.count == 2 ? word_RangeExtension : line_RangeExtension); 2901 (d->flags & movingSelectMarkStart_DocumentWidgetFlag ? moveStart_RangeExtension
2902 : moveEnd_RangeExtension) |
2903 (d->flags & selectWords_DocumentWidgetFlag ? word_RangeExtension
2904 : line_RangeExtension));
2905 if (d->flags & movingSelectMarkStart_DocumentWidgetFlag) {
2906 d->initialSelectMark.start =
2907 d->initialSelectMark.end = d->selectMark.start;
2908 }
2822 if (!isEmpty_Range(&d->initialSelectMark)) { 2909 if (!isEmpty_Range(&d->initialSelectMark)) {
2823 if (d->selectMark.end > d->selectMark.start) { 2910 if (d->selectMark.end > d->selectMark.start) {
2824 d->selectMark.start = d->initialSelectMark.start; 2911 d->selectMark.start = d->initialSelectMark.start;
@@ -2842,13 +2929,42 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
2842 if (isVisible_Widget(d->menu)) { 2929 if (isVisible_Widget(d->menu)) {
2843 closeMenu_Widget(d->menu); 2930 closeMenu_Widget(d->menu);
2844 } 2931 }
2932 d->flags &= ~(movingSelectMarkStart_DocumentWidgetFlag |
2933 movingSelectMarkEnd_DocumentWidgetFlag);
2845 if (!isMoved_Click(&d->click)) { 2934 if (!isMoved_Click(&d->click)) {
2846 setFocus_Widget(NULL); 2935 setFocus_Widget(NULL);
2936 /* Tap in tap selection mode. */
2937 if (flags_Widget(w) & touchDrag_WidgetFlag) {
2938 const iRangecc tapLoc = sourceLoc_DocumentWidget_(d, pos_Click(&d->click));
2939 /* Tapping on the selection will show a menu. */
2940 const iRangecc mark = selectMark_DocumentWidget_(d);
2941 if (tapLoc.start >= mark.start && tapLoc.end <= mark.end) {
2942 if (d->copyMenu) {
2943 closeMenu_Widget(d->copyMenu);
2944 destroy_Widget(d->copyMenu);
2945 d->copyMenu = NULL;
2946 }
2947 d->copyMenu = makeMenu_Widget(w, (iMenuItem[]){
2948 { clipCopy_Icon " ${menu.copy}", 0, 0, "copy" },
2949 { "---", 0, 0, NULL },
2950 { close_Icon " Clear Selection", 0, 0, "document.select arg:0" },
2951 }, 3);
2952 setFlags_Widget(d->copyMenu, noFadeBackground_WidgetFlag, iTrue);
2953 openMenu_Widget(d->copyMenu, pos_Click(&d->click));
2954 return iTrue;
2955 }
2956 else {
2957 /* Tapping elsewhere exits selection mode. */
2958 postCommand_Widget(d, "document.select arg:0");
2959 return iTrue;
2960 }
2961 }
2847 if (d->hoverPre) { 2962 if (d->hoverPre) {
2848 togglePreFold_DocumentWidget_(d, d->hoverPre->preId); 2963 togglePreFold_DocumentWidget_(d, d->hoverPre->preId);
2849 return iTrue; 2964 return iTrue;
2850 } 2965 }
2851 if (d->hoverLink) { 2966 if (d->hoverLink) {
2967 /* TODO: Move this to a method. */
2852 const iGmLinkId linkId = d->hoverLink->linkId; 2968 const iGmLinkId linkId = d->hoverLink->linkId;
2853 const int linkFlags = linkFlags_GmDocument(d->doc, linkId); 2969 const int linkFlags = linkFlags_GmDocument(d->doc, linkId);
2854 iAssert(linkId); 2970 iAssert(linkId);
@@ -3636,6 +3752,18 @@ static void draw_DocumentWidget_(const iDocumentWidget *d) {
3636 drawCentered_Text(font, bounds, iFalse, uiBackground_ColorId, "%d %%", 3752 drawCentered_Text(font, bounds, iFalse, uiBackground_ColorId, "%d %%",
3637 d->pinchZoomPosted); 3753 d->pinchZoomPosted);
3638 } 3754 }
3755 /* Touch selection indicator. */
3756 if (flags_Widget(w) & touchDrag_WidgetFlag) {
3757 iString msg;
3758 init_String(&msg);
3759 format_String(&msg, "Selecting: drag and tap");
3760 fillRect_Paint(&ctx.paint, (iRect){ topLeft_Rect(bounds),
3761 init_I2(width_Rect(bounds), lineHeight_Text(uiLabelBold_FontId))},
3762 uiTextAction_ColorId);
3763 drawRange_Text(uiLabelBold_FontId, addX_I2(topLeft_Rect(bounds), 3 * gap_UI),
3764 uiBackground_ColorId, range_String(&msg));
3765 deinit_String(&msg);
3766 }
3639} 3767}
3640 3768
3641/*----------------------------------------------------------------------------------------------*/ 3769/*----------------------------------------------------------------------------------------------*/
diff --git a/src/ui/scrollwidget.c b/src/ui/scrollwidget.c
index 55ede426..32b57c69 100644
--- a/src/ui/scrollwidget.c
+++ b/src/ui/scrollwidget.c
@@ -48,6 +48,7 @@ struct Impl_ScrollWidget {
48 iClick click; 48 iClick click;
49 int startThumb; 49 int startThumb;
50 iAnim opacity; 50 iAnim opacity;
51 iBool fadeEnabled;
51 uint32_t fadeStart; 52 uint32_t fadeStart;
52 iBool willCheckFade; 53 iBool willCheckFade;
53}; 54};
@@ -76,6 +77,7 @@ void init_ScrollWidget(iScrollWidget *d) {
76 init_Click(&d->click, d, SDL_BUTTON_LEFT); 77 init_Click(&d->click, d, SDL_BUTTON_LEFT);
77 init_Anim(&d->opacity, minOpacity_()); 78 init_Anim(&d->opacity, minOpacity_());
78 d->willCheckFade = iFalse; 79 d->willCheckFade = iFalse;
80 d->fadeEnabled = iTrue;
79} 81}
80 82
81void deinit_ScrollWidget(iScrollWidget *d) { 83void deinit_ScrollWidget(iScrollWidget *d) {
@@ -108,7 +110,7 @@ static void unfade_ScrollWidget_(iScrollWidget *d, float opacity) {
108 setValue_Anim(&d->opacity, opacity, 66); 110 setValue_Anim(&d->opacity, opacity, 66);
109 addTicker_App(animateOpacity_ScrollWidget_, d); 111 addTicker_App(animateOpacity_ScrollWidget_, d);
110 } 112 }
111 if (!d->willCheckFade) { 113 if (!d->willCheckFade && d->fadeEnabled) {
112 d->willCheckFade = iTrue; 114 d->willCheckFade = iTrue;
113 /* TODO: This causes an inexplicable refresh issue on macOS: the drawing of one frame 115 /* TODO: This causes an inexplicable refresh issue on macOS: the drawing of one frame
114 takes 100ms for some reason (not the current frame but some time after). */ 116 takes 100ms for some reason (not the current frame but some time after). */
@@ -142,6 +144,11 @@ void setThumb_ScrollWidget(iScrollWidget *d, int thumb, int thumbSize) {
142 } 144 }
143} 145}
144 146
147void setFadeEnabled_ScrollWidget(iScrollWidget *d, iBool fadeEnabled) {
148 d->fadeEnabled = fadeEnabled;
149 unfade_ScrollWidget_(d, 1.0f);
150}
151
145static iBool processEvent_ScrollWidget_(iScrollWidget *d, const SDL_Event *ev) { 152static iBool processEvent_ScrollWidget_(iScrollWidget *d, const SDL_Event *ev) {
146 iWidget *w = as_Widget(d); 153 iWidget *w = as_Widget(d);
147 if (isMetricsChange_UserEvent(ev)) { 154 if (isMetricsChange_UserEvent(ev)) {
@@ -156,7 +163,7 @@ static iBool processEvent_ScrollWidget_(iScrollWidget *d, const SDL_Event *ev) {
156 } 163 }
157 } 164 }
158 if (isCommand_UserEvent(ev, "scrollbar.fade")) { 165 if (isCommand_UserEvent(ev, "scrollbar.fade")) {
159 if (d->willCheckFade && SDL_GetTicks() > d->fadeStart) { 166 if (d->fadeEnabled && d->willCheckFade && SDL_GetTicks() > d->fadeStart) {
160 setValue_Anim(&d->opacity, minOpacity_(), 200); 167 setValue_Anim(&d->opacity, minOpacity_(), 200);
161 remove_Periodic(periodic_App(), d); 168 remove_Periodic(periodic_App(), d);
162 d->willCheckFade = iFalse; 169 d->willCheckFade = iFalse;
diff --git a/src/ui/scrollwidget.h b/src/ui/scrollwidget.h
index e6cda03d..e3f0a51f 100644
--- a/src/ui/scrollwidget.h
+++ b/src/ui/scrollwidget.h
@@ -31,3 +31,4 @@ iDeclareObjectConstruction(ScrollWidget)
31void setRange_ScrollWidget (iScrollWidget *, iRangei range); 31void setRange_ScrollWidget (iScrollWidget *, iRangei range);
32void setThumb_ScrollWidget (iScrollWidget *, int thumb, int thumbSize); 32void setThumb_ScrollWidget (iScrollWidget *, int thumb, int thumbSize);
33 33
34void setFadeEnabled_ScrollWidget (iScrollWidget *, iBool fadeEnabled);
diff --git a/src/ui/touch.c b/src/ui/touch.c
index 87bb7478..0f23e948 100644
--- a/src/ui/touch.c
+++ b/src/ui/touch.c
@@ -59,6 +59,7 @@ struct Impl_Touch {
59 iBool hasMoved; 59 iBool hasMoved;
60 iBool isTapBegun; 60 iBool isTapBegun;
61 iBool isTouchDrag; 61 iBool isTouchDrag;
62 iBool didConvertToTouchDrag;
62 iBool isTapAndHold; 63 iBool isTapAndHold;
63 int pinchId; 64 int pinchId;
64 enum iTouchEdge edge; 65 enum iTouchEdge edge;
@@ -226,7 +227,7 @@ static void update_TouchState_(void *ptr) {
226 /* Check for long presses to simulate right clicks. */ 227 /* Check for long presses to simulate right clicks. */
227 iForEach(Array, i, d->touches) { 228 iForEach(Array, i, d->touches) {
228 iTouch *touch = i.value; 229 iTouch *touch = i.value;
229 if (touch->pinchId) { 230 if (touch->pinchId || touch->isTouchDrag) {
230 continue; 231 continue;
231 } 232 }
232 /* Holding a touch will reset previous momentum for this widget. */ 233 /* Holding a touch will reset previous momentum for this widget. */
@@ -253,6 +254,14 @@ static void update_TouchState_(void *ptr) {
253#endif 254#endif
254 dispatchMotion_Touch_(init_F3(-100, -100, 0), 0); 255 dispatchMotion_Touch_(init_F3(-100, -100, 0), 0);
255 } 256 }
257 if (touch->isTapAndHold && touch->affinity &&
258 flags_Widget(touch->affinity) & touchDrag_WidgetFlag) {
259 /* Convert to touch drag. */
260 touch->isTapAndHold = iFalse;
261 touch->isTouchDrag = iTrue;
262 touch->didConvertToTouchDrag = iTrue;
263 dispatchButtonDown_Touch_(touch->pos[0]);
264 }
256 } 265 }
257 } 266 }
258 /* Update/cancel momentum scrolling. */ { 267 /* Update/cancel momentum scrolling. */ {
@@ -486,16 +495,7 @@ iBool processEvent_Touch(const SDL_Event *ev) {
486 touch->edge = none_TouchEdge; 495 touch->edge = none_TouchEdge;
487 pushPos_Touch_(touch, pos, fing->timestamp); 496 pushPos_Touch_(touch, pos, fing->timestamp);
488 dispatchMotion_Touch_(touch->startPos, 0); 497 dispatchMotion_Touch_(touch->startPos, 0);
489 dispatchEvent_Widget(window->root, (SDL_Event *) &(SDL_MouseButtonEvent){ 498 dispatchButtonDown_Touch_(touch->startPos);
490 .type = SDL_MOUSEBUTTONDOWN,
491 .timestamp = fing->timestamp,
492 .clicks = 1,
493 .state = SDL_PRESSED,
494 .which = SDL_TOUCH_MOUSEID,
495 .button = SDL_BUTTON_LEFT,
496 .x = x_F3(touch->startPos),
497 .y = y_F3(touch->startPos)
498 });
499 dispatchMotion_Touch_(pos, SDL_BUTTON_LMASK); 499 dispatchMotion_Touch_(pos, SDL_BUTTON_LMASK);
500 return iTrue; 500 return iTrue;
501 } 501 }
@@ -603,11 +603,10 @@ iBool processEvent_Touch(const SDL_Event *ev) {
603 continue; 603 continue;
604 } 604 }
605 if (flags_Widget(touch->affinity) & touchDrag_WidgetFlag) { 605 if (flags_Widget(touch->affinity) & touchDrag_WidgetFlag) {
606 if (!touch->isTouchDrag) { 606 if (!touch->isTouchDrag && !touch->didConvertToTouchDrag) {
607 dispatchButtonDown_Touch_(touch->startPos); 607 dispatchButtonDown_Touch_(touch->startPos);
608 } 608 }
609 dispatchButtonUp_Touch_(pos); 609 dispatchButtonUp_Touch_(pos);
610// setHover_Widget(NULL);
611 remove_ArrayIterator(&i); 610 remove_ArrayIterator(&i);
612 continue; 611 continue;
613 } 612 }
@@ -678,7 +677,7 @@ void widgetDestroyed_Touch(iWidget *widget) {
678 } 677 }
679 } 678 }
680 iForEach(Array, p, d->pinches) { 679 iForEach(Array, p, d->pinches) {
681 iPinch *pinch = i.value; 680 iPinch *pinch = p.value;
682 if (pinch->affinity == widget) { 681 if (pinch->affinity == widget) {
683 remove_ArrayIterator(&p); 682 remove_ArrayIterator(&p);
684 } 683 }
diff --git a/src/ui/util.c b/src/ui/util.c
index e6edb119..047fa7af 100644
--- a/src/ui/util.c
+++ b/src/ui/util.c
@@ -220,20 +220,18 @@ static const char *moveForward_(const char *pos, iRangecc bounds, int mode) {
220void extendRange_Rangecc(iRangecc *d, iRangecc bounds, int mode) { 220void extendRange_Rangecc(iRangecc *d, iRangecc bounds, int mode) {
221 if (!d->start) return; 221 if (!d->start) return;
222 if (d->end >= d->start) { 222 if (d->end >= d->start) {
223 if (mode & bothStartAndEnd_RangeExtension) { 223 if (mode & moveStart_RangeExtension) {
224 d->start = moveBackward_(d->start, bounds, mode); 224 d->start = moveBackward_(d->start, bounds, mode);
225 d->end = moveForward_(d->end, bounds, mode);
226 } 225 }
227 else { 226 if (mode & moveEnd_RangeExtension) {
228 d->end = moveForward_(d->end, bounds, mode); 227 d->end = moveForward_(d->end, bounds, mode);
229 } 228 }
230 } 229 }
231 else { 230 else {
232 if (mode & bothStartAndEnd_RangeExtension) { 231 if (mode & moveStart_RangeExtension) {
233 d->start = moveForward_(d->start, bounds, mode); 232 d->start = moveForward_(d->start, bounds, mode);
234 d->end = moveBackward_(d->end, bounds, mode);
235 } 233 }
236 else { 234 if (mode & moveEnd_RangeExtension) {
237 d->end = moveBackward_(d->end, bounds, mode); 235 d->end = moveBackward_(d->end, bounds, mode);
238 } 236 }
239 } 237 }
@@ -573,13 +571,22 @@ iWidget *makeMenu_Widget(iWidget *parent, const iMenuItem *items, size_t n) {
573 addChild_Widget(menu, iClob(makeMenuSeparator_())); 571 addChild_Widget(menu, iClob(makeMenuSeparator_()));
574 } 572 }
575 else { 573 else {
574 iBool isInfo = iFalse;
575 const char *labelText = item->label;
576 if (startsWith_CStr(labelText, "```")) {
577 labelText += 3;
578 isInfo = iTrue;
579 }
576 iLabelWidget *label = addChildFlags_Widget( 580 iLabelWidget *label = addChildFlags_Widget(
577 menu, 581 menu,
578 iClob(newKeyMods_LabelWidget(item->label, item->key, item->kmods, item->command)), 582 iClob(newKeyMods_LabelWidget(labelText, item->key, item->kmods, item->command)),
579 noBackground_WidgetFlag | frameless_WidgetFlag | alignLeft_WidgetFlag | 583 noBackground_WidgetFlag | frameless_WidgetFlag | alignLeft_WidgetFlag |
580 drawKey_WidgetFlag | itemFlags); 584 drawKey_WidgetFlag | (isInfo ? wrapText_WidgetFlag : 0) | itemFlags);
581 haveIcons |= checkIcon_LabelWidget(label); 585 haveIcons |= checkIcon_LabelWidget(label);
582 updateSize_LabelWidget(label); /* drawKey was set */ 586 updateSize_LabelWidget(label); /* drawKey was set */
587 if (isInfo) {
588 setTextColor_LabelWidget(label, uiTextAction_ColorId);
589 }
583 } 590 }
584 } 591 }
585 if (deviceType_App() == phone_AppDeviceType) { 592 if (deviceType_App() == phone_AppDeviceType) {
@@ -639,6 +646,9 @@ void openMenuFlags_Widget(iWidget *d, iInt2 coord, iBool postCommands) {
639 if (isInstance_Object(i.object, &Class_LabelWidget)) { 646 if (isInstance_Object(i.object, &Class_LabelWidget)) {
640 iLabelWidget *label = i.object; 647 iLabelWidget *label = i.object;
641 const iBool isCaution = startsWith_String(text_LabelWidget(label), uiTextCaution_ColorEscape); 648 const iBool isCaution = startsWith_String(text_LabelWidget(label), uiTextCaution_ColorEscape);
649 if (flags_Widget(as_Widget(label)) & wrapText_WidgetFlag) {
650 continue;
651 }
642 if (deviceType_App() == desktop_AppDeviceType) { 652 if (deviceType_App() == desktop_AppDeviceType) {
643 setFont_LabelWidget(label, isCaution ? uiLabelBold_FontId : uiLabel_FontId); 653 setFont_LabelWidget(label, isCaution ? uiLabelBold_FontId : uiLabel_FontId);
644 } 654 }
diff --git a/src/ui/util.h b/src/ui/util.h
index 42dad6e0..d39a00fa 100644
--- a/src/ui/util.h
+++ b/src/ui/util.h
@@ -86,7 +86,9 @@ iLocalDef iBool isOverlapping_Rangei(iRangei a, iRangei b) {
86enum iRangeExtension { 86enum iRangeExtension {
87 word_RangeExtension = iBit(1), 87 word_RangeExtension = iBit(1),
88 line_RangeExtension = iBit(2), 88 line_RangeExtension = iBit(2),
89 bothStartAndEnd_RangeExtension = iBit(3), 89 moveStart_RangeExtension = iBit(3),
90 moveEnd_RangeExtension = iBit(4),
91 bothStartAndEnd_RangeExtension = moveStart_RangeExtension | moveEnd_RangeExtension,
90}; 92};
91 93
92void extendRange_Rangecc (iRangecc *, iRangecc bounds, int mode); 94void extendRange_Rangecc (iRangecc *, iRangecc bounds, int mode);
diff --git a/src/ui/widget.c b/src/ui/widget.c
index 3eea761e..78a8a8bf 100644
--- a/src/ui/widget.c
+++ b/src/ui/widget.c
@@ -1004,8 +1004,7 @@ void drawBackground_Widget(const iWidget *d) {
1004 } 1004 }
1005 /* Popup menus have a shadowed border. */ 1005 /* Popup menus have a shadowed border. */
1006 iBool shadowBorder = (d->flags & keepOnTop_WidgetFlag && ~d->flags & mouseModal_WidgetFlag) != 0; 1006 iBool shadowBorder = (d->flags & keepOnTop_WidgetFlag && ~d->flags & mouseModal_WidgetFlag) != 0;
1007 iBool fadeBackground = (d->bgColor >= 0 || d->frameColor >= 0) && 1007 iBool fadeBackground = (d->bgColor >= 0 || d->frameColor >= 0) && d->flags & mouseModal_WidgetFlag;
1008 d->flags & mouseModal_WidgetFlag;
1009 if (deviceType_App() == phone_AppDeviceType) { 1008 if (deviceType_App() == phone_AppDeviceType) {
1010 if (shadowBorder) { 1009 if (shadowBorder) {
1011 fadeBackground = iTrue; 1010 fadeBackground = iTrue;
@@ -1017,7 +1016,7 @@ void drawBackground_Widget(const iWidget *d) {
1017 init_Paint(&p); 1016 init_Paint(&p);
1018 drawSoftShadow_Paint(&p, bounds_Widget(d), 12 * gap_UI, black_ColorId, 30); 1017 drawSoftShadow_Paint(&p, bounds_Widget(d), 12 * gap_UI, black_ColorId, 30);
1019 } 1018 }
1020 if (fadeBackground) { 1019 if (fadeBackground && ~d->flags & noFadeBackground_WidgetFlag) {
1021 iPaint p; 1020 iPaint p;
1022 init_Paint(&p); 1021 init_Paint(&p);
1023 p.alpha = 0x50; 1022 p.alpha = 0x50;
diff --git a/src/ui/widget.h b/src/ui/widget.h
index 27295149..2116e0d8 100644
--- a/src/ui/widget.h
+++ b/src/ui/widget.h
@@ -113,6 +113,7 @@ enum iWidgetFlag {
113#define moveToParentBottomEdge_WidgetFlag iBit64(57) 113#define moveToParentBottomEdge_WidgetFlag iBit64(57)
114#define parentCannotResizeHeight_WidgetFlag iBit64(58) 114#define parentCannotResizeHeight_WidgetFlag iBit64(58)
115#define ignoreForParentWidth_WidgetFlag iBit64(59) 115#define ignoreForParentWidth_WidgetFlag iBit64(59)
116#define noFadeBackground_WidgetFlag iBit64(60)
116 117
117enum iWidgetAddPos { 118enum iWidgetAddPos {
118 back_WidgetAddPos, 119 back_WidgetAddPos,
diff --git a/src/ui/window.c b/src/ui/window.c
index d00d19a7..0a5bab6f 100644
--- a/src/ui/window.c
+++ b/src/ui/window.c
@@ -362,7 +362,8 @@ static const iMenuItem identityButtonMenuItems_[] = {
362}; 362};
363#endif 363#endif
364 364
365static const char *reloadCStr_ = reload_Icon; 365static const char *reloadCStr_ = reload_Icon;
366static const char *pageMenuCStr_ = midEllipsis_Icon;
366 367
367/* TODO: A preference for these, maybe? */ 368/* TODO: A preference for these, maybe? */
368static const char *stopSeqCStr_[] = { 369static const char *stopSeqCStr_[] = {
@@ -476,8 +477,14 @@ static uint32_t updateReloadAnimation_Window_(uint32_t interval, void *window) {
476} 477}
477 478
478static void setReloadLabel_Window_(iWindow *d, iBool animating) { 479static void setReloadLabel_Window_(iWindow *d, iBool animating) {
480 const iBool isMobile = deviceType_App() != desktop_AppDeviceType;
479 iLabelWidget *label = findChild_Widget(d->root, "reload"); 481 iLabelWidget *label = findChild_Widget(d->root, "reload");
480 updateTextCStr_LabelWidget(label, animating ? loadAnimationCStr_() : reloadCStr_); 482 updateTextCStr_LabelWidget(label, animating ? loadAnimationCStr_() :
483 (isMobile ? pageMenuCStr_ : reloadCStr_));
484 if (isMobile) {
485 setCommand_LabelWidget(label,
486 collectNewCStr_String(animating ? "navigate.reload" : "menu.open"));
487 }
481} 488}
482 489
483static void checkLoadAnimation_Window_(iWindow *d) { 490static void checkLoadAnimation_Window_(iWindow *d) {
@@ -1047,11 +1054,35 @@ static void setupUserInterface_Window(iWindow *d) {
1047 addChildFlags_Widget( 1054 addChildFlags_Widget(
1048 rightEmbed, iClob(progress), collapse_WidgetFlag); 1055 rightEmbed, iClob(progress), collapse_WidgetFlag);
1049 } 1056 }
1050 /* Reload button. */ 1057 /* Reload button. */ {
1051 iLabelWidget *reload = newIcon_LabelWidget(reloadCStr_, 0, 0, "navigate.reload"); 1058 iLabelWidget *reload;
1052 setId_Widget(as_Widget(reload), "reload"); 1059 if (deviceType_App() == desktop_AppDeviceType) {
1053 addChildFlags_Widget(as_Widget(url), iClob(reload), embedFlags | moveToParentRightEdge_WidgetFlag); 1060 reload = newIcon_LabelWidget(reloadCStr_, 0, 0, "navigate.reload");
1054 updateSize_LabelWidget(reload); 1061 }
1062 else {
1063 /* In a mobile layout, the reload button is replaced with the Page/Ellipsis menu. */
1064 reload = makeMenuButton_LabelWidget(pageMenuCStr_,
1065 (iMenuItem[]){
1066 { reload_Icon " ${menu.reload}", reload_KeyShortcut, "navigate.reload" },
1067 { timer_Icon " ${menu.autoreload}", 0, 0, "document.autoreload.menu" },
1068 { "---", 0, 0, NULL },
1069 { upArrow_Icon " ${menu.parent}", navigateParent_KeyShortcut, "navigate.parent" },
1070 { upArrowBar_Icon " ${menu.root}", navigateRoot_KeyShortcut, "navigate.root" },
1071 { "---", 0, 0, NULL },
1072 { pin_Icon " ${menu.page.bookmark}", SDLK_d, KMOD_PRIMARY, "bookmark.add" },
1073 { star_Icon " ${menu.page.subscribe}", subscribeToPage_KeyModifier, "feeds.subscribe" },
1074 { book_Icon " ${menu.page.import}", 0, 0, "bookmark.links confirm:1" },
1075 { globe_Icon " ${menu.page.translate}", 0, 0, "document.translate" },
1076 { "---", 0, 0, NULL },
1077 { "${menu.page.copyurl}", 0, 0, "document.copylink" },
1078 { "${menu.page.copysource}", 'c', KMOD_PRIMARY, "copy" },
1079 { download_Icon " " saveToDownloads_Label, SDLK_s, KMOD_PRIMARY, "document.save" } },
1080 14);
1081 }
1082 setId_Widget(as_Widget(reload), "reload");
1083 addChildFlags_Widget(as_Widget(url), iClob(reload), embedFlags | moveToParentRightEdge_WidgetFlag);
1084 updateSize_LabelWidget(reload);
1085 }
1055 setId_Widget(addChild_Widget(rightEmbed, iClob(makePadding_Widget(0))), "url.embedpad"); 1086 setId_Widget(addChild_Widget(rightEmbed, iClob(makePadding_Widget(0))), "url.embedpad");
1056 } 1087 }
1057 if (deviceType_App() != desktop_AppDeviceType) { 1088 if (deviceType_App() != desktop_AppDeviceType) {