#include "documentwidget.h" #include "paint.h" #include "util.h" #include "../gemini.h" #include "../gmdocument.h" #include #include enum iDocumentState { blank_DocumentState, fetching_DocumentState, layout_DocumentState, ready_DocumentState, }; struct Impl_DocumentWidget { iWidget widget; enum iDocumentState state; iString *url; iTlsRequest *request; int statusCode; iString *newSource; iGmDocument *doc; }; iDeclareType(Url) struct Impl_Url { iRangecc protocol; iRangecc host; iRangecc port; iRangecc path; iRangecc query; }; void init_Url(iUrl *d, const iString *text) { iRegExp *pattern = new_RegExp("(.+)://([^/:?]+)(:[0-9]+)?([^?]*)(\\?.*)?", caseInsensitive_RegExpOption); iRegExpMatch m; if (matchString_RegExp(pattern, text, &m)) { capturedRange_RegExpMatch(&m, 1, &d->protocol); capturedRange_RegExpMatch(&m, 2, &d->host); capturedRange_RegExpMatch(&m, 3, &d->port); if (!isEmpty_Range(&d->port)) { /* Don't include the colon. */ d->port.start++; } capturedRange_RegExpMatch(&m, 4, &d->path); capturedRange_RegExpMatch(&m, 5, &d->query); } else { iZap(*d); } iRelease(pattern); } iDefineObjectConstruction(DocumentWidget) void init_DocumentWidget(iDocumentWidget *d) { iWidget *w = as_Widget(d); init_Widget(w); setBackgroundColor_Widget(w, gray25_ColorId); d->state = blank_DocumentState; d->url = new_String(); d->statusCode = 0; d->request = NULL; d->newSource = new_String(); d->doc = new_GmDocument(); setUrl_DocumentWidget(d, collectNewCStr_String("gemini.circumlunar.space/")); } void deinit_DocumentWidget(iDocumentWidget *d) { delete_String(d->url); delete_String(d->newSource); iRelease(d->doc); } static int documentWidth_DocumentWidget_(const iDocumentWidget *d) { const iWidget *w = constAs_Widget(d); const iRect bounds = bounds_Widget(w); return bounds.size.x; } void setSource_DocumentWidget(iDocumentWidget *d, const iString *source) { /* TODO: lock source during update */ setSource_GmDocument(d->doc, source, documentWidth_DocumentWidget_(d)); d->state = ready_DocumentState; } static iRangecc getLine_(iRangecc text) { iRangecc line = { text.start, text.start }; for (; *line.end != '\n' && line.end != text.end; line.end++) {} return line; } static void requestFinished_DocumentWidget_(iAnyObject *obj) { iDocumentWidget *d = obj; iBlock *response = readAll_TlsRequest(d->request); iRangecc responseRange = { constBegin_Block(response), constEnd_Block(response) }; iRangecc respLine = getLine_(responseRange); responseRange.start = respLine.end + 1; /* First line is the status code. */ { iString *line = newRange_String(respLine); trim_String(line); d->statusCode = toInt_String(line); printf("response (%02d): %s\n", d->statusCode, cstr_String(line)); /* TODO: post a command with the status code */ delete_String(line); } setCStrN_String(d->newSource, responseRange.start, size_Range(&responseRange)); delete_Block(response); iReleaseLater(d->request); d->request = NULL; fflush(stdout); } static void fetch_DocumentWidget_(iDocumentWidget *d) { iAssert(!d->request); d->state = fetching_DocumentState; d->statusCode = 0; iUrl url; init_Url(&url, d->url); d->request = new_TlsRequest(); uint16_t port = toInt_String(collect_String(newRange_String(url.port))); if (port == 0) { port = 1965; /* default Gemini port */ } setUrl_TlsRequest(d->request, collect_String(newRange_String(url.host)), port); /* The request string is an UTF-8 encoded absolute URL. */ iString *content = collectNew_String(); append_String(content, d->url); appendCStr_String(content, "\r\n"); setContent_TlsRequest(d->request, utf8_String(content)); iConnect(TlsRequest, d->request, finished, d, requestFinished_DocumentWidget_); submit_TlsRequest(d->request); } void setUrl_DocumentWidget(iDocumentWidget *d, const iString *url) { clear_String(d->url); if (indexOfCStr_String(url, "://") == iInvalidPos && !startsWithCase_String(url, "gemini:")) { /* Prepend default protocol. */ setCStr_String(d->url, "gemini://"); } append_String(d->url, url); fetch_DocumentWidget_(d); } static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *ev) { iWidget *w = as_Widget(d); if (isResize_UserEvent(ev)) { setWidth_GmDocument(d->doc, documentWidth_DocumentWidget_(d)); } return processEvent_Widget(w, ev); } iDeclareType(DrawContext) struct Impl_DrawContext { const iDocumentWidget *d; iRect bounds; iPaint paint; }; static void drawRun_DrawContext_(void *context, const iGmRun *run) { iDrawContext *d = context; iString text; /* TODO: making a copy is unnecessary; the text routines should accept Rangecc */ initRange_String(&text, run->text); drawString_Text(run->font, add_I2(d->bounds.pos, run->bounds.pos), run->color, &text); deinit_String(&text); } static void draw_DocumentWidget_(const iDocumentWidget *d) { const iWidget *w = constAs_Widget(d); draw_Widget(w); /* Update the document? */ if (!isEmpty_String(d->newSource)) { /* TODO: Do this in the background. However, that requires a text metrics calculator that does not try to cache the glyph bitmaps. */ setSource_GmDocument(d->doc, d->newSource, documentWidth_DocumentWidget_(d)); clear_String(d->newSource); iConstCast(iDocumentWidget *, d)->state = ready_DocumentState; } if (d->state != ready_DocumentState) return; iDrawContext ctx = {.d = d, .bounds = bounds_Widget(w) }; init_Paint(&ctx.paint); render_GmDocument(d->doc, (iRangei){ 0, height_Rect(ctx.bounds) }, drawRun_DrawContext_, &ctx); } iBeginDefineSubclass(DocumentWidget, Widget) .processEvent = (iAny *) processEvent_DocumentWidget_, .draw = (iAny *) draw_DocumentWidget_, iEndDefineSubclass(DocumentWidget)