summaryrefslogtreecommitdiff
path: root/src/ui/documentwidget.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/ui/documentwidget.c')
-rw-r--r--src/ui/documentwidget.c177
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
20struct Impl_DocumentWidget { 23struct 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
32iDeclareType(Url) 38iDeclareType(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)
65void init_DocumentWidget(iDocumentWidget *d) { 71void 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
81void deinit_DocumentWidget(iDocumentWidget *d) { 91void 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
104static 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
93void setSource_DocumentWidget(iDocumentWidget *d, const iString *source) { 115void 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
157void setUrl_DocumentWidget(iDocumentWidget *d, const iString *url) { 185void 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
199static 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
206static 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
212static 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
221static 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
227static 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
167static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *ev) { 274static 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}