summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/gmdocument.c109
-rw-r--r--src/gmdocument.h21
-rw-r--r--src/gmrequest.c11
-rw-r--r--src/ui/documentwidget.c227
-rw-r--r--src/ui/util.c4
-rw-r--r--src/ui/window.h1
6 files changed, 317 insertions, 56 deletions
diff --git a/src/gmdocument.c b/src/gmdocument.c
index 3dbef514..f8998c17 100644
--- a/src/gmdocument.c
+++ b/src/gmdocument.c
@@ -3,10 +3,15 @@
3#include "ui/color.h" 3#include "ui/color.h"
4#include "ui/text.h" 4#include "ui/text.h"
5#include "ui/metrics.h" 5#include "ui/metrics.h"
6#include "ui/window.h"
6 7
7#include <the_Foundation/ptrarray.h> 8#include <the_Foundation/ptrarray.h>
8#include <the_Foundation/regexp.h> 9#include <the_Foundation/regexp.h>
9 10
11#include <SDL_hints.h>
12#include <SDL_render.h>
13#include <stb_image.h>
14
10iDeclareType(GmLink) 15iDeclareType(GmLink)
11 16
12struct Impl_GmLink { 17struct Impl_GmLink {
@@ -25,6 +30,40 @@ void deinit_GmLink(iGmLink *d) {
25 30
26iDefineTypeConstruction(GmLink) 31iDefineTypeConstruction(GmLink)
27 32
33iDeclareType(GmImage)
34
35struct Impl_GmImage {
36 iInt2 size;
37 SDL_Texture *texture;
38 iGmLinkId linkId;
39};
40
41void init_GmImage(iGmImage *d, const iBlock *data) {
42 uint8_t *imgData = stbi_load_from_memory(
43 constData_Block(data), size_Block(data), &d->size.x, &d->size.y, NULL, 4);
44 if (!imgData) {
45 d->size = zero_I2();
46 d->texture = NULL;
47 }
48 else {
49 SDL_Surface *surface = SDL_CreateRGBSurfaceWithFormatFrom(
50 imgData, d->size.x, d->size.y, 32, d->size.x * 4, SDL_PIXELFORMAT_ABGR8888);
51 /* TODO: In multiwindow case, all windows must have the same shared renderer?
52 Or at least a shared context. */
53 SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "1"); /* linear scaling */
54 d->texture = SDL_CreateTextureFromSurface(renderer_Window(get_Window()), surface);
55 SDL_FreeSurface(surface);
56 stbi_image_free(imgData);
57 }
58 d->linkId = 0;
59}
60
61void deinit_GmImage(iGmImage *d) {
62 SDL_DestroyTexture(d->texture);
63}
64
65iDefineTypeConstructionArgs(GmImage, (const iBlock *data), data)
66
28struct Impl_GmDocument { 67struct Impl_GmDocument {
29 iObject object; 68 iObject object;
30 enum iGmDocumentFormat format; 69 enum iGmDocumentFormat format;
@@ -34,6 +73,7 @@ struct Impl_GmDocument {
34 iArray layout; /* contents of source, laid out in document space */ 73 iArray layout; /* contents of source, laid out in document space */
35 iPtrArray links; 74 iPtrArray links;
36 iString title; /* the first top-level title */ 75 iString title; /* the first top-level title */
76 iPtrArray images; /* persistent across layouts, references links by ID */
37}; 77};
38 78
39iDefineObjectConstruction(GmDocument) 79iDefineObjectConstruction(GmDocument)
@@ -178,6 +218,17 @@ static void clearLinks_GmDocument_(iGmDocument *d) {
178 clear_PtrArray(&d->links); 218 clear_PtrArray(&d->links);
179} 219}
180 220
221static size_t findLinkImage_GmDocument_(const iGmDocument *d, iGmLinkId linkId) {
222 /* TODO: use a hash */
223 iConstForEach(PtrArray, i, &d->images) {
224 const iGmImage *img = i.ptr;
225 if (img->linkId == linkId) {
226 return index_PtrArrayConstIterator(&i);
227 }
228 }
229 return iInvalidPos;
230}
231
181static void doLayout_GmDocument_(iGmDocument *d) { 232static void doLayout_GmDocument_(iGmDocument *d) {
182 /* TODO: Collect these parameters into a GmTheme. */ 233 /* TODO: Collect these parameters into a GmTheme. */
183 static const int fonts[max_GmLineType] = { 234 static const int fonts[max_GmLineType] = {
@@ -236,6 +287,7 @@ static void doLayout_GmDocument_(iGmDocument *d) {
236 iGmRun run; 287 iGmRun run;
237 run.color = white_ColorId; 288 run.color = white_ColorId;
238 run.linkId = 0; 289 run.linkId = 0;
290 run.imageId = 0;
239 enum iGmLineType type; 291 enum iGmLineType type;
240 int indent = 0; 292 int indent = 0;
241 if (!isPreformat) { 293 if (!isPreformat) {
@@ -373,6 +425,24 @@ static void doLayout_GmDocument_(iGmDocument *d) {
373 trimStart_Rangecc(&runLine); 425 trimStart_Rangecc(&runLine);
374 pos.y += lineHeight_Text(run.font); 426 pos.y += lineHeight_Text(run.font);
375 } 427 }
428 /* Image content. */
429 if (type == link_GmLineType) {
430 const size_t imgIndex = findLinkImage_GmDocument_(d, run.linkId);
431 if (imgIndex != iInvalidPos) {
432 const iGmImage *img = constAt_PtrArray(&d->images, imgIndex);
433 run.bounds.pos = pos;
434 run.bounds.size.x = d->size.x;
435 const float aspect = (float) img->size.y / (float) img->size.x;
436 run.bounds.size.y = d->size.x * aspect;
437 run.visBounds = run.bounds; /* TODO: limit max height? */
438 run.text = iNullRange;
439 run.font = 0;
440 run.color = 0;
441 run.imageId = imgIndex + 1;
442 pushBack_Array(&d->layout, &run);
443 pos.y += run.bounds.size.y;
444 }
445 }
376 prevType = type; 446 prevType = type;
377 } 447 }
378 d->size.y = pos.y; 448 d->size.y = pos.y;
@@ -386,6 +456,7 @@ void init_GmDocument(iGmDocument *d) {
386 init_Array(&d->layout, sizeof(iGmRun)); 456 init_Array(&d->layout, sizeof(iGmRun));
387 init_PtrArray(&d->links); 457 init_PtrArray(&d->links);
388 init_String(&d->title); 458 init_String(&d->title);
459 init_PtrArray(&d->images);
389} 460}
390 461
391void deinit_GmDocument(iGmDocument *d) { 462void deinit_GmDocument(iGmDocument *d) {
@@ -397,6 +468,16 @@ void deinit_GmDocument(iGmDocument *d) {
397 deinit_String(&d->source); 468 deinit_String(&d->source);
398} 469}
399 470
471void reset_GmDocument(iGmDocument *d) {
472 /* Free the loaded images. */
473 iForEach(PtrArray, i, &d->images) {
474 deinit_GmImage(i.ptr);
475 }
476 clear_PtrArray(&d->images);
477 clearLinks_GmDocument_(d);
478 clear_Array(&d->layout);
479}
480
400void setFormat_GmDocument(iGmDocument *d, enum iGmDocumentFormat format) { 481void setFormat_GmDocument(iGmDocument *d, enum iGmDocumentFormat format) {
401 d->format = format; 482 d->format = format;
402} 483}
@@ -477,6 +558,21 @@ void setSource_GmDocument(iGmDocument *d, const iString *source, int width) {
477 /* TODO: just flag need-layout and do it later */ 558 /* TODO: just flag need-layout and do it later */
478} 559}
479 560
561void setImage_GmDocument(iGmDocument *d, iGmLinkId linkId, const iString *mime, const iBlock *data) {
562 /* TODO: check if we know this MIME type */
563 /* Load the image. */ {
564 iGmImage *img = new_GmImage(data);
565 img->linkId = linkId; /* TODO: use a hash? */
566 if (img->texture) {
567 pushBack_PtrArray(&d->images, img);
568 }
569 else {
570 delete_GmImage(img);
571 }
572 }
573 doLayout_GmDocument_(d);
574}
575
480void render_GmDocument(const iGmDocument *d, iRangei visRangeY, iGmDocumentRenderFunc render, 576void render_GmDocument(const iGmDocument *d, iRangei visRangeY, iGmDocumentRenderFunc render,
481 void *context) { 577 void *context) {
482 iBool isInside = iFalse; 578 iBool isInside = iFalse;
@@ -579,6 +675,19 @@ enum iColorId linkColor_GmDocument(const iGmDocument *d, iGmLinkId linkId) {
579 return white_ColorId; 675 return white_ColorId;
580} 676}
581 677
678iBool isMediaLink_GmDocument(const iGmDocument *d, iGmLinkId linkId) {
679 return (linkFlags_GmDocument(d, linkId) &
680 (imageFileExtension_GmLinkFlag | audioFileExtension_GmLinkFlag)) != 0;
681}
682
683SDL_Texture *imageTexture_GmDocument(const iGmDocument *d, uint16_t imageId) {
684 if (imageId > 0 && imageId <= size_PtrArray(&d->images)) {
685 const iGmImage *img = constAt_PtrArray(&d->images, imageId - 1);
686 return img->texture;
687 }
688 return NULL;
689}
690
582const iString *title_GmDocument(const iGmDocument *d) { 691const iString *title_GmDocument(const iGmDocument *d) {
583 return &d->title; 692 return &d->title;
584} 693}
diff --git a/src/gmdocument.h b/src/gmdocument.h
index c671eaaf..c619c47b 100644
--- a/src/gmdocument.h
+++ b/src/gmdocument.h
@@ -5,6 +5,8 @@
5#include <the_Foundation/rect.h> 5#include <the_Foundation/rect.h>
6#include <the_Foundation/string.h> 6#include <the_Foundation/string.h>
7 7
8#include <SDL_render.h>
9
8iDeclareType(GmRun) 10iDeclareType(GmRun)
9 11
10typedef uint16_t iGmLinkId; 12typedef uint16_t iGmLinkId;
@@ -20,12 +22,13 @@ enum iGmLinkFlags {
20}; 22};
21 23
22struct Impl_GmRun { 24struct Impl_GmRun {
23 iRangecc text; 25 iRangecc text;
24 iRect bounds; /* used for hit testing, extends to edge */ 26 uint8_t font;
25 iRect visBounds; /* actual text bounds */ 27 uint8_t color;
26 uint8_t font; 28 iRect bounds; /* used for hit testing, may extend to edges */
27 uint8_t color; 29 iRect visBounds; /* actual visual bounds */
28 iGmLinkId linkId; /* zero for non-links */ 30 iGmLinkId linkId; /* zero for non-links */
31 uint16_t imageId; /* zero for images */
29}; 32};
30 33
31const char * findLoc_GmRun (const iGmRun *, iInt2 pos); 34const char * findLoc_GmRun (const iGmRun *, iInt2 pos);
@@ -42,6 +45,9 @@ void setFormat_GmDocument (iGmDocument *, enum iGmDocumentFormat format);
42void setWidth_GmDocument (iGmDocument *, int width); 45void setWidth_GmDocument (iGmDocument *, int width);
43void setHost_GmDocument (iGmDocument *, const iString *host); /* local host name */ 46void setHost_GmDocument (iGmDocument *, const iString *host); /* local host name */
44void setSource_GmDocument (iGmDocument *, const iString *source, int width); 47void setSource_GmDocument (iGmDocument *, const iString *source, int width);
48void setImage_GmDocument (iGmDocument *, iGmLinkId linkId, const iString *mime, const iBlock *data);
49
50void reset_GmDocument (iGmDocument *); /* free images */
45 51
46typedef void (*iGmDocumentRenderFunc)(void *, const iGmRun *); 52typedef void (*iGmDocumentRenderFunc)(void *, const iGmRun *);
47 53
@@ -57,4 +63,7 @@ const iGmRun * findRunAtLoc_GmDocument (const iGmDocument *, const char *loc);
57const iString * linkUrl_GmDocument (const iGmDocument *, iGmLinkId linkId); 63const iString * linkUrl_GmDocument (const iGmDocument *, iGmLinkId linkId);
58int linkFlags_GmDocument (const iGmDocument *, iGmLinkId linkId); 64int linkFlags_GmDocument (const iGmDocument *, iGmLinkId linkId);
59enum iColorId linkColor_GmDocument (const iGmDocument *, iGmLinkId linkId); 65enum iColorId linkColor_GmDocument (const iGmDocument *, iGmLinkId linkId);
66iBool isMediaLink_GmDocument (const iGmDocument *, iGmLinkId linkId);
60const iString * title_GmDocument (const iGmDocument *); 67const iString * title_GmDocument (const iGmDocument *);
68
69SDL_Texture * imageTexture_GmDocument (const iGmDocument *, uint16_t imageId);
diff --git a/src/gmrequest.c b/src/gmrequest.c
index ad0c5ed7..25ff90af 100644
--- a/src/gmrequest.c
+++ b/src/gmrequest.c
@@ -163,8 +163,6 @@ static void requestFinished_GmRequest_(iAnyObject *obj) {
163 } 163 }
164 SDL_RemoveTimer(d->timeoutId); 164 SDL_RemoveTimer(d->timeoutId);
165 d->timeoutId = 0; 165 d->timeoutId = 0;
166// iReleaseLater(d->req);
167// d->req = NULL;
168 d->state = finished_GmRequestState; 166 d->state = finished_GmRequestState;
169 unlock_Mutex(&d->mutex); 167 unlock_Mutex(&d->mutex);
170 iNotifyAudience(d, finished, GmRequestFinished); 168 iNotifyAudience(d, finished, GmRequestFinished);
@@ -193,6 +191,15 @@ void submit_GmRequest(iGmRequest *d) {
193 else if (endsWithCase_String(path, ".txt")) { 191 else if (endsWithCase_String(path, ".txt")) {
194 setCStr_String(&d->header, "text/plain"); 192 setCStr_String(&d->header, "text/plain");
195 } 193 }
194 else if (endsWithCase_String(path, ".png")) {
195 setCStr_String(&d->header, "image/png");
196 }
197 else if (endsWithCase_String(path, ".jpg") || endsWithCase_String(path, ".jpeg")) {
198 setCStr_String(&d->header, "image/jpeg");
199 }
200 else if (endsWithCase_String(path, ".gif")) {
201 setCStr_String(&d->header, "image/gif");
202 }
196 else { 203 else {
197 setCStr_String(&d->header, "application/octet-stream"); 204 setCStr_String(&d->header, "application/octet-stream");
198 } 205 }
diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c
index 10af43f3..cffd284f 100644
--- a/src/ui/documentwidget.c
+++ b/src/ui/documentwidget.c
@@ -11,6 +11,7 @@
11#include "../gmutil.h" 11#include "../gmutil.h"
12 12
13#include <the_Foundation/file.h> 13#include <the_Foundation/file.h>
14#include <the_Foundation/objectlist.h>
14#include <the_Foundation/path.h> 15#include <the_Foundation/path.h>
15#include <the_Foundation/ptrarray.h> 16#include <the_Foundation/ptrarray.h>
16#include <the_Foundation/regexp.h> 17#include <the_Foundation/regexp.h>
@@ -26,13 +27,59 @@ enum iDocumentState {
26 ready_DocumentState, 27 ready_DocumentState,
27}; 28};
28 29
30iDeclareClass(MediaRequest)
31
32struct Impl_MediaRequest {
33 iObject object;
34 iDocumentWidget *doc;
35 iGmLinkId linkId;
36 iGmRequest *req;
37 iAtomicInt isUpdated;
38};
39
40static void updated_MediaRequest_(iAnyObject *obj) {
41 iMediaRequest *d = obj;
42 int wasUpdated = exchange_Atomic(&d->isUpdated, iTrue);
43 if (!wasUpdated) {
44 postCommandf_App("media.updated link:%u request:%p", d->linkId, d);
45 }
46}
47
48static void finished_MediaRequest_(iAnyObject *obj) {
49 iMediaRequest *d = obj;
50 postCommandf_App("media.finished link:%u request:%p", d->linkId, d);
51}
52
53void init_MediaRequest(iMediaRequest *d, iDocumentWidget *doc, iGmLinkId linkId, const iString *url) {
54 d->doc = doc;
55 d->linkId = linkId;
56 d->req = new_GmRequest();
57 setUrl_GmRequest(d->req, url);
58 iConnect(GmRequest, d->req, updated, d, updated_MediaRequest_);
59 iConnect(GmRequest, d->req, finished, d, finished_MediaRequest_);
60 set_Atomic(&d->isUpdated, iFalse);
61 submit_GmRequest(d->req);
62}
63
64void deinit_MediaRequest(iMediaRequest *d) {
65 iDisconnect(GmRequest, d->req, updated, d, updated_MediaRequest_);
66 iDisconnect(GmRequest, d->req, finished, d, finished_MediaRequest_);
67 iRelease(d->req);
68}
69
70iDefineObjectConstructionArgs(MediaRequest,
71 (iDocumentWidget *doc, iGmLinkId linkId, const iString *url),
72 doc, linkId, url)
73iDefineClass(MediaRequest)
74
29struct Impl_DocumentWidget { 75struct Impl_DocumentWidget {
30 iWidget widget; 76 iWidget widget;
31 enum iDocumentState state; 77 enum iDocumentState state;
32 iString *url; 78 iString *url;
33 iString *titleUser; 79 iString *titleUser;
34 iGmRequest *request; 80 iGmRequest *request;
35 iAtomicInt isSourcePending; /* request has new content, need to parse it */ 81 iAtomicInt isRequestUpdated; /* request has new content, need to parse it */
82 iObjectList *media;
36 iGmDocument *doc; 83 iGmDocument *doc;
37 iBool selecting; 84 iBool selecting;
38 iRangecc selectMark; 85 iRangecc selectMark;
@@ -55,21 +102,22 @@ void init_DocumentWidget(iDocumentWidget *d) {
55 iWidget *w = as_Widget(d); 102 iWidget *w = as_Widget(d);
56 init_Widget(w); 103 init_Widget(w);
57 setId_Widget(w, "document"); 104 setId_Widget(w, "document");
58 d->state = blank_DocumentState; 105 d->state = blank_DocumentState;
59 d->url = new_String(); 106 d->url = new_String();
60 d->titleUser = new_String(); 107 d->titleUser = new_String();
61 d->request = NULL; 108 d->request = NULL;
62 d->isSourcePending = iFalse; 109 d->isRequestUpdated = iFalse;
63 d->doc = new_GmDocument(); 110 d->media = new_ObjectList();
64 d->selecting = iFalse; 111 d->doc = new_GmDocument();
65 d->selectMark = iNullRange; 112 d->selecting = iFalse;
66 d->foundMark = iNullRange; 113 d->selectMark = iNullRange;
67 d->pageMargin = 5; 114 d->foundMark = iNullRange;
68 d->scrollY = 0; 115 d->pageMargin = 5;
69 d->hoverLink = NULL; 116 d->scrollY = 0;
70 d->arrowCursor = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_ARROW); 117 d->hoverLink = NULL;
71 d->beamCursor = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_IBEAM); 118 d->arrowCursor = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_ARROW);
72 d->handCursor = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_HAND); 119 d->beamCursor = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_IBEAM);
120 d->handCursor = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_HAND);
73 init_PtrArray(&d->visibleLinks); 121 init_PtrArray(&d->visibleLinks);
74 init_Click(&d->click, d, SDL_BUTTON_LEFT); 122 init_Click(&d->click, d, SDL_BUTTON_LEFT);
75 addChild_Widget(w, iClob(d->scroll = new_ScrollWidget())); 123 addChild_Widget(w, iClob(d->scroll = new_ScrollWidget()));
@@ -85,16 +133,26 @@ void init_DocumentWidget(iDocumentWidget *d) {
85} 133}
86 134
87void deinit_DocumentWidget(iDocumentWidget *d) { 135void deinit_DocumentWidget(iDocumentWidget *d) {
136 iRelease(d->media);
137 iRelease(d->request);
138 iRelease(d->doc);
88 deinit_PtrArray(&d->visibleLinks); 139 deinit_PtrArray(&d->visibleLinks);
89 delete_String(d->url); 140 delete_String(d->url);
90 delete_String(d->titleUser); 141 delete_String(d->titleUser);
91 iRelease(d->request);
92 iRelease(d->doc);
93 SDL_FreeCursor(d->arrowCursor); 142 SDL_FreeCursor(d->arrowCursor);
94 SDL_FreeCursor(d->beamCursor); 143 SDL_FreeCursor(d->beamCursor);
95 SDL_FreeCursor(d->handCursor); 144 SDL_FreeCursor(d->handCursor);
96} 145}
97 146
147static iString *cleanUrl_(const iString *url) {
148 iString *clean = copy_String(url);
149 if (indexOfCStr_String(url, "://") == iInvalidPos && !startsWithCase_String(url, "gemini:")) {
150 /* Prepend default protocol. */
151 prependCStr_String(clean, "gemini://");
152 }
153 return clean;
154}
155
98static int documentWidth_DocumentWidget_(const iDocumentWidget *d) { 156static int documentWidth_DocumentWidget_(const iDocumentWidget *d) {
99 const iWidget *w = constAs_Widget(d); 157 const iWidget *w = constAs_Widget(d);
100 const iRect bounds = bounds_Widget(w); 158 const iRect bounds = bounds_Widget(w);
@@ -124,8 +182,8 @@ static iInt2 documentPos_DocumentWidget_(const iDocumentWidget *d, iInt2 pos) {
124 182
125static void requestUpdated_DocumentWidget_(iAnyObject *obj) { 183static void requestUpdated_DocumentWidget_(iAnyObject *obj) {
126 iDocumentWidget *d = obj; 184 iDocumentWidget *d = obj;
127 const int wasPending = exchange_Atomic(&d->isSourcePending, iTrue); 185 const int wasUpdated = exchange_Atomic(&d->isRequestUpdated, iTrue);
128 if (!wasPending) { 186 if (!wasUpdated) {
129 postCommand_Widget(obj, "document.request.updated request:%p", d->request); 187 postCommand_Widget(obj, "document.request.updated request:%p", d->request);
130 } 188 }
131} 189}
@@ -234,6 +292,10 @@ static void updateSource_DocumentWidget_(iDocumentWidget *d) {
234 else if (startsWith_String(mime, "text/gemini")) { 292 else if (startsWith_String(mime, "text/gemini")) {
235 setFormat_GmDocument(d->doc, gemini_GmDocumentFormat); 293 setFormat_GmDocument(d->doc, gemini_GmDocumentFormat);
236 } 294 }
295 else if (startsWith_String(mime, "image/")) {
296 /* TODO: Make a simple document with an image. */
297 clear_String(&str);
298 }
237 else { 299 else {
238 showErrorPage_DocumentWidget_(d, unsupportedMimeType_GmStatusCode); 300 showErrorPage_DocumentWidget_(d, unsupportedMimeType_GmStatusCode);
239 deinit_String(&str); 301 deinit_String(&str);
@@ -252,8 +314,9 @@ static void fetch_DocumentWidget_(iDocumentWidget *d) {
252 d->request = NULL; 314 d->request = NULL;
253 } 315 }
254 postCommandf_App("document.request.started url:%s", cstr_String(d->url)); 316 postCommandf_App("document.request.started url:%s", cstr_String(d->url));
317 clear_ObjectList(d->media);
255 d->state = fetching_DocumentState; 318 d->state = fetching_DocumentState;
256 set_Atomic(&d->isSourcePending, iFalse); 319 set_Atomic(&d->isRequestUpdated, iFalse);
257 d->request = new_GmRequest(); 320 d->request = new_GmRequest();
258 setUrl_GmRequest(d->request, d->url); 321 setUrl_GmRequest(d->request, d->url);
259 iConnect(GmRequest, d->request, updated, d, requestUpdated_DocumentWidget_); 322 iConnect(GmRequest, d->request, updated, d, requestUpdated_DocumentWidget_);
@@ -262,12 +325,7 @@ static void fetch_DocumentWidget_(iDocumentWidget *d) {
262} 325}
263 326
264void setUrl_DocumentWidget(iDocumentWidget *d, const iString *url) { 327void setUrl_DocumentWidget(iDocumentWidget *d, const iString *url) {
265 iString *newUrl = new_String(); 328 iString *newUrl = collect_String(cleanUrl_(url));
266 if (indexOfCStr_String(url, "://") == iInvalidPos && !startsWithCase_String(url, "gemini:")) {
267 /* Prepend default protocol. */
268 setCStr_String(newUrl, "gemini://");
269 }
270 append_String(newUrl, url);
271 if (cmpStringSc_String(d->url, newUrl, &iCaseInsensitive)) { 329 if (cmpStringSc_String(d->url, newUrl, &iCaseInsensitive)) {
272 set_String(d->url, newUrl); 330 set_String(d->url, newUrl);
273 fetch_DocumentWidget_(d); 331 fetch_DocumentWidget_(d);
@@ -284,7 +342,6 @@ void setUrl_DocumentWidget(iDocumentWidget *d, const iString *url) {
284 iRelease(userPats[i]); 342 iRelease(userPats[i]);
285 } 343 }
286 } 344 }
287 delete_String(newUrl);
288} 345}
289 346
290iBool isRequestOngoing_DocumentWidget(const iDocumentWidget *d) { 347iBool isRequestOngoing_DocumentWidget(const iDocumentWidget *d) {
@@ -372,6 +429,7 @@ static void checkResponseCode_DocumentWidget_(iDocumentWidget *d) {
372 if (d->state == fetching_DocumentState) { 429 if (d->state == fetching_DocumentState) {
373 d->state = receivedPartialResponse_DocumentState; 430 d->state = receivedPartialResponse_DocumentState;
374 d->scrollY = 0; 431 d->scrollY = 0;
432 reset_GmDocument(d->doc); /* new content incoming */
375 enum iGmStatusCode statusCode = status_GmRequest(d->request); 433 enum iGmStatusCode statusCode = status_GmRequest(d->request);
376 switch (statusCode) { 434 switch (statusCode) {
377 case none_GmStatusCode: 435 case none_GmStatusCode:
@@ -384,10 +442,10 @@ static void checkResponseCode_DocumentWidget_(iDocumentWidget *d) {
384 iWidget *dlg = makeValueInput_Widget( 442 iWidget *dlg = makeValueInput_Widget(
385 as_Widget(d), 443 as_Widget(d),
386 NULL, 444 NULL,
387 cstrFormat_String(cyan_ColorEscape "%s", 445 format_CStr(cyan_ColorEscape "%s",
388 cstr_String(collect_String(newRange_String(parts.host)))), 446 cstr_String(collect_String(newRange_String(parts.host)))),
389 isEmpty_String(meta_GmRequest(d->request)) 447 isEmpty_String(meta_GmRequest(d->request))
390 ? cstrFormat_String( 448 ? format_CStr(
391 "Please enter input for %s:", 449 "Please enter input for %s:",
392 cstr_String(collect_String(newRange_String(parts.path)))) 450 cstr_String(collect_String(newRange_String(parts.path))))
393 : cstr_String(meta_GmRequest(d->request)), 451 : cstr_String(meta_GmRequest(d->request)),
@@ -415,14 +473,73 @@ static void checkResponseCode_DocumentWidget_(iDocumentWidget *d) {
415 } 473 }
416} 474}
417 475
418const iString *valueString_Command(const char *cmd, const char *label) {
419 return collect_String(newCStr_String(suffixPtr_Command(cmd, label)));
420}
421
422static const char *sourceLoc_DocumentWidget_(const iDocumentWidget *d, iInt2 pos) { 476static const char *sourceLoc_DocumentWidget_(const iDocumentWidget *d, iInt2 pos) {
423 return findLoc_GmDocument(d->doc, documentPos_DocumentWidget_(d, pos)); 477 return findLoc_GmDocument(d->doc, documentPos_DocumentWidget_(d, pos));
424} 478}
425 479
480static void removeMediaRequest_DocumentWidget_(iDocumentWidget *d, iGmLinkId linkId) {
481 iForEach(ObjectList, i, d->media) {
482 iMediaRequest *req = (iMediaRequest *) i.object;
483 if (req->linkId == linkId) {
484 remove_ObjectListIterator(&i);
485 break;
486 }
487 }
488}
489
490static iMediaRequest *findMediaRequest_DocumentWidget_(const iDocumentWidget *d, iGmLinkId linkId) {
491 iConstForEach(ObjectList, i, d->media) {
492 const iMediaRequest *req = (const iMediaRequest *) i.object;
493 if (req->linkId == linkId) {
494 return iConstCast(iMediaRequest *, req);
495 }
496 }
497 return NULL;
498}
499
500static void requestMedia_DocumentWidget_(iDocumentWidget *d, iGmLinkId linkId) {
501 if (!findMediaRequest_DocumentWidget_(d, linkId)) {
502 pushBack_ObjectList(
503 d->media,
504 iClob(new_MediaRequest(
505 d, linkId, absoluteUrl_DocumentWidget_(d, linkUrl_GmDocument(d->doc, linkId)))));
506 }
507}
508
509static iBool handleMediaEvent_DocumentWidget_(iDocumentWidget *d, const char *cmd) {
510 iMediaRequest *req = pointerLabel_Command(cmd, "request");
511 if (!req || req->doc != d) {
512 return iFalse; /* not our request */
513 }
514 if (equal_Command(cmd, "media.updated")) {
515 /* TODO: Show a progress indicator */
516 return iTrue;
517 }
518 else if (equal_Command(cmd, "media.finished")) {
519 const enum iGmStatusCode code = status_GmRequest(req->req);
520 /* Give the media to the document for presentation. */
521 if (code == success_GmStatusCode) {
522 printf("media finished: %s\n size: %zu\n type: %s\n",
523 cstr_String(url_GmRequest(req->req)),
524 size_Block(body_GmRequest(req->req)),
525 cstr_String(meta_GmRequest(req->req)));
526 if (startsWith_String(meta_GmRequest(req->req), "image/")) {
527 setImage_GmDocument(d->doc, req->linkId, meta_GmRequest(req->req),
528 body_GmRequest(req->req));
529 updateVisible_DocumentWidget_(d);
530 refresh_Widget(as_Widget(d));
531 }
532 }
533 else {
534 const iGmError *err = get_GmError(code);
535 makeMessage_Widget(format_CStr(orange_ColorEscape "%s", err->title), err->info);
536 removeMediaRequest_DocumentWidget_(d, req->linkId);
537 }
538 return iTrue;
539 }
540 return iFalse;
541}
542
426static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *ev) { 543static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *ev) {
427 iWidget *w = as_Widget(d); 544 iWidget *w = as_Widget(d);
428 if (isResize_UserEvent(ev)) { 545 if (isResize_UserEvent(ev)) {
@@ -499,6 +616,9 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
499 } 616 }
500 return iTrue; 617 return iTrue;
501 } 618 }
619 else if (isCommand_UserEvent(ev, "media.updated") || isCommand_UserEvent(ev, "media.finished")) {
620 return handleMediaEvent_DocumentWidget_(d, command_UserEvent(ev));
621 }
502 else if (isCommand_UserEvent(ev, "document.reload")) { 622 else if (isCommand_UserEvent(ev, "document.reload")) {
503 fetch_DocumentWidget_(d); 623 fetch_DocumentWidget_(d);
504 return iTrue; 624 return iTrue;
@@ -653,10 +773,17 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
653 case finished_ClickResult: 773 case finished_ClickResult:
654 if (!isMoved_Click(&d->click)) { 774 if (!isMoved_Click(&d->click)) {
655 if (d->hoverLink) { 775 if (d->hoverLink) {
656 iAssert(d->hoverLink->linkId); 776 const iGmLinkId linkId = d->hoverLink->linkId;
657 postCommandf_App("open url:%s", 777 iAssert(linkId);
658 cstr_String(absoluteUrl_DocumentWidget_( 778 /* Media links are opened inline by default. */
659 d, linkUrl_GmDocument(d->doc, d->hoverLink->linkId)))); 779 if (isMediaLink_GmDocument(d->doc, linkId)) {
780 requestMedia_DocumentWidget_(d, linkId);
781 }
782 else {
783 postCommandf_App("open url:%s",
784 cstr_String(absoluteUrl_DocumentWidget_(
785 d, linkUrl_GmDocument(d->doc, linkId))));
786 }
660 } 787 }
661 if (d->selectMark.start) { 788 if (d->selectMark.start) {
662 d->selectMark = iNullRange; 789 d->selectMark = iNullRange;
@@ -714,20 +841,27 @@ static void fillRange_DrawContext_(iDrawContext *d, const iGmRun *run, enum iCol
714 841
715static void drawRun_DrawContext_(void *context, const iGmRun *run) { 842static void drawRun_DrawContext_(void *context, const iGmRun *run) {
716 iDrawContext *d = context; 843 iDrawContext *d = context;
844 const iInt2 origin = addY_I2(d->bounds.pos, -d->widget->scrollY);
845 if (run->imageId) {
846 SDL_Texture *tex = imageTexture_GmDocument(d->widget->doc, run->imageId);
847 if (tex) {
848 const iRect dst = moved_Rect(run->visBounds, origin);
849 SDL_RenderCopy(d->paint.dst->render, tex, NULL,
850 &(SDL_Rect){ dst.pos.x, dst.pos.y, dst.size.x, dst.size.y });
851 }
852 return;
853 }
717 iString text; 854 iString text;
718 /* TODO: making a copy is unnecessary; the text routines should accept Rangecc */ 855 /* TODO: making a copy is unnecessary; the text routines should accept Rangecc */
719 initRange_String(&text, run->text); 856 initRange_String(&text, run->text);
720 iInt2 origin = addY_I2(d->bounds.pos, -d->widget->scrollY);
721 enum iColorId fg = run->color; 857 enum iColorId fg = run->color;
722 if (run == d->widget->hoverLink) { 858 if (run == d->widget->hoverLink) {
723 //const char *desc = ""; 859 const iGmDocument *doc = d->widget->doc;
724 const iGmDocument *doc = d->widget->doc; 860 const iGmLinkId linkId = d->widget->hoverLink->linkId;
725 const iGmLinkId linkId = d->widget->hoverLink->linkId; 861 const iString * url = linkUrl_GmDocument(doc, linkId);
726 const iString *url = linkUrl_GmDocument(doc, linkId); 862 const int flags = linkFlags_GmDocument(doc, linkId);
727 const int flags = linkFlags_GmDocument(doc, linkId);
728 iUrl parts; 863 iUrl parts;
729 init_Url(&parts, url); 864 init_Url(&parts, url);
730// desc = cstrFormat_String("\u2192 %s", cstr_String(collect_String(newRange_String(parts.protocol))));
731 const iString *host = collect_String(newRange_String(parts.host)); 865 const iString *host = collect_String(newRange_String(parts.host));
732 fg = linkColor_GmDocument(doc, linkId); 866 fg = linkColor_GmDocument(doc, linkId);
733 const iBool showHost = (!isEmpty_String(host) && flags & userFriendly_GmLinkFlag); 867 const iBool showHost = (!isEmpty_String(host) && flags & userFriendly_GmLinkFlag);
@@ -739,9 +873,10 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) {
739 topRight_Rect(linkRect), 873 topRight_Rect(linkRect),
740 fg - 1, 874 fg - 1,
741 left_Alignment, 875 left_Alignment,
742 " \u2014%s%s\r%c%s", 876 " \u2014%s%s%s\r%c%s",
743 showHost ? " " : "", 877 showHost ? " " : "",
744 showHost ? cstr_String(host) : "", 878 showHost ? cstr_String(host) : "",
879 showHost && (showImage || showAudio) ? " \u2014" : "",
745 showImage || showAudio ? '0' + fg : ('0' + fg - 1), 880 showImage || showAudio ? '0' + fg : ('0' + fg - 1),
746 showImage ? " View Image \U0001f5bc" 881 showImage ? " View Image \U0001f5bc"
747 : showAudio ? " Play Audio \U0001f3b5" : ""); 882 : showAudio ? " Play Audio \U0001f3b5" : "");
diff --git a/src/ui/util.c b/src/ui/util.c
index cf75a2b0..785841c9 100644
--- a/src/ui/util.c
+++ b/src/ui/util.c
@@ -308,7 +308,7 @@ static void addTabPage_Widget_(iWidget *tabs, enum iWidgetAddPos addPos, iWidget
308 const iBool isSel = childCount_Widget(pages) == 0; 308 const iBool isSel = childCount_Widget(pages) == 0;
309 iWidget * button = addChildPos_Widget( 309 iWidget * button = addChildPos_Widget(
310 findChild_Widget(tabs, "tabs.buttons"), 310 findChild_Widget(tabs, "tabs.buttons"),
311 iClob(new_LabelWidget(label, key, kmods, cstrFormat_String("tabs.switch page:%p", page))), 311 iClob(new_LabelWidget(label, key, kmods, format_CStr("tabs.switch page:%p", page))),
312 addPos); 312 addPos);
313 setFlags_Widget(button, selected_WidgetFlag, isSel); 313 setFlags_Widget(button, selected_WidgetFlag, isSel);
314 addChildPos_Widget(pages, page, addPos); 314 addChildPos_Widget(pages, page, addPos);
@@ -615,7 +615,7 @@ static iBool toggleHandler_(iWidget *d, const char *cmd) {
615 if (equal_Command(cmd, "toggle") && pointer_Command(cmd) == d) { 615 if (equal_Command(cmd, "toggle") && pointer_Command(cmd) == d) {
616 setToggle_Widget(d, (flags_Widget(d) & selected_WidgetFlag) == 0); 616 setToggle_Widget(d, (flags_Widget(d) & selected_WidgetFlag) == 0);
617 postCommand_Widget(d, 617 postCommand_Widget(d,
618 cstrFormat_String("%s.changed arg:%d", 618 format_CStr("%s.changed arg:%d",
619 cstr_String(id_Widget(d)), 619 cstr_String(id_Widget(d)),
620 isSelected_Widget(d) ? 1 : 0)); 620 isSelected_Widget(d) ? 1 : 0));
621 return iTrue; 621 return iTrue;
diff --git a/src/ui/window.h b/src/ui/window.h
index 70e52751..413c454c 100644
--- a/src/ui/window.h
+++ b/src/ui/window.h
@@ -31,5 +31,6 @@ float uiScale_Window (const iWindow *);
31iInt2 coord_Window (const iWindow *, int x, int y); 31iInt2 coord_Window (const iWindow *, int x, int y);
32iInt2 mouseCoord_Window (const iWindow *); 32iInt2 mouseCoord_Window (const iWindow *);
33uint32_t frameTime_Window (const iWindow *); 33uint32_t frameTime_Window (const iWindow *);
34SDL_Renderer *renderer_Window (const iWindow *);
34 35
35iWindow * get_Window (void); 36iWindow * get_Window (void);