diff options
-rw-r--r-- | src/app.c | 3 | ||||
-rw-r--r-- | src/gmdocument.c | 87 | ||||
-rw-r--r-- | src/gmdocument.h | 7 | ||||
-rw-r--r-- | src/ui/documentwidget.c | 177 |
4 files changed, 254 insertions, 20 deletions
@@ -3,6 +3,7 @@ | |||
3 | #include "ui/window.h" | 3 | #include "ui/window.h" |
4 | #include "ui/inputwidget.h" | 4 | #include "ui/inputwidget.h" |
5 | #include "ui/labelwidget.h" | 5 | #include "ui/labelwidget.h" |
6 | #include "ui/documentwidget.h" | ||
6 | #include "ui/util.h" | 7 | #include "ui/util.h" |
7 | #include "ui/text.h" | 8 | #include "ui/text.h" |
8 | #include "ui/color.h" | 9 | #include "ui/color.h" |
@@ -290,6 +291,8 @@ iBool handleCommand_App(const char *cmd) { | |||
290 | iApp *d = &app_; | 291 | iApp *d = &app_; |
291 | iWidget *root = d->window->root; | 292 | iWidget *root = d->window->root; |
292 | if (equal_Command(cmd, "open")) { | 293 | if (equal_Command(cmd, "open")) { |
294 | setUrl_DocumentWidget(findChild_Widget(root, "document"), | ||
295 | collect_String(newCStr_String(valuePtr_Command(cmd, "url")))); | ||
293 | } | 296 | } |
294 | else if (equal_Command(cmd, "quit")) { | 297 | else if (equal_Command(cmd, "quit")) { |
295 | SDL_Event ev; | 298 | SDL_Event ev; |
diff --git a/src/gmdocument.c b/src/gmdocument.c index 4132a15e..1aefe133 100644 --- a/src/gmdocument.c +++ b/src/gmdocument.c | |||
@@ -3,13 +3,31 @@ | |||
3 | #include "ui/text.h" | 3 | #include "ui/text.h" |
4 | #include "ui/metrics.h" | 4 | #include "ui/metrics.h" |
5 | 5 | ||
6 | #include <the_Foundation/array.h> | 6 | #include <the_Foundation/ptrarray.h> |
7 | #include <the_Foundation/regexp.h> | ||
8 | |||
9 | iDeclareType(GmLink) | ||
10 | |||
11 | struct Impl_GmLink { | ||
12 | iString url; | ||
13 | }; | ||
14 | |||
15 | void init_GmLink(iGmLink *d) { | ||
16 | init_String(&d->url); | ||
17 | } | ||
18 | |||
19 | void deinit_GmLink(iGmLink *d) { | ||
20 | deinit_String(&d->url); | ||
21 | } | ||
22 | |||
23 | iDefineTypeConstruction(GmLink) | ||
7 | 24 | ||
8 | struct Impl_GmDocument { | 25 | struct Impl_GmDocument { |
9 | iObject object; | 26 | iObject object; |
10 | iString source; | 27 | iString source; |
11 | iInt2 size; | 28 | iInt2 size; |
12 | iArray layout; /* contents of source, laid out in document space */ | 29 | iArray layout; /* contents of source, laid out in document space */ |
30 | iPtrArray links; | ||
13 | }; | 31 | }; |
14 | 32 | ||
15 | iDefineObjectConstruction(GmDocument) | 33 | iDefineObjectConstruction(GmDocument) |
@@ -86,15 +104,45 @@ iInt2 measurePreformattedBlock_GmDocument_(const iGmDocument *d, const char *sta | |||
86 | return measureRange_Text(font, preBlock); | 104 | return measureRange_Text(font, preBlock); |
87 | } | 105 | } |
88 | 106 | ||
107 | static iRangecc addLink_GmDocument_(iGmDocument *d, iRangecc line, iGmLinkId *linkId) { | ||
108 | iRegExp *pattern = new_RegExp("=>\\s*([^\\s]+)(\\s.*)?", caseInsensitive_RegExpOption); | ||
109 | iRegExpMatch m; | ||
110 | if (matchRange_RegExp(pattern, line, &m)) { | ||
111 | iGmLink *link = new_GmLink(); | ||
112 | setRange_String(&link->url, capturedRange_RegExpMatch(&m, 1)); | ||
113 | pushBack_PtrArray(&d->links, link); | ||
114 | *linkId = size_PtrArray(&d->links); /* index + 1 */ | ||
115 | iRangecc desc = capturedRange_RegExpMatch(&m, 2); | ||
116 | trim_Rangecc(&desc); | ||
117 | if (!isEmpty_Range(&desc)) { | ||
118 | line = desc; /* Just show the description. */ | ||
119 | } | ||
120 | else { | ||
121 | line = capturedRange_RegExpMatch(&m, 1); /* Show the URL. */ | ||
122 | } | ||
123 | } | ||
124 | iRelease(pattern); | ||
125 | return line; | ||
126 | } | ||
127 | |||
128 | static void clearLinks_GmDocument_(iGmDocument *d) { | ||
129 | iForEach(PtrArray, i, &d->links) { | ||
130 | delete_GmLink(i.ptr); | ||
131 | } | ||
132 | clear_PtrArray(&d->links); | ||
133 | } | ||
134 | |||
89 | static void doLayout_GmDocument_(iGmDocument *d) { | 135 | static void doLayout_GmDocument_(iGmDocument *d) { |
90 | if (d->size.x <= 0 || isEmpty_String(&d->source)) { | 136 | if (d->size.x <= 0 || isEmpty_String(&d->source)) { |
91 | return; | 137 | return; |
92 | } | 138 | } |
93 | clear_Array(&d->layout); | 139 | clear_Array(&d->layout); |
140 | clearLinks_GmDocument_(d); | ||
94 | iBool isPreformat = iFalse; | 141 | iBool isPreformat = iFalse; |
95 | iInt2 pos = zero_I2(); | 142 | iInt2 pos = zero_I2(); |
96 | const iRangecc content = range_String(&d->source); | 143 | const iRangecc content = range_String(&d->source); |
97 | iRangecc line = iNullRange; | 144 | iRangecc line = iNullRange; |
145 | /* TODO: Collect these parameters into a GmTheme. */ | ||
98 | static const int fonts[max_GmLineType] = { | 146 | static const int fonts[max_GmLineType] = { |
99 | paragraph_FontId, | 147 | paragraph_FontId, |
100 | paragraph_FontId, /* bullet */ | 148 | paragraph_FontId, /* bullet */ |
@@ -151,6 +199,13 @@ static void doLayout_GmDocument_(iGmDocument *d) { | |||
151 | /* TODO: store and link the alt text to this run */ | 199 | /* TODO: store and link the alt text to this run */ |
152 | continue; | 200 | continue; |
153 | } | 201 | } |
202 | else if (type == link_GmLineType) { | ||
203 | line = addLink_GmDocument_(d, line, &run.linkId); | ||
204 | if (!run.linkId) { | ||
205 | /* Invalid formatting. */ | ||
206 | type = text_GmLineType; | ||
207 | } | ||
208 | } | ||
154 | trimLine_Rangecc_(&line, type); | 209 | trimLine_Rangecc_(&line, type); |
155 | run.font = fonts[type]; | 210 | run.font = fonts[type]; |
156 | } | 211 | } |
@@ -175,6 +230,10 @@ static void doLayout_GmDocument_(iGmDocument *d) { | |||
175 | if (!isPreformat || (prevType != preformatted_GmLineType)) { | 230 | if (!isPreformat || (prevType != preformatted_GmLineType)) { |
176 | int required = | 231 | int required = |
177 | iMax(topMargin[type], bottomMargin[prevType]) * lineHeight_Text(paragraph_FontId); | 232 | iMax(topMargin[type], bottomMargin[prevType]) * lineHeight_Text(paragraph_FontId); |
233 | if (type == link_GmLineType && prevType == link_GmLineType) { | ||
234 | /* No margin between consecutive links. */ | ||
235 | required = 0; | ||
236 | } | ||
178 | if (isEmpty_Array(&d->layout)) { | 237 | if (isEmpty_Array(&d->layout)) { |
179 | required = 0; /* top of document */ | 238 | required = 0; /* top of document */ |
180 | } | 239 | } |
@@ -202,6 +261,7 @@ static void doLayout_GmDocument_(iGmDocument *d) { | |||
202 | isFirstText = iFalse; | 261 | isFirstText = iFalse; |
203 | } | 262 | } |
204 | iRangecc runLine = line; | 263 | iRangecc runLine = line; |
264 | /* Create one or more runs for this line. */ | ||
205 | while (!isEmpty_Range(&runLine)) { | 265 | while (!isEmpty_Range(&runLine)) { |
206 | run.bounds.pos = addX_I2(pos, indent * gap_UI); | 266 | run.bounds.pos = addX_I2(pos, indent * gap_UI); |
207 | const char *contPos; | 267 | const char *contPos; |
@@ -222,9 +282,12 @@ void init_GmDocument(iGmDocument *d) { | |||
222 | init_String(&d->source); | 282 | init_String(&d->source); |
223 | d->size = zero_I2(); | 283 | d->size = zero_I2(); |
224 | init_Array(&d->layout, sizeof(iGmRun)); | 284 | init_Array(&d->layout, sizeof(iGmRun)); |
285 | init_PtrArray(&d->links); | ||
225 | } | 286 | } |
226 | 287 | ||
227 | void deinit_GmDocument(iGmDocument *d) { | 288 | void deinit_GmDocument(iGmDocument *d) { |
289 | clearLinks_GmDocument_(d); | ||
290 | deinit_PtrArray(&d->links); | ||
228 | deinit_Array(&d->layout); | 291 | deinit_Array(&d->layout); |
229 | deinit_String(&d->source); | 292 | deinit_String(&d->source); |
230 | } | 293 | } |
@@ -243,7 +306,7 @@ static void normalize_GmDocument(iGmDocument *d) { | |||
243 | iRangecc src = range_String(&d->source); | 306 | iRangecc src = range_String(&d->source); |
244 | iRangecc line = iNullRange; | 307 | iRangecc line = iNullRange; |
245 | iBool isPreformat = iFalse; | 308 | iBool isPreformat = iFalse; |
246 | const int preTabWidth = 4; /* TODO: user-configurable parameter */ | 309 | const int preTabWidth = 8; /* TODO: user-configurable parameter */ |
247 | while (nextSplit_Rangecc(&src, "\n", &line)) { | 310 | while (nextSplit_Rangecc(&src, "\n", &line)) { |
248 | if (isPreformat) { | 311 | if (isPreformat) { |
249 | /* Replace any tab characters with spaces for visualization. */ | 312 | /* Replace any tab characters with spaces for visualization. */ |
@@ -302,6 +365,7 @@ void setSource_GmDocument(iGmDocument *d, const iString *source, int width) { | |||
302 | void render_GmDocument(const iGmDocument *d, iRangei visRangeY, iGmDocumentRenderFunc render, | 365 | void render_GmDocument(const iGmDocument *d, iRangei visRangeY, iGmDocumentRenderFunc render, |
303 | void *context) { | 366 | void *context) { |
304 | iBool isInside = iFalse; | 367 | iBool isInside = iFalse; |
368 | /* TODO: Check lookup table for quick starting position. */ | ||
305 | iConstForEach(Array, i, &d->layout) { | 369 | iConstForEach(Array, i, &d->layout) { |
306 | const iGmRun *run = i.value; | 370 | const iGmRun *run = i.value; |
307 | if (isInside) { | 371 | if (isInside) { |
@@ -321,4 +385,23 @@ iInt2 size_GmDocument(const iGmDocument *d) { | |||
321 | return d->size; | 385 | return d->size; |
322 | } | 386 | } |
323 | 387 | ||
388 | const iGmRun *findRun_GmDocument(const iGmDocument *d, iInt2 pos) { | ||
389 | /* TODO: Perf optimization likely needed; use a block map? */ | ||
390 | iConstForEach(Array, i, &d->layout) { | ||
391 | const iGmRun *run = i.value; | ||
392 | if (contains_Rect(run->bounds, pos)) { | ||
393 | return run; | ||
394 | } | ||
395 | } | ||
396 | return NULL; | ||
397 | } | ||
398 | |||
399 | const iString *linkUrl_GmDocument(const iGmDocument *d, iGmLinkId linkId) { | ||
400 | if (linkId > 0 && linkId <= size_PtrArray(&d->links)) { | ||
401 | const iGmLink *link = constAt_PtrArray(&d->links, linkId - 1); | ||
402 | return &link->url; | ||
403 | } | ||
404 | return NULL; | ||
405 | } | ||
406 | |||
324 | iDefineClass(GmDocument) | 407 | iDefineClass(GmDocument) |
diff --git a/src/gmdocument.h b/src/gmdocument.h index 70e912df..b5102044 100644 --- a/src/gmdocument.h +++ b/src/gmdocument.h | |||
@@ -7,12 +7,14 @@ | |||
7 | 7 | ||
8 | iDeclareType(GmRun) | 8 | iDeclareType(GmRun) |
9 | 9 | ||
10 | typedef uint16_t iGmLinkId; | ||
11 | |||
10 | struct Impl_GmRun { | 12 | struct Impl_GmRun { |
11 | iRangecc text; | 13 | iRangecc text; |
12 | iRect bounds; /* advance metrics */ | 14 | iRect bounds; /* advance metrics */ |
13 | uint8_t font; | 15 | uint8_t font; |
14 | uint8_t color; | 16 | uint8_t color; |
15 | uint16_t linkId; | 17 | iGmLinkId linkId; /* zero for non-links */ |
16 | }; | 18 | }; |
17 | 19 | ||
18 | iDeclareType(GmDocument) | 20 | iDeclareType(GmDocument) |
@@ -27,3 +29,6 @@ typedef void (*iGmDocumentRenderFunc)(void *, const iGmRun *); | |||
27 | 29 | ||
28 | void render_GmDocument (const iGmDocument *, iRangei visRangeY, iGmDocumentRenderFunc render, void *); | 30 | void render_GmDocument (const iGmDocument *, iRangei visRangeY, iGmDocumentRenderFunc render, void *); |
29 | iInt2 size_GmDocument (const iGmDocument *); | 31 | iInt2 size_GmDocument (const iGmDocument *); |
32 | |||
33 | const iGmRun * findRun_GmDocument (const iGmDocument *, iInt2 pos); | ||
34 | const iString * linkUrl_GmDocument (const iGmDocument *, iGmLinkId linkId); | ||
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 | } |