summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJaakko Keränen <jaakko.keranen@iki.fi>2021-03-27 14:18:06 +0200
committerJaakko Keränen <jaakko.keranen@iki.fi>2021-03-27 14:18:41 +0200
commitdb7f835c320632ec4dea3b8baf5e21b62e2b75e1 (patch)
treebc0d497c8152b55b04c62fccb98d394ff8cd57fa
parent2a4f5d0f67fcd1412968ef967ed3009469a46b90 (diff)
DocumentWidget: Advanced text selection
Double click to select by word, triple click by paragraph. IssueID #134
-rw-r--r--src/gmdocument.c4
-rw-r--r--src/ui/documentwidget.c83
-rw-r--r--src/ui/text.c3
-rw-r--r--src/ui/util.c54
-rw-r--r--src/ui/util.h8
5 files changed, 126 insertions, 26 deletions
diff --git a/src/gmdocument.c b/src/gmdocument.c
index 30f5169a..f1471f0f 100644
--- a/src/gmdocument.c
+++ b/src/gmdocument.c
@@ -239,7 +239,7 @@ static iRangecc addLink_GmDocument_(iGmDocument *d, iRangecc line, iGmLinkId *li
239 if (link->flags & gemini_GmLinkFlag && ~link->flags & remote_GmLinkFlag) { 239 if (link->flags & gemini_GmLinkFlag && ~link->flags & remote_GmLinkFlag) {
240 iChar icon = 0; 240 iChar icon = 0;
241 int len = 0; 241 int len = 0;
242 if ((len = decodeBytes_MultibyteChar(desc.start, size_Range(&desc), &icon)) > 0) { 242 if ((len = decodeBytes_MultibyteChar(desc.start, desc.end, &icon)) > 0) {
243 if (desc.start + len < desc.end && 243 if (desc.start + len < desc.end &&
244 (isPictograph_Char(icon) || isEmoji_Char(icon) || icon == 0x2022 /* bullet */) && 244 (isPictograph_Char(icon) || isEmoji_Char(icon) || icon == 0x2022 /* bullet */) &&
245 !isFitzpatrickType_Char(icon)) { 245 !isFitzpatrickType_Char(icon)) {
@@ -1675,7 +1675,7 @@ iRangecc findLoc_GmRun(const iGmRun *d, iInt2 pos) {
1675 loc.end = loc.start; 1675 loc.end = loc.start;
1676 iChar ch; 1676 iChar ch;
1677 if (d->text.end != loc.start) { 1677 if (d->text.end != loc.start) {
1678 int chLen = decodeBytes_MultibyteChar(loc.start, d->text.end - loc.start, &ch); 1678 int chLen = decodeBytes_MultibyteChar(loc.start, d->text.end, &ch);
1679 if (chLen > 0) { 1679 if (chLen > 0) {
1680 /* End after the character. */ 1680 /* End after the character. */
1681 loc.end += chLen; 1681 loc.end += chLen;
diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c
index b3508b8d..6bb16a93 100644
--- a/src/ui/documentwidget.c
+++ b/src/ui/documentwidget.c
@@ -198,6 +198,7 @@ struct Impl_DocumentWidget {
198 iString * certSubject; 198 iString * certSubject;
199 int redirectCount; 199 int redirectCount;
200 iRangecc selectMark; 200 iRangecc selectMark;
201 iRangecc initialSelectMark; /* for word/line selection */
201 iRangecc foundMark; 202 iRangecc foundMark;
202 int pageMargin; 203 int pageMargin;
203 iPtrArray visibleLinks; 204 iPtrArray visibleLinks;
@@ -2325,6 +2326,15 @@ static iChar linkOrdinalChar_DocumentWidget_(const iDocumentWidget *d, size_t or
2325 return 0; 2326 return 0;
2326} 2327}
2327 2328
2329static void beginMarkingSelection_DocumentWidget_(iDocumentWidget *d, iInt2 pos) {
2330 setFocus_Widget(NULL); /* TODO: Focus this document? */
2331 invalidateWideRunsWithNonzeroOffset_DocumentWidget_(d);
2332 resetWideRuns_DocumentWidget_(d); /* Selections don't support horizontal scrolling. */
2333 iChangeFlags(d->flags, selecting_DocumentWidgetFlag, iTrue);
2334 d->selectMark = sourceLoc_DocumentWidget_(d, pos);
2335 refresh_Widget(as_Widget(d));
2336}
2337
2328static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *ev) { 2338static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *ev) {
2329 iWidget *w = as_Widget(d); 2339 iWidget *w = as_Widget(d);
2330 if (isMetricsChange_UserEvent(ev)) { 2340 if (isMetricsChange_UserEvent(ev)) {
@@ -2593,19 +2603,28 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
2593 return iTrue; 2603 return iTrue;
2594 } 2604 }
2595 /* The left mouse button. */ 2605 /* The left mouse button. */
2596 if (/*d->flags & selecting_DocumentWidgetFlag &&*/ ev->type == SDL_MOUSEBUTTONDOWN &&
2597 ev->button.button == SDL_BUTTON_LEFT) {
2598 if (ev->button.clicks == 2) {
2599 printf("double click\n");
2600 }
2601 else if (ev->button.clicks == 3) {
2602 printf("triple click\n");
2603 }
2604 fflush(stdout);
2605 }
2606 switch (processEvent_Click(&d->click, ev)) { 2606 switch (processEvent_Click(&d->click, ev)) {
2607 case started_ClickResult: 2607 case started_ClickResult:
2608 if (d->grabbedPlayer) {
2609 return iTrue;
2610 }
2608 iChangeFlags(d->flags, selecting_DocumentWidgetFlag, iFalse); 2611 iChangeFlags(d->flags, selecting_DocumentWidgetFlag, iFalse);
2612 iChangeFlags(d->flags, selectWords_DocumentWidgetFlag, d->click.count == 2);
2613 iChangeFlags(d->flags, selectLines_DocumentWidgetFlag, d->click.count >= 3);
2614 /* Double/triple clicks marks the selection immediately. */
2615 if (d->click.count >= 2) {
2616 beginMarkingSelection_DocumentWidget_(d, d->click.startPos);
2617 extendRange_Rangecc(
2618 &d->selectMark,
2619 range_String(source_GmDocument(d->doc)),
2620 bothStartAndEnd_RangeExtension |
2621 (d->click.count == 2 ? word_RangeExtension : line_RangeExtension));
2622 d->initialSelectMark = d->selectMark;
2623 refresh_Widget(w);
2624 }
2625 else {
2626 d->initialSelectMark = iNullRange;
2627 }
2609 return iTrue; 2628 return iTrue;
2610 case drag_ClickResult: { 2629 case drag_ClickResult: {
2611 if (d->grabbedPlayer) { 2630 if (d->grabbedPlayer) {
@@ -2620,12 +2639,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
2620 } 2639 }
2621 /* Begin selecting a range of text. */ 2640 /* Begin selecting a range of text. */
2622 if (~d->flags & selecting_DocumentWidgetFlag) { 2641 if (~d->flags & selecting_DocumentWidgetFlag) {
2623 setFocus_Widget(NULL); /* TODO: Focus this document? */ 2642 beginMarkingSelection_DocumentWidget_(d, d->click.startPos);
2624 invalidateWideRunsWithNonzeroOffset_DocumentWidget_(d);
2625 resetWideRuns_DocumentWidget_(d); /* Selections don't support horizontal scrolling. */
2626 iChangeFlags(d->flags, selecting_DocumentWidgetFlag, iTrue);
2627 d->selectMark = sourceLoc_DocumentWidget_(d, d->click.startPos);
2628 refresh_Widget(w);
2629 } 2643 }
2630 iRangecc loc = sourceLoc_DocumentWidget_(d, pos_Click(&d->click)); 2644 iRangecc loc = sourceLoc_DocumentWidget_(d, pos_Click(&d->click));
2631 if (!d->selectMark.start) { 2645 if (!d->selectMark.start) {
@@ -2634,6 +2648,23 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
2634 else if (loc.end) { 2648 else if (loc.end) {
2635 d->selectMark.end = (d->selectMark.end > d->selectMark.start ? loc.end : loc.start); 2649 d->selectMark.end = (d->selectMark.end > d->selectMark.start ? loc.end : loc.start);
2636 } 2650 }
2651 iAssert((!d->selectMark.start && !d->selectMark.end) ||
2652 ( d->selectMark.start && d->selectMark.end));
2653 /* Extend the selection when double/triple clicking. */
2654 if (d->flags & (selectWords_DocumentWidgetFlag | selectLines_DocumentWidgetFlag)) {
2655 extendRange_Rangecc(
2656 &d->selectMark,
2657 range_String(source_GmDocument(d->doc)),
2658 d->click.count == 2 ? word_RangeExtension : line_RangeExtension);
2659 if (!isEmpty_Range(&d->initialSelectMark)) {
2660 if (d->selectMark.end > d->selectMark.start) {
2661 d->selectMark.start = d->initialSelectMark.start;
2662 }
2663 else if (d->selectMark.end < d->selectMark.start) {
2664 d->selectMark.start = d->initialSelectMark.end;
2665 }
2666 }
2667 }
2637// printf("mark %zu ... %zu\n", d->selectMark.start - cstr_String(source_GmDocument(d->doc)), 2668// printf("mark %zu ... %zu\n", d->selectMark.start - cstr_String(source_GmDocument(d->doc)),
2638// d->selectMark.end - cstr_String(source_GmDocument(d->doc))); 2669// d->selectMark.end - cstr_String(source_GmDocument(d->doc)));
2639// fflush(stdout); 2670// fflush(stdout);
@@ -2727,7 +2758,8 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
2727 2); 2758 2);
2728 } 2759 }
2729 } 2760 }
2730 if (d->selectMark.start) { 2761 if (d->selectMark.start && !(d->flags & (selectLines_DocumentWidgetFlag |
2762 selectWords_DocumentWidgetFlag))) {
2731 d->selectMark = iNullRange; 2763 d->selectMark = iNullRange;
2732 refresh_Widget(w); 2764 refresh_Widget(w);
2733 } 2765 }
@@ -2776,16 +2808,21 @@ static void fillRange_DrawContext_(iDrawContext *d, const iGmRun *run, enum iCol
2776 /* Selection may be done in either direction. */ 2808 /* Selection may be done in either direction. */
2777 iSwap(const char *, mark.start, mark.end); 2809 iSwap(const char *, mark.start, mark.end);
2778 } 2810 }
2779 if ((!*isInside && (contains_Range(&run->text, mark.start) || mark.start == run->text.end)) || 2811 if (*isInside || (contains_Range(&run->text, mark.start) ||
2780 *isInside) { 2812 contains_Range(&mark, run->text.start))) {
2781 int x = 0; 2813 int x = 0;
2782 if (!*isInside) { 2814 if (!*isInside) {
2783 x = advanceRange_Text(run->font, (iRangecc){ run->text.start, mark.start }).x; 2815 x = advanceRange_Text(run->font,
2816 (iRangecc){ run->text.start, iMax(run->text.start, mark.start) })
2817 .x;
2784 } 2818 }
2785 int w = width_Rect(run->visBounds) - x; 2819 int w = width_Rect(run->visBounds) - x;
2786 if (contains_Range(&run->text, mark.end) || run->text.end == mark.end) { 2820 if (contains_Range(&run->text, mark.end) || mark.end < run->text.start) {
2787 w = advanceRange_Text(run->font, 2821 w = advanceRange_Text(
2788 !*isInside ? mark : (iRangecc){ run->text.start, mark.end }).x; 2822 run->font,
2823 !*isInside ? mark
2824 : (iRangecc){ run->text.start, iMax(run->text.start, mark.end) })
2825 .x;
2789 *isInside = iFalse; 2826 *isInside = iFalse;
2790 } 2827 }
2791 else { 2828 else {
diff --git a/src/ui/text.c b/src/ui/text.c
index 80d3634c..6aaf40f5 100644
--- a/src/ui/text.c
+++ b/src/ui/text.c
@@ -649,7 +649,7 @@ static iChar nextChar_(const char **chPos, const char *end) {
649 return 0; 649 return 0;
650 } 650 }
651 iChar ch; 651 iChar ch;
652 int len = decodeBytes_MultibyteChar(*chPos, end - *chPos, &ch); 652 int len = decodeBytes_MultibyteChar(*chPos, end, &ch);
653 if (len <= 0) { 653 if (len <= 0) {
654 (*chPos)++; /* skip it */ 654 (*chPos)++; /* skip it */
655 return 0; 655 return 0;
@@ -862,6 +862,7 @@ static iRect run_Font_(iFont *d, const iRunArgs *args) {
862 const enum iRunMode mode = args->mode; 862 const enum iRunMode mode = args->mode;
863 const char * lastWordEnd = args->text.start; 863 const char * lastWordEnd = args->text.start;
864 iAssert(args->xposLimit == 0 || isMeasuring_(mode)); 864 iAssert(args->xposLimit == 0 || isMeasuring_(mode));
865 iAssert(args->text.end >= args->text.start);
865 if (args->continueFrom_out) { 866 if (args->continueFrom_out) {
866 *args->continueFrom_out = args->text.end; 867 *args->continueFrom_out = args->text.end;
867 } 868 }
diff --git a/src/ui/util.c b/src/ui/util.c
index cb9006f6..8074223b 100644
--- a/src/ui/util.c
+++ b/src/ui/util.c
@@ -164,6 +164,60 @@ iRangei union_Rangei(iRangei a, iRangei b) {
164 return (iRangei){ iMin(a.start, b.start), iMax(a.end, b.end) }; 164 return (iRangei){ iMin(a.start, b.start), iMax(a.end, b.end) };
165} 165}
166 166
167static iBool isSelectionBreakingChar_(iChar c) {
168 return isSpace_Char(c) || (c == '@' || c == '-' || c == '/' || c == '\\' || c == ',');
169}
170
171static const char *moveBackward_(const char *pos, iRangecc bounds, int mode) {
172 iChar ch;
173 while (pos > bounds.start) {
174 int len = decodePrecedingBytes_MultibyteChar(pos, bounds.start, &ch);
175 if (len > 0) {
176 if (mode & word_RangeExtension && isSelectionBreakingChar_(ch)) break;
177 if (mode & line_RangeExtension && ch == '\n') break;
178 pos -= len;
179 }
180 else break;
181 }
182 return pos;
183}
184
185static const char *moveForward_(const char *pos, iRangecc bounds, int mode) {
186 iChar ch;
187 while (pos < bounds.end) {
188 int len = decodeBytes_MultibyteChar(pos, bounds.end, &ch);
189 if (len > 0) {
190 if (mode & word_RangeExtension && isSelectionBreakingChar_(ch)) break;
191 if (mode & line_RangeExtension && ch == '\n') break;
192 pos += len;
193 }
194 else break;
195 }
196 return pos;
197}
198
199void extendRange_Rangecc(iRangecc *d, iRangecc bounds, int mode) {
200 if (!d->start) return;
201 if (d->end >= d->start) {
202 if (mode & bothStartAndEnd_RangeExtension) {
203 d->start = moveBackward_(d->start, bounds, mode);
204 d->end = moveForward_(d->end, bounds, mode);
205 }
206 else {
207 d->end = moveForward_(d->end, bounds, mode);
208 }
209 }
210 else {
211 if (mode & bothStartAndEnd_RangeExtension) {
212 d->start = moveForward_(d->start, bounds, mode);
213 d->end = moveBackward_(d->end, bounds, mode);
214 }
215 else {
216 d->end = moveBackward_(d->end, bounds, mode);
217 }
218 }
219}
220
167/*----------------------------------------------------------------------------------------------*/ 221/*----------------------------------------------------------------------------------------------*/
168 222
169iBool isFinished_Anim(const iAnim *d) { 223iBool isFinished_Anim(const iAnim *d) {
diff --git a/src/ui/util.h b/src/ui/util.h
index da4d3a99..9e00e495 100644
--- a/src/ui/util.h
+++ b/src/ui/util.h
@@ -82,6 +82,14 @@ iLocalDef iBool isOverlapping_Rangei(iRangei a, iRangei b) {
82 return !isEmpty_Rangei(intersect_Rangei(a, b)); 82 return !isEmpty_Rangei(intersect_Rangei(a, b));
83} 83}
84 84
85enum iRangeExtension {
86 word_RangeExtension = iBit(1),
87 line_RangeExtension = iBit(2),
88 bothStartAndEnd_RangeExtension = iBit(3),
89};
90
91void extendRange_Rangecc (iRangecc *, iRangecc bounds, int mode);
92
85/*-----------------------------------------------------------------------------------------------*/ 93/*-----------------------------------------------------------------------------------------------*/
86 94
87iDeclareType(Anim) 95iDeclareType(Anim)