summaryrefslogtreecommitdiff
path: root/src/ui
diff options
context:
space:
mode:
Diffstat (limited to 'src/ui')
-rw-r--r--src/ui/color.c53
-rw-r--r--src/ui/color.h63
-rw-r--r--src/ui/command.c22
-rw-r--r--src/ui/command.h22
-rw-r--r--src/ui/documentwidget.c470
-rw-r--r--src/ui/documentwidget.h23
-rw-r--r--src/ui/inputwidget.c452
-rw-r--r--src/ui/inputwidget.h30
-rw-r--r--src/ui/labelwidget.c39
-rw-r--r--src/ui/labelwidget.h25
-rw-r--r--src/ui/macos.h9
-rw-r--r--src/ui/macos.m438
-rw-r--r--src/ui/metrics.c22
-rw-r--r--src/ui/metrics.h22
-rw-r--r--src/ui/paint.c22
-rw-r--r--src/ui/paint.h22
-rw-r--r--src/ui/scrollwidget.c23
-rw-r--r--src/ui/scrollwidget.h22
-rw-r--r--src/ui/sidebarwidget.c556
-rw-r--r--src/ui/sidebarwidget.h22
-rw-r--r--src/ui/text.c150
-rw-r--r--src/ui/text.h28
-rw-r--r--src/ui/util.c194
-rw-r--r--src/ui/util.h32
-rw-r--r--src/ui/widget.c81
-rw-r--r--src/ui/widget.h28
-rw-r--r--src/ui/window.c133
-rw-r--r--src/ui/window.h25
28 files changed, 2204 insertions, 824 deletions
diff --git a/src/ui/color.c b/src/ui/color.c
index 7d6ab2c2..723e9805 100644
--- a/src/ui/color.c
+++ b/src/ui/color.c
@@ -1,3 +1,25 @@
1/* Copyright 2020 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
1#include "color.h" 23#include "color.h"
2 24
3#include <the_Foundation/string.h> 25#include <the_Foundation/string.h>
@@ -62,6 +84,7 @@ void setThemePalette_Color(enum iColorTheme theme) {
62 copy_(uiTextStrong_ColorId, white_ColorId); 84 copy_(uiTextStrong_ColorId, white_ColorId);
63 copy_(uiTextSelected_ColorId, white_ColorId); 85 copy_(uiTextSelected_ColorId, white_ColorId);
64 copy_(uiTextFramelessHover_ColorId, white_ColorId); 86 copy_(uiTextFramelessHover_ColorId, white_ColorId);
87 copy_(uiTextDisabled_ColorId, gray25_ColorId);
65 copy_(uiTextShortcut_ColorId, cyan_ColorId); 88 copy_(uiTextShortcut_ColorId, cyan_ColorId);
66 copy_(uiTextAction_ColorId, cyan_ColorId); 89 copy_(uiTextAction_ColorId, cyan_ColorId);
67 copy_(uiTextCaution_ColorId, orange_ColorId); 90 copy_(uiTextCaution_ColorId, orange_ColorId);
@@ -86,7 +109,8 @@ void setThemePalette_Color(enum iColorTheme theme) {
86 copy_(uiInputCursor_ColorId, orange_ColorId); 109 copy_(uiInputCursor_ColorId, orange_ColorId);
87 copy_(uiInputCursorText_ColorId, black_ColorId); 110 copy_(uiInputCursorText_ColorId, black_ColorId);
88 copy_(uiHeading_ColorId, cyan_ColorId); 111 copy_(uiHeading_ColorId, cyan_ColorId);
89 copy_(uiIcon_ColorId, teal_ColorId); 112 copy_(uiAnnotation_ColorId, teal_ColorId);
113 copy_(uiIcon_ColorId, cyan_ColorId);
90 copy_(uiIconHover_ColorId, cyan_ColorId); 114 copy_(uiIconHover_ColorId, cyan_ColorId);
91 copy_(uiSeparator_ColorId, gray25_ColorId); 115 copy_(uiSeparator_ColorId, gray25_ColorId);
92 copy_(uiMarked_ColorId, brown_ColorId); 116 copy_(uiMarked_ColorId, brown_ColorId);
@@ -103,6 +127,7 @@ void setThemePalette_Color(enum iColorTheme theme) {
103 copy_(uiTextPressed_ColorId, black_ColorId); 127 copy_(uiTextPressed_ColorId, black_ColorId);
104 copy_(uiTextStrong_ColorId, white_ColorId); 128 copy_(uiTextStrong_ColorId, white_ColorId);
105 copy_(uiTextSelected_ColorId, white_ColorId); 129 copy_(uiTextSelected_ColorId, white_ColorId);
130 copy_(uiTextDisabled_ColorId, gray50_ColorId);
106 copy_(uiTextFramelessHover_ColorId, white_ColorId); 131 copy_(uiTextFramelessHover_ColorId, white_ColorId);
107 copy_(uiTextShortcut_ColorId, cyan_ColorId); 132 copy_(uiTextShortcut_ColorId, cyan_ColorId);
108 copy_(uiTextAction_ColorId, cyan_ColorId); 133 copy_(uiTextAction_ColorId, cyan_ColorId);
@@ -128,6 +153,7 @@ void setThemePalette_Color(enum iColorTheme theme) {
128 copy_(uiInputCursor_ColorId, orange_ColorId); 153 copy_(uiInputCursor_ColorId, orange_ColorId);
129 copy_(uiInputCursorText_ColorId, black_ColorId); 154 copy_(uiInputCursorText_ColorId, black_ColorId);
130 copy_(uiHeading_ColorId, cyan_ColorId); 155 copy_(uiHeading_ColorId, cyan_ColorId);
156 copy_(uiAnnotation_ColorId, teal_ColorId);
131 copy_(uiIcon_ColorId, cyan_ColorId); 157 copy_(uiIcon_ColorId, cyan_ColorId);
132 copy_(uiIconHover_ColorId, cyan_ColorId); 158 copy_(uiIconHover_ColorId, cyan_ColorId);
133 copy_(uiSeparator_ColorId, black_ColorId); 159 copy_(uiSeparator_ColorId, black_ColorId);
@@ -141,9 +167,10 @@ void setThemePalette_Color(enum iColorTheme theme) {
141 copy_(uiBackgroundPressed_ColorId, cyan_ColorId); 167 copy_(uiBackgroundPressed_ColorId, cyan_ColorId);
142 copy_(uiBackgroundFramelessHover_ColorId, orange_ColorId); 168 copy_(uiBackgroundFramelessHover_ColorId, orange_ColorId);
143 copy_(uiText_ColorId, black_ColorId); 169 copy_(uiText_ColorId, black_ColorId);
144 copy_(uiTextStrong_ColorId, brown_ColorId); 170 copy_(uiTextStrong_ColorId, teal_ColorId);
145 copy_(uiTextPressed_ColorId, black_ColorId); 171 copy_(uiTextPressed_ColorId, black_ColorId);
146 copy_(uiTextSelected_ColorId, black_ColorId); 172 copy_(uiTextSelected_ColorId, black_ColorId);
173 copy_(uiTextDisabled_ColorId, gray50_ColorId);
147 copy_(uiTextFramelessHover_ColorId, black_ColorId); 174 copy_(uiTextFramelessHover_ColorId, black_ColorId);
148 copy_(uiTextShortcut_ColorId, brown_ColorId); 175 copy_(uiTextShortcut_ColorId, brown_ColorId);
149 copy_(uiTextAction_ColorId, brown_ColorId); 176 copy_(uiTextAction_ColorId, brown_ColorId);
@@ -163,15 +190,16 @@ void setThemePalette_Color(enum iColorTheme theme) {
163 copy_(uiInputBackgroundFocused_ColorId, white_ColorId); 190 copy_(uiInputBackgroundFocused_ColorId, white_ColorId);
164 copy_(uiInputText_ColorId, gray25_ColorId); 191 copy_(uiInputText_ColorId, gray25_ColorId);
165 copy_(uiInputTextFocused_ColorId, black_ColorId); 192 copy_(uiInputTextFocused_ColorId, black_ColorId);
166 copy_(uiInputFrame_ColorId, gray25_ColorId); 193 copy_(uiInputFrame_ColorId, gray50_ColorId);
167 copy_(uiInputFrameHover_ColorId, brown_ColorId); 194 copy_(uiInputFrameHover_ColorId, brown_ColorId);
168 copy_(uiInputFrameFocused_ColorId, teal_ColorId); 195 copy_(uiInputFrameFocused_ColorId, teal_ColorId);
169 copy_(uiInputCursor_ColorId, teal_ColorId); 196 copy_(uiInputCursor_ColorId, teal_ColorId);
170 copy_(uiInputCursorText_ColorId, white_ColorId); 197 copy_(uiInputCursorText_ColorId, white_ColorId);
171 copy_(uiHeading_ColorId, brown_ColorId); 198 copy_(uiHeading_ColorId, brown_ColorId);
199 copy_(uiAnnotation_ColorId, gray50_ColorId);
172 copy_(uiIcon_ColorId, brown_ColorId); 200 copy_(uiIcon_ColorId, brown_ColorId);
173 copy_(uiIconHover_ColorId, brown_ColorId); 201 copy_(uiIconHover_ColorId, brown_ColorId);
174 copy_(uiSeparator_ColorId, gray25_ColorId); 202 copy_(uiSeparator_ColorId, gray50_ColorId);
175 copy_(uiMarked_ColorId, cyan_ColorId); 203 copy_(uiMarked_ColorId, cyan_ColorId);
176 copy_(uiMatching_ColorId, orange_ColorId); 204 copy_(uiMatching_ColorId, orange_ColorId);
177 break; 205 break;
@@ -183,6 +211,7 @@ void setThemePalette_Color(enum iColorTheme theme) {
183 copy_(uiBackgroundFramelessHover_ColorId, orange_ColorId); 211 copy_(uiBackgroundFramelessHover_ColorId, orange_ColorId);
184 copy_(uiText_ColorId, gray25_ColorId); 212 copy_(uiText_ColorId, gray25_ColorId);
185 copy_(uiTextPressed_ColorId, black_ColorId); 213 copy_(uiTextPressed_ColorId, black_ColorId);
214 copy_(uiTextDisabled_ColorId, gray75_ColorId);
186 copy_(uiTextStrong_ColorId, black_ColorId); 215 copy_(uiTextStrong_ColorId, black_ColorId);
187 copy_(uiTextSelected_ColorId, black_ColorId); 216 copy_(uiTextSelected_ColorId, black_ColorId);
188 copy_(uiTextFramelessHover_ColorId, black_ColorId); 217 copy_(uiTextFramelessHover_ColorId, black_ColorId);
@@ -210,13 +239,16 @@ void setThemePalette_Color(enum iColorTheme theme) {
210 copy_(uiInputCursor_ColorId, teal_ColorId); 239 copy_(uiInputCursor_ColorId, teal_ColorId);
211 copy_(uiInputCursorText_ColorId, white_ColorId); 240 copy_(uiInputCursorText_ColorId, white_ColorId);
212 copy_(uiHeading_ColorId, brown_ColorId); 241 copy_(uiHeading_ColorId, brown_ColorId);
242 copy_(uiAnnotation_ColorId, gray50_ColorId);
213 copy_(uiIcon_ColorId, brown_ColorId); 243 copy_(uiIcon_ColorId, brown_ColorId);
214 copy_(uiIconHover_ColorId, brown_ColorId); 244 copy_(uiIconHover_ColorId, brown_ColorId);
215 copy_(uiSeparator_ColorId, gray50_ColorId); 245 copy_(uiSeparator_ColorId, gray75_ColorId);
216 copy_(uiMarked_ColorId, cyan_ColorId); 246 copy_(uiMarked_ColorId, cyan_ColorId);
217 copy_(uiMatching_ColorId, orange_ColorId); 247 copy_(uiMatching_ColorId, orange_ColorId);
218 break; 248 break;
219 } 249 }
250 palette_[uiMarked_ColorId].a = 128;
251 palette_[uiMatching_ColorId].a = 128;
220} 252}
221 253
222iColor get_Color(int color) { 254iColor get_Color(int color) {
@@ -233,6 +265,14 @@ void set_Color(int color, iColor rgba) {
233 } 265 }
234} 266}
235 267
268iColor mix_Color(iColor c1, iColor c2, float t) {
269 t = iClamp(t, 0.0f, 1.0f);
270 return (iColor){ c1.r * (1 - t) + c2.r * t,
271 c1.g * (1 - t) + c2.g * t,
272 c1.b * (1 - t) + c2.b * t,
273 c1.a * (1 - t) + c2.a * t };
274}
275
236iLocalDef iBool equal_Color_(const iColor *x, const iColor *y) { 276iLocalDef iBool equal_Color_(const iColor *x, const iColor *y) {
237 return memcmp(x, y, sizeof(iColor)) == 0; 277 return memcmp(x, y, sizeof(iColor)) == 0;
238} 278}
@@ -361,7 +401,8 @@ const char *escape_Color(int color) {
361 if (color >= 0 && color < (int) iElemCount(esc)) { 401 if (color >= 0 && color < (int) iElemCount(esc)) {
362 return esc[color]; 402 return esc[color];
363 } 403 }
364 return format_CStr("\r%c", color + '0'); 404 iAssert(asciiBase_ColorEscape + color <= 127);
405 return format_CStr("\r%c", asciiBase_ColorEscape + color);
365} 406}
366 407
367iHSLColor setSat_HSLColor(iHSLColor d, float sat) { 408iHSLColor setSat_HSLColor(iHSLColor d, float sat) {
diff --git a/src/ui/color.h b/src/ui/color.h
index 596fec7a..76e5b2a7 100644
--- a/src/ui/color.h
+++ b/src/ui/color.h
@@ -1,3 +1,25 @@
1/* Copyright 2020 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
1#pragma once 23#pragma once
2 24
3#include <the_Foundation/range.h> 25#include <the_Foundation/range.h>
@@ -46,6 +68,7 @@ enum iColorId {
46 uiText_ColorId, 68 uiText_ColorId,
47 uiTextPressed_ColorId, 69 uiTextPressed_ColorId,
48 uiTextSelected_ColorId, 70 uiTextSelected_ColorId,
71 uiTextDisabled_ColorId,
49 uiTextFramelessHover_ColorId, 72 uiTextFramelessHover_ColorId,
50 uiTextFramelessSelected_ColorId, 73 uiTextFramelessSelected_ColorId,
51 uiTextStrong_ColorId, 74 uiTextStrong_ColorId,
@@ -73,6 +96,7 @@ enum iColorId {
73 uiInputCursor_ColorId, 96 uiInputCursor_ColorId,
74 uiInputCursorText_ColorId, 97 uiInputCursorText_ColorId,
75 uiHeading_ColorId, 98 uiHeading_ColorId,
99 uiAnnotation_ColorId,
76 uiIcon_ColorId, 100 uiIcon_ColorId,
77 uiIconHover_ColorId, 101 uiIconHover_ColorId,
78 uiSeparator_ColorId, 102 uiSeparator_ColorId,
@@ -139,24 +163,26 @@ iLocalDef iBool isRegularText_ColorId(enum iColorId d) {
139#define mask_ColorId 0x7f 163#define mask_ColorId 0x7f
140#define permanent_ColorId 0x80 /* cannot be changed via escapes */ 164#define permanent_ColorId 0x80 /* cannot be changed via escapes */
141 165
142#define black_ColorEscape "\r0" 166#define asciiBase_ColorEscape 33
143#define gray25_ColorEscape "\r1" 167
144#define gray50_ColorEscape "\r2" 168#define black_ColorEscape "\r!"
145#define gray75_ColorEscape "\r3" 169#define gray25_ColorEscape "\r\""
146#define white_ColorEscape "\r4" 170#define gray50_ColorEscape "\r#"
147#define brown_ColorEscape "\r5" 171#define gray75_ColorEscape "\r$"
148#define orange_ColorEscape "\r6" 172#define white_ColorEscape "\r%"
149#define teal_ColorEscape "\r7" 173#define brown_ColorEscape "\r&"
150#define cyan_ColorEscape "\r8" 174#define orange_ColorEscape "\r'"
151#define yellow_ColorEscape "\r9" 175#define teal_ColorEscape "\r("
152#define red_ColorEscape "\r:" 176#define cyan_ColorEscape "\r)"
153#define magenta_ColorEscape "\r;" 177#define yellow_ColorEscape "\r*"
154#define blue_ColorEscape "\r<" 178#define red_ColorEscape "\r+"
155#define green_ColorEscape "\r=" 179#define magenta_ColorEscape "\r,"
156#define uiText_ColorEscape "\rC" 180#define blue_ColorEscape "\r-"
157#define uiTextAction_ColorEscape "\rJ" 181#define green_ColorEscape "\r."
158#define uiTextCaution_ColorEscape "\rK" 182#define uiText_ColorEscape "\r4"
159#define uiHeading_ColorEscape "\r`" 183#define uiTextAction_ColorEscape "\r<"
184#define uiTextCaution_ColorEscape "\r="
185#define uiHeading_ColorEscape "\rR"
160 186
161iDeclareType(Color) 187iDeclareType(Color)
162iDeclareType(HSLColor) 188iDeclareType(HSLColor)
@@ -180,6 +206,7 @@ iColor get_Color (int color);
180int darker_Color (int color); 206int darker_Color (int color);
181int lighter_Color (int color); 207int lighter_Color (int color);
182void set_Color (int color, iColor rgba); 208void set_Color (int color, iColor rgba);
209iColor mix_Color (iColor c1, iColor c2, float t);
183 210
184iLocalDef void setHsl_Color(int color, iHSLColor hsl) { 211iLocalDef void setHsl_Color(int color, iHSLColor hsl) {
185 set_Color(color, rgb_HSLColor(hsl)); 212 set_Color(color, rgb_HSLColor(hsl));
diff --git a/src/ui/command.c b/src/ui/command.c
index 19228e46..cf8c7032 100644
--- a/src/ui/command.c
+++ b/src/ui/command.c
@@ -1,3 +1,25 @@
1/* Copyright 2020 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
1#include "command.h" 23#include "command.h"
2#include "app.h" 24#include "app.h"
3 25
diff --git a/src/ui/command.h b/src/ui/command.h
index 2124d527..4798baa6 100644
--- a/src/ui/command.h
+++ b/src/ui/command.h
@@ -1,3 +1,25 @@
1/* Copyright 2020 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
1#pragma once 23#pragma once
2 24
3#include <the_Foundation/vec2.h> 25#include <the_Foundation/vec2.h>
diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c
index 29803252..6ca6e4c2 100644
--- a/src/ui/documentwidget.c
+++ b/src/ui/documentwidget.c
@@ -1,3 +1,25 @@
1/* Copyright 2020 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
1#include "documentwidget.h" 23#include "documentwidget.h"
2#include "scrollwidget.h" 24#include "scrollwidget.h"
3#include "inputwidget.h" 25#include "inputwidget.h"
@@ -105,6 +127,43 @@ iDefineTypeConstruction(Model)
105 127
106/*----------------------------------------------------------------------------------------------*/ 128/*----------------------------------------------------------------------------------------------*/
107 129
130iDeclareType(VisBuffer)
131iDeclareTypeConstruction(VisBuffer)
132
133struct Impl_VisBuffer {
134 SDL_Texture * texture[2];
135 int index;
136 iInt2 size;
137 iRangei validRange;
138};
139
140void init_VisBuffer(iVisBuffer *d) {
141 iZap(*d);
142}
143
144void deinit_VisBuffer(iVisBuffer *d) {
145 iForIndices(i, d->texture) {
146 if (d->texture[i]) {
147 SDL_DestroyTexture(d->texture[i]);
148 }
149 }
150}
151
152void dealloc_VisBuffer(iVisBuffer *d) {
153 d->size = zero_I2();
154 iZap(d->validRange);
155 iForIndices(i, d->texture) {
156 SDL_DestroyTexture(d->texture[i]);
157 d->texture[i] = NULL;
158 }
159}
160
161iDefineTypeConstruction(VisBuffer)
162
163/*----------------------------------------------------------------------------------------------*/
164
165static const int smoothSpeed_DocumentWidget_ = 120; /* unit: gap_Text per second */
166
108enum iRequestState { 167enum iRequestState {
109 blank_RequestState, 168 blank_RequestState,
110 fetching_RequestState, 169 fetching_RequestState,
@@ -124,26 +183,26 @@ struct Impl_DocumentWidget {
124 int certFlags; 183 int certFlags;
125 iDate certExpiry; 184 iDate certExpiry;
126 iString * certSubject; 185 iString * certSubject;
186 int redirectCount;
127 iBool selecting; 187 iBool selecting;
128 iRangecc selectMark; 188 iRangecc selectMark;
129 iRangecc foundMark; 189 iRangecc foundMark;
130 int pageMargin; 190 int pageMargin;
131 iPtrArray visibleLinks; 191 iPtrArray visibleLinks;
132 const iGmRun * hoverLink; 192 const iGmRun * hoverLink;
193 const iGmRun * contextLink;
133 iBool noHoverWhileScrolling; 194 iBool noHoverWhileScrolling;
134 iBool showLinkNumbers; 195 iBool showLinkNumbers;
135 iClick click; 196 iClick click;
136 float initNormScrollY; 197 float initNormScrollY;
137 int scrollY; 198 int scrollY;
138 iScrollWidget *scroll; 199 iScrollWidget *scroll;
200 int smoothScroll;
201 int smoothSpeed;
202 int smoothLastOffset;
203 iBool smoothContinue;
139 iWidget * menu; 204 iWidget * menu;
140 SDL_Cursor * arrowCursor; /* TODO: cursors belong in Window */ 205 iVisBuffer * visBuffer;
141 SDL_Cursor * beamCursor;
142 SDL_Cursor * handCursor;
143 SDL_Texture * visBuffer[2];
144 int visBufferIndex;
145 iInt2 visBufferSize;
146 iRangei visBufferValidRange;
147}; 206};
148 207
149iDefineObjectConstruction(DocumentWidget) 208iDefineObjectConstruction(DocumentWidget)
@@ -163,22 +222,22 @@ void init_DocumentWidget(iDocumentWidget *d) {
163 d->isRequestUpdated = iFalse; 222 d->isRequestUpdated = iFalse;
164 d->media = new_ObjectList(); 223 d->media = new_ObjectList();
165 d->doc = new_GmDocument(); 224 d->doc = new_GmDocument();
225 d->redirectCount = 0;
166 d->initNormScrollY = 0; 226 d->initNormScrollY = 0;
167 d->scrollY = 0; 227 d->scrollY = 0;
228 d->smoothScroll = 0;
229 d->smoothSpeed = 0;
230 d->smoothLastOffset = 0;
231 d->smoothContinue = iFalse;
168 d->selecting = iFalse; 232 d->selecting = iFalse;
169 d->selectMark = iNullRange; 233 d->selectMark = iNullRange;
170 d->foundMark = iNullRange; 234 d->foundMark = iNullRange;
171 d->pageMargin = 5; 235 d->pageMargin = 5;
172 d->hoverLink = NULL; 236 d->hoverLink = NULL;
237 d->contextLink = NULL;
173 d->noHoverWhileScrolling = iFalse; 238 d->noHoverWhileScrolling = iFalse;
174 d->showLinkNumbers = iFalse; 239 d->showLinkNumbers = iFalse;
175 iZap(d->visBuffer); 240 d->visBuffer = new_VisBuffer();
176 d->visBufferIndex = 0;
177 d->visBufferSize = zero_I2();
178 iZap(d->visBufferValidRange);
179 d->arrowCursor = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_ARROW);
180 d->beamCursor = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_IBEAM);
181 d->handCursor = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_HAND);
182 init_PtrArray(&d->visibleLinks); 241 init_PtrArray(&d->visibleLinks);
183 init_Click(&d->click, d, SDL_BUTTON_LEFT); 242 init_Click(&d->click, d, SDL_BUTTON_LEFT);
184 addChild_Widget(w, iClob(d->scroll = new_ScrollWidget())); 243 addChild_Widget(w, iClob(d->scroll = new_ScrollWidget()));
@@ -197,18 +256,23 @@ void init_DocumentWidget(iDocumentWidget *d) {
197} 256}
198 257
199void deinit_DocumentWidget(iDocumentWidget *d) { 258void deinit_DocumentWidget(iDocumentWidget *d) {
259 delete_VisBuffer(d->visBuffer);
200 iRelease(d->media); 260 iRelease(d->media);
201 iRelease(d->request); 261 iRelease(d->request);
202 iRelease(d->doc); 262 iRelease(d->doc);
203 deinit_PtrArray(&d->visibleLinks); 263 deinit_PtrArray(&d->visibleLinks);
204 delete_String(d->certSubject); 264 delete_String(d->certSubject);
205 delete_String(d->titleUser); 265 delete_String(d->titleUser);
206 SDL_FreeCursor(d->arrowCursor);
207 SDL_FreeCursor(d->beamCursor);
208 SDL_FreeCursor(d->handCursor);
209 deinit_Model(&d->mod); 266 deinit_Model(&d->mod);
210} 267}
211 268
269static void resetSmoothScroll_DocumentWidget_(iDocumentWidget *d) {
270 d->smoothSpeed = 0;
271 d->smoothScroll = 0;
272 d->smoothLastOffset = 0;
273 d->smoothContinue = iFalse;
274}
275
212static int documentWidth_DocumentWidget_(const iDocumentWidget *d) { 276static int documentWidth_DocumentWidget_(const iDocumentWidget *d) {
213 const iWidget *w = constAs_Widget(d); 277 const iWidget *w = constAs_Widget(d);
214 const iRect bounds = bounds_Widget(w); 278 const iRect bounds = bounds_Widget(w);
@@ -242,9 +306,11 @@ iLocalDef int documentToWindowY_DocumentWidget_(const iDocumentWidget *d, int do
242 return docY - d->scrollY + documentBounds_DocumentWidget_(d).pos.y; 306 return docY - d->scrollY + documentBounds_DocumentWidget_(d).pos.y;
243} 307}
244 308
309#if 0
245iLocalDef int windowToDocumentY_DocumentWidget_(const iDocumentWidget *d, int localY) { 310iLocalDef int windowToDocumentY_DocumentWidget_(const iDocumentWidget *d, int localY) {
246 return localY + d->scrollY - documentBounds_DocumentWidget_(d).pos.y; 311 return localY + d->scrollY - documentBounds_DocumentWidget_(d).pos.y;
247} 312}
313#endif
248 314
249static iInt2 documentPos_DocumentWidget_(const iDocumentWidget *d, iInt2 pos) { 315static iInt2 documentPos_DocumentWidget_(const iDocumentWidget *d, iInt2 pos) {
250 return addY_I2(sub_I2(pos, topLeft_Rect(documentBounds_DocumentWidget_(d))), d->scrollY); 316 return addY_I2(sub_I2(pos, topLeft_Rect(documentBounds_DocumentWidget_(d))), d->scrollY);
@@ -291,15 +357,16 @@ static float normScrollPos_DocumentWidget_(const iDocumentWidget *d) {
291 357
292static int scrollMax_DocumentWidget_(const iDocumentWidget *d) { 358static int scrollMax_DocumentWidget_(const iDocumentWidget *d) {
293 return size_GmDocument(d->doc).y - height_Rect(bounds_Widget(constAs_Widget(d))) + 359 return size_GmDocument(d->doc).y - height_Rect(bounds_Widget(constAs_Widget(d))) +
294 2 * d->pageMargin * gap_UI; 360 (hasSiteBanner_GmDocument(d->doc) ? 1 : 2) * d->pageMargin * gap_UI;
295} 361}
296 362
297static void updateHover_DocumentWidget_(iDocumentWidget *d, iInt2 mouse) { 363static void updateHover_DocumentWidget_(iDocumentWidget *d, iInt2 mouse) {
298 const iRect docBounds = documentBounds_DocumentWidget_(d); 364 const iWidget *w = constAs_Widget(d);
299 const iGmRun *oldHoverLink = d->hoverLink; 365 const iRect docBounds = documentBounds_DocumentWidget_(d);
300 d->hoverLink = NULL; 366 const iGmRun * oldHoverLink = d->hoverLink;
301 const iInt2 hoverPos = addY_I2(sub_I2(mouse, topLeft_Rect(docBounds)), d->scrollY); 367 d->hoverLink = NULL;
302 if (!d->noHoverWhileScrolling && 368 const iInt2 hoverPos = addY_I2(sub_I2(mouse, topLeft_Rect(docBounds)), d->scrollY);
369 if (isHover_Widget(w) && !d->noHoverWhileScrolling &&
303 (d->state == ready_RequestState || d->state == receivedPartialResponse_RequestState)) { 370 (d->state == ready_RequestState || d->state == receivedPartialResponse_RequestState)) {
304 iConstForEach(PtrArray, i, &d->visibleLinks) { 371 iConstForEach(PtrArray, i, &d->visibleLinks) {
305 const iGmRun *run = i.ptr; 372 const iGmRun *run = i.ptr;
@@ -312,12 +379,9 @@ static void updateHover_DocumentWidget_(iDocumentWidget *d, iInt2 mouse) {
312 if (d->hoverLink != oldHoverLink) { 379 if (d->hoverLink != oldHoverLink) {
313 refresh_Widget(as_Widget(d)); 380 refresh_Widget(as_Widget(d));
314 } 381 }
315 if (!contains_Widget(constAs_Widget(d), mouse) || 382 if (isHover_Widget(w) && !contains_Widget(constAs_Widget(d->scroll), mouse)) {
316 contains_Widget(constAs_Widget(d->scroll), mouse)) { 383 setCursor_Window(get_Window(),
317 SDL_SetCursor(d->arrowCursor); 384 d->hoverLink ? SDL_SYSTEM_CURSOR_HAND : SDL_SYSTEM_CURSOR_IBEAM);
318 }
319 else {
320 SDL_SetCursor(d->hoverLink ? d->handCursor : d->beamCursor);
321 } 385 }
322} 386}
323 387
@@ -424,38 +488,52 @@ static void updateWindowTitle_DocumentWidget_(const iDocumentWidget *d) {
424 } 488 }
425} 489}
426 490
491static void invalidate_DocumentWidget_(iDocumentWidget *d) {
492 iZap(d->visBuffer->validRange);
493}
494
427static void setSource_DocumentWidget_(iDocumentWidget *d, const iString *source) { 495static void setSource_DocumentWidget_(iDocumentWidget *d, const iString *source) {
428 setUrl_GmDocument(d->doc, d->mod.url); 496 setUrl_GmDocument(d->doc, d->mod.url);
429 setSource_GmDocument(d->doc, source, documentWidth_DocumentWidget_(d)); 497 setSource_GmDocument(d->doc, source, documentWidth_DocumentWidget_(d));
430 d->foundMark = iNullRange; 498 d->foundMark = iNullRange;
431 d->selectMark = iNullRange; 499 d->selectMark = iNullRange;
432 d->hoverLink = NULL; 500 d->hoverLink = NULL;
501 d->contextLink = NULL;
433 updateWindowTitle_DocumentWidget_(d); 502 updateWindowTitle_DocumentWidget_(d);
434 updateVisible_DocumentWidget_(d); 503 updateVisible_DocumentWidget_(d);
504 invalidate_DocumentWidget_(d);
435 refresh_Widget(as_Widget(d)); 505 refresh_Widget(as_Widget(d));
436} 506}
437 507
438static void showErrorPage_DocumentWidget_(iDocumentWidget *d, enum iGmStatusCode code) { 508static void showErrorPage_DocumentWidget_(iDocumentWidget *d, enum iGmStatusCode code,
509 const iString *meta) {
439 iString *src = collectNewCStr_String("# "); 510 iString *src = collectNewCStr_String("# ");
440 const iGmError *msg = get_GmError(code); 511 const iGmError *msg = get_GmError(code);
441 appendChar_String(src, msg->icon ? msg->icon : 0x2327); /* X in a box */ 512 appendChar_String(src, msg->icon ? msg->icon : 0x2327); /* X in a box */
442 appendFormat_String(src, " %s\n%s", msg->title, msg->info); 513 appendFormat_String(src, " %s\n%s", msg->title, msg->info);
443 switch (code) { 514 if (meta) {
444 case failedToOpenFile_GmStatusCode: 515 switch (code) {
445 case certificateNotValid_GmStatusCode: 516 case nonGeminiRedirect_GmStatusCode:
446 appendFormat_String(src, "\n\n%s", cstr_String(meta_GmRequest(d->request))); 517 case tooManyRedirects_GmStatusCode:
447 break; 518 appendFormat_String(src, "\n=> %s\n", cstr_String(meta));
448 case unsupportedMimeType_GmStatusCode: 519 break;
449 appendFormat_String(src, "\n```\n%s\n```\n", cstr_String(meta_GmRequest(d->request))); 520 case failedToOpenFile_GmStatusCode:
450 break; 521 case certificateNotValid_GmStatusCode:
451 case slowDown_GmStatusCode: 522 appendFormat_String(src, "\n\n%s", cstr_String(meta));
452 appendFormat_String(src, "\n\nWait %s seconds before your next request.", 523 break;
453 cstr_String(meta_GmRequest(d->request))); 524 case unsupportedMimeType_GmStatusCode:
454 break; 525 appendFormat_String(src, "\n```\n%s\n```\n", cstr_String(meta));
455 default: 526 break;
456 break; 527 case slowDown_GmStatusCode:
528 appendFormat_String(src, "\n\nWait %s seconds before your next request.",
529 cstr_String(meta));
530 break;
531 default:
532 break;
533 }
457 } 534 }
458 setSource_DocumentWidget_(d, src); 535 setSource_DocumentWidget_(d, src);
536 resetSmoothScroll_DocumentWidget_(d);
459 d->scrollY = 0; 537 d->scrollY = 0;
460 d->state = ready_RequestState; 538 d->state = ready_RequestState;
461} 539}
@@ -470,10 +548,6 @@ static void updateTheme_DocumentWidget_(iDocumentWidget *d) {
470 } 548 }
471} 549}
472 550
473static void invalidate_DocumentWidget_(iDocumentWidget *d) {
474 iZap(d->visBufferValidRange);
475}
476
477static void updateDocument_DocumentWidget_(iDocumentWidget *d, const iGmResponse *response) { 551static void updateDocument_DocumentWidget_(iDocumentWidget *d, const iGmResponse *response) {
478 if (d->state == ready_RequestState) { 552 if (d->state == ready_RequestState) {
479 return; 553 return;
@@ -493,16 +567,16 @@ static void updateDocument_DocumentWidget_(iDocumentWidget *d, const iGmResponse
493 const iString *mimeStr = collect_String(lower_String(&response->meta)); /* for convenience */ 567 const iString *mimeStr = collect_String(lower_String(&response->meta)); /* for convenience */
494 iRangecc mime = range_String(mimeStr); 568 iRangecc mime = range_String(mimeStr);
495 iRangecc seg = iNullRange; 569 iRangecc seg = iNullRange;
496 while (nextSplit_Rangecc(&mime, ";", &seg)) { 570 while (nextSplit_Rangecc(mime, ";", &seg)) {
497 iRangecc param = seg; 571 iRangecc param = seg;
498 trim_Rangecc(&param); 572 trim_Rangecc(&param);
499 if (equal_Rangecc(&param, "text/plain")) { 573 if (equal_Rangecc(param, "text/plain")) {
500 docFormat = plainText_GmDocumentFormat; 574 docFormat = plainText_GmDocumentFormat;
501 } 575 }
502 else if (equal_Rangecc(&param, "text/gemini")) { 576 else if (equal_Rangecc(param, "text/gemini")) {
503 docFormat = gemini_GmDocumentFormat; 577 docFormat = gemini_GmDocumentFormat;
504 } 578 }
505 else if (startsWith_Rangecc(&param, "image/")) { 579 else if (startsWith_Rangecc(param, "image/")) {
506 docFormat = gemini_GmDocumentFormat; 580 docFormat = gemini_GmDocumentFormat;
507 if (!d->request || isFinished_GmRequest(d->request)) { 581 if (!d->request || isFinished_GmRequest(d->request)) {
508 /* Make a simple document with an image. */ 582 /* Make a simple document with an image. */
@@ -521,7 +595,7 @@ static void updateDocument_DocumentWidget_(iDocumentWidget *d, const iGmResponse
521 clear_String(&str); 595 clear_String(&str);
522 } 596 }
523 } 597 }
524 else if (startsWith_Rangecc(&param, "charset=")) { 598 else if (startsWith_Rangecc(param, "charset=")) {
525 charset = (iRangecc){ param.start + 8, param.end }; 599 charset = (iRangecc){ param.start + 8, param.end };
526 /* Remove whitespace and quotes. */ 600 /* Remove whitespace and quotes. */
527 trim_Rangecc(&charset); 601 trim_Rangecc(&charset);
@@ -532,12 +606,12 @@ static void updateDocument_DocumentWidget_(iDocumentWidget *d, const iGmResponse
532 } 606 }
533 } 607 }
534 if (docFormat == undefined_GmDocumentFormat) { 608 if (docFormat == undefined_GmDocumentFormat) {
535 showErrorPage_DocumentWidget_(d, unsupportedMimeType_GmStatusCode); 609 showErrorPage_DocumentWidget_(d, unsupportedMimeType_GmStatusCode, &response->meta);
536 deinit_String(&str); 610 deinit_String(&str);
537 return; 611 return;
538 } 612 }
539 /* Convert the source to UTF-8 if needed. */ 613 /* Convert the source to UTF-8 if needed. */
540 if (!equalCase_Rangecc(&charset, "utf-8")) { 614 if (!equalCase_Rangecc(charset, "utf-8")) {
541 set_String(&str, 615 set_String(&str,
542 collect_String(decode_Block(&str.chars, cstr_Rangecc(charset)))); 616 collect_String(decode_Block(&str.chars, cstr_Rangecc(charset))));
543 } 617 }
@@ -609,6 +683,7 @@ static void parseUser_DocumentWidget_(iDocumentWidget *d) {
609 iRegExp *userPats[2] = { new_RegExp("~([^/?]+)", 0), 683 iRegExp *userPats[2] = { new_RegExp("~([^/?]+)", 0),
610 new_RegExp("/users/([^/?]+)", caseInsensitive_RegExpOption) }; 684 new_RegExp("/users/([^/?]+)", caseInsensitive_RegExpOption) };
611 iRegExpMatch m; 685 iRegExpMatch m;
686 init_RegExpMatch(&m);
612 iForIndices(i, userPats) { 687 iForIndices(i, userPats) {
613 if (matchString_RegExp(userPats[i], d->mod.url, &m)) { 688 if (matchString_RegExp(userPats[i], d->mod.url, &m)) {
614 setRange_String(d->titleUser, capturedRange_RegExpMatch(&m, 1)); 689 setRange_String(d->titleUser, capturedRange_RegExpMatch(&m, 1));
@@ -621,6 +696,8 @@ static iBool updateFromHistory_DocumentWidget_(iDocumentWidget *d) {
621 const iRecentUrl *recent = findUrl_History(d->mod.history, d->mod.url); 696 const iRecentUrl *recent = findUrl_History(d->mod.history, d->mod.url);
622 if (recent && recent->cachedResponse) { 697 if (recent && recent->cachedResponse) {
623 const iGmResponse *resp = recent->cachedResponse; 698 const iGmResponse *resp = recent->cachedResponse;
699 clear_ObjectList(d->media);
700 reset_GmDocument(d->doc);
624 d->state = fetching_RequestState; 701 d->state = fetching_RequestState;
625 d->initNormScrollY = recent->normScrollY; 702 d->initNormScrollY = recent->normScrollY;
626 /* Use the cached response data. */ 703 /* Use the cached response data. */
@@ -676,6 +753,10 @@ void setInitialScroll_DocumentWidget(iDocumentWidget *d, float normScrollY) {
676 d->initNormScrollY = normScrollY; 753 d->initNormScrollY = normScrollY;
677} 754}
678 755
756void setRedirectCount_DocumentWidget(iDocumentWidget *d, int count) {
757 d->redirectCount = count;
758}
759
679iBool isRequestOngoing_DocumentWidget(const iDocumentWidget *d) { 760iBool isRequestOngoing_DocumentWidget(const iDocumentWidget *d) {
680 return d->state == fetching_RequestState || d->state == receivedPartialResponse_RequestState; 761 return d->state == fetching_RequestState || d->state == receivedPartialResponse_RequestState;
681} 762}
@@ -696,6 +777,37 @@ static void scroll_DocumentWidget_(iDocumentWidget *d, int offset) {
696 refresh_Widget(as_Widget(d)); 777 refresh_Widget(as_Widget(d));
697} 778}
698 779
780static void doScroll_DocumentWidget_(iAny *ptr) {
781 iDocumentWidget *d = ptr;
782 if (!d->smoothScroll) return; /* was cancelled */
783 const double elapsed = (double) elapsedSinceLastTicker_App() / 1000.0;
784 int delta = d->smoothSpeed * elapsed * iSign(d->smoothScroll);
785 if (iAbs(d->smoothScroll) <= iAbs(delta)) {
786 if (d->smoothContinue) {
787 d->smoothScroll += d->smoothLastOffset;
788 }
789 else {
790 delta = d->smoothScroll;
791 }
792 }
793 scroll_DocumentWidget_(d, delta);
794 d->smoothScroll -= delta;
795 if (d->smoothScroll != 0) {
796 addTicker_App(doScroll_DocumentWidget_, d);
797 }
798}
799
800static void smoothScroll_DocumentWidget_(iDocumentWidget *d, int offset, int speed) {
801 if (speed == 0) {
802 scroll_DocumentWidget_(d, offset);
803 return;
804 }
805 d->smoothSpeed = speed;
806 d->smoothScroll += offset;
807 d->smoothLastOffset = offset;
808 addTicker_App(doScroll_DocumentWidget_, d);
809}
810
699static void scrollTo_DocumentWidget_(iDocumentWidget *d, int documentY, iBool centered) { 811static void scrollTo_DocumentWidget_(iDocumentWidget *d, int documentY, iBool centered) {
700 d->scrollY = documentY - (centered ? documentBounds_DocumentWidget_(d).size.y / 2 : 812 d->scrollY = documentY - (centered ? documentBounds_DocumentWidget_(d).size.y / 2 :
701 lineHeight_Text(paragraph_FontId)); 813 lineHeight_Text(paragraph_FontId));
@@ -733,32 +845,43 @@ static void checkResponse_DocumentWidget_(iDocumentWidget *d) {
733 } 845 }
734 case categorySuccess_GmStatusCode: 846 case categorySuccess_GmStatusCode:
735 d->scrollY = 0; 847 d->scrollY = 0;
848 resetSmoothScroll_DocumentWidget_(d);
736 reset_GmDocument(d->doc); /* new content incoming */ 849 reset_GmDocument(d->doc); /* new content incoming */
737 updateDocument_DocumentWidget_(d, response_GmRequest(d->request)); 850 updateDocument_DocumentWidget_(d, response_GmRequest(d->request));
738 break; 851 break;
739 case categoryRedirect_GmStatusCode: 852 case categoryRedirect_GmStatusCode:
740 if (isEmpty_String(meta_GmRequest(d->request))) { 853 if (isEmpty_String(meta_GmRequest(d->request))) {
741 showErrorPage_DocumentWidget_(d, invalidRedirect_GmStatusCode); 854 showErrorPage_DocumentWidget_(d, invalidRedirect_GmStatusCode, NULL);
742 } 855 }
743 else { 856 else {
744 /* TODO: only accept redirects that use gemini protocol */ 857 /* Only accept redirects that use gemini scheme. */
745 postCommandf_App( 858 const iString *dstUrl = absoluteUrl_String(d->mod.url, meta_GmRequest(d->request));
746 "open redirect:1 url:%s", 859 if (d->redirectCount >= 5) {
747 cstr_String(absoluteUrl_String(d->mod.url, meta_GmRequest(d->request)))); 860 showErrorPage_DocumentWidget_(d, tooManyRedirects_GmStatusCode, dstUrl);
861 }
862 else if (equalCase_Rangecc(urlScheme_String(dstUrl), "gemini")) {
863 postCommandf_App(
864 "open redirect:%d url:%s", d->redirectCount + 1, cstr_String(dstUrl));
865 }
866 else {
867 showErrorPage_DocumentWidget_(d, nonGeminiRedirect_GmStatusCode, dstUrl);
868 }
748 iReleasePtr(&d->request); 869 iReleasePtr(&d->request);
749 } 870 }
750 break; 871 break;
751 default: 872 default:
752 if (isDefined_GmError(statusCode)) { 873 if (isDefined_GmError(statusCode)) {
753 showErrorPage_DocumentWidget_(d, statusCode); 874 showErrorPage_DocumentWidget_(d, statusCode, meta_GmRequest(d->request));
754 } 875 }
755 else if (category_GmStatusCode(statusCode) == 876 else if (category_GmStatusCode(statusCode) ==
756 categoryTemporaryFailure_GmStatusCode) { 877 categoryTemporaryFailure_GmStatusCode) {
757 showErrorPage_DocumentWidget_(d, temporaryFailure_GmStatusCode); 878 showErrorPage_DocumentWidget_(
879 d, temporaryFailure_GmStatusCode, meta_GmRequest(d->request));
758 } 880 }
759 else if (category_GmStatusCode(statusCode) == 881 else if (category_GmStatusCode(statusCode) ==
760 categoryPermanentFailure_GmStatusCode) { 882 categoryPermanentFailure_GmStatusCode) {
761 showErrorPage_DocumentWidget_(d, permanentFailure_GmStatusCode); 883 showErrorPage_DocumentWidget_(
884 d, permanentFailure_GmStatusCode, meta_GmRequest(d->request));
762 } 885 }
763 break; 886 break;
764 } 887 }
@@ -872,32 +995,33 @@ static iBool handleMediaCommand_DocumentWidget_(iDocumentWidget *d, const char *
872 return iFalse; 995 return iFalse;
873} 996}
874 997
875static void deallocVisBuffer_DocumentWidget_(iDocumentWidget *d) { 998static void deallocVisBuffer_DocumentWidget_(const iDocumentWidget *d) {
876 d->visBufferSize = zero_I2(); 999 d->visBuffer->size = zero_I2();
877 iZap(d->visBufferValidRange); 1000 iZap(d->visBuffer->validRange);
878 iForIndices(i, d->visBuffer) { 1001 iForIndices(i, d->visBuffer->texture) {
879 SDL_DestroyTexture(d->visBuffer[i]); 1002 SDL_DestroyTexture(d->visBuffer->texture[i]);
880 d->visBuffer[i] = NULL; 1003 d->visBuffer->texture[i] = NULL;
881 } 1004 }
882} 1005}
883 1006
884static void allocVisBuffer_DocumentWidget_(iDocumentWidget *d) { 1007static void allocVisBuffer_DocumentWidget_(const iDocumentWidget *d) {
885 iWidget *w = as_Widget(d); 1008 const iWidget *w = constAs_Widget(d);
886 const iBool isVisible = isVisible_Widget(w); 1009 const iBool isVisible = isVisible_Widget(w);
887 const iInt2 size = bounds_Widget(w).size; 1010 const iInt2 size = bounds_Widget(w).size;
888 if (!isEqual_I2(size, d->visBufferSize) || !isVisible) { 1011 if (!isEqual_I2(size, d->visBuffer->size) || !isVisible) {
889 deallocVisBuffer_DocumentWidget_(d); 1012 dealloc_VisBuffer(d->visBuffer);
890 } 1013 }
891 if (isVisible && !d->visBuffer[0]) { 1014 if (isVisible && !d->visBuffer->texture[0]) {
892 iZap(d->visBufferValidRange); 1015 iZap(d->visBuffer->validRange);
893 d->visBufferSize = size; 1016 d->visBuffer->size = size;
894 iForIndices(i, d->visBuffer) { 1017 iForIndices(i, d->visBuffer->texture) {
895 d->visBuffer[i] = SDL_CreateTexture(renderer_Window(get_Window()), 1018 d->visBuffer->texture[i] =
896 SDL_PIXELFORMAT_RGBA8888, 1019 SDL_CreateTexture(renderer_Window(get_Window()),
897 SDL_TEXTUREACCESS_STATIC | SDL_TEXTUREACCESS_TARGET, 1020 SDL_PIXELFORMAT_RGBA8888,
898 size.x, 1021 SDL_TEXTUREACCESS_STATIC | SDL_TEXTUREACCESS_TARGET,
899 size.y); 1022 size.x,
900 SDL_SetTextureBlendMode(d->visBuffer[i], SDL_BLENDMODE_NONE); 1023 size.y);
1024 SDL_SetTextureBlendMode(d->visBuffer->texture[i], SDL_BLENDMODE_NONE);
901 } 1025 }
902 } 1026 }
903} 1027}
@@ -973,7 +1097,7 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
973 : "Not trusted")); 1097 : "Not trusted"));
974 return iTrue; 1098 return iTrue;
975 } 1099 }
976 else if (equal_Command(cmd, "copy") && document_App() == d) { 1100 else if (equal_Command(cmd, "copy") && document_App() == d && !focus_Widget()) {
977 iString *copied; 1101 iString *copied;
978 if (d->selectMark.start) { 1102 if (d->selectMark.start) {
979 iRangecc mark = d->selectMark; 1103 iRangecc mark = d->selectMark;
@@ -990,10 +1114,10 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
990 delete_String(copied); 1114 delete_String(copied);
991 return iTrue; 1115 return iTrue;
992 } 1116 }
993 else if (equalWidget_Command(cmd, w, "document.copylink")) { 1117 else if (equal_Command(cmd, "document.copylink") && document_App() == d) {
994 if (d->hoverLink) { 1118 if (d->contextLink) {
995 SDL_SetClipboardText(cstr_String( 1119 SDL_SetClipboardText(cstr_String(
996 absoluteUrl_String(d->mod.url, linkUrl_GmDocument(d->doc, d->hoverLink->linkId)))); 1120 absoluteUrl_String(d->mod.url, linkUrl_GmDocument(d->doc, d->contextLink->linkId))));
997 } 1121 }
998 else { 1122 else {
999 SDL_SetClipboardText(cstr_String(d->mod.url)); 1123 SDL_SetClipboardText(cstr_String(d->mod.url));
@@ -1001,18 +1125,16 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
1001 return iTrue; 1125 return iTrue;
1002 } 1126 }
1003 else if (equal_Command(cmd, "document.input.submit")) { 1127 else if (equal_Command(cmd, "document.input.submit")) {
1004 if (arg_Command(cmd)) { 1128 iString *value = collect_String(suffix_Command(cmd, "value"));
1005 iString *value = collect_String(suffix_Command(cmd, "value")); 1129 urlEncode_String(value);
1006 urlEncode_String(value); 1130 iString *url = collect_String(copy_String(d->mod.url));
1007 iString *url = collect_String(copy_String(d->mod.url)); 1131 const size_t qPos = indexOfCStr_String(url, "?");
1008 const size_t qPos = indexOfCStr_String(url, "?"); 1132 if (qPos != iInvalidPos) {
1009 if (qPos != iInvalidPos) { 1133 remove_Block(&url->chars, qPos, iInvalidSize);
1010 remove_Block(&url->chars, qPos, iInvalidSize);
1011 }
1012 appendCStr_String(url, "?");
1013 append_String(url, value);
1014 postCommandf_App("open url:%s", cstr_String(url));
1015 } 1134 }
1135 appendCStr_String(url, "?");
1136 append_String(url, value);
1137 postCommandf_App("open url:%s", cstr_String(url));
1016 return iTrue; 1138 return iTrue;
1017 } 1139 }
1018 else if (equal_Command(cmd, "valueinput.cancelled") && 1140 else if (equal_Command(cmd, "valueinput.cancelled") &&
@@ -1028,11 +1150,11 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
1028 else if (equalWidget_Command(cmd, w, "document.request.finished") && 1150 else if (equalWidget_Command(cmd, w, "document.request.finished") &&
1029 pointerLabel_Command(cmd, "request") == d->request) { 1151 pointerLabel_Command(cmd, "request") == d->request) {
1030 checkResponse_DocumentWidget_(d); 1152 checkResponse_DocumentWidget_(d);
1153 resetSmoothScroll_DocumentWidget_(d);
1031 d->scrollY = d->initNormScrollY * size_GmDocument(d->doc).y; 1154 d->scrollY = d->initNormScrollY * size_GmDocument(d->doc).y;
1032 d->state = ready_RequestState; 1155 d->state = ready_RequestState;
1033 /* The response may be cached. */ { 1156 /* The response may be cached. */ {
1034 const iRangecc proto = urlProtocol_String(d->mod.url); 1157 if (!equal_Rangecc(urlScheme_String(d->mod.url), "about")) {
1035 if (!equal_Rangecc(&proto, "about")) {
1036 setCachedResponse_History(d->mod.history, response_GmRequest(d->request)); 1158 setCachedResponse_History(d->mod.history, response_GmRequest(d->request));
1037 } 1159 }
1038 } 1160 }
@@ -1076,12 +1198,24 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
1076 } 1198 }
1077 else if (equalWidget_Command(cmd, w, "scroll.moved")) { 1199 else if (equalWidget_Command(cmd, w, "scroll.moved")) {
1078 d->scrollY = arg_Command(cmd); 1200 d->scrollY = arg_Command(cmd);
1201 resetSmoothScroll_DocumentWidget_(d);
1079 updateVisible_DocumentWidget_(d); 1202 updateVisible_DocumentWidget_(d);
1080 return iTrue; 1203 return iTrue;
1081 } 1204 }
1082 else if (equalWidget_Command(cmd, w, "scroll.page")) { 1205 else if (equalWidget_Command(cmd, w, "scroll.page")) {
1083 scroll_DocumentWidget_(d, 1206 if (argLabel_Command(cmd, "repeat")) {
1084 arg_Command(cmd) * height_Rect(documentBounds_DocumentWidget_(d))); 1207 if (!d->smoothContinue) {
1208 d->smoothContinue = iTrue;
1209 }
1210 else {
1211 return iTrue;
1212 }
1213 }
1214 smoothScroll_DocumentWidget_(d,
1215 arg_Command(cmd) *
1216 (0.5f * height_Rect(documentBounds_DocumentWidget_(d)) -
1217 0 * lineHeight_Text(paragraph_FontId)),
1218 25 * smoothSpeed_DocumentWidget_);
1085 return iTrue; 1219 return iTrue;
1086 } 1220 }
1087 else if (equal_Command(cmd, "document.goto") && document_App() == d) { 1221 else if (equal_Command(cmd, "document.goto") && document_App() == d) {
@@ -1164,6 +1298,13 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
1164 refresh_Widget(w); 1298 refresh_Widget(w);
1165 } 1299 }
1166 break; 1300 break;
1301 case SDLK_PAGEUP:
1302 case SDLK_PAGEDOWN:
1303 case SDLK_SPACE:
1304 case SDLK_UP:
1305 case SDLK_DOWN:
1306 d->smoothContinue = iFalse;
1307 break;
1167 } 1308 }
1168 } 1309 }
1169 if (ev->type == SDL_KEYDOWN) { 1310 if (ev->type == SDL_KEYDOWN) {
@@ -1194,12 +1335,14 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
1194 break; 1335 break;
1195 case SDLK_HOME: 1336 case SDLK_HOME:
1196 d->scrollY = 0; 1337 d->scrollY = 0;
1338 resetSmoothScroll_DocumentWidget_(d);
1197 scroll_DocumentWidget_(d, 0); 1339 scroll_DocumentWidget_(d, 0);
1198 updateVisible_DocumentWidget_(d); 1340 updateVisible_DocumentWidget_(d);
1199 refresh_Widget(w); 1341 refresh_Widget(w);
1200 return iTrue; 1342 return iTrue;
1201 case SDLK_END: 1343 case SDLK_END:
1202 d->scrollY = scrollMax_DocumentWidget_(d); 1344 d->scrollY = scrollMax_DocumentWidget_(d);
1345 resetSmoothScroll_DocumentWidget_(d);
1203 scroll_DocumentWidget_(d, 0); 1346 scroll_DocumentWidget_(d, 0);
1204 updateVisible_DocumentWidget_(d); 1347 updateVisible_DocumentWidget_(d);
1205 refresh_Widget(w); 1348 refresh_Widget(w);
@@ -1207,24 +1350,37 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
1207 case SDLK_UP: 1350 case SDLK_UP:
1208 case SDLK_DOWN: 1351 case SDLK_DOWN:
1209 if (mods == 0) { 1352 if (mods == 0) {
1210 scroll_DocumentWidget_(d, 2 * lineHeight_Text(default_FontId) * 1353 if (ev->key.repeat) {
1211 (key == SDLK_UP ? -1 : 1)); 1354 if (!d->smoothContinue) {
1355 d->smoothContinue = iTrue;
1356 }
1357 else return iTrue;
1358 }
1359 smoothScroll_DocumentWidget_(d,
1360 3 * lineHeight_Text(paragraph_FontId) *
1361 (key == SDLK_UP ? -1 : 1),
1362 gap_Text * smoothSpeed_DocumentWidget_);
1212 return iTrue; 1363 return iTrue;
1213 } 1364 }
1214 break; 1365 break;
1215 case SDLK_PAGEUP: 1366 case SDLK_PAGEUP:
1216 case SDLK_PAGEDOWN: 1367 case SDLK_PAGEDOWN:
1217 case ' ': 1368 case SDLK_SPACE:
1218 postCommand_Widget(w, "scroll.page arg:%d", key == SDLK_PAGEUP ? -1 : +1); 1369 postCommand_Widget(
1370 w,
1371 "scroll.page arg:%d repeat:%d",
1372 (key == SDLK_SPACE && mods & KMOD_SHIFT) || key == SDLK_PAGEUP ? -1 : +1,
1373 ev->key.repeat != 0);
1219 return iTrue; 1374 return iTrue;
1220#if 0 1375#if 1
1221 case SDLK_9: { 1376 case SDLK_KP_1: {
1222 iBlock *seed = new_Block(64); 1377 iBlock *seed = new_Block(64);
1223 for (size_t i = 0; i < 64; ++i) { 1378 for (size_t i = 0; i < 64; ++i) {
1224 setByte_Block(seed, i, iRandom(0, 255)); 1379 setByte_Block(seed, i, iRandom(0, 255));
1225 } 1380 }
1226 setThemeSeed_GmDocument(d->doc, seed); 1381 setThemeSeed_GmDocument(d->doc, seed);
1227 delete_Block(seed); 1382 delete_Block(seed);
1383 invalidate_DocumentWidget_(d);
1228 refresh_Widget(w); 1384 refresh_Widget(w);
1229 break; 1385 break;
1230 } 1386 }
@@ -1258,7 +1414,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
1258 else if (ev->type == SDL_MOUSEMOTION) { 1414 else if (ev->type == SDL_MOUSEMOTION) {
1259 d->noHoverWhileScrolling = iFalse; 1415 d->noHoverWhileScrolling = iFalse;
1260 if (isVisible_Widget(d->menu)) { 1416 if (isVisible_Widget(d->menu)) {
1261 SDL_SetCursor(d->arrowCursor); 1417 setCursor_Window(get_Window(), SDL_SYSTEM_CURSOR_ARROW);
1262 } 1418 }
1263 else { 1419 else {
1264 updateHover_DocumentWidget_(d, init_I2(ev->motion.x, ev->motion.y)); 1420 updateHover_DocumentWidget_(d, init_I2(ev->motion.x, ev->motion.y));
@@ -1274,6 +1430,9 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
1274 return iTrue; 1430 return iTrue;
1275 } 1431 }
1276 } 1432 }
1433 if (!isVisible_Widget(d->menu)) {
1434 d->contextLink = d->hoverLink;
1435 }
1277 processContextMenuEvent_Widget(d->menu, ev, d->hoverLink = NULL); 1436 processContextMenuEvent_Widget(d->menu, ev, d->hoverLink = NULL);
1278 switch (processEvent_Click(&d->click, ev)) { 1437 switch (processEvent_Click(&d->click, ev)) {
1279 case started_ClickResult: 1438 case started_ClickResult:
@@ -1282,6 +1441,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
1282 case drag_ClickResult: { 1441 case drag_ClickResult: {
1283 /* Begin selecting a range of text. */ 1442 /* Begin selecting a range of text. */
1284 if (!d->selecting) { 1443 if (!d->selecting) {
1444 setFocus_Widget(NULL); /* TODO: Focus this document? */
1285 d->selecting = iTrue; 1445 d->selecting = iTrue;
1286 d->selectMark.start = d->selectMark.end = 1446 d->selectMark.start = d->selectMark.end =
1287 sourceLoc_DocumentWidget_(d, d->click.startPos); 1447 sourceLoc_DocumentWidget_(d, d->click.startPos);
@@ -1418,8 +1578,12 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) {
1418 } 1578 }
1419 /* Text markers. */ 1579 /* Text markers. */
1420 if (d->pass == dynamic_DrawRunPass) { 1580 if (d->pass == dynamic_DrawRunPass) {
1581 SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()),
1582 isDark_ColorTheme(colorTheme_App()) ? SDL_BLENDMODE_ADD
1583 : SDL_BLENDMODE_BLEND);
1421 fillRange_DrawContext_(d, run, uiMatching_ColorId, d->widget->foundMark, &d->inFoundMark); 1584 fillRange_DrawContext_(d, run, uiMatching_ColorId, d->widget->foundMark, &d->inFoundMark);
1422 fillRange_DrawContext_(d, run, uiMarked_ColorId, d->widget->selectMark, &d->inSelectMark); 1585 fillRange_DrawContext_(d, run, uiMarked_ColorId, d->widget->selectMark, &d->inSelectMark);
1586 SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_NONE);
1423 } 1587 }
1424 enum iColorId fg = run->color; 1588 enum iColorId fg = run->color;
1425 const iGmDocument *doc = d->widget->doc; 1589 const iGmDocument *doc = d->widget->doc;
@@ -1505,6 +1669,12 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) {
1505 appendFormat_String( 1669 appendFormat_String(
1506 &text, " %s\u2a2f", isHover ? escape_Color(tmLinkText_ColorId) : ""); 1670 &text, " %s\u2a2f", isHover ? escape_Color(tmLinkText_ColorId) : "");
1507 } 1671 }
1672 const iInt2 size = measureRange_Text(metaFont, range_String(&text));
1673 fillRect_Paint(
1674 &d->paint,
1675 (iRect){ add_I2(origin, addX_I2(topRight_Rect(run->bounds), -size.x - gap_UI)),
1676 addX_I2(size, 2 * gap_UI) },
1677 tmBackground_ColorId);
1508 drawAlign_Text(metaFont, 1678 drawAlign_Text(metaFont,
1509 add_I2(topRight_Rect(run->bounds), origin), 1679 add_I2(topRight_Rect(run->bounds), origin),
1510 fg, 1680 fg,
@@ -1527,9 +1697,8 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) {
1527 const int flags = linkFlags_GmDocument(doc, linkId); 1697 const int flags = linkFlags_GmDocument(doc, linkId);
1528 iUrl parts; 1698 iUrl parts;
1529 init_Url(&parts, url); 1699 init_Url(&parts, url);
1530 const iString *host = collect_String(newRange_String(parts.host));
1531 fg = linkColor_GmDocument(doc, linkId, textHover_GmLinkPart); 1700 fg = linkColor_GmDocument(doc, linkId, textHover_GmLinkPart);
1532 const iBool showHost = (!isEmpty_String(host) && flags & userFriendly_GmLinkFlag); 1701 const iBool showHost = (!isEmpty_Range(&parts.host) && flags & userFriendly_GmLinkFlag);
1533 const iBool showImage = (flags & imageFileExtension_GmLinkFlag) != 0; 1702 const iBool showImage = (flags & imageFileExtension_GmLinkFlag) != 0;
1534 const iBool showAudio = (flags & audioFileExtension_GmLinkFlag) != 0; 1703 const iBool showAudio = (flags & audioFileExtension_GmLinkFlag) != 0;
1535 iString str; 1704 iString str;
@@ -1541,11 +1710,16 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) {
1541 &str, 1710 &str,
1542 " \u2014%s%s%s\r%c%s", 1711 " \u2014%s%s%s\r%c%s",
1543 showHost ? " " : "", 1712 showHost ? " " : "",
1544 showHost ? cstr_String(host) : "", 1713 showHost ? (!equalCase_Rangecc(parts.scheme, "gemini")
1714 ? format_CStr("%s://%s",
1715 cstr_Rangecc(parts.scheme),
1716 cstr_Rangecc(parts.host))
1717 : cstr_Rangecc(parts.host))
1718 : "",
1545 showHost && (showImage || showAudio) ? " \u2014" : "", 1719 showHost && (showImage || showAudio) ? " \u2014" : "",
1546 showImage || showAudio 1720 showImage || showAudio
1547 ? '0' + fg 1721 ? asciiBase_ColorEscape + fg
1548 : ('0' + linkColor_GmDocument(doc, run->linkId, domain_GmLinkPart)), 1722 : (asciiBase_ColorEscape + linkColor_GmDocument(doc, run->linkId, domain_GmLinkPart)),
1549 showImage ? " View Image \U0001f5bc" 1723 showImage ? " View Image \U0001f5bc"
1550 : showAudio ? " Play Audio \U0001f3b5" : ""); 1724 : showAudio ? " Play Audio \U0001f3b5" : "");
1551 } 1725 }
@@ -1599,8 +1773,9 @@ static void draw_DocumentWidget_(const iDocumentWidget *d) {
1599 const iRect bounds = bounds_Widget(w); 1773 const iRect bounds = bounds_Widget(w);
1600 const iInt2 origin = topLeft_Rect(bounds); 1774 const iInt2 origin = topLeft_Rect(bounds);
1601 const iRangei visRange = visibleRange_DocumentWidget_(d); 1775 const iRangei visRange = visibleRange_DocumentWidget_(d);
1776 iVisBuffer * visBuf = d->visBuffer; /* this may be updated/modified here */
1602 draw_Widget(w); 1777 draw_Widget(w);
1603 allocVisBuffer_DocumentWidget_(iConstCast(iDocumentWidget *, d)); 1778 allocVisBuffer_DocumentWidget_(d);
1604 iDrawContext ctxDynamic = { 1779 iDrawContext ctxDynamic = {
1605 .pass = dynamic_DrawRunPass, 1780 .pass = dynamic_DrawRunPass,
1606 .widget = d, 1781 .widget = d,
@@ -1618,36 +1793,38 @@ static void draw_DocumentWidget_(const iDocumentWidget *d) {
1618 /* Static content. */ { 1793 /* Static content. */ {
1619 iPaint *p = &ctxStatic.paint; 1794 iPaint *p = &ctxStatic.paint;
1620 init_Paint(p); 1795 init_Paint(p);
1621 const int vbSrc = d->visBufferIndex; 1796 const int vbSrc = visBuf->index;
1622 const int vbDst = d->visBufferIndex ^ 1; 1797 const int vbDst = visBuf->index ^ 1;
1623 iRangei drawRange = visRange; 1798 iRangei drawRange = visRange;
1624 iAssert(d->visBuffer[vbDst]); 1799 iAssert(visBuf->texture[vbDst]);
1625 beginTarget_Paint(p, d->visBuffer[vbDst]); 1800 beginTarget_Paint(p, visBuf->texture[vbDst]);
1626 const iRect visBufferRect = { zero_I2(), d->visBufferSize }; 1801 const iRect visBufferRect = { zero_I2(), visBuf->size };
1627 iRect drawRect = visBufferRect; 1802 iRect drawRect = visBufferRect;
1628 if (!isEmpty_Rangei_(intersect_Rangei_(visRange, d->visBufferValidRange))) { 1803 if (!isEmpty_Rangei_(intersect_Rangei_(visRange, visBuf->validRange))) {
1629 if (visRange.start < d->visBufferValidRange.start) { 1804 if (visRange.start < visBuf->validRange.start) {
1630 drawRange = (iRangei){ visRange.start, d->visBufferValidRange.start }; 1805 drawRange = (iRangei){ visRange.start, visBuf->validRange.start };
1631 } 1806 }
1632 else { 1807 else {
1633 drawRange = (iRangei){ d->visBufferValidRange.end, visRange.end }; 1808 drawRange = (iRangei){ visBuf->validRange.end, visRange.end };
1634 } 1809 }
1635 if (isEmpty_Range(&drawRange)) { 1810 if (isEmpty_Range(&drawRange)) {
1636 SDL_RenderCopy(render, d->visBuffer[vbSrc], NULL, NULL); 1811 SDL_RenderCopy(render, visBuf->texture[vbSrc], NULL, NULL);
1637 } 1812 }
1638 else { 1813 else {
1639 SDL_RenderCopy( 1814 SDL_RenderCopy(
1640 render, 1815 render,
1641 d->visBuffer[vbSrc], 1816 visBuf->texture[vbSrc],
1642 NULL, 1817 NULL,
1643 &(SDL_Rect){ 0, 1818 &(SDL_Rect){ 0,
1644 documentToWindowY_DocumentWidget_(d, d->visBufferValidRange.start) - origin.y, 1819 documentToWindowY_DocumentWidget_(d, visBuf->validRange.start) -
1645 d->visBufferSize.x, 1820 origin.y,
1646 d->visBufferSize.y }); 1821 visBuf->size.x,
1647 drawRect = init_Rect(0, 1822 visBuf->size.y });
1648 documentToWindowY_DocumentWidget_(d, drawRange.start) - origin.y, 1823 drawRect =
1649 d->visBufferSize.x, 1824 init_Rect(0,
1650 size_Range(&drawRange)); 1825 documentToWindowY_DocumentWidget_(d, drawRange.start) - origin.y,
1826 visBuf->size.x,
1827 size_Range(&drawRange));
1651 } 1828 }
1652 } 1829 }
1653 if (!isEmpty_Range(&drawRange)) { 1830 if (!isEmpty_Range(&drawRange)) {
@@ -1657,17 +1834,20 @@ static void draw_DocumentWidget_(const iDocumentWidget *d) {
1657 unsetClip_Paint(p); 1834 unsetClip_Paint(p);
1658 } 1835 }
1659 endTarget_Paint(p); 1836 endTarget_Paint(p);
1660 SDL_RenderCopy(render, d->visBuffer[vbDst], NULL, 1837 SDL_RenderCopy(render, visBuf->texture[vbDst], NULL,
1661 &(SDL_Rect){ origin.x, origin.y, bounds.size.x, bounds.size.y } ); 1838 &(SDL_Rect){ origin.x, origin.y, bounds.size.x, bounds.size.y } );
1662 iConstCast(iDocumentWidget *, d)->visBufferValidRange = visRange; 1839 visBuf->validRange = visRange;
1663 iConstCast(iDocumentWidget *, d)->visBufferIndex = vbDst; 1840 visBuf->index = vbDst;
1664 } 1841 }
1665 /* Dynamic content. */ { 1842 /* Dynamic content. */ {
1843 extern int enableKerning_Text;
1844 enableKerning_Text = iFalse; /* need to be fast, these is redone on every redraw */
1666 iPaint *p = &ctxDynamic.paint; 1845 iPaint *p = &ctxDynamic.paint;
1667 init_Paint(p); 1846 init_Paint(p);
1668 setClip_Paint(p, bounds); 1847 setClip_Paint(p, bounds);
1669 render_GmDocument(d->doc, visRange, drawRun_DrawContext_, &ctxDynamic); 1848 render_GmDocument(d->doc, visRange, drawRun_DrawContext_, &ctxDynamic);
1670 unsetClip_Paint(p); 1849 unsetClip_Paint(p);
1850 enableKerning_Text = iTrue;
1671 } 1851 }
1672 1852
1673// drawRect_Paint(&ctx.paint, 1853// drawRect_Paint(&ctx.paint,
diff --git a/src/ui/documentwidget.h b/src/ui/documentwidget.h
index 074516c3..5a3125af 100644
--- a/src/ui/documentwidget.h
+++ b/src/ui/documentwidget.h
@@ -1,3 +1,25 @@
1/* Copyright 2020 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
1#pragma once 23#pragma once
2 24
3#include "widget.h" 25#include "widget.h"
@@ -23,5 +45,6 @@ const iString * bookmarkTitle_DocumentWidget (const iDocumentWidget *);
23void setUrl_DocumentWidget (iDocumentWidget *, const iString *url); 45void setUrl_DocumentWidget (iDocumentWidget *, const iString *url);
24void setUrlFromCache_DocumentWidget (iDocumentWidget *, const iString *url, iBool isFromCache); 46void setUrlFromCache_DocumentWidget (iDocumentWidget *, const iString *url, iBool isFromCache);
25void setInitialScroll_DocumentWidget (iDocumentWidget *, float normScrollY); /* set after content received */ 47void setInitialScroll_DocumentWidget (iDocumentWidget *, float normScrollY); /* set after content received */
48void setRedirectCount_DocumentWidget (iDocumentWidget *, int count);
26 49
27void updateSize_DocumentWidget (iDocumentWidget *); 50void updateSize_DocumentWidget (iDocumentWidget *);
diff --git a/src/ui/inputwidget.c b/src/ui/inputwidget.c
index d367952d..d583b109 100644
--- a/src/ui/inputwidget.c
+++ b/src/ui/inputwidget.c
@@ -1,3 +1,25 @@
1/* Copyright 2020 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
1#include "inputwidget.h" 23#include "inputwidget.h"
2#include "paint.h" 24#include "paint.h"
3#include "util.h" 25#include "util.h"
@@ -7,17 +29,40 @@
7#include <SDL_clipboard.h> 29#include <SDL_clipboard.h>
8#include <SDL_timer.h> 30#include <SDL_timer.h>
9 31
10static const int REFRESH_INTERVAL = 256; 32static const int refreshInterval_InputWidget_ = 256;
33static const size_t maxUndo_InputWidget_ = 64;
34
35iDeclareType(InputUndo)
36
37struct Impl_InputUndo {
38 iArray text;
39 size_t cursor;
40};
41
42static void init_InputUndo_(iInputUndo *d, const iArray *text, size_t cursor) {
43 initCopy_Array(&d->text, text);
44 d->cursor = cursor;
45}
46
47static void deinit_InputUndo_(iInputUndo *d) {
48 deinit_Array(&d->text);
49}
11 50
12struct Impl_InputWidget { 51struct Impl_InputWidget {
13 iWidget widget; 52 iWidget widget;
14 enum iInputMode mode; 53 enum iInputMode mode;
15 iBool isSensitive; 54 iBool isSensitive;
16 iBool enterPressed; 55 iBool enterPressed;
56 iBool selectAllOnFocus;
17 size_t maxLen; 57 size_t maxLen;
18 iArray text; /* iChar[] */ 58 iArray text; /* iChar[] */
19 iArray oldText; /* iChar[] */ 59 iArray oldText; /* iChar[] */
60 iString hint;
20 size_t cursor; 61 size_t cursor;
62 size_t lastCursor;
63 iBool isMarking;
64 iRanges mark;
65 iArray undoStack;
21 int font; 66 int font;
22 iClick click; 67 iClick click;
23 uint32_t timer; 68 uint32_t timer;
@@ -25,16 +70,29 @@ struct Impl_InputWidget {
25 70
26iDefineObjectConstructionArgs(InputWidget, (size_t maxLen), maxLen) 71iDefineObjectConstructionArgs(InputWidget, (size_t maxLen), maxLen)
27 72
73static void clearUndo_InputWidget_(iInputWidget *d) {
74 iForEach(Array, i, &d->undoStack) {
75 deinit_InputUndo_(i.value);
76 }
77 clear_Array(&d->undoStack);
78}
79
28void init_InputWidget(iInputWidget *d, size_t maxLen) { 80void init_InputWidget(iInputWidget *d, size_t maxLen) {
29 iWidget *w = &d->widget; 81 iWidget *w = &d->widget;
30 init_Widget(w); 82 init_Widget(w);
31 setFlags_Widget(w, focusable_WidgetFlag | hover_WidgetFlag, iTrue); 83 setFlags_Widget(w, focusable_WidgetFlag | hover_WidgetFlag, iTrue);
32 init_Array(&d->text, sizeof(iChar)); 84 init_Array(&d->text, sizeof(iChar));
33 init_Array(&d->oldText, sizeof(iChar)); 85 init_Array(&d->oldText, sizeof(iChar));
86 init_String(&d->hint);
87 init_Array(&d->undoStack, sizeof(iInputUndo));
34 d->font = uiInput_FontId; 88 d->font = uiInput_FontId;
35 d->cursor = 0; 89 d->cursor = 0;
36 d->isSensitive = iFalse; 90 d->lastCursor = 0;
37 d->enterPressed = iFalse; 91 d->isMarking = iFalse;
92 iZap(d->mark);
93 d->isSensitive = iFalse;
94 d->enterPressed = iFalse;
95 d->selectAllOnFocus = iFalse;
38 setMaxLen_InputWidget(d, maxLen); 96 setMaxLen_InputWidget(d, maxLen);
39 /* Caller must arrange the width, but the height is fixed. */ 97 /* Caller must arrange the width, but the height is fixed. */
40 w->rect.size.y = lineHeight_Text(default_FontId) + 2 * gap_UI; 98 w->rect.size.y = lineHeight_Text(default_FontId) + 2 * gap_UI;
@@ -44,13 +102,39 @@ void init_InputWidget(iInputWidget *d, size_t maxLen) {
44} 102}
45 103
46void deinit_InputWidget(iInputWidget *d) { 104void deinit_InputWidget(iInputWidget *d) {
105 clearUndo_InputWidget_(d);
106 deinit_Array(&d->undoStack);
47 if (d->timer) { 107 if (d->timer) {
48 SDL_RemoveTimer(d->timer); 108 SDL_RemoveTimer(d->timer);
49 } 109 }
110 deinit_String(&d->hint);
50 deinit_Array(&d->oldText); 111 deinit_Array(&d->oldText);
51 deinit_Array(&d->text); 112 deinit_Array(&d->text);
52} 113}
53 114
115static void pushUndo_InputWidget_(iInputWidget *d) {
116 iInputUndo undo;
117 init_InputUndo_(&undo, &d->text, d->cursor);
118 pushBack_Array(&d->undoStack, &undo);
119 if (size_Array(&d->undoStack) > maxUndo_InputWidget_) {
120 deinit_InputUndo_(front_Array(&d->undoStack));
121 popFront_Array(&d->undoStack);
122 }
123}
124
125static iBool popUndo_InputWidget_(iInputWidget *d) {
126 if (!isEmpty_Array(&d->undoStack)) {
127 iInputUndo *undo = back_Array(&d->undoStack);
128 setCopy_Array(&d->text, &undo->text);
129 d->cursor = undo->cursor;
130 deinit_InputUndo_(undo);
131 popBack_Array(&d->undoStack);
132 iZap(d->mark);
133 return iTrue;
134 }
135 return iFalse;
136}
137
54void setMode_InputWidget(iInputWidget *d, enum iInputMode mode) { 138void setMode_InputWidget(iInputWidget *d, enum iInputMode mode) {
55 d->mode = mode; 139 d->mode = mode;
56} 140}
@@ -78,7 +162,12 @@ void setMaxLen_InputWidget(iInputWidget *d, size_t maxLen) {
78 } 162 }
79} 163}
80 164
165void setHint_InputWidget(iInputWidget *d, const char *hintText) {
166 setCStr_String(&d->hint, hintText);
167}
168
81void setText_InputWidget(iInputWidget *d, const iString *text) { 169void setText_InputWidget(iInputWidget *d, const iString *text) {
170 clearUndo_InputWidget_(d);
82 clear_Array(&d->text); 171 clear_Array(&d->text);
83 iConstForEach(String, i, text) { 172 iConstForEach(String, i, text) {
84 pushBack_Array(&d->text, &i.value); 173 pushBack_Array(&d->text, &i.value);
@@ -92,10 +181,6 @@ void setTextCStr_InputWidget(iInputWidget *d, const char *cstr) {
92 delete_String(str); 181 delete_String(str);
93} 182}
94 183
95void setCursor_InputWidget(iInputWidget *d, size_t pos) {
96 d->cursor = iMin(pos, size_Array(&d->text));
97}
98
99static uint32_t refreshTimer_(uint32_t interval, void *d) { 184static uint32_t refreshTimer_(uint32_t interval, void *d) {
100 refresh_Widget(d); 185 refresh_Widget(d);
101 return interval; 186 return interval;
@@ -118,8 +203,14 @@ void begin_InputWidget(iInputWidget *d) {
118 SDL_StartTextInput(); 203 SDL_StartTextInput();
119 setFlags_Widget(w, selected_WidgetFlag, iTrue); 204 setFlags_Widget(w, selected_WidgetFlag, iTrue);
120 refresh_Widget(w); 205 refresh_Widget(w);
121 d->timer = SDL_AddTimer(REFRESH_INTERVAL, refreshTimer_, d); 206 d->timer = SDL_AddTimer(refreshInterval_InputWidget_, refreshTimer_, d);
122 d->enterPressed = iFalse; 207 d->enterPressed = iFalse;
208 if (d->selectAllOnFocus) {
209 d->mark = (iRanges){ 0, size_Array(&d->text) };
210 }
211 else {
212 iZap(d->mark);
213 }
123} 214}
124 215
125void end_InputWidget(iInputWidget *d, iBool accept) { 216void end_InputWidget(iInputWidget *d, iBool accept) {
@@ -159,6 +250,168 @@ static void insertChar_InputWidget_(iInputWidget *d, iChar chr) {
159 refresh_Widget(as_Widget(d)); 250 refresh_Widget(as_Widget(d));
160} 251}
161 252
253iLocalDef size_t cursorMax_InputWidget_(const iInputWidget *d) {
254 return iMin(size_Array(&d->text), d->maxLen - 1);
255}
256
257iLocalDef iBool isMarking_(void) {
258 return (SDL_GetModState() & KMOD_SHIFT) != 0;
259}
260
261void setCursor_InputWidget(iInputWidget *d, size_t pos) {
262 if (isEmpty_Array(&d->text)) {
263 d->cursor = 0;
264 }
265 else {
266 d->cursor = iClamp(pos, 0, cursorMax_InputWidget_(d));
267 }
268 /* Update selection. */
269 if (isMarking_()) {
270 if (isEmpty_Range(&d->mark)) {
271 d->mark.start = d->lastCursor;
272 d->mark.end = d->cursor;
273 }
274 else {
275 d->mark.end = d->cursor;
276 }
277 }
278 else {
279 iZap(d->mark);
280 }
281}
282
283void setSelectAllOnFocus_InputWidget(iInputWidget *d, iBool selectAllOnFocus) {
284 d->selectAllOnFocus = selectAllOnFocus;
285}
286
287static iRanges mark_InputWidget_(const iInputWidget *d) {
288 return (iRanges){ iMin(d->mark.start, d->mark.end), iMax(d->mark.start, d->mark.end) };
289}
290
291static iBool deleteMarked_InputWidget_(iInputWidget *d) {
292 const iRanges m = mark_InputWidget_(d);
293 if (!isEmpty_Range(&m)) {
294 removeRange_Array(&d->text, m);
295 setCursor_InputWidget(d, m.start);
296 iZap(d->mark);
297 return iTrue;
298 }
299 return iFalse;
300}
301
302static iBool isWordChar_InputWidget_(const iInputWidget *d, size_t pos) {
303 const iChar ch = pos < size_Array(&d->text) ? constValue_Array(&d->text, pos, iChar) : ' ';
304 return isAlphaNumeric_Char(ch);
305}
306
307iLocalDef iBool movePos_InputWidget_(const iInputWidget *d, size_t *pos, int dir) {
308 if (dir < 0) {
309 if (*pos > 0) (*pos)--; else return iFalse;
310 }
311 else {
312 if (*pos < cursorMax_InputWidget_(d)) (*pos)++; else return iFalse;
313 }
314 return iTrue;
315}
316
317static size_t skipWord_InputWidget_(const iInputWidget *d, size_t pos, int dir) {
318 const iBool startedAtNonWord = !isWordChar_InputWidget_(d, pos);
319 if (!movePos_InputWidget_(d, &pos, dir)) {
320 return pos;
321 }
322 /* Skip any non-word characters at start position. */
323 while (!isWordChar_InputWidget_(d, pos)) {
324 if (!movePos_InputWidget_(d, &pos, dir)) {
325 return pos;
326 }
327 }
328 if (startedAtNonWord && dir > 0) {
329 return pos; /* Found the start of a word. */
330 }
331 /* Skip the word. */
332 while (isWordChar_InputWidget_(d, pos)) {
333 if (!movePos_InputWidget_(d, &pos, dir)) {
334 return pos;
335 }
336 }
337 if (dir > 0) {
338 /* Skip to the beginning of the word. */
339 while (!isWordChar_InputWidget_(d, pos)) {
340 if (!movePos_InputWidget_(d, &pos, dir)) {
341 return pos;
342 }
343 }
344 }
345 else {
346 movePos_InputWidget_(d, &pos, +1);
347 }
348 return pos;
349}
350
351static const iChar sensitiveChar_ = 0x25cf; /* black circle */
352
353static iString *visText_InputWidget_(const iInputWidget *d) {
354 iString *text;
355 if (!d->isSensitive) {
356 text = newUnicodeN_String(constData_Array(&d->text), size_Array(&d->text));
357 }
358 else {
359 text = new_String();
360 for (size_t i = 0; i < size_Array(&d->text); ++i) {
361 appendChar_String(text, sensitiveChar_);
362 }
363 }
364 return text;
365}
366
367iLocalDef iInt2 padding_(void) {
368 return init_I2(gap_UI / 2, gap_UI / 2);
369}
370
371static iInt2 textOrigin_InputWidget_(const iInputWidget *d, const char *visText) {
372 const iWidget *w = constAs_Widget(d);
373 iRect bounds = adjusted_Rect(bounds_Widget(w), padding_(), neg_I2(padding_()));
374 const iInt2 emSize = advance_Text(d->font, "M");
375 const int textWidth = advance_Text(d->font, visText).x;
376 const int cursorX = advanceN_Text(d->font, visText, d->cursor).x;
377 int xOff = 0;
378 shrink_Rect(&bounds, init_I2(gap_UI * (flags_Widget(w) & tight_WidgetFlag ? 1 : 2), 0));
379 if (d->maxLen == 0) {
380 if (textWidth > width_Rect(bounds) - emSize.x) {
381 xOff = width_Rect(bounds) - emSize.x - textWidth;
382 }
383 if (cursorX + xOff < width_Rect(bounds) / 2) {
384 xOff = width_Rect(bounds) / 2 - cursorX;
385 }
386 xOff = iMin(xOff, 0);
387 }
388 const int yOff = (height_Rect(bounds) - lineHeight_Text(d->font)) / 2;
389 return add_I2(topLeft_Rect(bounds), init_I2(xOff, yOff));
390}
391
392static size_t coordIndex_InputWidget_(const iInputWidget *d, iInt2 coord) {
393 iString *visText = visText_InputWidget_(d);
394 iInt2 pos = sub_I2(coord, textOrigin_InputWidget_(d, cstr_String(visText)));
395 size_t index = 0;
396 if (pos.x > 0) {
397 const char *endPos;
398 tryAdvanceNoWrap_Text(d->font, range_String(visText), pos.x, &endPos);
399 if (endPos == constEnd_String(visText)) {
400 index = cursorMax_InputWidget_(d);
401 }
402 else {
403 /* Need to know the actual character index. */
404 /* TODO: tryAdvance could tell us this directly with an extra return value */
405 iConstForEach(String, i, visText) {
406 if (i.pos >= endPos) break;
407 index++;
408 }
409 }
410 }
411 delete_String(visText);
412 return index;
413}
414
162static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) { 415static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) {
163 iWidget *w = as_Widget(d); 416 iWidget *w = as_Widget(d);
164 if (isCommand_Widget(w, ev, "focus.gained")) { 417 if (isCommand_Widget(w, ev, "focus.gained")) {
@@ -173,25 +426,51 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) {
173 case none_ClickResult: 426 case none_ClickResult:
174 break; 427 break;
175 case started_ClickResult: 428 case started_ClickResult:
176 case drag_ClickResult: 429 setFocus_Widget(w);
430 setCursor_InputWidget(d, coordIndex_InputWidget_(d, pos_Click(&d->click)));
431 iZap(d->mark);
432 d->isMarking = iFalse;
433 return iTrue;
177 case double_ClickResult: 434 case double_ClickResult:
178 case aborted_ClickResult: 435 case aborted_ClickResult:
179 return iTrue; 436 return iTrue;
437 case drag_ClickResult:
438 d->cursor = coordIndex_InputWidget_(d, pos_Click(&d->click));
439 if (!d->isMarking) {
440 d->isMarking = iTrue;
441 d->mark.start = d->cursor;
442 }
443 d->mark.end = d->cursor;
444 refresh_Widget(w);
445 return iTrue;
180 case finished_ClickResult: 446 case finished_ClickResult:
181 setFocus_Widget(as_Widget(d));
182 return iTrue; 447 return iTrue;
183 } 448 }
184 if (ev->type == SDL_KEYUP && isFocused_Widget(w)) { 449 if (ev->type == SDL_KEYUP && isFocused_Widget(w)) {
185 return iTrue; 450 return iTrue;
186 } 451 }
187 const size_t curMax = iMin(size_Array(&d->text), d->maxLen - 1); 452 const size_t curMax = cursorMax_InputWidget_(d);
188 if (ev->type == SDL_KEYDOWN && isFocused_Widget(w)) { 453 if (ev->type == SDL_KEYDOWN && isFocused_Widget(w)) {
189 const int key = ev->key.keysym.sym; 454 const int key = ev->key.keysym.sym;
190 const int mods = keyMods_Sym(ev->key.keysym.mod); 455 const int mods = keyMods_Sym(ev->key.keysym.mod);
191 if (mods == KMOD_PRIMARY) { 456 if (mods == KMOD_PRIMARY) {
192 switch (key) { 457 switch (key) {
458 case 'c':
459 case 'x':
460 if (!isEmpty_Range(&d->mark)) {
461 const iRanges m = mark_InputWidget_(d);
462 SDL_SetClipboardText(cstrCollect_String(
463 newUnicodeN_String(constAt_Array(&d->text, m.start), size_Range(&m))));
464 if (key == 'x') {
465 pushUndo_InputWidget_(d);
466 deleteMarked_InputWidget_(d);
467 }
468 }
469 return iTrue;
193 case 'v': 470 case 'v':
194 if (SDL_HasClipboardText()) { 471 if (SDL_HasClipboardText()) {
472 pushUndo_InputWidget_(d);
473 deleteMarked_InputWidget_(d);
195 char *text = SDL_GetClipboardText(); 474 char *text = SDL_GetClipboardText();
196 iString *paste = collect_String(newCStr_String(text)); 475 iString *paste = collect_String(newCStr_String(text));
197 SDL_free(text); 476 SDL_free(text);
@@ -200,8 +479,14 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) {
200 } 479 }
201 } 480 }
202 return iTrue; 481 return iTrue;
482 case 'z':
483 if (popUndo_InputWidget_(d)) {
484 refresh_Widget(w);
485 }
486 return iTrue;
203 } 487 }
204 } 488 }
489 d->lastCursor = d->cursor;
205 switch (key) { 490 switch (key) {
206 case SDLK_RETURN: 491 case SDLK_RETURN:
207 case SDLK_KP_ENTER: 492 case SDLK_KP_ENTER:
@@ -213,11 +498,18 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) {
213 setFocus_Widget(NULL); 498 setFocus_Widget(NULL);
214 return iTrue; 499 return iTrue;
215 case SDLK_BACKSPACE: 500 case SDLK_BACKSPACE:
216 if (mods & KMOD_ALT) { 501 if (!isEmpty_Range(&d->mark)) {
217 clear_Array(&d->text); 502 pushUndo_InputWidget_(d);
218 d->cursor = 0; 503 deleteMarked_InputWidget_(d);
504 }
505 else if (mods & KMOD_ALT) {
506 pushUndo_InputWidget_(d);
507 d->mark.start = d->cursor;
508 d->mark.end = skipWord_InputWidget_(d, d->cursor, -1);
509 deleteMarked_InputWidget_(d);
219 } 510 }
220 else if (d->cursor > 0) { 511 else if (d->cursor > 0) {
512 pushUndo_InputWidget_(d);
221 remove_Array(&d->text, --d->cursor); 513 remove_Array(&d->text, --d->cursor);
222 } 514 }
223 refresh_Widget(w); 515 refresh_Widget(w);
@@ -225,49 +517,79 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) {
225 case SDLK_d: 517 case SDLK_d:
226 if (mods != KMOD_CTRL) break; 518 if (mods != KMOD_CTRL) break;
227 case SDLK_DELETE: 519 case SDLK_DELETE:
228 if (d->cursor < size_Array(&d->text)) { 520 if (!isEmpty_Range(&d->mark)) {
521 pushUndo_InputWidget_(d);
522 deleteMarked_InputWidget_(d);
523 }
524 else if (mods & KMOD_ALT) {
525 pushUndo_InputWidget_(d);
526 d->mark.start = d->cursor;
527 d->mark.end = skipWord_InputWidget_(d, d->cursor, +1);
528 deleteMarked_InputWidget_(d);
529 }
530 else if (d->cursor < size_Array(&d->text)) {
531 pushUndo_InputWidget_(d);
229 remove_Array(&d->text, d->cursor); 532 remove_Array(&d->text, d->cursor);
230 refresh_Widget(w);
231 } 533 }
534 refresh_Widget(w);
232 return iTrue; 535 return iTrue;
233 case SDLK_k: 536 case SDLK_k:
234 if (mods == KMOD_CTRL) { 537 if (mods == KMOD_CTRL) {
235 removeN_Array(&d->text, d->cursor, size_Array(&d->text) - d->cursor); 538 if (!isEmpty_Range(&d->mark)) {
539 pushUndo_InputWidget_(d);
540 deleteMarked_InputWidget_(d);
541 }
542 else {
543 pushUndo_InputWidget_(d);
544 removeN_Array(&d->text, d->cursor, size_Array(&d->text) - d->cursor);
545 }
236 refresh_Widget(w); 546 refresh_Widget(w);
237 return iTrue; 547 return iTrue;
238 } 548 }
239 break; 549 break;
240 case SDLK_HOME: 550 case SDLK_HOME:
241 case SDLK_END: 551 case SDLK_END:
242 d->cursor = (key == SDLK_HOME ? 0 : curMax); 552 setCursor_InputWidget(d, key == SDLK_HOME ? 0 : curMax);
243 refresh_Widget(w); 553 refresh_Widget(w);
244 return iTrue; 554 return iTrue;
245 case SDLK_a: 555 case SDLK_a:
556#if defined (iPlatformApple)
557 if (mods == KMOD_PRIMARY) {
558 d->mark.start = 0;
559 d->mark.end = curMax;
560 d->cursor = curMax;
561 refresh_Widget(w);
562 return iTrue;
563 }
564#endif
565 /* fall through for Emacs-style Home/End */
246 case SDLK_e: 566 case SDLK_e:
247 if (mods == KMOD_CTRL) { 567 if (mods == KMOD_CTRL || mods == (KMOD_CTRL | KMOD_SHIFT)) {
248 d->cursor = (key == 'a' ? 0 : curMax); 568 setCursor_InputWidget(d, key == 'a' ? 0 : curMax);
249 refresh_Widget(w); 569 refresh_Widget(w);
250 return iTrue; 570 return iTrue;
251 } 571 }
252 break; 572 break;
253 case SDLK_LEFT: 573 case SDLK_LEFT:
574 case SDLK_RIGHT: {
575 const int dir = (key == SDLK_LEFT ? -1 : +1);
254 if (mods & KMOD_PRIMARY) { 576 if (mods & KMOD_PRIMARY) {
255 d->cursor = 0; 577 setCursor_InputWidget(d, dir < 0 ? 0 : curMax);
256 } 578 }
257 else if (d->cursor > 0) { 579 else if (mods & KMOD_ALT) {
258 d->cursor--; 580 setCursor_InputWidget(d, skipWord_InputWidget_(d, d->cursor, dir));
259 } 581 }
260 refresh_Widget(w); 582 else if (!isMarking_() && !isEmpty_Range(&d->mark)) {
261 return iTrue; 583 const iRanges m = mark_InputWidget_(d);
262 case SDLK_RIGHT: 584 setCursor_InputWidget(d, dir < 0 ? m.start : m.end);
263 if (mods & KMOD_PRIMARY) { 585 iZap(d->mark);
264 d->cursor = curMax;
265 } 586 }
266 else if (d->cursor < curMax) { 587 else if ((dir < 0 && d->cursor > 0) || (dir > 0 && d->cursor < curMax)) {
267 d->cursor++; 588 setCursor_InputWidget(d, d->cursor + dir);
268 } 589 }
269 refresh_Widget(w); 590 refresh_Widget(w);
270 return iTrue; 591 return iTrue;
592 }
271 case SDLK_TAB: 593 case SDLK_TAB:
272 /* Allow focus switching. */ 594 /* Allow focus switching. */
273 return processEvent_Widget(as_Widget(d), ev); 595 return processEvent_Widget(as_Widget(d), ev);
@@ -278,6 +600,8 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) {
278 return iTrue; 600 return iTrue;
279 } 601 }
280 else if (ev->type == SDL_TEXTINPUT && isFocused_Widget(w)) { 602 else if (ev->type == SDL_TEXTINPUT && isFocused_Widget(w)) {
603 pushUndo_InputWidget_(d);
604 deleteMarked_InputWidget_(d);
281 const iString *uni = collectNewCStr_String(ev->text.text); 605 const iString *uni = collectNewCStr_String(ev->text.text);
282 iConstForEach(String, i, uni) { 606 iConstForEach(String, i, uni) {
283 insertChar_InputWidget_(d, i.value); 607 insertChar_InputWidget_(d, i.value);
@@ -287,27 +611,29 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) {
287 return processEvent_Widget(w, ev); 611 return processEvent_Widget(w, ev);
288} 612}
289 613
290static const iChar sensitiveChar_ = 0x25cf; /* black circle */ 614static iBool isWhite_(const iString *str) {
615 iConstForEach(String, i, str) {
616 if (!isSpace_Char(i.value)) {
617 return iFalse;
618 }
619 }
620 return iTrue;
621}
291 622
292static void draw_InputWidget_(const iInputWidget *d) { 623static void draw_InputWidget_(const iInputWidget *d) {
293 const iWidget *w = constAs_Widget(d); 624 const iWidget *w = constAs_Widget(d);
294 const uint32_t time = frameTime_Window(get_Window()); 625 const uint32_t time = frameTime_Window(get_Window());
295 const iInt2 padding = init_I2(gap_UI / 2, gap_UI / 2); 626 iRect bounds = adjusted_Rect(bounds_Widget(w), padding_(), neg_I2(padding_()));
296 iRect bounds = adjusted_Rect(bounds_Widget(w), padding, neg_I2(padding)); 627 iBool isHint = iFalse;
297 const iBool isFocused = isFocused_Widget(w); 628 const iBool isFocused = isFocused_Widget(w);
298 const iBool isHover = isHover_Widget(w) && 629 const iBool isHover = isHover_Widget(w) &&
299 contains_Widget(w, mouseCoord_Window(get_Window())); 630 contains_Widget(w, mouseCoord_Window(get_Window()));
300 iPaint p; 631 iPaint p;
301 init_Paint(&p); 632 init_Paint(&p);
302 iString text; 633 iString *text = visText_InputWidget_(d);
303 if (!d->isSensitive) { 634 if (isWhite_(text) && !isEmpty_String(&d->hint)) {
304 initUnicodeN_String(&text, constData_Array(&d->text), size_Array(&d->text)); 635 set_String(text, &d->hint);
305 } 636 isHint = iTrue;
306 else {
307 init_String(&text);
308 for (size_t i = 0; i < size_Array(&d->text); ++i) {
309 appendChar_String(&text, sensitiveChar_);
310 }
311 } 637 }
312 fillRect_Paint( 638 fillRect_Paint(
313 &p, bounds, isFocused ? uiInputBackgroundFocused_ColorId : uiInputBackground_ColorId); 639 &p, bounds, isFocused ? uiInputBackgroundFocused_ColorId : uiInputBackground_ColorId);
@@ -317,34 +643,27 @@ static void draw_InputWidget_(const iInputWidget *d) {
317 isFocused ? uiInputFrameFocused_ColorId 643 isFocused ? uiInputFrameFocused_ColorId
318 : isHover ? uiInputFrameHover_ColorId : uiInputFrame_ColorId); 644 : isHover ? uiInputFrameHover_ColorId : uiInputFrame_ColorId);
319 setClip_Paint(&p, bounds); 645 setClip_Paint(&p, bounds);
320 shrink_Rect(&bounds, init_I2(gap_UI * (flags_Widget(w) & tight_WidgetFlag ? 1 : 2), 0)); 646 const iInt2 textOrigin = textOrigin_InputWidget_(d, cstr_String(text));
321 const iInt2 emSize = advance_Text(d->font, "M"); 647 if (isFocused && !isEmpty_Range(&d->mark)) {
322 const int textWidth = advance_Text(d->font, cstr_String(&text)).x; 648 /* Draw the selected range. */
323 const int cursorX = advanceN_Text(d->font, cstr_String(&text), d->cursor).x; 649 const int m1 = advanceN_Text(d->font, cstr_String(text), d->mark.start).x;
324 int xOff = 0; 650 const int m2 = advanceN_Text(d->font, cstr_String(text), d->mark.end).x;
325 if (d->maxLen == 0) { 651 fillRect_Paint(&p,
326 if (textWidth > width_Rect(bounds) - emSize.x) { 652 (iRect){ addX_I2(textOrigin, iMin(m1, m2)),
327 xOff = width_Rect(bounds) - emSize.x - textWidth; 653 init_I2(iAbs(m2 - m1), lineHeight_Text(d->font)) },
328 } 654 uiMarked_ColorId);
329 if (cursorX + xOff < width_Rect(bounds) / 2) {
330 xOff = width_Rect(bounds) / 2 - cursorX;
331 }
332 xOff = iMin(xOff, 0);
333 } 655 }
334 const int yOff = (height_Rect(bounds) - lineHeight_Text(d->font)) / 2;
335 draw_Text(d->font, 656 draw_Text(d->font,
336 add_I2(topLeft_Rect(bounds), init_I2(xOff, yOff)), 657 textOrigin,
337 isFocused ? uiInputTextFocused_ColorId : uiInputText_ColorId, 658 isHint ? uiAnnotation_ColorId
659 : isFocused && !isEmpty_Array(&d->text) ? uiInputTextFocused_ColorId
660 : uiInputText_ColorId,
338 "%s", 661 "%s",
339 cstr_String(&text)); 662 cstr_String(text));
340 unsetClip_Paint(&p); 663 unsetClip_Paint(&p);
341 /* Cursor blinking. */ 664 /* Cursor blinking. */
342 if (isFocused && (time & 256)) { 665 if (isFocused && (time & 256)) {
343 const iInt2 prefixSize = advanceN_Text(d->font, cstr_String(&text), d->cursor); 666 iString cur;
344 const iInt2 curPos = init_I2(xOff + left_Rect(bounds) + prefixSize.x,
345 yOff + top_Rect(bounds));
346 const iRect curRect = { curPos, addX_I2(emSize, 1) };
347 iString cur;
348 if (d->cursor < size_Array(&d->text)) { 667 if (d->cursor < size_Array(&d->text)) {
349 if (!d->isSensitive) { 668 if (!d->isSensitive) {
350 initUnicodeN_String(&cur, constAt_Array(&d->text, d->cursor), 1); 669 initUnicodeN_String(&cur, constAt_Array(&d->text, d->cursor), 1);
@@ -356,11 +675,14 @@ static void draw_InputWidget_(const iInputWidget *d) {
356 else { 675 else {
357 initCStr_String(&cur, " "); 676 initCStr_String(&cur, " ");
358 } 677 }
678 const iInt2 prefixSize = advanceN_Text(d->font, cstr_String(text), d->cursor);
679 const iInt2 curPos = addX_I2(textOrigin, prefixSize.x);
680 const iRect curRect = { curPos, addX_I2(advance_Text(d->font, cstr_String(&cur)), 1) };
359 fillRect_Paint(&p, curRect, uiInputCursor_ColorId); 681 fillRect_Paint(&p, curRect, uiInputCursor_ColorId);
360 draw_Text(d->font, curPos, uiInputCursorText_ColorId, cstr_String(&cur)); 682 draw_Text(d->font, curPos, uiInputCursorText_ColorId, cstr_String(&cur));
361 deinit_String(&cur); 683 deinit_String(&cur);
362 } 684 }
363 deinit_String(&text); 685 delete_String(text);
364} 686}
365 687
366iBeginDefineSubclass(InputWidget, Widget) 688iBeginDefineSubclass(InputWidget, Widget)
diff --git a/src/ui/inputwidget.h b/src/ui/inputwidget.h
index 903e428a..fed9c3d9 100644
--- a/src/ui/inputwidget.h
+++ b/src/ui/inputwidget.h
@@ -1,3 +1,25 @@
1/* Copyright 2020 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
1#pragma once 23#pragma once
2 24
3#include "widget.h" 25#include "widget.h"
@@ -10,13 +32,21 @@ enum iInputMode {
10 overwrite_InputMode, 32 overwrite_InputMode,
11}; 33};
12 34
35void setHint_InputWidget (iInputWidget *, const char *hintText);
13void setSensitive_InputWidget(iInputWidget *, iBool isSensitive); 36void setSensitive_InputWidget(iInputWidget *, iBool isSensitive);
14void setMode_InputWidget (iInputWidget *, enum iInputMode mode); 37void setMode_InputWidget (iInputWidget *, enum iInputMode mode);
15void setMaxLen_InputWidget (iInputWidget *, size_t maxLen); 38void setMaxLen_InputWidget (iInputWidget *, size_t maxLen);
16void setText_InputWidget (iInputWidget *, const iString *text); 39void setText_InputWidget (iInputWidget *, const iString *text);
17void setTextCStr_InputWidget (iInputWidget *, const char *cstr); 40void setTextCStr_InputWidget (iInputWidget *, const char *cstr);
18void setCursor_InputWidget (iInputWidget *, size_t pos); 41void setCursor_InputWidget (iInputWidget *, size_t pos);
42void setSelectAllOnFocus_InputWidget(iInputWidget *, iBool selectAllOnFocus);
19void begin_InputWidget (iInputWidget *); 43void begin_InputWidget (iInputWidget *);
20void end_InputWidget (iInputWidget *, iBool accept); 44void end_InputWidget (iInputWidget *, iBool accept);
21 45
22const iString * text_InputWidget (const iInputWidget *); 46const iString * text_InputWidget (const iInputWidget *);
47
48iLocalDef iInputWidget *newHint_InputWidget(size_t maxLen, const char *hint) {
49 iInputWidget *d = new_InputWidget(maxLen);
50 setHint_InputWidget(d, hint);
51 return d;
52}
diff --git a/src/ui/labelwidget.c b/src/ui/labelwidget.c
index a3daff44..8b2506e7 100644
--- a/src/ui/labelwidget.c
+++ b/src/ui/labelwidget.c
@@ -1,3 +1,25 @@
1/* Copyright 2020 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
1#include "labelwidget.h" 23#include "labelwidget.h"
2#include "text.h" 24#include "text.h"
3#include "color.h" 25#include "color.h"
@@ -133,6 +155,9 @@ static void getColors_LabelWidget_(const iLabelWidget *d, int *bg, int *fg, int
133 *fg = uiText_ColorId; 155 *fg = uiText_ColorId;
134 *frame1 = isButton ? uiEmboss1_ColorId : uiFrame_ColorId; 156 *frame1 = isButton ? uiEmboss1_ColorId : uiFrame_ColorId;
135 *frame2 = isButton ? uiEmboss2_ColorId : *frame1; 157 *frame2 = isButton ? uiEmboss2_ColorId : *frame1;
158 if (flags_Widget(w) & disabled_WidgetFlag && isButton) {
159 *fg = uiTextDisabled_ColorId;
160 }
136 if (isSel) { 161 if (isSel) {
137 *bg = uiBackgroundSelected_ColorId; 162 *bg = uiBackgroundSelected_ColorId;
138 *fg = uiTextSelected_ColorId; 163 *fg = uiTextSelected_ColorId;
@@ -150,11 +175,11 @@ static void getColors_LabelWidget_(const iLabelWidget *d, int *bg, int *fg, int
150 /* Frames matching color escaped text. */ 175 /* Frames matching color escaped text. */
151 if (startsWith_String(&d->label, "\r")) { 176 if (startsWith_String(&d->label, "\r")) {
152 if (isDark_ColorTheme(colorTheme_App())) { 177 if (isDark_ColorTheme(colorTheme_App())) {
153 *frame1 = cstr_String(&d->label)[1] - '0'; 178 *frame1 = cstr_String(&d->label)[1] - asciiBase_ColorEscape;
154 *frame2 = darker_Color(*frame1); 179 *frame2 = darker_Color(*frame1);
155 } 180 }
156 else { 181 else {
157 *bg = *frame1 = *frame2 = cstr_String(&d->label)[1] - '0'; 182 *bg = *frame1 = *frame2 = cstr_String(&d->label)[1] - asciiBase_ColorEscape;
158 *fg = uiBackground_ColorId | permanent_ColorId; 183 *fg = uiBackground_ColorId | permanent_ColorId;
159 } 184 }
160 } 185 }
@@ -311,10 +336,20 @@ void setTextCStr_LabelWidget(iLabelWidget *d, const char *text) {
311 updateSize_LabelWidget(d); 336 updateSize_LabelWidget(d);
312} 337}
313 338
339const iString *label_LabelWidget(const iLabelWidget *d) {
340 return &d->label;
341}
342
314const iString *command_LabelWidget(const iLabelWidget *d) { 343const iString *command_LabelWidget(const iLabelWidget *d) {
315 return &d->command; 344 return &d->command;
316} 345}
317 346
347iLabelWidget *newColor_LabelWidget(const char *text, int color) {
348 iLabelWidget *d = new_LabelWidget(format_CStr("%s%s", escape_Color(color), text), 0, 0, NULL);
349 setFlags_Widget(as_Widget(d), frameless_WidgetFlag, iTrue);
350 return d;
351}
352
318iBeginDefineSubclass(LabelWidget, Widget) 353iBeginDefineSubclass(LabelWidget, Widget)
319 .processEvent = (iAny *) processEvent_LabelWidget_, 354 .processEvent = (iAny *) processEvent_LabelWidget_,
320 .draw = (iAny *) draw_LabelWidget_, 355 .draw = (iAny *) draw_LabelWidget_,
diff --git a/src/ui/labelwidget.h b/src/ui/labelwidget.h
index aaa897c9..c55ecd08 100644
--- a/src/ui/labelwidget.h
+++ b/src/ui/labelwidget.h
@@ -1,3 +1,25 @@
1/* Copyright 2020 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
1#pragma once 23#pragma once
2 24
3/* Text label/button. */ 25/* Text label/button. */
@@ -16,8 +38,11 @@ void updateSize_LabelWidget (iLabelWidget *);
16void updateText_LabelWidget (iLabelWidget *, const iString *text); /* not resized */ 38void updateText_LabelWidget (iLabelWidget *, const iString *text); /* not resized */
17void updateTextCStr_LabelWidget (iLabelWidget *, const char *text); /* not resized */ 39void updateTextCStr_LabelWidget (iLabelWidget *, const char *text); /* not resized */
18 40
41const iString *label_LabelWidget (const iLabelWidget *);
19const iString *command_LabelWidget (const iLabelWidget *); 42const iString *command_LabelWidget (const iLabelWidget *);
20 43
44iLabelWidget *newColor_LabelWidget(const char *text, int color);
45
21iLocalDef iLabelWidget *newEmpty_LabelWidget(void) { 46iLocalDef iLabelWidget *newEmpty_LabelWidget(void) {
22 return new_LabelWidget("", 0, 0, NULL); 47 return new_LabelWidget("", 0, 0, NULL);
23} 48}
diff --git a/src/ui/macos.h b/src/ui/macos.h
deleted file mode 100644
index a491e721..00000000
--- a/src/ui/macos.h
+++ /dev/null
@@ -1,9 +0,0 @@
1#pragma once
2
3#include "util.h"
4
5/* Platform-specific functionality for macOS */
6
7void setupApplication_MacOS (void);
8void insertMenuItems_MacOS (const char *menuLabel, int atIndex, const iMenuItem *items, size_t count);
9void handleCommand_MacOS (const char *cmd);
diff --git a/src/ui/macos.m b/src/ui/macos.m
deleted file mode 100644
index 9ff2f96e..00000000
--- a/src/ui/macos.m
+++ /dev/null
@@ -1,438 +0,0 @@
1#include "macos.h"
2#include "app.h"
3#include "command.h"
4#include "widget.h"
5#include "color.h"
6
7#import <AppKit/AppKit.h>
8
9#if 0
10static NSTouchBarItemIdentifier play_TouchId_ = @"fi.skyjake.BitwiseHarmony.play";
11static NSTouchBarItemIdentifier restart_TouchId_ = @"fi.skyjake.BitwiseHarmony.restart";
12
13static NSTouchBarItemIdentifier seqMoveUp_TouchId_ = @"fi.skyjake.BitwiseHarmony.sequence.move.up";
14static NSTouchBarItemIdentifier seqMoveDown_TouchId_ = @"fi.skyjake.BitwiseHarmony.sequence.move.down";
15
16static NSTouchBarItemIdentifier goto_TouchId_ = @"fi.skyjake.BitwiseHarmony.goto";
17static NSTouchBarItemIdentifier mute_TouchId_ = @"fi.skyjake.BitwiseHarmony.mute";
18static NSTouchBarItemIdentifier solo_TouchId_ = @"fi.skyjake.BitwiseHarmony.solo";
19static NSTouchBarItemIdentifier color_TouchId_ = @"fi.skyjake.BitwiseHarmony.color";
20static NSTouchBarItemIdentifier event_TouchId_ = @"fi.skyjake.BitwiseHarmony.event";
21
22static NSTouchBarItemIdentifier eventList_TouchId_ = @"fi.skyjake.BitwiseHarmony.eventlist";
23static NSTouchBarItemIdentifier masterGainEvent_TouchId_ = @"fi.skyjake.BitwiseHarmony.event.mastergain";
24static NSTouchBarItemIdentifier resetEvent_TouchId_ = @"fi.skyjake.BitwiseHarmony.event.reset";
25static NSTouchBarItemIdentifier voiceEvent_TouchId_ = @"fi.skyjake.BitwiseHarmony.event.voice";
26static NSTouchBarItemIdentifier panEvent_TouchId_ = @"fi.skyjake.BitwiseHarmony.event.pan";
27static NSTouchBarItemIdentifier gainEvent_TouchId_ = @"fi.skyjake.BitwiseHarmony.event.gain";
28static NSTouchBarItemIdentifier fadeEvent_TouchId_ = @"fi.skyjake.BitwiseHarmony.event.fade";
29static NSTouchBarItemIdentifier pitchSpeedEvent_TouchId_ = @"fi.skyjake.BitwiseHarmony.event.pitchspeed";
30static NSTouchBarItemIdentifier pitchBendUpEvent_TouchId_ = @"fi.skyjake.BitwiseHarmony.event.pitchbendup";
31static NSTouchBarItemIdentifier pitchBendDownEvent_TouchId_ = @"fi.skyjake.BitwiseHarmony.event.pitchbenddown";
32static NSTouchBarItemIdentifier tremoloEvent_TouchId_ = @"fi.skyjake.BitwiseHarmony.event.tremolo";
33#endif
34
35enum iTouchBarVariant {
36 default_TouchBarVariant,
37};
38
39@interface CommandButton : NSButtonTouchBarItem {
40 NSString *command;
41 iWidget *widget;
42}
43- (id)initWithIdentifier:(NSTouchBarItemIdentifier)identifier
44 title:(NSString *)title
45 command:(NSString *)cmd;
46- (id)initWithIdentifier:(NSTouchBarItemIdentifier)identifier
47 title:(NSString *)title
48 widget:(iWidget *)widget
49 command:(NSString *)cmd;
50- (void)dealloc;
51@end
52
53@implementation CommandButton
54
55- (id)initWithIdentifier:(NSTouchBarItemIdentifier)identifier
56 title:(NSString *)title
57 command:(NSString *)cmd {
58 [super initWithIdentifier:identifier];
59 self.title = title;
60 self.target = self;
61 self.action = @selector(buttonPressed);
62 command = cmd;
63 return self;
64}
65
66- (id)initWithIdentifier:(NSTouchBarItemIdentifier)identifier
67 title:(NSString *)title
68 widget:(iWidget *)aWidget
69 command:(NSString *)cmd {
70 [self initWithIdentifier:identifier title:title command:[cmd retain]];
71 widget = aWidget;
72 return self;
73}
74
75- (void)dealloc {
76 [command release];
77 [super dealloc];
78}
79
80- (void)buttonPressed {
81 const char *cmd = [command cStringUsingEncoding:NSUTF8StringEncoding];
82 if (widget) {
83 postCommand_Widget(widget, "%s", cmd);
84 }
85 else {
86 postCommand_App(cmd);
87 }
88}
89
90@end
91
92@interface MyDelegate : NSResponder<NSApplicationDelegate, NSTouchBarDelegate> {
93 enum iTouchBarVariant touchBarVariant;
94 NSObject<NSApplicationDelegate> *sdlDelegate;
95 NSMutableDictionary<NSString *, NSString*> *menuCommands;
96}
97- (id)initWithSDLDelegate:(NSObject<NSApplicationDelegate> *)sdl;
98//- (NSTouchBar *)makeTouchBar;
99/* SDL needs to do its own thing. */
100- (BOOL)application:(NSApplication *)theApplication openFile:(NSString *)filename;
101- (void)applicationDidFinishLaunching:(NSNotification *)notifications;
102@end
103
104@implementation MyDelegate
105
106- (id)initWithSDLDelegate:(NSObject<NSApplicationDelegate> *)sdl {
107 [super init];
108 menuCommands = [[NSMutableDictionary<NSString *, NSString *> alloc] init];
109 touchBarVariant = default_TouchBarVariant;
110 sdlDelegate = sdl;
111 return self;
112}
113
114- (void)dealloc {
115 [menuCommands release];
116 [super dealloc];
117}
118
119- (void)setTouchBarVariant:(enum iTouchBarVariant)variant {
120 touchBarVariant = variant;
121 self.touchBar = nil;
122}
123
124- (void)setCommand:(NSString *)command forMenuItem:(NSMenuItem *)menuItem {
125 [menuCommands setObject:command forKey:[menuItem title]];
126}
127
128- (BOOL)application:(NSApplication *)theApplication openFile:(NSString *)filename {
129 return [sdlDelegate application:theApplication openFile:filename];
130}
131
132- (void)applicationDidFinishLaunching:(NSNotification *)notification {
133 [sdlDelegate applicationDidFinishLaunching:notification];
134}
135
136#if 0
137- (NSTouchBar *)makeTouchBar {
138 NSTouchBar *bar = [[NSTouchBar alloc] init];
139 bar.delegate = self;
140 switch (touchBarVariant) {
141 case default_TouchBarVariant:
142 bar.defaultItemIdentifiers = @[ play_TouchId_, restart_TouchId_,
143 NSTouchBarItemIdentifierFixedSpaceSmall,
144 NSTouchBarItemIdentifierOtherItemsProxy ];
145 break;
146 case sequence_TouchBarVariant:
147 bar.defaultItemIdentifiers = @[ play_TouchId_, restart_TouchId_,
148 NSTouchBarItemIdentifierFlexibleSpace,
149 seqMoveUp_TouchId_, seqMoveDown_TouchId_,
150 NSTouchBarItemIdentifierFlexibleSpace,
151 NSTouchBarItemIdentifierOtherItemsProxy];
152 break;
153 case tracker_TouchBarVariant:
154 bar.defaultItemIdentifiers = @[ play_TouchId_, restart_TouchId_,
155 NSTouchBarItemIdentifierFlexibleSpace,
156 goto_TouchId_,
157 event_TouchId_,
158 NSTouchBarItemIdentifierFlexibleSpace,
159 solo_TouchId_, mute_TouchId_, color_TouchId_,
160 NSTouchBarItemIdentifierFlexibleSpace,
161 NSTouchBarItemIdentifierOtherItemsProxy ];
162 break;
163 case wide_TouchBarVariant:
164 bar.defaultItemIdentifiers = @[ play_TouchId_, restart_TouchId_,
165 NSTouchBarItemIdentifierFlexibleSpace,
166 event_TouchId_,
167 NSTouchBarItemIdentifierFlexibleSpace,
168 solo_TouchId_, mute_TouchId_, color_TouchId_,
169 NSTouchBarItemIdentifierFlexibleSpace,
170 seqMoveUp_TouchId_, seqMoveDown_TouchId_,
171 NSTouchBarItemIdentifierFlexibleSpace,
172 NSTouchBarItemIdentifierOtherItemsProxy ];
173 break;
174 }
175 return bar;
176}
177#endif
178
179- (void)showPreferences {
180 postCommand_App("preferences");
181}
182
183- (void)closeTab {
184 postCommand_App("tabs.close");
185}
186
187- (void)postMenuItemCommand:(id)sender {
188 NSString *command = [menuCommands objectForKey:[(NSMenuItem *)sender title]];
189 if (command) {
190 postCommand_App([command cStringUsingEncoding:NSUTF8StringEncoding]);
191 }
192}
193
194- (nullable NSTouchBarItem *)touchBar:(NSTouchBar *)touchBar
195 makeItemForIdentifier:(NSTouchBarItemIdentifier)identifier {
196 iUnused(touchBar);
197#if 0
198 if ([identifier isEqualToString:play_TouchId_]) {
199 return [NSButtonTouchBarItem
200 buttonTouchBarItemWithIdentifier:identifier
201 image:[NSImage imageNamed:NSImageNameTouchBarPlayPauseTemplate]
202 target:self
203 action:@selector(playPressed)];
204 }
205 else if ([identifier isEqualToString:restart_TouchId_]) {
206 return [NSButtonTouchBarItem
207 buttonTouchBarItemWithIdentifier:identifier
208 image:[NSImage imageNamed:NSImageNameTouchBarSkipToStartTemplate]
209 target:self
210 action:@selector(restartPressed)];
211 }
212 else if ([identifier isEqualToString:seqMoveUp_TouchId_]) {
213 return [[CommandButton alloc] initWithIdentifier:identifier
214 title:@"Seq\u2b06"
215 widget:findWidget_App("sequence")
216 command:@"sequence.swap arg:-1"];
217 }
218 else if ([identifier isEqualToString:seqMoveDown_TouchId_]) {
219 return [[CommandButton alloc] initWithIdentifier:identifier
220 title:@"Seq\u2b07"
221 widget:findWidget_App("sequence")
222 command:@"sequence.swap arg:1"];
223 }
224 else if ([identifier isEqualToString:goto_TouchId_]) {
225 return [[CommandButton alloc] initWithIdentifier:identifier
226 title:@"Go to…"
227 command:@"pattern.goto arg:-1"];
228 }
229 else if ([identifier isEqualToString:event_TouchId_]) {
230 NSTouchBar *events = [[NSTouchBar alloc] init];
231 events.delegate = self;
232 events.defaultItemIdentifiers = @[ eventList_TouchId_ ];
233 NSPopoverTouchBarItem *pop = [[NSPopoverTouchBarItem alloc] initWithIdentifier:identifier];
234 pop.collapsedRepresentationLabel = @"Event";
235 pop.popoverTouchBar = events;
236 [events release];
237 return pop;
238 }
239 else if ([identifier isEqualToString:eventList_TouchId_]) {
240 const struct {
241 NSTouchBarItemIdentifier id;
242 const char *title;
243 const char *command;
244 } buttonDefs_[] = {
245 { voiceEvent_TouchId_, "Voice", "tracker.setevent type:2" },
246 { panEvent_TouchId_, "Pan", "tracker.setevent type:3 arg:128" },
247 { gainEvent_TouchId_, "Gain", "tracker.setevent type:4 arg:128" },
248 { fadeEvent_TouchId_, "Fade", "tracker.setevent type:5" },
249 { tremoloEvent_TouchId_, "Trem", "tracker.setevent type:9" },
250 { pitchSpeedEvent_TouchId_, "P.Spd", "tracker.setevent type:6" },
251 { pitchBendUpEvent_TouchId_, "BnUp", "tracker.setevent type:7" },
252 { pitchBendDownEvent_TouchId_, "BnDn", "tracker.setevent type:8" },
253 { masterGainEvent_TouchId_, "M.Gain", "tracker.setevent type:10 arg:64" },
254 { resetEvent_TouchId_, "Reset", "tracker.setevent type:1" },
255 };
256 NSMutableArray *items = [[NSMutableArray alloc] init];
257 iForIndices(i, buttonDefs_) {
258 CommandButton *button = [[CommandButton alloc]
259 initWithIdentifier:buttonDefs_[i].id
260 title:[NSString stringWithUTF8String:buttonDefs_[i].title]
261 widget:findWidget_App("tracker")
262 command:[NSString stringWithUTF8String:buttonDefs_[i].command]
263 ];
264 [items addObject:button];
265 }
266 NSGroupTouchBarItem *group = [NSGroupTouchBarItem groupItemWithIdentifier:identifier
267 items:items];
268 [items release];
269 return group;
270 }
271 else if ([identifier isEqualToString:mute_TouchId_]) {
272 return [[CommandButton alloc] initWithIdentifier:identifier
273 title:@"Mute"
274 widget:findWidget_App("tracker")
275 command:@"tracker.mute"];
276 }
277 else if ([identifier isEqualToString:solo_TouchId_]) {
278 return [[CommandButton alloc] initWithIdentifier:identifier
279 title:@"Solo"
280 widget:findWidget_App("tracker")
281 command:@"tracker.solo"];
282 }
283 else if ([identifier isEqualToString:color_TouchId_]) {
284 NSTouchBar *colors = [[NSTouchBar alloc] init];
285 colors.delegate = self;
286 colors.defaultItemIdentifiers = @[ NSTouchBarItemIdentifierFlexibleSpace,
287 whiteColor_TouchId_,
288 yellowColor_TouchId_,
289 orangeColor_TouchId_,
290 redColor_TouchId_,
291 magentaColor_TouchId_,
292 blueColor_TouchId_,
293 cyanColor_TouchId_,
294 greenColor_TouchId_,
295 NSTouchBarItemIdentifierFlexibleSpace ];
296 NSPopoverTouchBarItem *pop = [[NSPopoverTouchBarItem alloc] initWithIdentifier:identifier];
297 pop.collapsedRepresentationImage = [NSImage imageNamed:NSImageNameTouchBarColorPickerFill];
298 pop.popoverTouchBar = colors;
299 [colors release];
300 return pop;
301 }
302 else if ([identifier isEqualToString:whiteColor_TouchId_]) {
303 return [[ColorButton alloc] initWithIdentifier:identifier
304 trackColor:white_TrackColor];
305 }
306 else if ([identifier isEqualToString:yellowColor_TouchId_]) {
307 return [[ColorButton alloc] initWithIdentifier:identifier
308 trackColor:yellow_TrackColor];
309 }
310 else if ([identifier isEqualToString:orangeColor_TouchId_]) {
311 return [[ColorButton alloc] initWithIdentifier:identifier
312 trackColor:orange_TrackColor];
313 }
314 else if ([identifier isEqualToString:redColor_TouchId_]) {
315 return [[ColorButton alloc] initWithIdentifier:identifier
316 trackColor:red_TrackColor];
317 }
318 else if ([identifier isEqualToString:magentaColor_TouchId_]) {
319 return [[ColorButton alloc] initWithIdentifier:identifier
320 trackColor:magenta_TrackColor];
321 }
322 else if ([identifier isEqualToString:blueColor_TouchId_]) {
323 return [[ColorButton alloc] initWithIdentifier:identifier
324 trackColor:blue_TrackColor];
325 }
326 else if ([identifier isEqualToString:cyanColor_TouchId_]) {
327 return [[ColorButton alloc] initWithIdentifier:identifier
328 trackColor:cyan_TrackColor];
329 }
330 else if ([identifier isEqualToString:greenColor_TouchId_]) {
331 return [[ColorButton alloc] initWithIdentifier:identifier
332 trackColor:green_TrackColor];
333 }
334#endif
335 return nil;
336}
337
338@end
339
340void enableMomentumScroll_MacOS(void) {
341 [[NSUserDefaults standardUserDefaults] setBool: YES
342 forKey: @"AppleMomentumScrollSupported"];
343}
344
345void setupApplication_MacOS(void) {
346 NSApplication *app = [NSApplication sharedApplication];
347 /* Our delegate will override SDL's delegate. */
348 MyDelegate *myDel = [[MyDelegate alloc] initWithSDLDelegate:app.delegate];
349 app.delegate = myDel;
350 NSMenu *appMenu = [[[NSApp mainMenu] itemAtIndex:0] submenu];
351 NSMenuItem *prefsItem = [appMenu itemWithTitle:@"Preferences…"];
352 prefsItem.target = myDel;
353 prefsItem.action = @selector(showPreferences);
354 /* Get rid of the default window close item */
355 NSMenu *windowMenu = [[[NSApp mainMenu] itemWithTitle:@"Window"] submenu];
356 NSMenuItem *windowCloseItem = [windowMenu itemWithTitle:@"Close"];
357 windowCloseItem.target = myDel;
358 windowCloseItem.action = @selector(closeTab);
359}
360
361void insertMenuItems_MacOS(const char *menuLabel, int atIndex, const iMenuItem *items, size_t count) {
362 NSApplication *app = [NSApplication sharedApplication];
363 MyDelegate *myDel = (MyDelegate *) app.delegate;
364 NSMenu *appMenu = [app mainMenu];
365 NSMenuItem *mainItem = [appMenu insertItemWithTitle:[NSString stringWithUTF8String:menuLabel]
366 action:nil
367 keyEquivalent:@""
368 atIndex:atIndex];
369 NSMenu *menu = [[NSMenu alloc] initWithTitle:[NSString stringWithUTF8String:menuLabel]];
370 for (size_t i = 0; i < count; ++i) {
371 const char *label = items[i].label;
372 if (label[0] == '\r') {
373 /* Skip the formatting escape. */
374 label += 2;
375 }
376 if (equal_CStr(label, "---")) {
377 [menu addItem:[NSMenuItem separatorItem]];
378 }
379 else {
380 const iBool hasCommand = (items[i].command && items[i].command[0]);
381 iString key;
382 init_String(&key);
383 if (items[i].key == SDLK_LEFT) {
384 appendChar_String(&key, 0x2190);
385 }
386 else if (items[i].key == SDLK_RIGHT) {
387 appendChar_String(&key, 0x2192);
388 }
389 else if (items[i].key) {
390 appendChar_String(&key, items[i].key);
391 }
392 NSMenuItem *item = [menu addItemWithTitle:[NSString stringWithUTF8String:label]
393 action:(hasCommand ? @selector(postMenuItemCommand:) : nil)
394 keyEquivalent:[NSString stringWithUTF8String:cstr_String(&key)]];
395 NSEventModifierFlags modMask = 0;
396 if (items[i].kmods & KMOD_GUI) {
397 modMask |= NSEventModifierFlagCommand;
398 }
399 if (items[i].kmods & KMOD_ALT) {
400 modMask |= NSEventModifierFlagOption;
401 }
402 if (items[i].kmods & KMOD_CTRL) {
403 modMask |= NSEventModifierFlagControl;
404 }
405 if (items[i].kmods & KMOD_SHIFT) {
406 modMask |= NSEventModifierFlagShift;
407 }
408 [item setKeyEquivalentModifierMask:modMask];
409 if (hasCommand) {
410 [myDel setCommand:[NSString stringWithUTF8String:items[i].command] forMenuItem:item];
411 }
412 deinit_String(&key);
413 }
414 }
415 [mainItem setSubmenu:menu];
416 [menu release];
417}
418
419void handleCommand_MacOS(const char *cmd) {
420#if 0
421 if (equal_Command(cmd, "tabs.changed")) {
422 MyDelegate *myDel = (MyDelegate *) [[NSApplication sharedApplication] delegate];
423 const char *tabId = suffixPtr_Command(cmd, "id");
424 if (equal_CStr(tabId, "tracker")) {
425 [myDel setTouchBarVariant:tracker_TouchBarVariant];
426 }
427 else if (equal_CStr(tabId, "sequence")) {
428 [myDel setTouchBarVariant:sequence_TouchBarVariant];
429 }
430 else if (equal_CStr(tabId, "trackertab")) {
431 [myDel setTouchBarVariant:wide_TouchBarVariant];
432 }
433 else {
434 [myDel setTouchBarVariant:default_TouchBarVariant];
435 }
436 }
437#endif
438}
diff --git a/src/ui/metrics.c b/src/ui/metrics.c
index 3c4bfd75..537aaf98 100644
--- a/src/ui/metrics.c
+++ b/src/ui/metrics.c
@@ -1,3 +1,25 @@
1/* Copyright 2020 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
1#include "metrics.h" 23#include "metrics.h"
2 24
3#include <the_Foundation/math.h> 25#include <the_Foundation/math.h>
diff --git a/src/ui/metrics.h b/src/ui/metrics.h
index 39fc6415..207dd59d 100644
--- a/src/ui/metrics.h
+++ b/src/ui/metrics.h
@@ -1,3 +1,25 @@
1/* Copyright 2020 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
1#pragma once 23#pragma once
2 24
3#include <the_Foundation/vec2.h> 25#include <the_Foundation/vec2.h>
diff --git a/src/ui/paint.c b/src/ui/paint.c
index 264ca0d8..0166e398 100644
--- a/src/ui/paint.c
+++ b/src/ui/paint.c
@@ -1,3 +1,25 @@
1/* Copyright 2020 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
1#include "paint.h" 23#include "paint.h"
2 24
3#include <SDL_version.h> 25#include <SDL_version.h>
diff --git a/src/ui/paint.h b/src/ui/paint.h
index 5b29b176..7c9e5e51 100644
--- a/src/ui/paint.h
+++ b/src/ui/paint.h
@@ -1,3 +1,25 @@
1/* Copyright 2020 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
1#pragma once 23#pragma once
2 24
3#include <the_Foundation/rect.h> 25#include <the_Foundation/rect.h>
diff --git a/src/ui/scrollwidget.c b/src/ui/scrollwidget.c
index 3d8f5eaa..f5340897 100644
--- a/src/ui/scrollwidget.c
+++ b/src/ui/scrollwidget.c
@@ -1,3 +1,25 @@
1/* Copyright 2020 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
1#include "scrollwidget.h" 23#include "scrollwidget.h"
2#include "paint.h" 24#include "paint.h"
3#include "util.h" 25#include "util.h"
@@ -39,6 +61,7 @@ static iRect thumbRect_ScrollWidget_(const iScrollWidget *d) {
39 const int total = size_Range(&d->range); 61 const int total = size_Range(&d->range);
40 if (total > 0) { 62 if (total > 0) {
41 const int tsize = thumbSize_ScrollWidget_(d); 63 const int tsize = thumbSize_ScrollWidget_(d);
64// iAssert(tsize <= height_Rect(bounds));
42 const int tpos = 65 const int tpos =
43 iClamp((float) d->thumb / (float) total, 0, 1) * (height_Rect(bounds) - tsize); 66 iClamp((float) d->thumb / (float) total, 0, 1) * (height_Rect(bounds) - tsize);
44 rect.pos.y = bounds.pos.y + tpos; 67 rect.pos.y = bounds.pos.y + tpos;
diff --git a/src/ui/scrollwidget.h b/src/ui/scrollwidget.h
index 7b44dced..e6cda03d 100644
--- a/src/ui/scrollwidget.h
+++ b/src/ui/scrollwidget.h
@@ -1,3 +1,25 @@
1/* Copyright 2020 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
1#pragma once 23#pragma once
2 24
3#include "widget.h" 25#include "widget.h"
diff --git a/src/ui/sidebarwidget.c b/src/ui/sidebarwidget.c
index 71b641d4..40c3f55e 100644
--- a/src/ui/sidebarwidget.c
+++ b/src/ui/sidebarwidget.c
@@ -1,16 +1,41 @@
1/* Copyright 2020 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
1#include "sidebarwidget.h" 23#include "sidebarwidget.h"
2#include "labelwidget.h" 24
3#include "scrollwidget.h" 25#include "app.h"
26#include "bookmarks.h"
27#include "command.h"
4#include "documentwidget.h" 28#include "documentwidget.h"
29#include "gmcerts.h"
30#include "gmdocument.h"
5#include "inputwidget.h" 31#include "inputwidget.h"
6#include "bookmarks.h" 32#include "labelwidget.h"
7#include "paint.h" 33#include "paint.h"
34#include "scrollwidget.h"
8#include "util.h" 35#include "util.h"
9#include "command.h" 36#include "visited.h"
10#include "../gmdocument.h"
11#include "app.h"
12 37
13#include <the_Foundation/array.h> 38#include <the_Foundation/stringarray.h>
14#include <SDL_clipboard.h> 39#include <SDL_clipboard.h>
15#include <SDL_mouse.h> 40#include <SDL_mouse.h>
16 41
@@ -23,6 +48,8 @@ struct Impl_SidebarItem {
23 iString label; 48 iString label;
24 iString meta; 49 iString meta;
25 iString url; 50 iString url;
51 iBool isSeparator;
52 iBool isSelected;
26}; 53};
27 54
28void init_SidebarItem(iSidebarItem *d) { 55void init_SidebarItem(iSidebarItem *d) {
@@ -32,6 +59,8 @@ void init_SidebarItem(iSidebarItem *d) {
32 init_String(&d->label); 59 init_String(&d->label);
33 init_String(&d->meta); 60 init_String(&d->meta);
34 init_String(&d->url); 61 init_String(&d->url);
62 d->isSeparator = iFalse;
63 d->isSelected = iFalse;
35} 64}
36 65
37void deinit_SidebarItem(iSidebarItem *d) { 66void deinit_SidebarItem(iSidebarItem *d) {
@@ -49,6 +78,7 @@ struct Impl_SidebarWidget {
49 enum iSidebarMode mode; 78 enum iSidebarMode mode;
50 iScrollWidget *scroll; 79 iScrollWidget *scroll;
51 int scrollY; 80 int scrollY;
81 int modeScroll[max_SidebarMode];
52 int width; 82 int width;
53 iLabelWidget *modeButtons[max_SidebarMode]; 83 iLabelWidget *modeButtons[max_SidebarMode];
54 int itemHeight; 84 int itemHeight;
@@ -141,12 +171,7 @@ static void updateItems_SidebarWidget_(iSidebarWidget *d) {
141 item.icon = bm->icon; 171 item.icon = bm->icon;
142 set_String(&item.url, &bm->url); 172 set_String(&item.url, &bm->url);
143 set_String(&item.label, &bm->title); 173 set_String(&item.label, &bm->title);
144// iDate date;
145// init_Date(&date, &bm->when);
146// iString *ds = format_Date(&date, "%Y %b %d");
147// set_String(&item.meta, ds);
148 set_String(&item.meta, &bm->tags); 174 set_String(&item.meta, &bm->tags);
149// delete_String(ds);
150 pushBack_Array(&d->items, &item); 175 pushBack_Array(&d->items, &item);
151 } 176 }
152 d->menu = makeMenu_Widget( 177 d->menu = makeMenu_Widget(
@@ -158,8 +183,97 @@ static void updateItems_SidebarWidget_(iSidebarWidget *d) {
158 4); 183 4);
159 break; 184 break;
160 } 185 }
161 case history_SidebarMode: 186 case history_SidebarMode: {
187 iDate on;
188 initCurrent_Date(&on);
189 const int thisYear = on.year;
190 iConstForEach(PtrArray, i, list_Visited(visited_App(), 200)) {
191 const iVisitedUrl *visit = i.ptr;
192 iSidebarItem item;
193 init_SidebarItem(&item);
194 set_String(&item.url, &visit->url);
195 iDate date;
196 init_Date(&date, &visit->when);
197 if (date.day != on.day || date.month != on.month || date.year != on.year) {
198 on = date;
199 /* Date separator. */
200 iSidebarItem sep;
201 init_SidebarItem(&sep);
202 sep.isSeparator = iTrue;
203 set_String(&sep.meta,
204 collect_String(format_Date(
205 &date, date.year != thisYear ? "%b %d %Y" : "%b %d")));
206 pushBack_Array(&d->items, &sep);
207 /* Date separators are two items tall. */
208 init_SidebarItem(&sep);
209 sep.isSeparator = iTrue;
210 pushBack_Array(&d->items, &sep);
211 }
212 pushBack_Array(&d->items, &item);
213 }
214 d->menu = makeMenu_Widget(
215 as_Widget(d),
216 (iMenuItem[]){
217 { "Copy URL", 0, 0, "history.copy" },
218 { "Add Bookmark...", 0, 0, "history.addbookmark" },
219 { "---", 0, 0, NULL },
220 { "Remove URL", 0, 0, "history.delete" },
221 { "---", 0, 0, NULL },
222 { uiTextCaution_ColorEscape "Clear History...", 0, 0, "history.clear confirm:1" },
223 }, 6);
224 break;
225 }
226 case identities_SidebarMode: {
227 const iString *tabUrl = url_DocumentWidget(document_App());
228 iConstForEach(PtrArray, i, identities_GmCerts(certs_App())) {
229 const iGmIdentity *ident = i.ptr;
230 iSidebarItem item;
231 init_SidebarItem(&item);
232 item.id = index_PtrArrayConstIterator(&i);
233 item.icon = ident->icon;
234 set_String(&item.label, collect_String(subject_TlsCertificate(ident->cert)));
235 iDate until;
236 validUntil_TlsCertificate(ident->cert, &until);
237 const iBool isActive = isUsedOn_GmIdentity(ident, tabUrl);
238 format_String(
239 &item.meta,
240 "%s",
241 isActive ? "Using"
242 : isUsed_GmIdentity(ident)
243 ? format_CStr("Used on %zu URLs", size_StringSet(ident->useUrls))
244 : "Not used");
245 const char *expiry =
246 ident->flags & temporary_GmIdentityFlag
247 ? "Temporary"
248 : cstrCollect_String(format_Date(&until, "Expires %b %d, %Y"));
249 if (isEmpty_String(&ident->notes)) {
250 appendFormat_String(&item.meta, "\n%s", expiry);
251 }
252 else {
253 appendFormat_String(&item.meta,
254 " \u2014 %s\n%s%s",
255 expiry,
256 escape_Color(uiHeading_ColorId),
257 cstr_String(&ident->notes));
258 }
259 item.isSelected = isActive;
260 pushBack_Array(&d->items, &item);
261 }
262 const iMenuItem menuItems[] = {
263 { "Use on This Page", 0, 0, "ident.use arg:1" },
264 { "Stop Using This Page", 0, 0, "ident.use arg:0" },
265 { "Stop Using Everywhere", 0, 0, "ident.use arg:0 clear:1" },
266 { "Show Usage", 0, 0, "ident.showuse" },
267 { "---", 0, 0, NULL },
268 { "Edit Notes...", 0, 0, "ident.edit" },
269 { "Pick Icon...", 0, 0, "ident.pickicon" },
270 { "---", 0, 0, NULL },
271 { "Reveal Files", 0, 0, "ident.reveal" },
272 { uiTextCaution_ColorEscape "Delete Identity...", 0, 0, "ident.delete confirm:1" },
273 };
274 d->menu = makeMenu_Widget(as_Widget(d), menuItems, iElemCount(menuItems));
162 break; 275 break;
276 }
163 default: 277 default:
164 break; 278 break;
165 } 279 }
@@ -167,14 +281,35 @@ static void updateItems_SidebarWidget_(iSidebarWidget *d) {
167 invalidate_SidebarWidget_(d); 281 invalidate_SidebarWidget_(d);
168} 282}
169 283
284static void scroll_SidebarWidget_(iSidebarWidget *d, int offset) {
285 const int oldScroll = d->scrollY;
286 d->scrollY += offset;
287 if (d->scrollY < 0) {
288 d->scrollY = 0;
289 }
290 const int scrollMax = scrollMax_SidebarWidget_(d);
291 d->scrollY = iMin(d->scrollY, scrollMax);
292 if (oldScroll != d->scrollY) {
293 d->hoverItem = iInvalidPos;
294 updateVisible_SidebarWidget_(d);
295 invalidate_SidebarWidget_(d);
296 }
297}
298
170void setMode_SidebarWidget(iSidebarWidget *d, enum iSidebarMode mode) { 299void setMode_SidebarWidget(iSidebarWidget *d, enum iSidebarMode mode) {
171 if (d->mode == mode) return; 300 if (d->mode == mode) return;
301 if (d->mode >= 0 && d->mode < max_SidebarMode) {
302 d->modeScroll[d->mode] = d->scrollY; /* saved for later */
303 }
172 d->mode = mode; 304 d->mode = mode;
173 for (enum iSidebarMode i = 0; i < max_SidebarMode; i++) { 305 for (enum iSidebarMode i = 0; i < max_SidebarMode; i++) {
174 setFlags_Widget(as_Widget(d->modeButtons[i]), selected_WidgetFlag, i == d->mode); 306 setFlags_Widget(as_Widget(d->modeButtons[i]), selected_WidgetFlag, i == d->mode);
175 } 307 }
176 const float heights[max_SidebarMode] = { 1.333f, 3, 3, 1.2f }; 308 const float heights[max_SidebarMode] = { 1.333f, 1.333f, 3.5f, 1.2f };
177 d->itemHeight = heights[mode] * lineHeight_Text(uiContent_FontId); 309 d->itemHeight = heights[mode] * lineHeight_Text(uiContent_FontId);
310 invalidate_SidebarWidget_(d);
311 /* Restore previous scroll position. */
312 d->scrollY = d->modeScroll[mode];
178} 313}
179 314
180enum iSidebarMode mode_SidebarWidget(const iSidebarWidget *d) { 315enum iSidebarMode mode_SidebarWidget(const iSidebarWidget *d) {
@@ -209,8 +344,9 @@ void init_SidebarWidget(iSidebarWidget *d) {
209 resizeWidthOfChildren_WidgetFlag | collapse_WidgetFlag, 344 resizeWidthOfChildren_WidgetFlag | collapse_WidgetFlag,
210 iTrue); 345 iTrue);
211 d->scrollY = 0; 346 d->scrollY = 0;
212 d->mode = -1; 347 iZap(d->modeScroll);
213 d->width = 75 * gap_UI; 348 d->mode = -1;
349 d->width = 60 * gap_UI;
214 init_Array(&d->items, sizeof(iSidebarItem)); 350 init_Array(&d->items, sizeof(iSidebarItem));
215 d->hoverItem = iInvalidPos; 351 d->hoverItem = iInvalidPos;
216 init_Click(&d->click, d, SDL_BUTTON_LEFT); 352 init_Click(&d->click, d, SDL_BUTTON_LEFT);
@@ -220,7 +356,7 @@ void init_SidebarWidget(iSidebarWidget *d) {
220 d->modeButtons[i] = addChildFlags_Widget( 356 d->modeButtons[i] = addChildFlags_Widget(
221 w, 357 w,
222 iClob( 358 iClob(
223 new_LabelWidget(normalModeLabels_[i], 0, 0, format_CStr("sidebar.mode arg:%d", i))), 359 new_LabelWidget(tightModeLabels_[i], 0, 0, format_CStr("sidebar.mode arg:%d", i))),
224 frameless_WidgetFlag | expand_WidgetFlag); 360 frameless_WidgetFlag | expand_WidgetFlag);
225 d->maxButtonLabelWidth = 361 d->maxButtonLabelWidth =
226 iMaxi(d->maxButtonLabelWidth, 362 iMaxi(d->maxButtonLabelWidth,
@@ -270,7 +406,53 @@ static size_t itemIndex_SidebarWidget_(const iSidebarWidget *d, iInt2 pos) {
270 return index; 406 return index;
271} 407}
272 408
409static const iSidebarItem *constHoverItem_SidebarWidget_(const iSidebarWidget *d) {
410 if (d->hoverItem < size_Array(&d->items)) {
411 return constAt_Array(&d->items, d->hoverItem);
412 }
413 return NULL;
414}
415
416static iSidebarItem *hoverItem_SidebarWidget_(iSidebarWidget *d) {
417 if (d->hoverItem < size_Array(&d->items)) {
418 return at_Array(&d->items, d->hoverItem);
419 }
420 return NULL;
421}
422
423static const iGmIdentity *constHoverIdentity_SidebarWidget_(const iSidebarWidget *d) {
424 if (d->mode == identities_SidebarMode) {
425 const iSidebarItem *hoverItem = constHoverItem_SidebarWidget_(d);
426 if (hoverItem) {
427 return identity_GmCerts(certs_App(), hoverItem->id);
428 }
429 }
430 return NULL;
431}
432
433static iGmIdentity *hoverIdentity_SidebarWidget_(const iSidebarWidget *d) {
434 return iConstCast(iGmIdentity *, constHoverIdentity_SidebarWidget_(d));
435}
436
437static void setHoverItem_SidebarWidget_(iSidebarWidget *d, size_t index) {
438 if (index < size_Array(&d->items)) {
439 if (constValue_Array(&d->items, index, iSidebarItem).isSeparator) {
440 index = iInvalidPos;
441 }
442 }
443 if (d->hoverItem != index) {
444 d->hoverItem = index;
445 invalidate_SidebarWidget_(d);
446 }
447}
448
449static void updateMouseHover_SidebarWidget_(iSidebarWidget *d) {
450 const iInt2 mouse = mouseCoord_Window(get_Window());
451 setHoverItem_SidebarWidget_(d, itemIndex_SidebarWidget_(d, mouse));
452}
453
273static void itemClicked_SidebarWidget_(iSidebarWidget *d, size_t index) { 454static void itemClicked_SidebarWidget_(iSidebarWidget *d, size_t index) {
455 setFocus_Widget(NULL);
274 const iSidebarItem *item = constAt_Array(&d->items, index); 456 const iSidebarItem *item = constAt_Array(&d->items, index);
275 switch (d->mode) { 457 switch (d->mode) {
276 case documentOutline_SidebarMode: { 458 case documentOutline_SidebarMode: {
@@ -279,25 +461,30 @@ static void itemClicked_SidebarWidget_(iSidebarWidget *d, size_t index) {
279 postCommandf_App("document.goto loc:%p", head->text.start); 461 postCommandf_App("document.goto loc:%p", head->text.start);
280 break; 462 break;
281 } 463 }
282 case bookmarks_SidebarMode: { 464 case bookmarks_SidebarMode:
283 postCommandf_App("open url:%s", cstr_String(&item->url)); 465 case history_SidebarMode: {
466 if (!isEmpty_String(&item->url)) {
467 postCommandf_App("open url:%s", cstr_String(&item->url));
468 }
284 break; 469 break;
285 } 470 }
286 } 471 case identities_SidebarMode: {
287} 472 iGmIdentity *ident = hoverIdentity_SidebarWidget_(d);
288 473 if (ident) {
289static void scroll_SidebarWidget_(iSidebarWidget *d, int offset) { 474 const iString *tabUrl = url_DocumentWidget(document_App());
290 const int oldScroll = d->scrollY; 475 if (isUsedOn_GmIdentity(ident, tabUrl)) {
291 d->scrollY += offset; 476 signOut_GmCerts(certs_App(), tabUrl);
292 if (d->scrollY < 0) { 477 }
293 d->scrollY = 0; 478 else {
294 } 479 signIn_GmCerts(certs_App(), ident, tabUrl);
295 const int scrollMax = scrollMax_SidebarWidget_(d); 480 }
296 d->scrollY = iMin(d->scrollY, scrollMax); 481 updateItems_SidebarWidget_(d);
297 if (oldScroll != d->scrollY) { 482 updateMouseHover_SidebarWidget_(d);
298 d->hoverItem = iInvalidPos; 483 }
299 updateVisible_SidebarWidget_(d); 484 break;
300 invalidate_SidebarWidget_(d); 485 }
486 default:
487 break;
301 } 488 }
302} 489}
303 490
@@ -316,13 +503,6 @@ static void checkModeButtonLayout_SidebarWidget_(iSidebarWidget *d) {
316 } 503 }
317} 504}
318 505
319static iSidebarItem *hoverItem_SidebarWidget_(iSidebarWidget *d) {
320 if (d->hoverItem < size_Array(&d->items)) {
321 return at_Array(&d->items, d->hoverItem);
322 }
323 return NULL;
324}
325
326void setWidth_SidebarWidget(iSidebarWidget *d, int width) { 506void setWidth_SidebarWidget(iSidebarWidget *d, int width) {
327 iWidget *w = as_Widget(d); 507 iWidget *w = as_Widget(d);
328 width = iMax(30 * gap_UI, width); 508 width = iMax(30 * gap_UI, width);
@@ -339,15 +519,18 @@ void setWidth_SidebarWidget(iSidebarWidget *d, int width) {
339} 519}
340 520
341iBool handleBookmarkEditorCommands_SidebarWidget_(iWidget *editor, const char *cmd) { 521iBool handleBookmarkEditorCommands_SidebarWidget_(iWidget *editor, const char *cmd) {
342 iSidebarWidget *d = findWidget_App("sidebar");
343 if (equal_Command(cmd, "bmed.accept") || equal_Command(cmd, "cancel")) { 522 if (equal_Command(cmd, "bmed.accept") || equal_Command(cmd, "cancel")) {
523 iSidebarWidget *d = findWidget_App("sidebar");
344 if (equal_Command(cmd, "bmed.accept")) { 524 if (equal_Command(cmd, "bmed.accept")) {
525 const iString *title = text_InputWidget(findChild_Widget(editor, "bmed.title"));
526 const iString *url = text_InputWidget(findChild_Widget(editor, "bmed.url"));
527 const iString *tags = text_InputWidget(findChild_Widget(editor, "bmed.tags"));
345 const iSidebarItem *item = hoverItem_SidebarWidget_(d); 528 const iSidebarItem *item = hoverItem_SidebarWidget_(d);
346 iAssert(item); /* hover item cannot have been changed */ 529 iAssert(item); /* hover item cannot have been changed */
347 iBookmark *bm = get_Bookmarks(bookmarks_App(), item->id); 530 iBookmark *bm = get_Bookmarks(bookmarks_App(), item->id);
348 set_String(&bm->title, text_InputWidget(findChild_Widget(editor, "bmed.title"))); 531 set_String(&bm->title, title);
349 set_String(&bm->url, text_InputWidget(findChild_Widget(editor, "bmed.url"))); 532 set_String(&bm->url, url);
350 set_String(&bm->tags, text_InputWidget(findChild_Widget(editor, "bmed.tags"))); 533 set_String(&bm->tags, tags);
351 postCommand_App("bookmarks.changed"); 534 postCommand_App("bookmarks.changed");
352 } 535 }
353 setFlags_Widget(as_Widget(d), disabled_WidgetFlag, iFalse); 536 setFlags_Widget(as_Widget(d), disabled_WidgetFlag, iFalse);
@@ -399,11 +582,15 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev)
399 setMode_SidebarWidget(d, arg_Command(cmd)); 582 setMode_SidebarWidget(d, arg_Command(cmd));
400 updateItems_SidebarWidget_(d); 583 updateItems_SidebarWidget_(d);
401 if (argLabel_Command(cmd, "show") && !isVisible_Widget(w)) { 584 if (argLabel_Command(cmd, "show") && !isVisible_Widget(w)) {
402 postCommand_App("sidebar.toggle"); 585 postCommand_App("sidebar.toggle arg:1");
403 } 586 }
587 scroll_SidebarWidget_(d, 0);
404 return iTrue; 588 return iTrue;
405 } 589 }
406 else if (equal_Command(cmd, "sidebar.toggle")) { 590 else if (equal_Command(cmd, "sidebar.toggle")) {
591 if (arg_Command(cmd) && isVisible_Widget(w)) {
592 return iTrue;
593 }
407 setFlags_Widget(w, hidden_WidgetFlag, isVisible_Widget(w)); 594 setFlags_Widget(w, hidden_WidgetFlag, isVisible_Widget(w));
408 if (isVisible_Widget(w)) { 595 if (isVisible_Widget(w)) {
409 w->rect.size.x = d->width; 596 w->rect.size.x = d->width;
@@ -421,7 +608,6 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev)
421 return iTrue; 608 return iTrue;
422 } 609 }
423 else if (equal_Command(cmd, "tabs.changed") || equal_Command(cmd, "document.changed")) { 610 else if (equal_Command(cmd, "tabs.changed") || equal_Command(cmd, "document.changed")) {
424 d->scrollY = 0;
425 updateItems_SidebarWidget_(d); 611 updateItems_SidebarWidget_(d);
426 } 612 }
427 else if (equal_Command(cmd, "theme.changed")) { 613 else if (equal_Command(cmd, "theme.changed")) {
@@ -455,25 +641,156 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev)
455 } 641 }
456 return iTrue; 642 return iTrue;
457 } 643 }
458 else if (equal_Command(cmd, "bookmarks.changed")) { 644 else if (equal_Command(cmd, "bookmarks.changed") && d->mode == bookmarks_SidebarMode) {
645 updateItems_SidebarWidget_(d);
646 }
647 else if (equal_Command(cmd, "idents.changed") && d->mode == identities_SidebarMode) {
648 updateItems_SidebarWidget_(d);
649 }
650 else if (isCommand_Widget(w, ev, "ident.use")) {
651 iGmIdentity * ident = hoverIdentity_SidebarWidget_(d);
652 const iString *tabUrl = url_DocumentWidget(document_App());
653 if (ident) {
654 if (argLabel_Command(cmd, "clear")) {
655 clearUse_GmIdentity(ident);
656 }
657 else if (arg_Command(cmd)) {
658 signIn_GmCerts(certs_App(), ident, tabUrl);
659 }
660 else {
661 signOut_GmCerts(certs_App(), tabUrl);
662 }
663 updateItems_SidebarWidget_(d);
664 }
665 return iTrue;
666 }
667 else if (isCommand_Widget(w, ev, "ident.showuse")) {
668 const iGmIdentity *ident = constHoverIdentity_SidebarWidget_(d);
669 if (ident) {
670 makeMessage_Widget(uiHeading_ColorEscape "IDENTITY USAGE",
671 cstrCollect_String(joinCStr_StringSet(ident->useUrls, "\n")));
672 }
673 return iTrue;
674 }
675 else if (isCommand_Widget(w, ev, "ident.edit")) {
676 const iGmIdentity *ident = constHoverIdentity_SidebarWidget_(d);
677 if (ident) {
678 makeValueInput_Widget(get_Window()->root,
679 &ident->notes,
680 uiHeading_ColorEscape "IDENTITY NOTES",
681 format_CStr("Notes about %s:", cstr_String(name_GmIdentity(ident))),
682 uiTextAction_ColorEscape "OK",
683 format_CStr("ident.setnotes ident:%p", ident));
684 }
685 return iTrue;
686 }
687 else if (equal_Command(cmd, "ident.setnotes")) {
688 iGmIdentity *ident = pointerLabel_Command(cmd, "ident");
689 if (ident) {
690 setCStr_String(&ident->notes, suffixPtr_Command(cmd, "value"));
691 updateItems_SidebarWidget_(d);
692 }
693 return iTrue;
694 }
695 else if (isCommand_Widget(w, ev, "ident.pickicon")) {
696 return iTrue;
697 }
698 else if (isCommand_Widget(w, ev, "ident.reveal")) {
699 const iGmIdentity *ident = constHoverIdentity_SidebarWidget_(d);
700 if (ident) {
701 const iString *crtPath = certificatePath_GmCerts(certs_App(), ident);
702 if (crtPath) {
703 revealPath_App(crtPath);
704 }
705 }
706 return iTrue;
707 }
708 else if (equal_Command(cmd, "ident.delete")) {
709 iSidebarItem *item = hoverItem_SidebarWidget_(d);
710 if (argLabel_Command(cmd, "confirm")) {
711 makeQuestion_Widget(uiTextCaution_ColorEscape "DELETE IDENTITY",
712 format_CStr("Do you really want to delete the identity\n"
713 uiTextAction_ColorEscape "%s\n"
714 uiText_ColorEscape
715 "including its certificate and private key files?",
716 cstr_String(&item->label)),
717 (const char *[]){ "Cancel",
718 uiTextCaution_ColorEscape
719 "Delete Identity and Files" },
720 (const char *[]){ "cancel", "ident.delete confirm:0" },
721 2);
722 return iTrue;
723 }
724 deleteIdentity_GmCerts(certs_App(), hoverIdentity_SidebarWidget_(d));
459 updateItems_SidebarWidget_(d); 725 updateItems_SidebarWidget_(d);
726 return iTrue;
727 }
728 else if (equal_Command(cmd, "history.delete")) {
729 const iSidebarItem *item = hoverItem_SidebarWidget_(d);
730 if (item && !isEmpty_String(&item->url)) {
731 removeUrl_Visited(visited_App(), &item->url);
732 updateItems_SidebarWidget_(d);
733 scroll_SidebarWidget_(d, 0);
734 }
735 return iTrue;
736 }
737 else if (equal_Command(cmd, "history.copy")) {
738 const iSidebarItem *item = hoverItem_SidebarWidget_(d);
739 if (item && !isEmpty_String(&item->url)) {
740 SDL_SetClipboardText(cstr_String(&item->url));
741 }
742 return iTrue;
743 }
744 else if (equal_Command(cmd, "history.addbookmark")) {
745 const iSidebarItem *item = hoverItem_SidebarWidget_(d);
746 if (!isEmpty_String(&item->url)) {
747 makeBookmarkCreation_Widget(
748 &item->url,
749 collect_String(newRange_String(urlHost_String(&item->url))),
750 0x1f310 /* globe */);
751 }
752 }
753 else if (equal_Command(cmd, "history.clear")) {
754 if (argLabel_Command(cmd, "confirm")) {
755 makeQuestion_Widget(
756 uiTextCaution_ColorEscape "CLEAR HISTORY",
757 "Do you really want to erase the history of all visited pages?",
758 (const char *[]){ "Cancel", uiTextCaution_ColorEscape "Clear History" },
759 (const char *[]){ "cancel", "history.clear confirm:0" },
760 2);
761 }
762 else {
763 clear_Visited(visited_App());
764 updateItems_SidebarWidget_(d);
765 scroll_SidebarWidget_(d, 0);
766 }
767 return iTrue;
460 } 768 }
461 } 769 }
462 if (ev->type == SDL_MOUSEMOTION && !isVisible_Widget(d->menu)) { 770 if (ev->type == SDL_MOUSEMOTION && !isVisible_Widget(d->menu)) {
463 const iInt2 mouse = init_I2(ev->motion.x, ev->motion.y); 771 const iInt2 mouse = init_I2(ev->motion.x, ev->motion.y);
464 size_t hover = iInvalidPos; 772 size_t hover = iInvalidPos;
465 if (contains_Widget(d->resizer, mouse)) { 773 if (contains_Widget(d->resizer, mouse)) {
466 SDL_SetCursor(d->resizeCursor); 774 setCursor_Window(get_Window(), SDL_SYSTEM_CURSOR_SIZEWE);
467 } 775 }
468 else { 776 else if (contains_Widget(constAs_Widget(d->scroll), mouse)) {
469 SDL_SetCursor(NULL); 777 setCursor_Window(get_Window(), SDL_SYSTEM_CURSOR_ARROW);
470 if (contains_Widget(w, mouse)) {
471 hover = itemIndex_SidebarWidget_(d, mouse);
472 }
473 } 778 }
474 if (hover != d->hoverItem) { 779 else if (contains_Widget(w, mouse)) {
475 d->hoverItem = hover; 780 hover = itemIndex_SidebarWidget_(d, mouse);
476 invalidate_SidebarWidget_(d); 781 }
782 setHoverItem_SidebarWidget_(d, hover);
783 /* Update cursor. */
784 if (contains_Widget(w, mouse) && !contains_Widget(d->resizer, mouse) &&
785 !contains_Widget(constAs_Widget(d->scroll), mouse)) {
786 const iSidebarItem *item = constHoverItem_SidebarWidget_(d);
787 if (item && d->mode != identities_SidebarMode) {
788 setCursor_Window(get_Window(), item->isSeparator ? SDL_SYSTEM_CURSOR_ARROW
789 : SDL_SYSTEM_CURSOR_HAND);
790 }
791 else {
792 setCursor_Window(get_Window(), SDL_SYSTEM_CURSOR_ARROW);
793 }
477 } 794 }
478 } 795 }
479 if (ev->type == SDL_MOUSEWHEEL && isHover_Widget(w)) { 796 if (ev->type == SDL_MOUSEWHEEL && isHover_Widget(w)) {
@@ -486,10 +803,42 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev)
486 return iTrue; 803 return iTrue;
487 } 804 }
488 if (d->menu && ev->type == SDL_MOUSEBUTTONDOWN) { 805 if (d->menu && ev->type == SDL_MOUSEBUTTONDOWN) {
489 if (d->hoverItem != iInvalidPos || isVisible_Widget(d->menu)) { 806 if (ev->button.button == SDL_BUTTON_RIGHT) {
490 processContextMenuEvent_Widget(d->menu, ev, {}); 807 if (!isVisible_Widget(d->menu)) {
808 setHoverItem_SidebarWidget_(
809 d, itemIndex_SidebarWidget_(d, init_I2(ev->button.x, ev->button.y)));
810 }
811 if (d->hoverItem != iInvalidPos || isVisible_Widget(d->menu)) {
812 /* Update menu items. */
813 if (d->mode == identities_SidebarMode) {
814 const iGmIdentity *ident = constHoverIdentity_SidebarWidget_(d);
815 const iString * docUrl = url_DocumentWidget(document_App());
816 iForEach(ObjectList, i, children_Widget(d->menu)) {
817 if (isInstance_Object(i.object, &Class_LabelWidget)) {
818 iLabelWidget *menuItem = i.object;
819 const char * cmdItem = cstr_String(command_LabelWidget(menuItem));
820 if (equal_Command(cmdItem, "ident.use")) {
821 const iBool cmdUse = arg_Command(cmdItem) != 0;
822 const iBool cmdClear = argLabel_Command(cmdItem, "clear") != 0;
823 setFlags_Widget(
824 as_Widget(menuItem),
825 disabled_WidgetFlag,
826 (cmdClear && !isUsed_GmIdentity(ident)) ||
827 (!cmdClear && cmdUse && isUsedOn_GmIdentity(ident, docUrl)) ||
828 (!cmdClear && !cmdUse && !isUsedOn_GmIdentity(ident, docUrl)));
829 }
830 else if (equal_Command(cmdItem, "ident.showuse")) {
831 setFlags_Widget(as_Widget(menuItem),
832 disabled_WidgetFlag,
833 !isUsed_GmIdentity(ident));
834 }
835 }
836 }
837 }
838 }
491 } 839 }
492 } 840 }
841 processContextMenuEvent_Widget(d->menu, ev, {});
493 switch (processEvent_Click(&d->click, ev)) { 842 switch (processEvent_Click(&d->click, ev)) {
494 case started_ClickResult: 843 case started_ClickResult:
495 invalidate_SidebarWidget_(d); 844 invalidate_SidebarWidget_(d);
@@ -524,8 +873,8 @@ static void allocVisBuffer_SidebarWidget_(iSidebarWidget *d) {
524} 873}
525 874
526static void draw_SidebarWidget_(const iSidebarWidget *d) { 875static void draw_SidebarWidget_(const iSidebarWidget *d) {
527 const iWidget *w = constAs_Widget(d); 876 const iWidget *w = constAs_Widget(d);
528 const iRect bounds = contentBounds_SidebarWidget_(d); 877 const iRect bounds = contentBounds_SidebarWidget_(d);
529 const iBool isPressing = d->click.isActive && contains_Rect(bounds, pos_Click(&d->click)); 878 const iBool isPressing = d->click.isActive && contains_Rect(bounds, pos_Click(&d->click));
530 iPaint p; 879 iPaint p;
531 init_Paint(&p); 880 init_Paint(&p);
@@ -545,9 +894,12 @@ static void draw_SidebarWidget_(const iSidebarWidget *d) {
545 for (size_t i = visRange.start; i < visRange.end; i++) { 894 for (size_t i = visRange.start; i < visRange.end; i++) {
546 const iSidebarItem *item = constAt_Array(&d->items, i); 895 const iSidebarItem *item = constAt_Array(&d->items, i);
547 const iRect itemRect = { pos, init_I2(width_Rect(bufBounds), d->itemHeight) }; 896 const iRect itemRect = { pos, init_I2(width_Rect(bufBounds), d->itemHeight) };
548 const iBool isHover = (d->hoverItem == i); 897 const iBool isHover = isHover_Widget(w) && (d->hoverItem == i);
898 const int iconColor =
899 isHover ? (isPressing ? uiTextPressed_ColorId : uiIconHover_ColorId)
900 : uiIcon_ColorId;
549 setClip_Paint(&p, intersect_Rect(itemRect, bufBounds)); 901 setClip_Paint(&p, intersect_Rect(itemRect, bufBounds));
550 if (isHover) { 902 if (isHover && !item->isSeparator) {
551 fillRect_Paint(&p, 903 fillRect_Paint(&p,
552 itemRect, 904 itemRect,
553 isPressing ? uiBackgroundPressed_ColorId 905 isPressing ? uiBackgroundPressed_ColorId
@@ -576,8 +928,7 @@ static void draw_SidebarWidget_(const iSidebarWidget *d) {
576 font, 928 font,
577 iconArea, 929 iconArea,
578 iTrue, 930 iTrue,
579 isHover ? (isPressing ? uiTextPressed_ColorId : uiIconHover_ColorId) 931 iconColor,
580 : uiIcon_ColorId,
581 "%s", 932 "%s",
582 cstr_String(&str)); 933 cstr_String(&str));
583 deinit_String(&str); 934 deinit_String(&str);
@@ -585,6 +936,81 @@ static void draw_SidebarWidget_(const iSidebarWidget *d) {
585 (d->itemHeight - lineHeight_Text(font)) / 2); 936 (d->itemHeight - lineHeight_Text(font)) / 2);
586 drawRange_Text(font, textPos, fg, range_String(&item->label)); 937 drawRange_Text(font, textPos, fg, range_String(&item->label));
587 } 938 }
939 else if (d->mode == history_SidebarMode) {
940 iBeginCollect();
941 const int fg = isHover ? (isPressing ? uiTextPressed_ColorId
942 : uiTextFramelessHover_ColorId)
943 : uiText_ColorId;
944 if (item->isSeparator) {
945 if (!isEmpty_String(&item->meta)) {
946 unsetClip_Paint(&p);
947 iInt2 drawPos = addY_I2(topLeft_Rect(itemRect), d->itemHeight * 0.666f);
948 drawHLine_Paint(
949 &p, drawPos, width_Rect(itemRect), uiIcon_ColorId);
950 drawRange_Text(
951 default_FontId,
952 add_I2(drawPos,
953 init_I2(3 * gap_UI,
954 (d->itemHeight - lineHeight_Text(default_FontId)) / 2)),
955 uiIcon_ColorId,
956 range_String(&item->meta));
957 }
958 }
959 else {
960 iUrl parts;
961 init_Url(&parts, &item->url);
962 const iBool isGemini = equalCase_Rangecc(parts.scheme, "gemini");
963 draw_Text(
964 font,
965 add_I2(topLeft_Rect(itemRect),
966 init_I2(3 * gap_UI, (d->itemHeight - lineHeight_Text(font)) / 2)),
967 fg,
968 "%s%s%s%s%s%s",
969 isGemini ? "" : cstr_Rangecc(parts.scheme),
970 isGemini ? "" : "://",
971 escape_Color(isHover ? (isPressing ? uiTextPressed_ColorId
972 : uiTextFramelessHover_ColorId)
973 : uiTextStrong_ColorId),
974 cstr_Rangecc(parts.host),
975 escape_Color(fg),
976 cstr_Rangecc(parts.path));
977 }
978 iEndCollect();
979 }
980 else if (d->mode == identities_SidebarMode) {
981 const int fg = isHover ? (isPressing ? uiTextPressed_ColorId
982 : uiTextFramelessHover_ColorId)
983 : uiTextStrong_ColorId;
984 if (item->isSelected) {
985 drawRectThickness_Paint(&p,
986 adjusted_Rect(itemRect, zero_I2(), init_I2(-2, -1)),
987 gap_UI / 4,
988 isHover && isPressing ? uiTextPressed_ColorId
989 : uiIcon_ColorId);
990 }
991 iString icon;
992 initUnicodeN_String(&icon, &item->icon, 1);
993 iInt2 cPos = topLeft_Rect(itemRect);
994 addv_I2(&cPos,
995 init_I2(3 * gap_UI,
996 (d->itemHeight - lineHeight_Text(default_FontId) * 2 -
997 lineHeight_Text(font)) /
998 2));
999 const int metaFg =
1000 isHover ? permanent_ColorId | (isPressing ? uiTextPressed_ColorId
1001 : uiTextFramelessHover_ColorId)
1002 : uiText_ColorId;
1003 drawRange_Text(
1004 font, cPos, item->isSelected ? iconColor : metaFg, range_String(&icon));
1005 deinit_String(&icon);
1006 drawRange_Text(font, add_I2(cPos, init_I2(6 * gap_UI, 0)),
1007 fg, range_String(&item->label));
1008 drawRange_Text(
1009 default_FontId,
1010 add_I2(cPos, init_I2(6 * gap_UI, lineHeight_Text(font))),
1011 metaFg,
1012 range_String(&item->meta));
1013 }
588 unsetClip_Paint(&p); 1014 unsetClip_Paint(&p);
589 pos.y += d->itemHeight; 1015 pos.y += d->itemHeight;
590 } 1016 }
diff --git a/src/ui/sidebarwidget.h b/src/ui/sidebarwidget.h
index 3ff20b7f..7ccc64dd 100644
--- a/src/ui/sidebarwidget.h
+++ b/src/ui/sidebarwidget.h
@@ -1,3 +1,25 @@
1/* Copyright 2020 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
1#pragma once 23#pragma once
2 24
3#include "widget.h" 25#include "widget.h"
diff --git a/src/ui/text.c b/src/ui/text.c
index 1e702eee..836d540f 100644
--- a/src/ui/text.c
+++ b/src/ui/text.c
@@ -1,3 +1,25 @@
1/* Copyright 2020 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
1#include "text.h" 23#include "text.h"
2#include "color.h" 24#include "color.h"
3#include "metrics.h" 25#include "metrics.h"
@@ -11,6 +33,7 @@
11#include <the_Foundation/file.h> 33#include <the_Foundation/file.h>
12#include <the_Foundation/hash.h> 34#include <the_Foundation/hash.h>
13#include <the_Foundation/math.h> 35#include <the_Foundation/math.h>
36#include <the_Foundation/stringlist.h>
14#include <the_Foundation/regexp.h> 37#include <the_Foundation/regexp.h>
15#include <the_Foundation/path.h> 38#include <the_Foundation/path.h>
16#include <the_Foundation/vec2.h> 39#include <the_Foundation/vec2.h>
@@ -25,6 +48,7 @@ iDeclareTypeConstructionArgs(Glyph, iChar ch)
25 48
26int gap_Text; /* cf. gap_UI in metrics.h */ 49int gap_Text; /* cf. gap_UI in metrics.h */
27int enableHalfPixelGlyphs_Text = iTrue; /* debug setting */ 50int enableHalfPixelGlyphs_Text = iTrue; /* debug setting */
51int enableKerning_Text = iTrue; /* looking up kern pairs is slow */
28 52
29struct Impl_Glyph { 53struct Impl_Glyph {
30 iHashNode node; 54 iHashNode node;
@@ -62,6 +86,7 @@ struct Impl_Font {
62 int baseline; 86 int baseline;
63 iHash glyphs; 87 iHash glyphs;
64 iBool isMonospaced; 88 iBool isMonospaced;
89 iBool manualKernOnly;
65 enum iFontId symbolsFont; /* font to use for symbols */ 90 enum iFontId symbolsFont; /* font to use for symbols */
66}; 91};
67 92
@@ -120,7 +145,7 @@ static void initFonts_Text_(iText *d) {
120 int symbolsFont; 145 int symbolsFont;
121 } fontData[max_FontId] = { 146 } fontData[max_FontId] = {
122 { &fontSourceSansProRegular_Embedded, fontSize_UI, defaultSymbols_FontId }, 147 { &fontSourceSansProRegular_Embedded, fontSize_UI, defaultSymbols_FontId },
123 { &fontSourceSansProRegular_Embedded, fontSize_UI * 1.150f, defaultMediumSymbols_FontId }, 148 { &fontSourceSansProRegular_Embedded, fontSize_UI * 1.125f, defaultMediumSymbols_FontId },
124 { &fontFiraMonoRegular_Embedded, fontSize_UI * 0.866f, defaultSymbols_FontId }, 149 { &fontFiraMonoRegular_Embedded, fontSize_UI * 0.866f, defaultSymbols_FontId },
125 { &fontFiraSansRegular_Embedded, textSize, symbols_FontId }, 150 { &fontFiraSansRegular_Embedded, textSize, symbols_FontId },
126 { &fontFiraMonoRegular_Embedded, textSize * 0.866f, smallSymbols_FontId }, 151 { &fontFiraMonoRegular_Embedded, textSize * 0.866f, smallSymbols_FontId },
@@ -133,14 +158,14 @@ static void initFonts_Text_(iText *d) {
133 { &fontFiraSansBold_Embedded, textSize * 2.000f, hugeSymbols_FontId }, 158 { &fontFiraSansBold_Embedded, textSize * 2.000f, hugeSymbols_FontId },
134 { &fontFiraSansLight_Embedded, textSize * 1.666f, largeSymbols_FontId }, 159 { &fontFiraSansLight_Embedded, textSize * 1.666f, largeSymbols_FontId },
135 { &fontSymbola_Embedded, fontSize_UI, defaultSymbols_FontId }, 160 { &fontSymbola_Embedded, fontSize_UI, defaultSymbols_FontId },
136 { &fontSymbola_Embedded, fontSize_UI * 1.150f, defaultMediumSymbols_FontId }, 161 { &fontSymbola_Embedded, fontSize_UI * 1.125f, defaultMediumSymbols_FontId },
137 { &fontSymbola_Embedded, textSize, symbols_FontId }, 162 { &fontSymbola_Embedded, textSize, symbols_FontId },
138 { &fontSymbola_Embedded, textSize * 1.333f, mediumSymbols_FontId }, 163 { &fontSymbola_Embedded, textSize * 1.333f, mediumSymbols_FontId },
139 { &fontSymbola_Embedded, textSize * 1.666f, largeSymbols_FontId }, 164 { &fontSymbola_Embedded, textSize * 1.666f, largeSymbols_FontId },
140 { &fontSymbola_Embedded, textSize * 2.000f, hugeSymbols_FontId }, 165 { &fontSymbola_Embedded, textSize * 2.000f, hugeSymbols_FontId },
141 { &fontSymbola_Embedded, textSize * 0.866f, smallSymbols_FontId }, 166 { &fontSymbola_Embedded, textSize * 0.866f, smallSymbols_FontId },
142 { &fontNotoEmojiRegular_Embedded, fontSize_UI, defaultSymbols_FontId }, 167 { &fontNotoEmojiRegular_Embedded, fontSize_UI, defaultSymbols_FontId },
143 { &fontNotoEmojiRegular_Embedded, fontSize_UI * 1.150f, defaultMediumSymbols_FontId }, 168 { &fontNotoEmojiRegular_Embedded, fontSize_UI * 1.125f, defaultMediumSymbols_FontId },
144 { &fontNotoEmojiRegular_Embedded, textSize, symbols_FontId }, 169 { &fontNotoEmojiRegular_Embedded, textSize, symbols_FontId },
145 { &fontNotoEmojiRegular_Embedded, textSize * 1.333f, mediumSymbols_FontId }, 170 { &fontNotoEmojiRegular_Embedded, textSize * 1.333f, mediumSymbols_FontId },
146 { &fontNotoEmojiRegular_Embedded, textSize * 1.666f, largeSymbols_FontId }, 171 { &fontNotoEmojiRegular_Embedded, textSize * 1.666f, largeSymbols_FontId },
@@ -153,6 +178,9 @@ static void initFonts_Text_(iText *d) {
153 if (fontData[i].ttf == &fontFiraMonoRegular_Embedded) { 178 if (fontData[i].ttf == &fontFiraMonoRegular_Embedded) {
154 font->isMonospaced = iTrue; 179 font->isMonospaced = iTrue;
155 } 180 }
181 if (i == default_FontId || i == defaultMedium_FontId) {
182 font->manualKernOnly = iTrue;
183 }
156 } 184 }
157 gap_Text = iRound(gap_UI * d->contentFontSize); 185 gap_Text = iRound(gap_UI * d->contentFontSize);
158} 186}
@@ -287,8 +315,8 @@ static void cache_Font_(iFont *d, iGlyph *glyph, int hoff) {
287 /* Rasterize the glyph using stbtt. */ { 315 /* Rasterize the glyph using stbtt. */ {
288 surface = rasterizeGlyph_Font_(d, ch, hoff * 0.5f); 316 surface = rasterizeGlyph_Font_(d, ch, hoff * 0.5f);
289 if (hoff == 0) { 317 if (hoff == 0) {
290 int adv, lsb; 318 int adv;
291 stbtt_GetCodepointHMetrics(&d->font, ch, &adv, &lsb); 319 stbtt_GetCodepointHMetrics(&d->font, ch, &adv, NULL);
292 glyph->advance = d->scale * adv; 320 glyph->advance = d->scale * adv;
293 } 321 }
294 stbtt_GetCodepointBitmapBoxSubpixel(&d->font, 322 stbtt_GetCodepointBitmapBoxSubpixel(&d->font,
@@ -406,6 +434,7 @@ static iRect run_Font_(iFont *d, enum iRunMode mode, iRangecc text, size_t maxLe
406 /* ANSI escape. */ 434 /* ANSI escape. */
407 chPos++; 435 chPos++;
408 iRegExpMatch m; 436 iRegExpMatch m;
437 init_RegExpMatch(&m);
409 if (match_RegExp(text_.ansiEscape, chPos, text.end - chPos, &m)) { 438 if (match_RegExp(text_.ansiEscape, chPos, text.end - chPos, &m)) {
410 if (mode == draw_RunMode) { 439 if (mode == draw_RunMode) {
411 /* Change the color. */ 440 /* Change the color. */
@@ -431,7 +460,7 @@ static iRect run_Font_(iFont *d, enum iRunMode mode, iRangecc text, size_t maxLe
431 if (ch == '\r') { 460 if (ch == '\r') {
432 const iChar esc = nextChar_(&chPos, text.end); 461 const iChar esc = nextChar_(&chPos, text.end);
433 if (mode == draw_RunMode) { 462 if (mode == draw_RunMode) {
434 const iColor clr = get_Color(esc - '0'); 463 const iColor clr = get_Color(esc - asciiBase_ColorEscape);
435 SDL_SetTextureColorMod(text_.cache, clr.r, clr.g, clr.b); 464 SDL_SetTextureColorMod(text_.cache, clr.r, clr.g, clr.b);
436 } 465 }
437 prevCh = 0; 466 prevCh = 0;
@@ -488,9 +517,11 @@ static iRect run_Font_(iFont *d, enum iRunMode mode, iRangecc text, size_t maxLe
488 /* Manual kerning for double-slash. */ 517 /* Manual kerning for double-slash. */
489 xpos -= glyph->rect[hoff].size.x * 0.5f; 518 xpos -= glyph->rect[hoff].size.x * 0.5f;
490 } 519 }
491 else if (next) { 520#if defined (LAGRANGE_ENABLE_KERNING)
521 else if (enableKerning_Text && !d->manualKernOnly && next) {
492 xpos += d->scale * stbtt_GetCodepointKernAdvance(&d->font, ch, next); 522 xpos += d->scale * stbtt_GetCodepointKernAdvance(&d->font, ch, next);
493 } 523 }
524#endif
494 } 525 }
495 prevCh = ch; 526 prevCh = ch;
496 if (--maxLen == 0) { 527 if (--maxLen == 0) {
@@ -658,6 +689,111 @@ SDL_Texture *glyphCache_Text(void) {
658 return text_.cache; 689 return text_.cache;
659} 690}
660 691
692static void freeBitmap_(void *ptr) {
693 stbtt_FreeBitmap(ptr, NULL);
694}
695
696iString *renderBlockChars_Text(const iBlock *fontData, int height, enum iTextBlockMode mode,
697 const iString *text) {
698 iBeginCollect();
699 stbtt_fontinfo font;
700 iZap(font);
701 stbtt_InitFont(&font, constData_Block(fontData), 0);
702 int ascent;
703 stbtt_GetFontVMetrics(&font, &ascent, NULL, NULL);
704 iDeclareType(CharBuf);
705 struct Impl_CharBuf {
706 uint8_t *pixels;
707 iInt2 size;
708 int dy;
709 int advance;
710 };
711 iArray * chars = collectNew_Array(sizeof(iCharBuf));
712 int pxRatio = (mode == quadrants_TextBlockMode ? 2 : 1);
713 int pxHeight = height * pxRatio;
714 const float scale = stbtt_ScaleForPixelHeight(&font, pxHeight);
715 const float xScale = scale * 2; /* character aspect ratio */
716 const int baseline = ascent * scale;
717 int width = 0;
718 size_t strRemain = length_String(text);
719 iConstForEach(String, i, text) {
720 if (!strRemain) break;
721 if (i.value == variationSelectorEmoji_Char) {
722 strRemain--;
723 continue;
724 }
725 iCharBuf buf;
726 buf.pixels = stbtt_GetCodepointBitmap(
727 &font, xScale, scale, i.value, &buf.size.x, &buf.size.y, 0, &buf.dy);
728 stbtt_GetCodepointHMetrics(&font, i.value, &buf.advance, NULL);
729 buf.advance *= xScale;
730 if (!isSpace_Char(i.value)) {
731 if (mode == quadrants_TextBlockMode) {
732 buf.advance = (buf.size.x - 1) / 2 * 2 + 2;
733 }
734 else {
735 buf.advance = buf.size.x + 1;
736 }
737 }
738 pushBack_Array(chars, &buf);
739 collect_Garbage(buf.pixels, freeBitmap_);
740 width += buf.advance;
741 strRemain--;
742 }
743 const size_t len = (mode == quadrants_TextBlockMode ? height * ((width + 1) / 2 + 1)
744 : (height * (width + 1)));
745 iChar *outBuf = iCollectMem(malloc(sizeof(iChar) * len));
746 for (size_t i = 0; i < len; ++i) {
747 outBuf[i] = 0x20;
748 }
749 iChar *outPos = outBuf;
750 for (int y = 0; y < pxHeight; y += pxRatio) {
751 const iCharBuf *ch = constData_Array(chars);
752 int lx = 0;
753 for (int x = 0; x < width; x += pxRatio, lx += pxRatio) {
754 if (lx >= ch->advance) {
755 ch++;
756 lx = 0;
757 }
758 const int ly = y - baseline - ch->dy;
759 if (mode == quadrants_TextBlockMode) {
760 #define checkPixel_(offx, offy) \
761 (lx + offx < ch->size.x && ly + offy < ch->size.y && ly + offy >= 0 ? \
762 ch->pixels[(lx + offx) + (ly + offy) * ch->size.x] > 155 \
763 : iFalse)
764 const int mask = (checkPixel_(0, 0) ? 1 : 0) |
765 (checkPixel_(1, 0) ? 2 : 0) |
766 (checkPixel_(0, 1) ? 4 : 0) |
767 (checkPixel_(1, 1) ? 8 : 0);
768 #undef checkPixel_
769 static const iChar blocks[16] = { 0x0020, 0x2598, 0x259D, 0x2580, 0x2596, 0x258C,
770 0x259E, 0x259B, 0x2597, 0x259A, 0x2590, 0x259C,
771 0x2584, 0x2599, 0x259F, 0x2588 };
772 *outPos++ = blocks[mask];
773 }
774 else {
775 static const iChar shades[5] = { 0x0020, 0x2591, 0x2592, 0x2593, 0x2588 };
776 *outPos++ = shades[lx < ch->size.x && ly < ch->size.y && ly >= 0 ?
777 ch->pixels[lx + ly * ch->size.x] * 5 / 256 : 0];
778 }
779 }
780 *outPos++ = '\n';
781 }
782 /* We could compose the lines separately, but we'd still need to convert them to Strings
783 individually to trim them. */
784 iStringList *lines = split_String(collect_String(newUnicodeN_String(outBuf, len)), "\n");
785 while (!isEmpty_StringList(lines) &&
786 isEmpty_String(collect_String(trimmed_String(at_StringList(lines, 0))))) {
787 popFront_StringList(lines);
788 }
789 while (!isEmpty_StringList(lines) && isEmpty_String(collect_String(trimmed_String(
790 at_StringList(lines, size_StringList(lines) - 1))))) {
791 popBack_StringList(lines);
792 }
793 iEndCollect();
794 return joinCStr_StringList(iClob(lines), "\n");
795}
796
661/*-----------------------------------------------------------------------------------------------*/ 797/*-----------------------------------------------------------------------------------------------*/
662 798
663iDefineTypeConstructionArgs(TextBuf, (int font, const char *text), font, text) 799iDefineTypeConstructionArgs(TextBuf, (int font, const char *text), font, text)
diff --git a/src/ui/text.h b/src/ui/text.h
index 2d49a1a6..2b4ec5c3 100644
--- a/src/ui/text.h
+++ b/src/ui/text.h
@@ -1,3 +1,25 @@
1/* Copyright 2020 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
1#pragma once 23#pragma once
2 24
3#include <the_Foundation/rect.h> 25#include <the_Foundation/rect.h>
@@ -36,6 +58,7 @@ enum iFontId {
36 hugeEmoji_FontId, 58 hugeEmoji_FontId,
37 smallEmoji_FontId, 59 smallEmoji_FontId,
38 max_FontId, 60 max_FontId,
61
39 /* Meta: */ 62 /* Meta: */
40 fromSymbolsToEmojiOffset_FontId = 7, 63 fromSymbolsToEmojiOffset_FontId = 7,
41 /* UI fonts: */ 64 /* UI fonts: */
@@ -90,6 +113,11 @@ void drawRange_Text (int fontId, iInt2 pos, int color, iRangecc text);
90 113
91SDL_Texture * glyphCache_Text (void); 114SDL_Texture * glyphCache_Text (void);
92 115
116enum iTextBlockMode { quadrants_TextBlockMode, shading_TextBlockMode };
117
118iString * renderBlockChars_Text (const iBlock *fontData, int height, enum iTextBlockMode,
119 const iString *text);
120
93/*-----------------------------------------------------------------------------------------------*/ 121/*-----------------------------------------------------------------------------------------------*/
94 122
95iDeclareType(TextBuf) 123iDeclareType(TextBuf)
diff --git a/src/ui/util.c b/src/ui/util.c
index 9f4768d9..dfe364a5 100644
--- a/src/ui/util.c
+++ b/src/ui/util.c
@@ -1,8 +1,32 @@
1/* Copyright 2020 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
1#include "util.h" 23#include "util.h"
2 24
3#include "app.h" 25#include "app.h"
26#include "bookmarks.h"
4#include "color.h" 27#include "color.h"
5#include "command.h" 28#include "command.h"
29#include "gmutil.h"
6#include "labelwidget.h" 30#include "labelwidget.h"
7#include "inputwidget.h" 31#include "inputwidget.h"
8#include "widget.h" 32#include "widget.h"
@@ -157,7 +181,13 @@ static iBool menuHandler_(iWidget *menu, const char *cmd) {
157 /* Don't reopen self; instead, root will close the menu. */ 181 /* Don't reopen self; instead, root will close the menu. */
158 return iFalse; 182 return iFalse;
159 } 183 }
160 if (!equal_Command(cmd, "window.resized")) { 184 if (equal_Command(cmd, "mouse.clicked") && arg_Command(cmd)) {
185 /* Dismiss open menus when clicking outside them. */
186 closeMenu_Widget(menu);
187 return iTrue;
188 }
189 if (!equal_Command(cmd, "window.resized") &&
190 !(equal_Command(cmd, "mouse.clicked") && !arg_Command(cmd)) /* ignore button release */) {
161 closeMenu_Widget(menu); 191 closeMenu_Widget(menu);
162 } 192 }
163 } 193 }
@@ -234,7 +264,7 @@ void closeMenu_Widget(iWidget *d) {
234} 264}
235 265
236int checkContextMenu_Widget(iWidget *menu, const SDL_Event *ev) { 266int checkContextMenu_Widget(iWidget *menu, const SDL_Event *ev) {
237 if (ev->type == SDL_MOUSEBUTTONDOWN && ev->button.button == SDL_BUTTON_RIGHT) { 267 if (menu && ev->type == SDL_MOUSEBUTTONDOWN && ev->button.button == SDL_BUTTON_RIGHT) {
238 if (isVisible_Widget(menu)) { 268 if (isVisible_Widget(menu)) {
239 closeMenu_Widget(menu); 269 closeMenu_Widget(menu);
240 return 0x1; 270 return 0x1;
@@ -476,6 +506,7 @@ iBool filePathHandler_(iWidget *dlg, const char *cmd) {
476 506
477iWidget *makeSheet_Widget(const char *id) { 507iWidget *makeSheet_Widget(const char *id) {
478 iWidget *sheet = new_Widget(); 508 iWidget *sheet = new_Widget();
509 setPadding1_Widget(sheet, 3 * gap_UI);
479 setId_Widget(sheet, id); 510 setId_Widget(sheet, id);
480 setFrameColor_Widget(sheet, uiSeparator_ColorId); 511 setFrameColor_Widget(sheet, uiSeparator_ColorId);
481 setBackgroundColor_Widget(sheet, uiBackground_ColorId); 512 setBackgroundColor_Widget(sheet, uiBackground_ColorId);
@@ -483,9 +514,6 @@ iWidget *makeSheet_Widget(const char *id) {
483 mouseModal_WidgetFlag | keepOnTop_WidgetFlag | arrangeVertical_WidgetFlag | 514 mouseModal_WidgetFlag | keepOnTop_WidgetFlag | arrangeVertical_WidgetFlag |
484 arrangeSize_WidgetFlag, 515 arrangeSize_WidgetFlag,
485 iTrue); 516 iTrue);
486 // const iInt2 rootSize = rootSize_Window(get_Window());
487 // setSize_Widget(sheet, init_I2(rootSize.x / 2, 0));
488 // setFlags_Widget(sheet, fixedHeight_WidgetFlag, iFalse);
489 return sheet; 517 return sheet;
490} 518}
491 519
@@ -529,11 +557,13 @@ void makeFilePath_Widget(iWidget * parent,
529 557
530static void acceptValueInput_(iWidget *dlg) { 558static void acceptValueInput_(iWidget *dlg) {
531 const iInputWidget *input = findChild_Widget(dlg, "input"); 559 const iInputWidget *input = findChild_Widget(dlg, "input");
532 const iString *val = text_InputWidget(input); 560 if (!isEmpty_String(id_Widget(dlg))) {
533 postCommandf_App("%s arg:%d value:%s", 561 const iString *val = text_InputWidget(input);
534 cstr_String(id_Widget(dlg)), 562 postCommandf_App("%s arg:%d value:%s",
535 toInt_String(val), 563 cstr_String(id_Widget(dlg)),
536 cstr_String(val)); 564 toInt_String(val),
565 cstr_String(val));
566 }
537} 567}
538 568
539static void updateValueInputWidth_(iWidget *dlg) { 569static void updateValueInputWidth_(iWidget *dlg) {
@@ -560,6 +590,7 @@ iBool valueInputHandler_(iWidget *dlg, const char *cmd) {
560 } 590 }
561 else { 591 else {
562 postCommandf_App("valueinput.cancelled id:%s", cstr_String(id_Widget(dlg))); 592 postCommandf_App("valueinput.cancelled id:%s", cstr_String(id_Widget(dlg)));
593 setId_Widget(dlg, ""); /* no further commands to emit */
563 } 594 }
564 destroy_Widget(dlg); 595 destroy_Widget(dlg);
565 return iTrue; 596 return iTrue;
@@ -568,6 +599,7 @@ iBool valueInputHandler_(iWidget *dlg, const char *cmd) {
568 } 599 }
569 else if (equal_Command(cmd, "cancel")) { 600 else if (equal_Command(cmd, "cancel")) {
570 postCommandf_App("valueinput.cancelled id:%s", cstr_String(id_Widget(dlg))); 601 postCommandf_App("valueinput.cancelled id:%s", cstr_String(id_Widget(dlg)));
602 setId_Widget(dlg, ""); /* no further commands to emit */
571 destroy_Widget(dlg); 603 destroy_Widget(dlg);
572 return iTrue; 604 return iTrue;
573 } 605 }
@@ -633,11 +665,12 @@ static iBool messageHandler_(iWidget *msg, const char *cmd) {
633 return iFalse; 665 return iFalse;
634} 666}
635 667
636void makeMessage_Widget(const char *title, const char *msg) { 668iWidget *makeMessage_Widget(const char *title, const char *msg) {
637 iWidget *dlg = makeQuestion_Widget( 669 iWidget *dlg = makeQuestion_Widget(
638 title, msg, (const char *[]){ "Continue" }, (const char *[]){ "message.ok" }, 1); 670 title, msg, (const char *[]){ "Continue" }, (const char *[]){ "message.ok" }, 1);
639 addAction_Widget(dlg, SDLK_ESCAPE, 0, "message.ok"); 671 addAction_Widget(dlg, SDLK_ESCAPE, 0, "message.ok");
640 addAction_Widget(dlg, SDLK_SPACE, 0, "message.ok"); 672 addAction_Widget(dlg, SDLK_SPACE, 0, "message.ok");
673 return dlg;
641} 674}
642 675
643iWidget *makeQuestion_Widget(const char *title, 676iWidget *makeQuestion_Widget(const char *title,
@@ -666,9 +699,11 @@ iWidget *makeQuestion_Widget(const char *title,
666} 699}
667 700
668void setToggle_Widget(iWidget *d, iBool active) { 701void setToggle_Widget(iWidget *d, iBool active) {
669 setFlags_Widget(d, selected_WidgetFlag, active); 702 if (d) {
670 updateText_LabelWidget((iLabelWidget *) d, 703 setFlags_Widget(d, selected_WidgetFlag, active);
671 collectNewFormat_String("%s", isSelected_Widget(d) ? "YES" : "NO")); 704 updateText_LabelWidget((iLabelWidget *) d,
705 collectNewFormat_String("%s", isSelected_Widget(d) ? "YES" : "NO"));
706 }
672} 707}
673 708
674static iBool toggleHandler_(iWidget *d, const char *cmd) { 709static iBool toggleHandler_(iWidget *d, const char *cmd) {
@@ -684,8 +719,9 @@ static iBool toggleHandler_(iWidget *d, const char *cmd) {
684} 719}
685 720
686iWidget *makeToggle_Widget(const char *id) { 721iWidget *makeToggle_Widget(const char *id) {
687 iWidget *toggle = as_Widget(new_LabelWidget("YES", 0, 0, "toggle")); 722 iWidget *toggle = as_Widget(new_LabelWidget("YES", 0, 0, "toggle")); /* "YES" for sizing */
688 setId_Widget(toggle, id); 723 setId_Widget(toggle, id);
724 updateTextCStr_LabelWidget((iLabelWidget *) toggle, "NO"); /* actual initial value */
689 setCommandHandler_Widget(toggle, toggleHandler_); 725 setCommandHandler_Widget(toggle, toggleHandler_);
690 return toggle; 726 return toggle;
691} 727}
@@ -702,9 +738,11 @@ iWidget *makePreferences_Widget(void) {
702 page, iClob(new_Widget()), arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag); 738 page, iClob(new_Widget()), arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag);
703 iWidget *values = addChildFlags_Widget( 739 iWidget *values = addChildFlags_Widget(
704 page, iClob(new_Widget()), arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag); 740 page, iClob(new_Widget()), arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag);
705// setBackgroundColor_Widget(headings, none_ColorId); 741#if defined (iPlatformApple) || defined (iPlatformMSys)
706// setBackgroundColor_Widget(values, none_ColorId); 742 addChild_Widget(headings, iClob(makeHeading_Widget("Use system theme:")));
707 addChild_Widget(headings, iClob(makeHeading_Widget("Theme:"))); 743 addChild_Widget(values, iClob(makeToggle_Widget("prefs.ostheme")));
744#endif
745 addChild_Widget(headings, iClob(makeHeading_Widget("Theme:")));
708 iWidget *themes = new_Widget(); 746 iWidget *themes = new_Widget();
709 /* Themes. */ { 747 /* Themes. */ {
710 setId_Widget(addChild_Widget(themes, iClob(new_LabelWidget("Pure Black", 0, 0, "theme.set arg:0"))), "prefs.theme.0"); 748 setId_Widget(addChild_Widget(themes, iClob(new_LabelWidget("Pure Black", 0, 0, "theme.set arg:0"))), "prefs.theme.0");
@@ -717,8 +755,18 @@ iWidget *makePreferences_Widget(void) {
717 addChild_Widget(values, iClob(makeToggle_Widget("prefs.retainwindow"))); 755 addChild_Widget(values, iClob(makeToggle_Widget("prefs.retainwindow")));
718 addChild_Widget(headings, iClob(makeHeading_Widget("UI scale factor:"))); 756 addChild_Widget(headings, iClob(makeHeading_Widget("UI scale factor:")));
719 setId_Widget(addChild_Widget(values, iClob(new_InputWidget(8))), "prefs.uiscale"); 757 setId_Widget(addChild_Widget(values, iClob(new_InputWidget(8))), "prefs.uiscale");
758 addChild_Widget(headings, iClob(makeHeading_Widget(uiHeading_ColorEscape "Proxies")));
759 addChild_Widget(values, iClob(makeHeading_Widget("")));
760 addChild_Widget(headings, iClob(makeHeading_Widget("HTTP proxy:")));
761 setId_Widget(addChild_Widget(values, iClob(new_InputWidget(0))), "prefs.proxy.http");
762 addChild_Widget(headings, iClob(makeHeading_Widget("Gopher proxy:")));
763 setId_Widget(addChild_Widget(values, iClob(new_InputWidget(0))), "prefs.proxy.gopher");
720 arrange_Widget(dlg); 764 arrange_Widget(dlg);
721// as_Widget(songDir)->rect.size.x = dlg->rect.size.x - headings->rect.size.x; 765 /* Text input widths. */ {
766 const int inputWidth = width_Rect(page->rect) - width_Rect(headings->rect);
767 as_Widget(findChild_Widget(values, "prefs.proxy.http"))->rect.size.x = inputWidth;
768 as_Widget(findChild_Widget(values, "prefs.proxy.gopher"))->rect.size.x = inputWidth;
769 }
722 iWidget *div = new_Widget(); { 770 iWidget *div = new_Widget(); {
723 setFlags_Widget(div, arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag, iTrue); 771 setFlags_Widget(div, arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag, iTrue);
724 addChild_Widget(div, iClob(new_LabelWidget("Dismiss", SDLK_ESCAPE, 0, "prefs.dismiss"))); 772 addChild_Widget(div, iClob(new_LabelWidget("Dismiss", SDLK_ESCAPE, 0, "prefs.dismiss")));
@@ -731,9 +779,11 @@ iWidget *makePreferences_Widget(void) {
731 779
732iWidget *makeBookmarkEditor_Widget(void) { 780iWidget *makeBookmarkEditor_Widget(void) {
733 iWidget *dlg = makeSheet_Widget("bmed"); 781 iWidget *dlg = makeSheet_Widget("bmed");
734 addChildFlags_Widget(dlg, 782 setId_Widget(addChildFlags_Widget(
735 iClob(new_LabelWidget(uiHeading_ColorEscape "EDIT BOOKMARK", 0, 0, NULL)), 783 dlg,
736 frameless_WidgetFlag); 784 iClob(new_LabelWidget(uiHeading_ColorEscape "EDIT BOOKMARK", 0, 0, NULL)),
785 frameless_WidgetFlag),
786 "bmed.heading");
737 iWidget *page = new_Widget(); 787 iWidget *page = new_Widget();
738 addChild_Widget(dlg, iClob(page)); 788 addChild_Widget(dlg, iClob(page));
739 setFlags_Widget(page, arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag, iTrue); 789 setFlags_Widget(page, arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag, iTrue);
@@ -741,9 +791,8 @@ iWidget *makeBookmarkEditor_Widget(void) {
741 page, iClob(new_Widget()), arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag); 791 page, iClob(new_Widget()), arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag);
742 iWidget *values = addChildFlags_Widget( 792 iWidget *values = addChildFlags_Widget(
743 page, iClob(new_Widget()), arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag); 793 page, iClob(new_Widget()), arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag);
744 iInputWidget *inputs[4]; 794 iInputWidget *inputs[3];
745 iWidget *hd; 795 addChild_Widget(headings, iClob(makeHeading_Widget("Title:")));
746 addChild_Widget(headings, iClob(hd = makeHeading_Widget("Title:")));
747 setId_Widget(addChild_Widget(values, iClob(inputs[0] = new_InputWidget(0))), "bmed.title"); 796 setId_Widget(addChild_Widget(values, iClob(inputs[0] = new_InputWidget(0))), "bmed.title");
748 addChild_Widget(headings, iClob(makeHeading_Widget("URL:"))); 797 addChild_Widget(headings, iClob(makeHeading_Widget("URL:")));
749 setId_Widget(addChild_Widget(values, iClob(inputs[1] = new_InputWidget(0))), "bmed.url"); 798 setId_Widget(addChild_Widget(values, iClob(inputs[1] = new_InputWidget(0))), "bmed.url");
@@ -759,7 +808,100 @@ iWidget *makeBookmarkEditor_Widget(void) {
759 addChild_Widget( 808 addChild_Widget(
760 div, 809 div,
761 iClob(new_LabelWidget( 810 iClob(new_LabelWidget(
762 uiTextCaution_ColorEscape "Save", SDLK_RETURN, KMOD_PRIMARY, "bmed.accept"))); 811 uiTextCaution_ColorEscape "Save Bookmark", SDLK_RETURN, KMOD_PRIMARY, "bmed.accept")));
812 }
813 addChild_Widget(dlg, iClob(div));
814 addChild_Widget(get_Window()->root, iClob(dlg));
815 centerSheet_Widget(dlg);
816 return dlg;
817}
818
819static iBool handleBookmarkCreationCommands_SidebarWidget_(iWidget *editor, const char *cmd) {
820 if (equal_Command(cmd, "bmed.accept") || equal_Command(cmd, "cancel")) {
821 if (equal_Command(cmd, "bmed.accept")) {
822 const iString *title = text_InputWidget(findChild_Widget(editor, "bmed.title"));
823 const iString *url = text_InputWidget(findChild_Widget(editor, "bmed.url"));
824 const iString *tags = text_InputWidget(findChild_Widget(editor, "bmed.tags"));
825 add_Bookmarks(bookmarks_App(),
826 url,
827 title,
828 tags,
829 first_String(label_LabelWidget(findChild_Widget(editor, "bmed.icon"))));
830 postCommand_App("bookmarks.changed");
831 }
832 destroy_Widget(editor);
833 return iTrue;
834 }
835 return iFalse;
836}
837
838iWidget *makeBookmarkCreation_Widget(const iString *url, const iString *title, iChar icon) {
839 iWidget *dlg = makeBookmarkEditor_Widget();
840 setId_Widget(dlg, "bmed.create");
841 setTextCStr_LabelWidget(findChild_Widget(dlg, "bmed.heading"),
842 uiHeading_ColorEscape "ADD BOOKMARK");
843 iUrl parts;
844 init_Url(&parts, url);
845 setTextCStr_InputWidget(findChild_Widget(dlg, "bmed.title"),
846 title ? cstr_String(title) : cstr_Rangecc(parts.host));
847 setText_InputWidget(findChild_Widget(dlg, "bmed.url"), url);
848 setId_Widget(
849 addChildFlags_Widget(
850 dlg,
851 iClob(new_LabelWidget(cstrCollect_String(newUnicodeN_String(&icon, 1)), 0, 0, NULL)),
852 collapse_WidgetFlag | hidden_WidgetFlag | disabled_WidgetFlag),
853 "bmed.icon");
854 setCommandHandler_Widget(dlg, handleBookmarkCreationCommands_SidebarWidget_);
855 return dlg;
856}
857
858iWidget *makeIdentityCreation_Widget(void) {
859 iWidget *dlg = makeSheet_Widget("ident");
860 setId_Widget(addChildFlags_Widget(
861 dlg,
862 iClob(new_LabelWidget(uiHeading_ColorEscape "NEW IDENTITY", 0, 0, NULL)),
863 frameless_WidgetFlag),
864 "ident.heading");
865 iWidget *page = new_Widget();
866 addChildFlags_Widget(
867 dlg,
868 iClob(
869 new_LabelWidget("Creating a 2048-bit self-signed RSA certificate.", 0, 0, NULL)),
870 frameless_WidgetFlag);
871 addChild_Widget(dlg, iClob(page));
872 setFlags_Widget(page, arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag, iTrue);
873 iWidget *headings = addChildFlags_Widget(
874 page, iClob(new_Widget()), arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag);
875 iWidget *values = addChildFlags_Widget(
876 page, iClob(new_Widget()), arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag);
877 iInputWidget *inputs[6];
878 addChild_Widget(headings, iClob(makeHeading_Widget("Common name:")));
879 setId_Widget(addChild_Widget(values, iClob(inputs[0] = new_InputWidget(0))), "ident.common");
880 addChild_Widget(headings, iClob(makeHeading_Widget("Email:")));
881 setId_Widget(addChild_Widget(values, iClob(inputs[1] = newHint_InputWidget(0, "optional"))), "ident.email");
882 addChild_Widget(headings, iClob(makeHeading_Widget("User ID:")));
883 setId_Widget(addChild_Widget(values, iClob(inputs[2] = newHint_InputWidget(0, "optional"))), "ident.userid");
884 addChild_Widget(headings, iClob(makeHeading_Widget("Domain:")));
885 setId_Widget(addChild_Widget(values, iClob(inputs[3] = newHint_InputWidget(0, "optional"))), "ident.domain");
886 addChild_Widget(headings, iClob(makeHeading_Widget("Organization:")));
887 setId_Widget(addChild_Widget(values, iClob(inputs[4] = newHint_InputWidget(0, "optional"))), "ident.org");
888 addChild_Widget(headings, iClob(makeHeading_Widget("Country:")));
889 setId_Widget(addChild_Widget(values, iClob(inputs[5] = newHint_InputWidget(0, "optional"))), "ident.country");
890 addChild_Widget(headings, iClob(makeHeading_Widget("Valid until:")));
891 setId_Widget(addChild_Widget(values, iClob(newHint_InputWidget(19, "YYYY-MM-DD HH:MM:SS"))), "ident.until");
892 addChild_Widget(headings, iClob(makeHeading_Widget("Temporary:")));
893 addChild_Widget(values, iClob(makeToggle_Widget("ident.temp")));
894 arrange_Widget(dlg);
895 for (size_t i = 0; i < iElemCount(inputs); ++i) {
896 as_Widget(inputs[i])->rect.size.x = 100 * gap_UI - headings->rect.size.x;
897 }
898 iWidget *div = new_Widget(); {
899 setFlags_Widget(div, arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag, iTrue);
900 addChild_Widget(div, iClob(new_LabelWidget("Cancel", SDLK_ESCAPE, 0, "cancel")));
901 addChild_Widget(
902 div,
903 iClob(new_LabelWidget(
904 uiTextAction_ColorEscape "Create Identity", SDLK_RETURN, KMOD_PRIMARY, "ident.accept")));
763 } 905 }
764 addChild_Widget(dlg, iClob(div)); 906 addChild_Widget(dlg, iClob(div));
765 addChild_Widget(get_Window()->root, iClob(dlg)); 907 addChild_Widget(get_Window()->root, iClob(dlg));
diff --git a/src/ui/util.h b/src/ui/util.h
index 6595b94c..9ef166de 100644
--- a/src/ui/util.h
+++ b/src/ui/util.h
@@ -1,5 +1,28 @@
1/* Copyright 2020 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
1#pragma once 23#pragma once
2 24
25#include <the_Foundation/string.h>
3#include <the_Foundation/rect.h> 26#include <the_Foundation/rect.h>
4#include <the_Foundation/vec2.h> 27#include <the_Foundation/vec2.h>
5#include <SDL_events.h> 28#include <SDL_events.h>
@@ -117,8 +140,11 @@ void makeFilePath_Widget (iWidget *parent, const iString *initialPath
117iWidget * makeValueInput_Widget (iWidget *parent, const iString *initialValue, const char *title, 140iWidget * makeValueInput_Widget (iWidget *parent, const iString *initialValue, const char *title,
118 const char *prompt, const char *acceptLabel, const char *command); 141 const char *prompt, const char *acceptLabel, const char *command);
119void updateValueInput_Widget (iWidget *, const char *title, const char *prompt); 142void updateValueInput_Widget (iWidget *, const char *title, const char *prompt);
120void makeMessage_Widget (const char *title, const char *msg); 143iWidget * makeMessage_Widget (const char *title, const char *msg);
121iWidget * makeQuestion_Widget (const char *title, const char *msg, 144iWidget * makeQuestion_Widget (const char *title, const char *msg,
122 const char *labels[], const char *commands[], size_t count); 145 const char *labels[], const char *commands[], size_t count);
123iWidget * makePreferences_Widget (void); 146
124iWidget * makeBookmarkEditor_Widget(void); 147iWidget * makePreferences_Widget (void);
148iWidget * makeBookmarkEditor_Widget (void);
149iWidget * makeBookmarkCreation_Widget (const iString *url, const iString *title, iChar icon);
150iWidget * makeIdentityCreation_Widget (void);
diff --git a/src/ui/widget.c b/src/ui/widget.c
index 1c19b70f..b5ea3b0f 100644
--- a/src/ui/widget.c
+++ b/src/ui/widget.c
@@ -1,3 +1,25 @@
1/* Copyright 2020 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
1#include "widget.h" 23#include "widget.h"
2 24
3#include "app.h" 25#include "app.h"
@@ -6,6 +28,7 @@
6#include "util.h" 28#include "util.h"
7#include "window.h" 29#include "window.h"
8 30
31#include <the_Foundation/ptrarray.h>
9#include <the_Foundation/ptrset.h> 32#include <the_Foundation/ptrset.h>
10#include <SDL_mouse.h> 33#include <SDL_mouse.h>
11#include <stdarg.h> 34#include <stdarg.h>
@@ -16,15 +39,15 @@ struct Impl_RootData {
16 iWidget *hover; 39 iWidget *hover;
17 iWidget *mouseGrab; 40 iWidget *mouseGrab;
18 iWidget *focus; 41 iWidget *focus;
19 iPtrSet *onTop; 42 iPtrArray *onTop; /* order is important; last one is topmost */
20 iPtrSet *pendingDestruction; 43 iPtrSet *pendingDestruction;
21}; 44};
22 45
23static iRootData rootData_; 46static iRootData rootData_;
24 47
25iPtrSet *onTop_RootData_(void) { 48iPtrArray *onTop_RootData_(void) {
26 if (!rootData_.onTop) { 49 if (!rootData_.onTop) {
27 rootData_.onTop = new_PtrSet(); 50 rootData_.onTop = new_PtrArray();
28 } 51 }
29 return rootData_.onTop; 52 return rootData_.onTop;
30} 53}
@@ -32,7 +55,7 @@ iPtrSet *onTop_RootData_(void) {
32void destroyPending_Widget(void) { 55void destroyPending_Widget(void) {
33 iForEach(PtrSet, i, rootData_.pendingDestruction) { 56 iForEach(PtrSet, i, rootData_.pendingDestruction) {
34 iWidget *widget = *i.value; 57 iWidget *widget = *i.value;
35 remove_PtrSet(onTop_RootData_(), widget); 58 removeOne_PtrArray(onTop_RootData_(), widget);
36 if (widget->parent) { 59 if (widget->parent) {
37 iRelease(removeChild_Widget(widget->parent, widget)); 60 iRelease(removeChild_Widget(widget->parent, widget));
38 } 61 }
@@ -54,6 +77,7 @@ void init_Widget(iWidget *d) {
54 d->children = NULL; 77 d->children = NULL;
55 d->parent = NULL; 78 d->parent = NULL;
56 d->commandHandler = NULL; 79 d->commandHandler = NULL;
80 iZap(d->padding);
57} 81}
58 82
59void deinit_Widget(iWidget *d) { 83void deinit_Widget(iWidget *d) {
@@ -103,10 +127,10 @@ void setFlags_Widget(iWidget *d, int flags, iBool set) {
103 iChangeFlags(d->flags, flags, set); 127 iChangeFlags(d->flags, flags, set);
104 if (flags & keepOnTop_WidgetFlag) { 128 if (flags & keepOnTop_WidgetFlag) {
105 if (set) { 129 if (set) {
106 insert_PtrSet(onTop_RootData_(), d); 130 pushBack_PtrArray(onTop_RootData_(), d);
107 } 131 }
108 else { 132 else {
109 remove_PtrSet(onTop_RootData_(), d); 133 removeOne_PtrArray(onTop_RootData_(), d);
110 } 134 }
111 } 135 }
112} 136}
@@ -120,6 +144,13 @@ void setSize_Widget(iWidget *d, iInt2 size) {
120 setFlags_Widget(d, fixedSize_WidgetFlag, iTrue); 144 setFlags_Widget(d, fixedSize_WidgetFlag, iTrue);
121} 145}
122 146
147void setPadding_Widget(iWidget *d, int left, int top, int right, int bottom) {
148 d->padding[0] = left;
149 d->padding[1] = top;
150 d->padding[2] = right;
151 d->padding[3] = bottom;
152}
153
123void setBackgroundColor_Widget(iWidget *d, int bgColor) { 154void setBackgroundColor_Widget(iWidget *d, int bgColor) {
124 d->bgColor = bgColor; 155 d->bgColor = bgColor;
125} 156}
@@ -169,19 +200,26 @@ iLocalDef iBool isCollapsed_Widget_(const iWidget *d) {
169 (hidden_WidgetFlag | collapse_WidgetFlag); 200 (hidden_WidgetFlag | collapse_WidgetFlag);
170} 201}
171 202
203iLocalDef iRect innerRect_Widget_(const iWidget *d) {
204 return init_Rect(d->padding[0],
205 d->padding[1],
206 width_Rect(d->rect) - d->padding[0] - d->padding[2],
207 height_Rect(d->rect) - d->padding[1] - d->padding[3]);
208}
209
172void arrange_Widget(iWidget *d) { 210void arrange_Widget(iWidget *d) {
173 if (isCollapsed_Widget_(d)) { 211 if (isCollapsed_Widget_(d)) {
174 setFlags_Widget(d, wasCollapsed_WidgetFlag, iTrue); 212 setFlags_Widget(d, wasCollapsed_WidgetFlag, iTrue);
175 return; 213 return;
176 } 214 }
177 if (d->flags & moveToParentRightEdge_WidgetFlag) { 215 if (d->flags & moveToParentRightEdge_WidgetFlag) {
178 d->rect.pos.x = width_Rect(d->parent->rect) - width_Rect(d->rect); 216 d->rect.pos.x = width_Rect(innerRect_Widget_(d->parent)) - width_Rect(d->rect);
179 } 217 }
180 if (d->flags & resizeToParentWidth_WidgetFlag) { 218 if (d->flags & resizeToParentWidth_WidgetFlag) {
181 setWidth_Widget_(d, d->parent->rect.size.x); 219 setWidth_Widget_(d, width_Rect(innerRect_Widget_(d->parent)));
182 } 220 }
183 if (d->flags & resizeToParentHeight_WidgetFlag) { 221 if (d->flags & resizeToParentHeight_WidgetFlag) {
184 setHeight_Widget_(d, d->parent->rect.size.y); 222 setHeight_Widget_(d, height_Rect(innerRect_Widget_(d->parent)));
185 } 223 }
186 /* The rest of the arrangement depends on child widgets. */ 224 /* The rest of the arrangement depends on child widgets. */
187 if (!d->children) { 225 if (!d->children) {
@@ -214,7 +252,7 @@ void arrange_Widget(iWidget *d) {
214 const int expCount = numExpandingChildren_Widget_(d); 252 const int expCount = numExpandingChildren_Widget_(d);
215 /* Only resize the expanding children, not touching the others. */ 253 /* Only resize the expanding children, not touching the others. */
216 if (expCount > 0) { 254 if (expCount > 0) {
217 iInt2 avail = d->rect.size; 255 iInt2 avail = innerRect_Widget_(d).size;
218 iConstForEach(ObjectList, i, d->children) { 256 iConstForEach(ObjectList, i, d->children) {
219 const iWidget *child = constAs_Widget(i.object); 257 const iWidget *child = constAs_Widget(i.object);
220 if (~child->flags & expand_WidgetFlag) { 258 if (~child->flags & expand_WidgetFlag) {
@@ -228,27 +266,27 @@ void arrange_Widget(iWidget *d) {
228 if (child->flags & expand_WidgetFlag) { 266 if (child->flags & expand_WidgetFlag) {
229 if (d->flags & arrangeHorizontal_WidgetFlag) { 267 if (d->flags & arrangeHorizontal_WidgetFlag) {
230 if (dirs.x) setWidth_Widget_(child, avail.x); 268 if (dirs.x) setWidth_Widget_(child, avail.x);
231 if (dirs.y) setHeight_Widget_(child, d->rect.size.y); 269 if (dirs.y) setHeight_Widget_(child, height_Rect(innerRect_Widget_(d)));
232 } 270 }
233 else if (d->flags & arrangeVertical_WidgetFlag) { 271 else if (d->flags & arrangeVertical_WidgetFlag) {
234 if (dirs.x) setWidth_Widget_(child, d->rect.size.x); 272 if (dirs.x) setWidth_Widget_(child, width_Rect(innerRect_Widget_(d)));
235 if (dirs.y) setHeight_Widget_(child, avail.y); 273 if (dirs.y) setHeight_Widget_(child, avail.y);
236 } 274 }
237 } 275 }
238 else { 276 else {
239 /* Fill the off axis, though. */ 277 /* Fill the off axis, though. */
240 if (d->flags & arrangeHorizontal_WidgetFlag) { 278 if (d->flags & arrangeHorizontal_WidgetFlag) {
241 if (dirs.y) setHeight_Widget_(child, d->rect.size.y); 279 if (dirs.y) setHeight_Widget_(child, height_Rect(innerRect_Widget_(d)));
242 } 280 }
243 else if (d->flags & arrangeVertical_WidgetFlag) { 281 else if (d->flags & arrangeVertical_WidgetFlag) {
244 if (dirs.x) setWidth_Widget_(child, d->rect.size.x); 282 if (dirs.x) setWidth_Widget_(child, width_Rect(innerRect_Widget_(d)));
245 } 283 }
246 } 284 }
247 } 285 }
248 } 286 }
249 else { 287 else {
250 /* Evenly size all children. */ 288 /* Evenly size all children. */
251 iInt2 childSize = d->rect.size; 289 iInt2 childSize = innerRect_Widget_(d).size;
252 if (d->flags & arrangeHorizontal_WidgetFlag) { 290 if (d->flags & arrangeHorizontal_WidgetFlag) {
253 childSize.x /= childCount; 291 childSize.x /= childCount;
254 } 292 }
@@ -270,7 +308,7 @@ void arrange_Widget(iWidget *d) {
270 setWidth_Widget_(as_Widget(i.object), widest); 308 setWidth_Widget_(as_Widget(i.object), widest);
271 } 309 }
272 } 310 }
273 iInt2 pos = zero_I2(); 311 iInt2 pos = initv_I2(d->padding);
274 iForEach(ObjectList, i, d->children) { 312 iForEach(ObjectList, i, d->children) {
275 iWidget *child = as_Widget(i.object); 313 iWidget *child = as_Widget(i.object);
276 arrange_Widget(child); 314 arrange_Widget(child);
@@ -286,6 +324,9 @@ void arrange_Widget(iWidget *d) {
286 pos.y += child->rect.size.y; 324 pos.y += child->rect.size.y;
287 } 325 }
288 } 326 }
327 else if (d->flags & resizeChildren_WidgetFlag) {
328 child->rect.pos = pos;
329 }
289 } 330 }
290 /* Update the size of the widget according to the arrangement. */ 331 /* Update the size of the widget according to the arrangement. */
291 if (d->flags & arrangeSize_WidgetFlag) { 332 if (d->flags & arrangeSize_WidgetFlag) {
@@ -299,6 +340,7 @@ void arrange_Widget(iWidget *d) {
299 bounds = union_Rect(bounds, child->rect); 340 bounds = union_Rect(bounds, child->rect);
300 } 341 }
301 } 342 }
343 adjustEdges_Rect(&bounds, -d->padding[1], d->padding[2], d->padding[3], -d->padding[0]);
302 if (d->flags & arrangeWidth_WidgetFlag) { 344 if (d->flags & arrangeWidth_WidgetFlag) {
303 setWidth_Widget_(d, bounds.size.x); 345 setWidth_Widget_(d, bounds.size.x);
304 /* Parent size changed, must update the children.*/ 346 /* Parent size changed, must update the children.*/
@@ -381,7 +423,7 @@ iBool dispatchEvent_Widget(iWidget *d, const SDL_Event *ev) {
381 } 423 }
382 } 424 }
383 /* Root offers events first to widgets on top. */ 425 /* Root offers events first to widgets on top. */
384 iForEach(PtrSet, i, rootData_.onTop) { 426 iReverseForEach(PtrArray, i, rootData_.onTop) {
385 iWidget *widget = *i.value; 427 iWidget *widget = *i.value;
386 if (isVisible_Widget(widget) && dispatchEvent_Widget(widget, ev)) { 428 if (isVisible_Widget(widget) && dispatchEvent_Widget(widget, ev)) {
387 return iTrue; 429 return iTrue;
@@ -454,6 +496,7 @@ iBool processEvent_Widget(iWidget *d, const SDL_Event *ev) {
454 } 496 }
455 } 497 }
456 if (d->flags & mouseModal_WidgetFlag && isMouseEvent_(ev)) { 498 if (d->flags & mouseModal_WidgetFlag && isMouseEvent_(ev)) {
499 setCursor_Window(get_Window(), SDL_SYSTEM_CURSOR_ARROW);
457 return iTrue; 500 return iTrue;
458 } 501 }
459 return iFalse; 502 return iFalse;
@@ -480,7 +523,7 @@ void draw_Widget(const iWidget *d) {
480 } 523 }
481 /* Root draws the on-top widgets on top of everything else. */ 524 /* Root draws the on-top widgets on top of everything else. */
482 if (!d->parent) { 525 if (!d->parent) {
483 iConstForEach(PtrSet, i, onTop_RootData_()) { 526 iConstForEach(PtrArray, i, onTop_RootData_()) {
484 draw_Widget(*i.value); 527 draw_Widget(*i.value);
485 } 528 }
486 } 529 }
@@ -594,7 +637,7 @@ iBool isHover_Widget(const iWidget *d) {
594} 637}
595 638
596iBool isSelected_Widget(const iWidget *d) { 639iBool isSelected_Widget(const iWidget *d) {
597 return (d->flags & selected_WidgetFlag) != 0; 640 return d && (d->flags & selected_WidgetFlag) != 0;
598} 641}
599 642
600iBool equalWidget_Command(const char *cmd, const iWidget *widget, const char *checkCommand) { 643iBool equalWidget_Command(const char *cmd, const iWidget *widget, const char *checkCommand) {
diff --git a/src/ui/widget.h b/src/ui/widget.h
index e88a10dc..3f03fc07 100644
--- a/src/ui/widget.h
+++ b/src/ui/widget.h
@@ -1,3 +1,25 @@
1/* Copyright 2020 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
1#pragma once 23#pragma once
2 24
3/* Base class for UI widgets. */ 25/* Base class for UI widgets. */
@@ -73,6 +95,7 @@ struct Impl_Widget {
73 iString id; 95 iString id;
74 int flags; 96 int flags;
75 iRect rect; 97 iRect rect;
98 int padding[4]; /* left, top, right, bottom */
76 int bgColor; 99 int bgColor;
77 int frameColor; 100 int frameColor;
78 iObjectList *children; 101 iObjectList *children;
@@ -107,7 +130,8 @@ void destroyPending_Widget(void);
107 130
108const iString *id_Widget (const iWidget *); 131const iString *id_Widget (const iWidget *);
109int flags_Widget (const iWidget *); 132int flags_Widget (const iWidget *);
110iRect bounds_Widget (const iWidget *); 133iRect bounds_Widget (const iWidget *); /* outer bounds */
134iRect innerBounds_Widget (const iWidget *);
111iInt2 localCoord_Widget (const iWidget *, iInt2 coord); 135iInt2 localCoord_Widget (const iWidget *, iInt2 coord);
112iBool contains_Widget (const iWidget *, iInt2 coord); 136iBool contains_Widget (const iWidget *, iInt2 coord);
113iAny * findChild_Widget (const iWidget *, const char *id); 137iAny * findChild_Widget (const iWidget *, const char *id);
@@ -131,6 +155,8 @@ void setId_Widget (iWidget *, const char *id);
131void setFlags_Widget (iWidget *, int flags, iBool set); 155void setFlags_Widget (iWidget *, int flags, iBool set);
132void setPos_Widget (iWidget *, iInt2 pos); 156void setPos_Widget (iWidget *, iInt2 pos);
133void setSize_Widget (iWidget *, iInt2 size); 157void setSize_Widget (iWidget *, iInt2 size);
158void setPadding_Widget (iWidget *, int left, int top, int right, int bottom);
159iLocalDef void setPadding1_Widget (iWidget *d, int padding) { setPadding_Widget(d, padding, padding, padding, padding); }
134void setBackgroundColor_Widget (iWidget *, int bgColor); 160void setBackgroundColor_Widget (iWidget *, int bgColor);
135void setFrameColor_Widget (iWidget *, int frameColor); 161void setFrameColor_Widget (iWidget *, int frameColor);
136void setCommandHandler_Widget (iWidget *, iBool (*handler)(iWidget *, const char *)); 162void setCommandHandler_Widget (iWidget *, iBool (*handler)(iWidget *, const char *));
diff --git a/src/ui/window.c b/src/ui/window.c
index 3d9d98d1..650bc9ee 100644
--- a/src/ui/window.c
+++ b/src/ui/window.c
@@ -1,17 +1,39 @@
1/* Copyright 2020 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
1#include "window.h" 23#include "window.h"
2 24
25#include "labelwidget.h"
26#include "inputwidget.h"
27#include "documentwidget.h"
28#include "sidebarwidget.h"
3#include "embedded.h" 29#include "embedded.h"
4#include "app.h"
5#include "command.h" 30#include "command.h"
6#include "paint.h" 31#include "paint.h"
7#include "text.h"
8#include "util.h" 32#include "util.h"
33#include "../app.h"
9#include "../visited.h" 34#include "../visited.h"
10#include "labelwidget.h" 35#include "../gmcerts.h"
11#include "inputwidget.h" 36#include "../gmutil.h"
12#include "documentwidget.h"
13#include "sidebarwidget.h"
14#include "gmutil.h"
15#if defined (iPlatformMsys) 37#if defined (iPlatformMsys)
16# include "../win32.h" 38# include "../win32.h"
17#endif 39#endif
@@ -80,6 +102,8 @@ static const iMenuItem navMenuItems[] = {
80 { "Reset Zoom", SDLK_0, KMOD_PRIMARY, "zoom.set arg:100" }, 102 { "Reset Zoom", SDLK_0, KMOD_PRIMARY, "zoom.set arg:100" },
81 { "---", 0, 0, NULL }, 103 { "---", 0, 0, NULL },
82 { "Preferences...", SDLK_COMMA, KMOD_PRIMARY, "preferences" }, 104 { "Preferences...", SDLK_COMMA, KMOD_PRIMARY, "preferences" },
105 { "Help", 0, 0, "!open url:about:help" },
106 { "Release Notes", 0, 0, "!open url:about:version" },
83 { "---", 0, 0, NULL }, 107 { "---", 0, 0, NULL },
84 { "Quit Lagrange", 'q', KMOD_PRIMARY, "quit" } 108 { "Quit Lagrange", 'q', KMOD_PRIMARY, "quit" }
85}; 109};
@@ -94,8 +118,13 @@ static const iMenuItem fileMenuItems[] = {
94 118
95static const iMenuItem editMenuItems[] = { 119static const iMenuItem editMenuItems[] = {
96 { "Copy Source Text", SDLK_c, KMOD_PRIMARY, "copy" }, 120 { "Copy Source Text", SDLK_c, KMOD_PRIMARY, "copy" },
121 { "Copy Link to Page", SDLK_c, KMOD_PRIMARY | KMOD_SHIFT, "document.copylink" },
97 { "---", 0, 0, NULL }, 122 { "---", 0, 0, NULL },
98 { "Bookmark This Page", SDLK_d, KMOD_PRIMARY, "bookmark.add" }, 123 { "Bookmark This Page...", SDLK_d, KMOD_PRIMARY, "bookmark.add" },
124};
125
126static const iMenuItem identityMenuItems[] = {
127 { "New Identity...", SDLK_n, KMOD_PRIMARY | KMOD_SHIFT, "ident.new" },
99}; 128};
100 129
101static const iMenuItem viewMenuItems[] = { 130static const iMenuItem viewMenuItems[] = {
@@ -115,14 +144,43 @@ static const iMenuItem viewMenuItems[] = {
115}; 144};
116 145
117static const iMenuItem helpMenuItems[] = { 146static const iMenuItem helpMenuItems[] = {
118 { "Help", 0, 0, "open url:about:help" }, 147 { "Help", 0, 0, "!open url:about:help" },
119 { "Release Notes", 0, 0, "open url:about:version" }, 148 { "Release Notes", 0, 0, "!open url:about:version" },
120}; 149};
121#endif 150#endif
122 151
152static const iMenuItem identityButtonMenuItems[] = {
153 { "No Active Identity", 0, 0, "ident.showactive" },
154 { "---", 0, 0, NULL },
155#if !defined (iHaveNativeMenus)
156 { "New Identity...", SDLK_n, KMOD_PRIMARY | KMOD_SHIFT, "ident.new" },
157 { "---", 0, 0, NULL },
158 { "Show Identities", '3', KMOD_PRIMARY, "sidebar.mode arg:2 show:1" },
159#else
160 { "New Identity...", 0, 0, "ident.new" },
161 { "---", 0, 0, NULL },
162 { "Show Identities", 0, 0, "sidebar.mode arg:2 show:1" },
163#endif
164};
165
123static const char *reloadCStr_ = "\U0001f503"; 166static const char *reloadCStr_ = "\U0001f503";
124static const char *stopCStr_ = uiTextCaution_ColorEscape "\U0001f310"; 167static const char *stopCStr_ = uiTextCaution_ColorEscape "\U0001f310";
125 168
169static void updateNavBarIdentity_(iWidget *navBar) {
170 const iGmIdentity *ident =
171 identityForUrl_GmCerts(certs_App(), url_DocumentWidget(document_App()));
172 iWidget *button = findChild_Widget(navBar, "navbar.ident");
173 setFlags_Widget(button, selected_WidgetFlag, ident != NULL);
174 /* Update menu. */
175 iLabelWidget *idItem = child_Widget(findChild_Widget(button, "menu"), 0);
176 setTextCStr_LabelWidget(
177 idItem,
178 ident ? format_CStr(uiTextAction_ColorEscape "%s",
179 cstrCollect_String(subject_TlsCertificate(ident->cert)))
180 : "No Active Identity");
181 setFlags_Widget(as_Widget(idItem), disabled_WidgetFlag, !ident);
182}
183
126static iBool handleNavBarCommands_(iWidget *navBar, const char *cmd) { 184static iBool handleNavBarCommands_(iWidget *navBar, const char *cmd) {
127 if (equal_Command(cmd, "window.resized")) { 185 if (equal_Command(cmd, "window.resized")) {
128 const iBool isNarrow = width_Rect(bounds_Widget(navBar)) / gap_UI < 140; 186 const iBool isNarrow = width_Rect(bounds_Widget(navBar)) / gap_UI < 140;
@@ -160,6 +218,7 @@ static iBool handleNavBarCommands_(iWidget *navBar, const char *cmd) {
160 const iString *urlStr = collect_String(suffix_Command(cmd, "url")); 218 const iString *urlStr = collect_String(suffix_Command(cmd, "url"));
161 setText_InputWidget(url, urlStr); 219 setText_InputWidget(url, urlStr);
162 updateTextCStr_LabelWidget(reloadButton, reloadCStr_); 220 updateTextCStr_LabelWidget(reloadButton, reloadCStr_);
221 updateNavBarIdentity_(navBar);
163 return iFalse; 222 return iFalse;
164 } 223 }
165 else if (equal_Command(cmd, "document.request.cancelled")) { 224 else if (equal_Command(cmd, "document.request.cancelled")) {
@@ -184,12 +243,13 @@ static iBool handleNavBarCommands_(iWidget *navBar, const char *cmd) {
184 setText_InputWidget(findChild_Widget(navBar, "url"), url_DocumentWidget(doc)); 243 setText_InputWidget(findChild_Widget(navBar, "url"), url_DocumentWidget(doc));
185 updateTextCStr_LabelWidget(findChild_Widget(navBar, "reload"), 244 updateTextCStr_LabelWidget(findChild_Widget(navBar, "reload"),
186 isRequestOngoing_DocumentWidget(doc) ? stopCStr_ : reloadCStr_); 245 isRequestOngoing_DocumentWidget(doc) ? stopCStr_ : reloadCStr_);
246 updateNavBarIdentity_(navBar);
187 } 247 }
188 } 248 }
189 else if (equal_Command(cmd, "mouse.clicked")) { 249 else if (equal_Command(cmd, "mouse.clicked") && arg_Command(cmd)) {
190 iWidget *widget = pointer_Command(cmd); 250 iWidget *widget = pointer_Command(cmd);
191 iWidget *menu = findWidget_App("doctabs.menu"); 251 iWidget *menu = findWidget_App("doctabs.menu");
192 if (isTabButton_Widget(widget)) { 252 if (isTabButton_Widget(widget) && !isVisible_Widget(menu)) {
193 iWidget *tabs = findWidget_App("doctabs"); 253 iWidget *tabs = findWidget_App("doctabs");
194 showTabPage_Widget(tabs, 254 showTabPage_Widget(tabs,
195 tabPage_Widget(tabs, childIndex_Widget(widget->parent, widget))); 255 tabPage_Widget(tabs, childIndex_Widget(widget->parent, widget)));
@@ -261,6 +321,7 @@ static void setupUserInterface_Window(iWindow *d) {
261 /* Navigation bar. */ { 321 /* Navigation bar. */ {
262 iWidget *navBar = new_Widget(); 322 iWidget *navBar = new_Widget();
263 setId_Widget(navBar, "navbar"); 323 setId_Widget(navBar, "navbar");
324 setPadding_Widget(navBar, gap_UI / 2, 0, gap_UI / 2, 0);
264 setFlags_Widget(navBar, 325 setFlags_Widget(navBar,
265 arrangeHeight_WidgetFlag | resizeChildren_WidgetFlag | 326 arrangeHeight_WidgetFlag | resizeChildren_WidgetFlag |
266 arrangeHorizontal_WidgetFlag, 327 arrangeHorizontal_WidgetFlag,
@@ -270,7 +331,11 @@ static void setupUserInterface_Window(iWindow *d) {
270 setCommandHandler_Widget(navBar, handleNavBarCommands_); 331 setCommandHandler_Widget(navBar, handleNavBarCommands_);
271 addChild_Widget(navBar, iClob(newIcon_LabelWidget("\U0001f850", 0, 0, "navigate.back"))); 332 addChild_Widget(navBar, iClob(newIcon_LabelWidget("\U0001f850", 0, 0, "navigate.back")));
272 addChild_Widget(navBar, iClob(newIcon_LabelWidget("\U0001f852", 0, 0, "navigate.forward"))); 333 addChild_Widget(navBar, iClob(newIcon_LabelWidget("\U0001f852", 0, 0, "navigate.forward")));
273 addChild_Widget(navBar, iClob(newIcon_LabelWidget("\U0001f3e0", 0, 0, "navigate.home"))); 334 iLabelWidget *idMenu =
335 makeMenuButton_LabelWidget("\U0001f464", identityButtonMenuItems, iElemCount(identityButtonMenuItems));
336 setAlignVisually_LabelWidget(idMenu, iTrue);
337 addChild_Widget(navBar, iClob(idMenu));
338 setId_Widget(as_Widget(idMenu), "navbar.ident");
274 iLabelWidget *lock = 339 iLabelWidget *lock =
275 addChildFlags_Widget(navBar, 340 addChildFlags_Widget(navBar,
276 iClob(newIcon_LabelWidget("\U0001f513", 0, 0, "server.showcert")), 341 iClob(newIcon_LabelWidget("\U0001f513", 0, 0, "server.showcert")),
@@ -279,14 +344,16 @@ static void setupUserInterface_Window(iWindow *d) {
279 setFont_LabelWidget(lock, defaultSymbols_FontId); 344 setFont_LabelWidget(lock, defaultSymbols_FontId);
280 updateTextCStr_LabelWidget(lock, "\U0001f512"); 345 updateTextCStr_LabelWidget(lock, "\U0001f512");
281 iInputWidget *url = new_InputWidget(0); 346 iInputWidget *url = new_InputWidget(0);
347 setSelectAllOnFocus_InputWidget(url, iTrue);
282 setId_Widget(as_Widget(url), "url"); 348 setId_Widget(as_Widget(url), "url");
283 setTextCStr_InputWidget(url, "gemini://"); 349 setTextCStr_InputWidget(url, "gemini://");
284 addChildFlags_Widget(navBar, iClob(url), expand_WidgetFlag); 350 addChildFlags_Widget(navBar, iClob(url), expand_WidgetFlag);
285 setId_Widget( 351 setId_Widget(addChild_Widget(
286 addChild_Widget(navBar, iClob(newIcon_LabelWidget(reloadCStr_, 0, 0, "navigate.reload"))), 352 navBar, iClob(newIcon_LabelWidget(reloadCStr_, 0, 0, "navigate.reload"))),
287 "reload"); 353 "reload");
288 addChild_Widget(navBar, iClob(newIcon_LabelWidget("\U0001f464", 0, 0, "cert.client"))); 354 addChild_Widget(navBar,
289 355 iClob(newIcon_LabelWidget(
356 "\U0001f3e0", SDLK_h, KMOD_PRIMARY | KMOD_SHIFT, "navigate.home")));
290#if !defined (iHaveNativeMenus) 357#if !defined (iHaveNativeMenus)
291 iLabelWidget *navMenu = 358 iLabelWidget *navMenu =
292 makeMenuButton_LabelWidget("\U0001d362", navMenuItems, iElemCount(navMenuItems)); 359 makeMenuButton_LabelWidget("\U0001d362", navMenuItems, iElemCount(navMenuItems));
@@ -296,7 +363,8 @@ static void setupUserInterface_Window(iWindow *d) {
296 insertMenuItems_MacOS("File", 1, fileMenuItems, iElemCount(fileMenuItems)); 363 insertMenuItems_MacOS("File", 1, fileMenuItems, iElemCount(fileMenuItems));
297 insertMenuItems_MacOS("Edit", 2, editMenuItems, iElemCount(editMenuItems)); 364 insertMenuItems_MacOS("Edit", 2, editMenuItems, iElemCount(editMenuItems));
298 insertMenuItems_MacOS("View", 3, viewMenuItems, iElemCount(viewMenuItems)); 365 insertMenuItems_MacOS("View", 3, viewMenuItems, iElemCount(viewMenuItems));
299 insertMenuItems_MacOS("Help", 5, helpMenuItems, iElemCount(helpMenuItems)); 366 insertMenuItems_MacOS("Identity", 4, identityMenuItems, iElemCount(identityMenuItems));
367 insertMenuItems_MacOS("Help", 6, helpMenuItems, iElemCount(helpMenuItems));
300#endif 368#endif
301 } 369 }
302 /* Tab bar. */ { 370 /* Tab bar. */ {
@@ -380,6 +448,8 @@ static void drawBlank_Window_(iWindow *d) {
380 448
381void init_Window(iWindow *d, iRect rect) { 449void init_Window(iWindow *d, iRect rect) {
382 theWindow_ = d; 450 theWindow_ = d;
451 iZap(d->cursors);
452 d->pendingCursor = NULL;
383 d->isDrawFrozen = iTrue; 453 d->isDrawFrozen = iTrue;
384 uint32_t flags = SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI; 454 uint32_t flags = SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI;
385#if defined (iPlatformApple) 455#if defined (iPlatformApple)
@@ -396,7 +466,7 @@ void init_Window(iWindow *d, iRect rect) {
396 if (left_Rect(rect) >= 0) { 466 if (left_Rect(rect) >= 0) {
397 SDL_SetWindowPosition(d->win, left_Rect(rect), top_Rect(rect)); 467 SDL_SetWindowPosition(d->win, left_Rect(rect), top_Rect(rect));
398 } 468 }
399 SDL_SetWindowMinimumSize(d->win, 400, 200); 469 SDL_SetWindowMinimumSize(d->win, 400, 250);
400 SDL_SetWindowTitle(d->win, "Lagrange"); 470 SDL_SetWindowTitle(d->win, "Lagrange");
401 /* Some info. */ { 471 /* Some info. */ {
402 SDL_RendererInfo info; 472 SDL_RendererInfo info;
@@ -442,6 +512,11 @@ void deinit_Window(iWindow *d) {
442 if (theWindow_ == d) { 512 if (theWindow_ == d) {
443 theWindow_ = NULL; 513 theWindow_ = NULL;
444 } 514 }
515 iForIndices(i, d->cursors) {
516 if (d->cursors[i]) {
517 SDL_FreeCursor(d->cursors[i]);
518 }
519 }
445 iReleasePtr(&d->root); 520 iReleasePtr(&d->root);
446 deinit_Text(); 521 deinit_Text();
447 SDL_DestroyRenderer(d->render); 522 SDL_DestroyRenderer(d->render);
@@ -470,6 +545,13 @@ static iBool handleWindowEvent_Window_(iWindow *d, const SDL_WindowEvent *ev) {
470 return iFalse; 545 return iFalse;
471} 546}
472 547
548static void applyCursor_Window_(iWindow *d) {
549 if (d->pendingCursor) {
550 SDL_SetCursor(d->pendingCursor);
551 d->pendingCursor = NULL;
552 }
553}
554
473iBool processEvent_Window(iWindow *d, const SDL_Event *ev) { 555iBool processEvent_Window(iWindow *d, const SDL_Event *ev) {
474 switch (ev->type) { 556 switch (ev->type) {
475 case SDL_WINDOWEVENT: { 557 case SDL_WINDOWEVENT: {
@@ -484,6 +566,7 @@ iBool processEvent_Window(iWindow *d, const SDL_Event *ev) {
484 } 566 }
485 /* Map mouse pointer coordinate to our coordinate system. */ 567 /* Map mouse pointer coordinate to our coordinate system. */
486 if (event.type == SDL_MOUSEMOTION) { 568 if (event.type == SDL_MOUSEMOTION) {
569 setCursor_Window(d, SDL_SYSTEM_CURSOR_ARROW); /* default cursor */
487 const iInt2 pos = coord_Window(d, event.motion.x, event.motion.y); 570 const iInt2 pos = coord_Window(d, event.motion.x, event.motion.y);
488 event.motion.x = pos.x; 571 event.motion.x = pos.x;
489 event.motion.y = pos.y; 572 event.motion.y = pos.y;
@@ -506,6 +589,9 @@ iBool processEvent_Window(iWindow *d, const SDL_Event *ev) {
506 if (oldHover != hover_Widget()) { 589 if (oldHover != hover_Widget()) {
507 postRefresh_App(); 590 postRefresh_App();
508 } 591 }
592 if (event.type == SDL_MOUSEMOTION) {
593 applyCursor_Window_(d);
594 }
509 return wasUsed; 595 return wasUsed;
510 } 596 }
511 } 597 }
@@ -564,6 +650,13 @@ void setFreezeDraw_Window(iWindow *d, iBool freezeDraw) {
564 d->isDrawFrozen = freezeDraw; 650 d->isDrawFrozen = freezeDraw;
565} 651}
566 652
653void setCursor_Window(iWindow *d, int cursor) {
654 if (!d->cursors[cursor]) {
655 d->cursors[cursor] = SDL_CreateSystemCursor(cursor);
656 }
657 d->pendingCursor = d->cursors[cursor];
658}
659
567iInt2 rootSize_Window(const iWindow *d) { 660iInt2 rootSize_Window(const iWindow *d) {
568 return d->root->rect.size; 661 return d->root->rect.size;
569} 662}
diff --git a/src/ui/window.h b/src/ui/window.h
index d6eed841..4aec2fa7 100644
--- a/src/ui/window.h
+++ b/src/ui/window.h
@@ -1,3 +1,25 @@
1/* Copyright 2020 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
1#pragma once 23#pragma once
2 24
3#include "widget.h" 25#include "widget.h"
@@ -19,6 +41,8 @@ struct Impl_Window {
19 float uiScale; 41 float uiScale;
20 uint32_t frameTime; 42 uint32_t frameTime;
21 double presentTime; 43 double presentTime;
44 SDL_Cursor * cursors[SDL_NUM_SYSTEM_CURSORS];
45 SDL_Cursor * pendingCursor;
22}; 46};
23 47
24iBool processEvent_Window (iWindow *, const SDL_Event *); 48iBool processEvent_Window (iWindow *, const SDL_Event *);
@@ -27,6 +51,7 @@ void resize_Window (iWindow *, int w, int h);
27void setTitle_Window (iWindow *, const iString *title); 51void setTitle_Window (iWindow *, const iString *title);
28void setUiScale_Window (iWindow *, float uiScale); 52void setUiScale_Window (iWindow *, float uiScale);
29void setFreezeDraw_Window (iWindow *, iBool freezeDraw); 53void setFreezeDraw_Window (iWindow *, iBool freezeDraw);
54void setCursor_Window (iWindow *, int cursor);
30 55
31iInt2 rootSize_Window (const iWindow *); 56iInt2 rootSize_Window (const iWindow *);
32float uiScale_Window (const iWindow *); 57float uiScale_Window (const iWindow *);