diff options
Diffstat (limited to 'src/ui/documentwidget.c')
-rw-r--r-- | src/ui/documentwidget.c | 177 |
1 files changed, 160 insertions, 17 deletions
diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c index 56726320..ca91d896 100644 --- a/src/ui/documentwidget.c +++ b/src/ui/documentwidget.c | |||
@@ -7,6 +7,7 @@ | |||
7 | 7 | ||
8 | #include <the_Foundation/file.h> | 8 | #include <the_Foundation/file.h> |
9 | #include <the_Foundation/path.h> | 9 | #include <the_Foundation/path.h> |
10 | #include <the_Foundation/ptrarray.h> | ||
10 | #include <the_Foundation/regexp.h> | 11 | #include <the_Foundation/regexp.h> |
11 | #include <the_Foundation/tlsrequest.h> | 12 | #include <the_Foundation/tlsrequest.h> |
12 | 13 | ||
@@ -17,6 +18,8 @@ enum iDocumentState { | |||
17 | ready_DocumentState, | 18 | ready_DocumentState, |
18 | }; | 19 | }; |
19 | 20 | ||
21 | |||
22 | |||
20 | struct Impl_DocumentWidget { | 23 | struct Impl_DocumentWidget { |
21 | iWidget widget; | 24 | iWidget widget; |
22 | enum iDocumentState state; | 25 | enum iDocumentState state; |
@@ -27,6 +30,9 @@ struct Impl_DocumentWidget { | |||
27 | iGmDocument *doc; | 30 | iGmDocument *doc; |
28 | int pageMargin; | 31 | int pageMargin; |
29 | int scrollY; | 32 | int scrollY; |
33 | iPtrArray visibleLinks; | ||
34 | const iGmRun *hoverLink; | ||
35 | iClick click; | ||
30 | }; | 36 | }; |
31 | 37 | ||
32 | iDeclareType(Url) | 38 | iDeclareType(Url) |
@@ -44,15 +50,15 @@ void init_Url(iUrl *d, const iString *text) { | |||
44 | new_RegExp("(.+)://([^/:?]*)(:[0-9]+)?([^?]*)(\\?.*)?", caseInsensitive_RegExpOption); | 50 | new_RegExp("(.+)://([^/:?]*)(:[0-9]+)?([^?]*)(\\?.*)?", caseInsensitive_RegExpOption); |
45 | iRegExpMatch m; | 51 | iRegExpMatch m; |
46 | if (matchString_RegExp(pattern, text, &m)) { | 52 | if (matchString_RegExp(pattern, text, &m)) { |
47 | capturedRange_RegExpMatch(&m, 1, &d->protocol); | 53 | d->protocol = capturedRange_RegExpMatch(&m, 1); |
48 | capturedRange_RegExpMatch(&m, 2, &d->host); | 54 | d->host = capturedRange_RegExpMatch(&m, 2); |
49 | capturedRange_RegExpMatch(&m, 3, &d->port); | 55 | d->port = capturedRange_RegExpMatch(&m, 3); |
50 | if (!isEmpty_Range(&d->port)) { | 56 | if (!isEmpty_Range(&d->port)) { |
51 | /* Don't include the colon. */ | 57 | /* Don't include the colon. */ |
52 | d->port.start++; | 58 | d->port.start++; |
53 | } | 59 | } |
54 | capturedRange_RegExpMatch(&m, 4, &d->path); | 60 | d->path = capturedRange_RegExpMatch(&m, 4); |
55 | capturedRange_RegExpMatch(&m, 5, &d->query); | 61 | d->query = capturedRange_RegExpMatch(&m, 5); |
56 | } | 62 | } |
57 | else { | 63 | else { |
58 | iZap(*d); | 64 | iZap(*d); |
@@ -65,6 +71,7 @@ iDefineObjectConstruction(DocumentWidget) | |||
65 | void init_DocumentWidget(iDocumentWidget *d) { | 71 | void init_DocumentWidget(iDocumentWidget *d) { |
66 | iWidget *w = as_Widget(d); | 72 | iWidget *w = as_Widget(d); |
67 | init_Widget(w); | 73 | init_Widget(w); |
74 | setId_Widget(w, "document"); | ||
68 | setBackgroundColor_Widget(w, gray25_ColorId); | 75 | setBackgroundColor_Widget(w, gray25_ColorId); |
69 | d->state = blank_DocumentState; | 76 | d->state = blank_DocumentState; |
70 | d->url = new_String(); | 77 | d->url = new_String(); |
@@ -74,11 +81,15 @@ void init_DocumentWidget(iDocumentWidget *d) { | |||
74 | d->doc = new_GmDocument(); | 81 | d->doc = new_GmDocument(); |
75 | d->pageMargin = 5; | 82 | d->pageMargin = 5; |
76 | d->scrollY = 0; | 83 | d->scrollY = 0; |
84 | init_PtrArray(&d->visibleLinks); | ||
85 | d->hoverLink = NULL; | ||
86 | init_Click(&d->click, d, SDL_BUTTON_LEFT); | ||
77 | setUrl_DocumentWidget( | 87 | setUrl_DocumentWidget( |
78 | d, collectNewFormat_String("file://%s/test.gmi", cstr_String(collect_String(home_Path())))); | 88 | d, collectNewFormat_String("file://%s/test.gmi", cstr_String(collect_String(home_Path())))); |
79 | } | 89 | } |
80 | 90 | ||
81 | void deinit_DocumentWidget(iDocumentWidget *d) { | 91 | void deinit_DocumentWidget(iDocumentWidget *d) { |
92 | deinit_PtrArray(&d->visibleLinks); | ||
82 | delete_String(d->url); | 93 | delete_String(d->url); |
83 | delete_String(d->newSource); | 94 | delete_String(d->newSource); |
84 | iRelease(d->doc); | 95 | iRelease(d->doc); |
@@ -90,6 +101,17 @@ static int documentWidth_DocumentWidget_(const iDocumentWidget *d) { | |||
90 | return iMini(bounds.size.x - gap_UI * d->pageMargin * 2, fontSize_UI * 40); | 101 | return iMini(bounds.size.x - gap_UI * d->pageMargin * 2, fontSize_UI * 40); |
91 | } | 102 | } |
92 | 103 | ||
104 | static iRect documentBounds_DocumentWidget_(const iDocumentWidget *d) { | ||
105 | const iRect bounds = bounds_Widget(constAs_Widget(d)); | ||
106 | const int margin = gap_UI * d->pageMargin; | ||
107 | iRect rect; | ||
108 | rect.size.x = documentWidth_DocumentWidget_(d); | ||
109 | rect.pos.x = bounds.size.x / 2 - rect.size.x / 2; | ||
110 | rect.pos.y = top_Rect(bounds) + margin; | ||
111 | rect.size.y = height_Rect(bounds) - 2 * margin; | ||
112 | return rect; | ||
113 | } | ||
114 | |||
93 | void setSource_DocumentWidget(iDocumentWidget *d, const iString *source) { | 115 | void setSource_DocumentWidget(iDocumentWidget *d, const iString *source) { |
94 | /* TODO: lock source during update */ | 116 | /* TODO: lock source during update */ |
95 | setSource_GmDocument(d->doc, source, documentWidth_DocumentWidget_(d)); | 117 | setSource_GmDocument(d->doc, source, documentWidth_DocumentWidget_(d)); |
@@ -114,6 +136,12 @@ static void requestFinished_DocumentWidget_(iAnyObject *obj) { | |||
114 | d->statusCode = toInt_String(line); | 136 | d->statusCode = toInt_String(line); |
115 | printf("response (%02d): %s\n", d->statusCode, cstr_String(line)); | 137 | printf("response (%02d): %s\n", d->statusCode, cstr_String(line)); |
116 | /* TODO: post a command with the status code */ | 138 | /* TODO: post a command with the status code */ |
139 | switch (d->statusCode) { | ||
140 | case redirectPermanent_GmStatusCode: | ||
141 | case redirectTemporary_GmStatusCode: | ||
142 | postCommandf_App("open url:%s", cstr_String(line) + 3); | ||
143 | break; | ||
144 | } | ||
117 | delete_String(line); | 145 | delete_String(line); |
118 | } | 146 | } |
119 | setCStrN_String(d->newSource, responseRange.start, size_Range(&responseRange)); | 147 | setCStrN_String(d->newSource, responseRange.start, size_Range(&responseRange)); |
@@ -155,19 +183,99 @@ static void fetch_DocumentWidget_(iDocumentWidget *d) { | |||
155 | } | 183 | } |
156 | 184 | ||
157 | void setUrl_DocumentWidget(iDocumentWidget *d, const iString *url) { | 185 | void setUrl_DocumentWidget(iDocumentWidget *d, const iString *url) { |
158 | clear_String(d->url); | 186 | iString *newUrl = new_String(); |
159 | if (indexOfCStr_String(url, "://") == iInvalidPos && !startsWithCase_String(url, "gemini:")) { | 187 | if (indexOfCStr_String(url, "://") == iInvalidPos && !startsWithCase_String(url, "gemini:")) { |
160 | /* Prepend default protocol. */ | 188 | /* Prepend default protocol. */ |
161 | setCStr_String(d->url, "gemini://"); | 189 | setCStr_String(newUrl, "gemini://"); |
190 | } | ||
191 | append_String(newUrl, url); | ||
192 | if (cmpStringSc_String(d->url, newUrl, &iCaseInsensitive)) { | ||
193 | set_String(d->url, newUrl); | ||
194 | fetch_DocumentWidget_(d); | ||
195 | } | ||
196 | delete_String(newUrl); | ||
197 | } | ||
198 | |||
199 | static void addVisibleLink_DocumentWidget_(void *context, const iGmRun *run) { | ||
200 | iDocumentWidget *d = context; | ||
201 | if (run->linkId) { | ||
202 | pushBack_PtrArray(&d->visibleLinks, run); | ||
203 | } | ||
204 | } | ||
205 | |||
206 | static iRangei visibleRange_DocumentWidget_(const iDocumentWidget *d) { | ||
207 | const int margin = gap_UI * d->pageMargin; | ||
208 | return (iRangei){ d->scrollY - margin, | ||
209 | d->scrollY + height_Rect(bounds_Widget(constAs_Widget(d))) - margin }; | ||
210 | } | ||
211 | |||
212 | static void updateVisible_DocumentWidget_(iDocumentWidget *d) { | ||
213 | clear_PtrArray(&d->visibleLinks); | ||
214 | render_GmDocument( | ||
215 | d->doc, | ||
216 | visibleRange_DocumentWidget_(d), | ||
217 | addVisibleLink_DocumentWidget_, | ||
218 | d); | ||
219 | } | ||
220 | |||
221 | static iRangecc dirPath_(iRangecc path) { | ||
222 | const size_t pos = lastIndexOfCStr_Rangecc(&path, "/"); | ||
223 | if (pos == iInvalidPos) return path; | ||
224 | return (iRangecc){ path.start, path.start + pos }; | ||
225 | } | ||
226 | |||
227 | static const iString *absoluteUrl_DocumentWidget_(const iDocumentWidget *d, const iString *url) { | ||
228 | if (indexOfCStr_String(url, "://") != iInvalidPos) { | ||
229 | /* Already absolute. */ | ||
230 | return url; | ||
231 | } | ||
232 | iUrl parts; | ||
233 | init_Url(&parts, d->url); | ||
234 | iString *absolute = new_String(); | ||
235 | appendRange_String(absolute, parts.protocol); | ||
236 | appendCStr_String(absolute, "://"); | ||
237 | appendRange_String(absolute, parts.host); | ||
238 | if (!isEmpty_Range(&parts.port)) { | ||
239 | appendCStr_String(absolute, ":"); | ||
240 | appendRange_String(absolute, parts.port); | ||
241 | } | ||
242 | if (startsWith_String(url, "/")) { | ||
243 | append_String(absolute, url); | ||
244 | } | ||
245 | else { | ||
246 | iRangecc relPath = range_String(url); | ||
247 | iRangecc dir = dirPath_(parts.path); | ||
248 | for (;;) { | ||
249 | if (equal_Rangecc(&relPath, ".")) { | ||
250 | relPath.start++; | ||
251 | } | ||
252 | else if (startsWith_Rangecc(&relPath, "./")) { | ||
253 | relPath.start += 2; | ||
254 | } | ||
255 | else if (equal_Rangecc(&relPath, "..")) { | ||
256 | relPath.start += 2; | ||
257 | dir = dirPath_(dir); | ||
258 | } | ||
259 | else if (startsWith_Rangecc(&relPath, "../")) { | ||
260 | relPath.start += 3; | ||
261 | dir = dirPath_(dir); | ||
262 | } | ||
263 | else break; | ||
264 | } | ||
265 | appendRange_String(absolute, dir); | ||
266 | if (!endsWith_String(absolute, "/")) { | ||
267 | appendCStr_String(absolute, "/"); | ||
268 | } | ||
269 | appendRange_String(absolute, relPath); | ||
162 | } | 270 | } |
163 | append_String(d->url, url); | 271 | return collect_String(absolute); |
164 | fetch_DocumentWidget_(d); | ||
165 | } | 272 | } |
166 | 273 | ||
167 | static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *ev) { | 274 | static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *ev) { |
168 | iWidget *w = as_Widget(d); | 275 | iWidget *w = as_Widget(d); |
169 | if (isResize_UserEvent(ev)) { | 276 | if (isResize_UserEvent(ev)) { |
170 | setWidth_GmDocument(d->doc, documentWidth_DocumentWidget_(d)); | 277 | setWidth_GmDocument(d->doc, documentWidth_DocumentWidget_(d)); |
278 | updateVisible_DocumentWidget_(d); | ||
171 | } | 279 | } |
172 | if (ev->type == SDL_KEYDOWN) { | 280 | if (ev->type == SDL_KEYDOWN) { |
173 | const int mods = keyMods_Sym(ev->key.keysym.mod); | 281 | const int mods = keyMods_Sym(ev->key.keysym.mod); |
@@ -202,9 +310,46 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e | |||
202 | else { | 310 | else { |
203 | d->scrollY = 0; | 311 | d->scrollY = 0; |
204 | } | 312 | } |
313 | updateVisible_DocumentWidget_(d); | ||
205 | postRefresh_App(); | 314 | postRefresh_App(); |
206 | return iTrue; | 315 | return iTrue; |
207 | } | 316 | } |
317 | else if (ev->type == SDL_MOUSEMOTION) { | ||
318 | const iGmRun *oldHoverLink = d->hoverLink; | ||
319 | d->hoverLink = NULL; | ||
320 | const iRect docBounds = documentBounds_DocumentWidget_(d); | ||
321 | const iInt2 hoverPos = | ||
322 | addY_I2(sub_I2(localCoord_Widget(w, init_I2(ev->motion.x, ev->motion.y)), | ||
323 | topLeft_Rect(docBounds)), | ||
324 | d->scrollY); | ||
325 | iConstForEach(PtrArray, i, &d->visibleLinks) { | ||
326 | const iGmRun *run = i.ptr; | ||
327 | if (contains_Rect(run->bounds, hoverPos)) { | ||
328 | d->hoverLink = run; | ||
329 | break; | ||
330 | } | ||
331 | } | ||
332 | if (d->hoverLink != oldHoverLink) { | ||
333 | postRefresh_App(); | ||
334 | } | ||
335 | } | ||
336 | switch (processEvent_Click(&d->click, ev)) { | ||
337 | case finished_ClickResult: | ||
338 | if (d->hoverLink) { | ||
339 | iAssert(d->hoverLink->linkId); | ||
340 | postCommandf_App("open url:%s", | ||
341 | cstr_String(absoluteUrl_DocumentWidget_( | ||
342 | d, linkUrl_GmDocument(d->doc, d->hoverLink->linkId)))); | ||
343 | } | ||
344 | return iTrue; | ||
345 | case started_ClickResult: | ||
346 | case double_ClickResult: | ||
347 | case drag_ClickResult: | ||
348 | case aborted_ClickResult: | ||
349 | return iTrue; | ||
350 | default: | ||
351 | break; | ||
352 | } | ||
208 | return processEvent_Widget(w, ev); | 353 | return processEvent_Widget(w, ev); |
209 | } | 354 | } |
210 | 355 | ||
@@ -223,7 +368,9 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) { | |||
223 | initRange_String(&text, run->text); | 368 | initRange_String(&text, run->text); |
224 | iInt2 origin = addY_I2(d->bounds.pos, -d->widget->scrollY); | 369 | iInt2 origin = addY_I2(d->bounds.pos, -d->widget->scrollY); |
225 | drawString_Text(run->font, add_I2(run->bounds.pos, origin), run->color, &text); | 370 | drawString_Text(run->font, add_I2(run->bounds.pos, origin), run->color, &text); |
226 | // drawRect_Paint(&d->paint, moved_Rect(run->bounds, origin), red_ColorId); | 371 | if (run == d->widget->hoverLink) { |
372 | drawRect_Paint(&d->paint, moved_Rect(run->bounds, origin), orange_ColorId); | ||
373 | } | ||
227 | deinit_String(&text); | 374 | deinit_String(&text); |
228 | } | 375 | } |
229 | 376 | ||
@@ -237,18 +384,14 @@ static void draw_DocumentWidget_(const iDocumentWidget *d) { | |||
237 | setSource_GmDocument(d->doc, d->newSource, documentWidth_DocumentWidget_(d)); | 384 | setSource_GmDocument(d->doc, d->newSource, documentWidth_DocumentWidget_(d)); |
238 | clear_String(d->newSource); | 385 | clear_String(d->newSource); |
239 | iConstCast(iDocumentWidget *, d)->state = ready_DocumentState; | 386 | iConstCast(iDocumentWidget *, d)->state = ready_DocumentState; |
387 | updateVisible_DocumentWidget_(iConstCast(iDocumentWidget *, d)); | ||
240 | } | 388 | } |
241 | if (d->state != ready_DocumentState) return; | 389 | if (d->state != ready_DocumentState) return; |
242 | iDrawContext ctx = {.widget = d, .bounds = bounds_Widget(w) }; | 390 | iDrawContext ctx = { .widget = d, .bounds = documentBounds_DocumentWidget_(d) }; |
243 | const int margin = gap_UI * d->pageMargin; | ||
244 | shrink_Rect(&ctx.bounds, init1_I2(margin)); | ||
245 | ctx.bounds.size.x = documentWidth_DocumentWidget_(d); | ||
246 | ctx.bounds.pos.x = bounds_Widget(w).size.x / 2 - ctx.bounds.size.x / 2; | ||
247 | init_Paint(&ctx.paint); | 391 | init_Paint(&ctx.paint); |
248 | // drawRect_Paint(&ctx.paint, ctx.bounds, teal_ColorId); | ||
249 | render_GmDocument( | 392 | render_GmDocument( |
250 | d->doc, | 393 | d->doc, |
251 | (iRangei){ d->scrollY - margin, d->scrollY + height_Rect(ctx.bounds) + margin }, | 394 | visibleRange_DocumentWidget_(d), |
252 | drawRun_DrawContext_, | 395 | drawRun_DrawContext_, |
253 | &ctx); | 396 | &ctx); |
254 | } | 397 | } |