From b55e07bcc11237570a69695dbc617c3088b9306b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jaakko=20Kera=CC=88nen?= jaakko.keranen@iki.fi
Date: Sun, 19 Dec 2021 15:18:59 +0200
Subject: [PATCH 1/1] Cleanup: Group together DocumentView methods
src/ui/documentwidget.c | 8538 ++++++++++++++++++++-------------------
1 file changed, 4271 insertions(+), 4267 deletions(-)
diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c
index f5b9a4fc..4af3dd72 100644
--- a/src/ui/documentwidget.c
+++ b/src/ui/documentwidget.c
@@ -199,16 +199,6 @@ static void visBufInvalidated_(iVisBuf *d, size_t index) {
/----------------------------------------------------------------------------------------------/
-static void animate_DocumentWidget_ (void *ticker);
-static void animateMedia_DocumentWidget_ (iDocumentWidget *d);
-static void updateSideIconBuf_DocumentWidget_ (const iDocumentWidget *d);
-static void prerender_DocumentWidget_ (iAny *);
-static void scrollBegan_DocumentWidget_ (iAnyObject *, int, uint32_t);
-static const int smoothDuration_DocumentWidget_(enum iScrollType type) {
-}
enum iRequestState {
blank_RequestState,
fetching_RequestState,
@@ -343,9 +333,148 @@ struct Impl_DocumentWidget {
};
iDefineObjectConstruction(DocumentWidget)
+/* Sorted by proximity to F and J. */
+static const int homeRowKeys_[] = {
+};
static int docEnum_ = 0;
+static void animate_DocumentWidget_ (void *ticker);
+static void animateMedia_DocumentWidget_ (iDocumentWidget *d);
+static void updateSideIconBuf_DocumentWidget_ (const iDocumentWidget *d);
+static void prerender_DocumentWidget_ (iAny *);
+static void scrollBegan_DocumentWidget_ (iAnyObject *, int, uint32_t);
+static void refreshWhileScrolling_DocumentWidget_ (iAny *);
+/* TODO: The following methods are called from DocumentView, which goes the wrong way. */
+static iRangecc selectMark_DocumentWidget_(const iDocumentWidget *d) {
iSwap(const char *, norm.start, norm.end);
+}
+static int phoneToolbarHeight_DocumentWidget_(const iDocumentWidget *d) {
return 0;
+}
+static int footerHeight_DocumentWidget_(const iDocumentWidget *d) {
hgt += phoneToolbarHeight_DocumentWidget_(d);
+}
+static iBool isHoverAllowed_DocumentWidget_(const iDocumentWidget *d) {
return iFalse;
return iFalse;
return iFalse;
return iFalse;
return iFalse;
return iFalse;
+}
+static iMediaRequest *findMediaRequest_DocumentWidget_(const iDocumentWidget *d, iGmLinkId linkId) {
const iMediaRequest *req = (const iMediaRequest *) i.object;
if (req->linkId == linkId) {
return iConstCast(iMediaRequest *, req);
}
+}
+static size_t linkOrdinalFromKey_DocumentWidget_(const iDocumentWidget *d, int key) {
if (key >= '1' && key <= '9') {
return key - '1';
}
if (key < 'a' || key > 'z') {
return iInvalidPos;
}
ord = key - 'a' + 9;
+#if defined (iPlatformApple)
/* Skip keys that would conflict with default system shortcuts: hide, minimize, quit, close. */
if (key == 'h' || key == 'm' || key == 'q' || key == 'w') {
return iInvalidPos;
}
if (key > 'h') ord--;
if (key > 'm') ord--;
if (key > 'q') ord--;
if (key > 'w') ord--;
+#endif
iForIndices(i, homeRowKeys_) {
if (homeRowKeys_[i] == key) {
return i;
}
}
+}
+static iChar linkOrdinalChar_DocumentWidget_(const iDocumentWidget *d, size_t ord) {
if (ord < 9) {
return '1' + ord;
}
+#if defined (iPlatformApple)
if (ord < 9 + 22) {
int key = 'a' + ord - 9;
if (key >= 'h') key++;
if (key >= 'm') key++;
if (key >= 'q') key++;
if (key >= 'w') key++;
return 'A' + key - 'a';
}
+#else
if (ord < 9 + 26) {
return 'A' + ord - 9;
}
+#endif
if (ord < iElemCount(homeRowKeys_)) {
return 'A' + homeRowKeys_[ord] - 'a';
}
+}
+/----------------------------------------------------------------------------------------------/
void init_DocumentView(iDocumentView *d) {
d->owner = NULL;
d->doc = new_GmDocument();
@@ -379,91 +508,6 @@ void init_DocumentView(iDocumentView *d) {
init_PtrArray(&d->visibleMedia);
}
-static void setOwner_DocumentView_(iDocumentView *d, iDocumentWidget *doc) {
-}
-void init_DocumentWidget(iDocumentWidget *d) {
-#if defined (iPlatformAppleDesktop)
-#else
-#endif
setFlags_Widget(w, leftEdgeDraggable_WidgetFlag | rightEdgeDraggable_WidgetFlag |
horizontalOffset_WidgetFlag, iTrue);
iClob(new_IndicatorWidget()),
resizeToParentWidth_WidgetFlag | resizeToParentHeight_WidgetFlag);
-#if !defined (iPlatformAppleDesktop) /* in system menu */
-#endif
-}
-void cancelAllRequests_DocumentWidget(iDocumentWidget *d) {
iMediaRequest *mr = i.object;
cancel_GmRequest(mr->req);
cancel_GmRequest(d->request);
-}
void deinit_DocumentView(iDocumentView *d) {
delete_DrawBufs(d->drawBufs);
delete_VisBuf(d->visBuf);
@@ -477,60 +521,9 @@ void deinit_DocumentView(iDocumentView *d) {
iReleasePtr(&d->doc);
}
-void deinit_DocumentWidget(iDocumentWidget *d) {
-// printf("\n* * * * * * * \nDEINIT DOCUMENT: %s\n * * * * * * *\n\n",
-// cstr_String(&d->widget.id)); fflush(stdout);
SDL_RemoveTimer(d->mediaTimer);
-}
-static iRangecc selectMark_DocumentWidget_(const iDocumentWidget *d) {
iSwap(const char *, norm.start, norm.end);
-}
-static void enableActions_DocumentWidget_(iDocumentWidget *d, iBool enable) {
if (isAction_Widget(i.object)) {
setFlags_Widget(i.object, disabled_WidgetFlag, !enable);
}
-}
-static void setLinkNumberMode_DocumentWidget_(iDocumentWidget *d, iBool set) {
setFlags_Widget(d->menu, disabled_WidgetFlag, set);
+static void setOwner_DocumentView_(iDocumentView *d, iDocumentWidget *doc) {
}
static void resetWideRuns_DocumentView_(iDocumentView *d) {
@@ -540,38 +533,17 @@ static void resetWideRuns_DocumentView_(iDocumentView *d) {
iZap(d->animWideRunRange);
}
-static void requestUpdated_DocumentWidget_(iAnyObject *obj) {
postCommand_Widget(obj,
"document.request.updated doc:%p reqid:%u request:%p",
d,
id_GmRequest(d->request),
d->request);
-}
-static void requestFinished_DocumentWidget_(iAnyObject *obj) {
"document.request.finished doc:%p reqid:%u request:%p",
d,
id_GmRequest(d->request),
d->request);
-}
static int documentWidth_DocumentView_(const iDocumentView *d) {
const iWidget *w = constAs_Widget(d->owner);
const iRect bounds = bounds_Widget(w);
const iPrefs * prefs = prefs_App();
const int minWidth = 50 * gap_UI; /* lines must fit a word at least */
const float adjust = iClamp((float) bounds.size.x / gap_UI / 11 - 12,
-1.0f, 10.0f); /* adapt to width */
-1.0f, 10.0f); /* adapt to width */
//printf("%f\n", adjust); fflush(stdout);
return iMini(iMax(minWidth, bounds.size.x - gap_UI * (d->pageMargin + adjust) * 2),
fontSize_UI * //emRatio_Text(paragraph_FontId) * /* dependent on avg. glyph width */
prefs->lineWidth * prefs->zoomPercent / 100);
prefs->lineWidth * prefs->zoomPercent / 100);
}
static int documentTopPad_DocumentView_(const iDocumentView *d) {
@@ -588,22 +560,6 @@ static int pageHeight_DocumentView_(const iDocumentView *d) {
return height_Banner(d->owner->banner) + documentTopPad_DocumentView_(d) + size_GmDocument(d->doc).y;
}
-static int phoneToolbarHeight_DocumentWidget_(const iDocumentWidget *d) {
return 0;
-}
-static int footerHeight_DocumentWidget_(const iDocumentWidget *d) {
hgt += phoneToolbarHeight_DocumentWidget_(d);
-}
static iRect documentBounds_DocumentView_(const iDocumentView *d) {
const iRect bounds = bounds_Widget(constAs_Widget(d->owner));
const int margin = gap_UI * d->pageMargin;
@@ -761,38 +717,6 @@ static void invalidateWideRunsWithNonzeroOffset_DocumentView_(iDocumentView *d)
}
}
-static void animate_DocumentWidget_(void *ticker) {
(d->linkInfo && !isFinished_Anim(&d->linkInfo->opacity))) {
addTicker_App(animate_DocumentWidget_, d);
-}
-static iBool isHoverAllowed_DocumentWidget_(const iDocumentWidget *d) {
return iFalse;
return iFalse;
return iFalse;
return iFalse;
return iFalse;
return iFalse;
-}
static void updateHover_DocumentView_(iDocumentView *d, iInt2 mouse) {
const iWidget *w = constAs_Widget(d->owner);
const iRect docBounds = documentBounds_DocumentView_(d);
@@ -872,72 +796,6 @@ static void updateSideOpacity_DocumentView_(iDocumentView *d, iBool isAnimated)
animate_DocumentWidget_(d->owner);
}
-static uint32_t mediaUpdateInterval_DocumentWidget_(const iDocumentWidget *d) {
return 0;
return 0;
const iGmRun *run = i.ptr;
if (run->mediaType == audio_MediaType) {
iPlayer *plr = audioPlayer_Media(media_GmDocument(d->view.doc), mediaId_GmRun(run));
if (flags_Player(plr) & adjustingVolume_PlayerFlag ||
(isStarted_Player(plr) && !isPaused_Player(plr))) {
interval = iMin(interval, 1000 / 15);
}
}
else if (run->mediaType == download_MediaType) {
interval = iMin(interval, 1000);
}
-}
-static uint32_t postMediaUpdate_DocumentWidget_(uint32_t interval, void *context) {
-}
-static void updateMedia_DocumentWidget_(iDocumentWidget *d) {
refresh_Widget(d);
iConstForEach(PtrArray, i, &d->view.visibleMedia) {
const iGmRun *run = i.ptr;
if (run->mediaType == audio_MediaType) {
iPlayer *plr = audioPlayer_Media(media_GmDocument(d->view.doc), mediaId_GmRun(run));
if (idleTimeMs_Player(plr) > 3000 && ~flags_Player(plr) & volumeGrabbed_PlayerFlag &&
flags_Player(plr) & adjustingVolume_PlayerFlag) {
setFlags_Player(plr, adjustingVolume_PlayerFlag, iFalse);
}
}
}
SDL_RemoveTimer(d->mediaTimer);
d->mediaTimer = 0;
-}
-static void animateMedia_DocumentWidget_(iDocumentWidget *d) {
if (d->mediaTimer) {
SDL_RemoveTimer(d->mediaTimer);
d->mediaTimer = 0;
}
return;
d->mediaTimer = SDL_AddTimer(interval, postMediaUpdate_DocumentWidget_, d);
-}
static iRangecc currentHeading_DocumentView_(const iDocumentView *d) {
iRangecc heading = iNullRange;
if (d->visibleRuns.start) {
@@ -971,7 +829,7 @@ static void updateVisible_DocumentView_(iDocumentView *d) {
!isSuccess_GmStatusCode(d->owner->sourceStatus));
iScrollWidget *scrollBar = d->owner->scroll;
const iRangei visRange = visibleRange_DocumentView_(d);
-// printf("visRange: %d...%d\n", visRange.start, visRange.end);
const iRect bounds = bounds_Widget(as_Widget(d->owner));
const int scrollMax = updateScrollMax_DocumentView_(d);
/* Reposition the footer buttons as appropriate. */
@@ -1021,84 +879,14 @@ static void updateVisible_DocumentView_(iDocumentView *d) {
}
}
-static void updateWindowTitle_DocumentWidget_(const iDocumentWidget *d) {
"doctabs"), d);
/* Not part of the UI at the moment. */
return;
pushBack_StringArray(title, title_GmDocument(d->view.doc));
pushBack_StringArray(title, d->titleUser);
iUrl parts;
init_Url(&parts, d->mod.url);
if (equalCase_Rangecc(parts.scheme, "about")) {
if (!findWidget_App("winbar")) {
pushBackCStr_StringArray(title, "Lagrange");
}
}
else if (!isEmpty_Range(&parts.host)) {
pushBackRange_StringArray(title, parts.host);
}
pushBackCStr_StringArray(title, "Lagrange");
iString *text = collect_String(joinCStr_StringArray(title, " \u2014 "));
if (setWindow) {
/* Longest version for the window title, and omit the icon. */
setTitle_MainWindow(get_MainWindow(), text);
setWindow = iFalse;
}
const iChar siteIcon = siteIcon_GmDocument(d->view.doc);
if (siteIcon) {
if (!isEmpty_String(text)) {
prependCStr_String(text, " " restore_ColorEscape);
}
prependChar_String(text, siteIcon);
prependCStr_String(text, escape_Color(uiIcon_ColorId));
}
const int width = measureRange_Text(font, range_String(text)).advance.x;
const int ellipsisWidth = measure_Text(font, "...").advance.x;
setTextColor_LabelWidget(tabButton, none_ColorId);
iWidget *tabCloseButton = child_Widget(as_Widget(tabButton), 0);
setFlags_Widget(tabCloseButton, visibleOnParentHover_WidgetFlag,
avail > width_Widget(tabCloseButton));
if (width <= avail || isEmpty_StringArray(title)) {
updateText_LabelWidget(tabButton, text);
break;
}
if (size_StringArray(title) == 1) {
/* Just truncate to fit. */
if (siteIcon && avail <= 4 * ellipsisWidth) {
updateText_LabelWidget(tabButton, collect_String(newUnicodeN_String(&siteIcon, 1)));
setTextColor_LabelWidget(tabButton, uiIcon_ColorId);
break;
}
const char *endPos;
tryAdvanceNoWrap_Text(font,
range_String(text),
avail - ellipsisWidth,
&endPos);
updateText_LabelWidget(
tabButton,
collectNewFormat_String(
"%s...", cstr_Rangecc((iRangecc){ constBegin_String(text), endPos })));
break;
}
remove_StringArray(title, size_StringArray(title) - 1);
+static void swap_DocumentView_(iDocumentView *d, iDocumentView *swapBuffersWith) {
}
static void updateTimestampBuf_DocumentView_(const iDocumentView *d) {
@@ -1125,27 +913,6 @@ static void invalidate_DocumentView_(iDocumentView *d) {
clear_PtrSet(d->invalidRuns);
}
-static void invalidate_DocumentWidget_(iDocumentWidget *d) {
return;
return;
d->flags |= invalidationPending_DocumentWidgetFlag;
return;
-// printf("[%p] '%s' invalidated\n", d, cstr_String(id_Widget(as_Widget(d))));
-}
-static iRangecc siteText_DocumentWidget_(const iDocumentWidget *d) {
: range_String(d->titleUser);
-}
static void documentRunsInvalidated_DocumentView_(iDocumentView *d) {
d->hoverPre = NULL;
d->hoverAltPre = NULL;
@@ -1154,4299 +921,4414 @@ static void documentRunsInvalidated_DocumentView_(iDocumentView *d) {
iZap(d->renderRuns);
}
-static void documentRunsInvalidated_DocumentWidget_(iDocumentWidget *d) {
+static void resetScroll_DocumentView_(iDocumentView *d) {
}
-iBool isPinned_DocumentWidget_(const iDocumentWidget *d) {
return iFalse;
return iTrue;
return iFalse;
(prefs->pinSplit == 2 && w->root == win->roots[1]);
+static void updateWidth_DocumentView_(iDocumentView *d) {
}
-static void showOrHidePinningIndicator_DocumentWidget_(iDocumentWidget *d) {
isPinned_DocumentWidget_(d));
+static void updateWidthAndRedoLayout_DocumentView_(iDocumentView *d) {
}
-static void documentWasChanged_DocumentWidget_(iDocumentWidget *d) {
const iBookmark *bm = get_Bookmarks(bookmarks_App(), bmid);
if (bm->flags & linkSplit_BookmarkFlag) {
d->flags |= otherRootByDefault_DocumentWidgetFlag;
}
+static void clampScroll_DocumentView_(iDocumentView *d) {
+}
+static void immediateScroll_DocumentView_(iDocumentView *d, int offset) {
+}
+static void smoothScroll_DocumentView_(iDocumentView *d, int offset, int duration) {
+}
+static void scrollTo_DocumentView_(iDocumentView *d, int documentY, iBool centered) {
documentY += height_Banner(d->owner->banner) + documentTopPad_DocumentView_(d);
}
setCachedDocument_History(d->mod.history,
d->view.doc, /* keeps a ref */
(d->flags & openedFromSidebar_DocumentWidgetFlag) != 0);
documentY += documentTopPad_DocumentView_(d) + d->pageMargin * gap_UI;
}
documentY - (centered ? documentBounds_DocumentView_(d).size.y / 2
: lineHeight_Text(paragraph_FontId)));
}
-void setSource_DocumentWidget(iDocumentWidget *d, const iString *source) {
source,
docWidth,
width_Widget(d),
isFinished_GmRequest(d->request) ? final_GmDocumentUpdate
: partial_GmDocumentUpdate);
+static void scrollToHeading_DocumentView_(iDocumentView *d, const char *heading) {
const iGmHeading *head = h.value;
if (startsWithCase_Rangecc(head->text, heading)) {
postCommandf_Root(as_Widget(d->owner)->root, "document.goto loc:%p", head->text.start);
break;
}
}
-static void replaceDocument_DocumentWidget_(iDocumentWidget *d, iGmDocument *newDoc) {
+static iBool scrollWideBlock_DocumentView_(iDocumentView *d, iInt2 mousePos, int delta,
int duration) {
return iFalse;
const iGmRun *run = i.ptr;
if (docPos.y >= top_Rect(run->bounds) && docPos.y <= bottom_Rect(run->bounds)) {
/* We can scroll this run. First find out how much is allowed. */
const iGmRunRange range = findPreformattedRange_GmDocument(d->doc, run);
int maxWidth = 0;
for (const iGmRun *r = range.start; r != range.end; r++) {
maxWidth = iMax(maxWidth, width_Rect(r->visBounds));
}
const int maxOffset = maxWidth - documentWidth_DocumentView_(d) + d->pageMargin * gap_UI;
if (size_Array(&d->wideRunOffsets) <= preId_GmRun(run)) {
resize_Array(&d->wideRunOffsets, preId_GmRun(run) + 1);
}
int *offset = at_Array(&d->wideRunOffsets, preId_GmRun(run) - 1);
const int oldOffset = *offset;
*offset = iClamp(*offset + delta, 0, maxOffset);
/* Make sure the whole block gets redraw. */
if (oldOffset != *offset) {
for (const iGmRun *r = range.start; r != range.end; r++) {
insert_PtrSet(d->invalidRuns, r);
}
refresh_Widget(d);
d->owner->selectMark = iNullRange;
d->owner->foundMark = iNullRange;
}
if (duration) {
if (d->animWideRunId != preId_GmRun(run) || isFinished_Anim(&d->animWideRunOffset)) {
d->animWideRunId = preId_GmRun(run);
init_Anim(&d->animWideRunOffset, oldOffset);
}
setValueEased_Anim(&d->animWideRunOffset, *offset, duration);
d->animWideRunRange = range;
addTicker_App(refreshWhileScrolling_DocumentWidget_, d);
}
else {
d->animWideRunId = 0;
init_Anim(&d->animWideRunOffset, 0);
}
return iTrue;
}
}
-static void updateBanner_DocumentWidget_(iDocumentWidget *d) {
+static iRangecc sourceLoc_DocumentView_(const iDocumentView *d, iInt2 pos) {
}
-static void updateTheme_DocumentWidget_(iDocumentWidget *d) {
+iDeclareType(MiddleRunParams)
+struct Impl_MiddleRunParams {
+};
+static void find_MiddleRunParams_(void *params, const iGmRun *run) {
return;
d->closest = run;
d->distance = distance;
}
-static void makeFooterButtons_DocumentWidget_(iDocumentWidget *d, const iMenuItem *items, size_t count) {
return;
+static const iGmRun *middleRun_DocumentView_(const iDocumentView *d) {
+}
+static void allocVisBuffer_DocumentView_(const iDocumentView *d) {
alloc_VisBuf(d->visBuf, size, 1);
}
unhittable_WidgetFlag | arrangeVertical_WidgetFlag |
resizeWidthOfChildren_WidgetFlag | arrangeHeight_WidgetFlag |
fixedPosition_WidgetFlag | resizeToParentWidth_WidgetFlag,
iTrue);
iLabelWidget *button = addChildFlags_Widget(
d->footerButtons,
iClob(newKeyMods_LabelWidget(
items[i].label, items[i].key, items[i].kmods, items[i].command)),
alignLeft_WidgetFlag | drawKey_WidgetFlag | extraPadding_WidgetFlag);
setPadding1_Widget(as_Widget(button), gap_UI / 2);
checkIcon_LabelWidget(button);
setFont_LabelWidget(button, uiContent_FontId);
setBackgroundColor_Widget(as_Widget(button), uiBackgroundSidebar_ColorId);
dealloc_VisBuf(d->visBuf);
}
}
-static void resetScroll_DocumentView_(iDocumentView *d) {
+static size_t visibleLinkOrdinal_DocumentView_(const iDocumentView *d, iGmLinkId linkId) {
const iGmRun *run = i.ptr;
if (top_Rect(run->visBounds) >= visRange.start + gap_UI * d->pageMargin * 4 / 5) {
if (run->flags & decoration_GmRunFlag && run->linkId) {
if (run->linkId == linkId) return ord;
ord++;
}
}
}
-static void showErrorPage_DocumentWidget_(iDocumentWidget *d, enum iGmStatusCode code,
const iString *meta) {
switch (code) {
case schemeChangeRedirect_GmStatusCode:
case tooManyRedirects_GmStatusCode:
appendFormat_String(src, "=> %s\n", cstr_String(meta));
break;
case tlsFailure_GmStatusCode:
-// useBanner = iFalse; /* valid data wasn't received from host */
-// appendFormat_String(src, ">%s\n", cstr_String(meta));
break;
case tlsServerCertificateExpired_GmStatusCode:
makeFooterButtons_DocumentWidget_(
d,
(iMenuItem[]){ { rightArrowhead_Icon " ${menu.unexpire}",
SDLK_RETURN, 0, "server.unexpire"
},
{ info_Icon " ${menu.pageinfo}",
SDLK_i,
KMOD_PRIMARY,
"document.info" } },
2);
break;
case tlsServerCertificateNotVerified_GmStatusCode:
makeFooterButtons_DocumentWidget_(
d,
(iMenuItem[]){ { info_Icon " ${menu.pageinfo}",
SDLK_i,
KMOD_PRIMARY,
"document.info" } },
1);
break;
case failedToOpenFile_GmStatusCode:
case certificateNotValid_GmStatusCode:
-// appendFormat_String(src, "%s", cstr_String(meta));
break;
case unsupportedMimeType_GmStatusCode: {
iString *key = collectNew_String();
toString_Sym(SDLK_s, KMOD_PRIMARY, key);
-// appendFormat_String(src, "\n\n%s\n
\n", cstr_String(meta));
const char *mtype = mediaTypeFromFileExtension_String(d->mod.url);
iArray items;
init_Array(&items, sizeof(iMenuItem));
if (iCmpStr(mtype, "application/octet-stream")) {
pushBack_Array(
&items,
&(iMenuItem){ translateCStr_Lang(format_CStr("View as \"%s\"", mtype)),
SDLK_RETURN,
0,
format_CStr("document.setmediatype mime:%s", mtype) });
}
pushBack_Array(
&items,
&(iMenuItem){ translateCStr_Lang(download_Icon " " saveToDownloads_Label),
0,
0,
"document.save" });
makeFooterButtons_DocumentWidget_(d, data_Array(&items), size_Array(&items));
deinit_Array(&items);
serverErrorMsg = collectNewFormat_String("%s (%s)", msg->title, cstr_String(meta));
break;
}
default:
if (!isEmpty_String(meta)) {
serverErrorMsg = meta;
}
break;
+static void documentRunsInvalidated_DocumentWidget_(iDocumentWidget *d) {
+}
+static iBool updateDocumentWidthRetainingScrollPosition_DocumentView_(iDocumentView *d,
iBool keepCenter) {
return iFalse;
of the visible area fixed. */
/* Keep the first visible run visible at the same position. */
/* TODO: First *fully* visible run? */
voffset = visibleRange_DocumentView_(d).start - top_Rect(run->visBounds);
run = findRunAtLoc_GmDocument(d->doc, runLoc);
if (run) {
scrollTo_DocumentView_(
d, top_Rect(run->visBounds) + lineHeight_Text(paragraph_FontId) + voffset, iFalse);
}
}
makeFooterButtons_DocumentWidget_(
d,
(iMenuItem[]){
{ leftHalf_Icon " ${menu.show.identities}",
'4',
KMOD_PRIMARY,
deviceType_App() == desktop_AppDeviceType ? "sidebar.mode arg:3 show:1"
: "preferences idents:1" },
{ person_Icon " ${menu.identity.new}", newIdentity_KeyShortcut, "ident.new" } },
2);
run = findRunAtLoc_GmDocument(d->doc, runLoc);
if (run) {
scrollTo_DocumentView_(d, mid_Rect(run->bounds).y, iTrue);
}
}
}
-static void updateFetchProgress_DocumentWidget_(iDocumentWidget *d) {
updateText_LabelWidget(prog,
collectNewFormat_String("%s%.3f ${mb}",
isFinished_GmRequest(d->request)
? uiHeading_ColorEscape
: uiTextCaution_ColorEscape,
dlSize / 1.0e6f));
+static iRect runRect_DocumentView_(const iDocumentView *d, const iGmRun *run) {
}
-static const char *zipPageHeading_(const iRangecc mime) {
return book_Icon " Gempub";
+iDeclareType(DrawContext)
+struct Impl_DrawContext {
+};
+static void fillRange_DrawContext_(iDrawContext *d, const iGmRun *run, enum iColorId color,
iRangecc mark, iBool *isInside) {
/* Selection may be done in either direction. */
iSwap(const char *, mark.start, mark.end);
}
return fontpack_Icon " Fontpack";
contains_Range(&mark, run->text.start))) {
int x = 0;
if (!*isInside) {
x = measureRange_Text(run->font,
(iRangecc){ run->text.start, iMax(run->text.start, mark.start) })
.advance.x;
}
int w = width_Rect(run->visBounds) - x;
if (contains_Range(&run->text, mark.end) || mark.end < run->text.start) {
iRangecc mk = !*isInside ? mark
: (iRangecc){ run->text.start, iMax(run->text.start, mark.end) };
mk.start = iMax(mk.start, run->text.start);
w = measureRange_Text(run->font, mk).advance.x;
*isInside = iFalse;
}
else {
*isInside = iTrue; /* at least until the next run */
}
if (w > width_Rect(run->visBounds) - x) {
w = width_Rect(run->visBounds) - x;
}
if (~run->flags & decoration_GmRunFlag) {
const iInt2 visPos =
add_I2(run->bounds.pos, addY_I2(d->viewPos, viewPos_DocumentView_(d->view)));
const iRect rangeRect = { addX_I2(visPos, x), init_I2(w, height_Rect(run->bounds)) };
if (rangeRect.size.x) {
fillRect_Paint(&d->paint, rangeRect, color);
/* Keep track of the first and last marked rects. */
if (d->firstMarkRect.size.x == 0) {
d->firstMarkRect = rangeRect;
}
d->lastMarkRect = rangeRect;
}
}
}
type.start += 2;
these ranges as a special case. */
const iRangecc url = linkUrlRange_GmDocument(d->view->doc, run->linkId);
if (contains_Range(&url, mark.start) &&
(contains_Range(&url, mark.end) || url.end == mark.end)) {
fillRect_Paint(
&d->paint,
moved_Rect(run->visBounds, addY_I2(d->viewPos, viewPos_DocumentView_(d->view))),
color);
}
}
}
-static void postProcessRequestContent_DocumentWidget_(iDocumentWidget *d, iBool isCached) {
/* TODO: move this to gempub.c */
delete_Gempub(d->sourceGempub);
d->sourceGempub = NULL;
if (!cmpCase_String(&d->sourceMime, "application/octet-stream") ||
!cmpCase_String(&d->sourceMime, mimeType_Gempub) ||
endsWithCase_String(d->mod.url, ".gpub")) {
iGempub *gempub = new_Gempub();
if (open_Gempub(gempub, &d->sourceContent)) {
setBaseUrl_Gempub(gempub, d->mod.url);
setSource_DocumentWidget(d, collect_String(coverPageSource_Gempub(gempub)));
setCStr_String(&d->sourceMime, mimeType_Gempub);
d->sourceGempub = gempub;
}
else {
delete_Gempub(gempub);
}
+static void drawMark_DrawContext_(void *context, const iGmRun *run) {
fillRange_DrawContext_(d, run, uiMatching_ColorId, d->view->owner->foundMark, &d->inFoundMark);
fillRange_DrawContext_(d, run, uiMarked_ColorId, d->view->owner->selectMark, &d->inSelectMark);
+}
+static void drawRun_DrawContext_(void *context, const iGmRun *run) {
if (!d->runsDrawn.start || run < d->runsDrawn.start) {
d->runsDrawn.start = run;
}
if (!d->sourceGempub) {
const iString *localPath = collect_String(localFilePathFromUrl_String(d->mod.url));
iBool isInside = iFalse;
if (localPath && !fileExists_FileInfo(localPath)) {
/* This URL may refer to a file inside the archive. */
localPath = findContainerArchive_Path(localPath);
isInside = iTrue;
}
if (localPath && equal_CStr(mediaType_Path(localPath), mimeType_Gempub)) {
iGempub *gempub = new_Gempub();
if (openFile_Gempub(gempub, localPath)) {
setBaseUrl_Gempub(gempub, collect_String(makeFileUrl_String(localPath)));
if (!isInside) {
setSource_DocumentWidget(d, collect_String(coverPageSource_Gempub(gempub)));
setCStr_String(&d->sourceMime, mimeType_Gempub);
}
d->sourceGempub = gempub;
}
else {
delete_Gempub(gempub);
}
}
if (!d->runsDrawn.end || run > d->runsDrawn.end) {
d->runsDrawn.end = run;
}
if (d->sourceGempub) {
if (equal_String(d->mod.url, coverPageUrl_Gempub(d->sourceGempub))) {
if (!isRemote_Gempub(d->sourceGempub)) {
iArray *items = collectNew_Array(sizeof(iMenuItem));
pushBack_Array(
items,
&(iMenuItem){ book_Icon " ${gempub.cover.view}",
0,
0,
format_CStr("!open url:%s",
cstr_String(indexPageUrl_Gempub(d->sourceGempub))) });
if (navSize_Gempub(d->sourceGempub) > 0) {
pushBack_Array(
items,
&(iMenuItem){
format_CStr(forwardArrow_Icon " %s",
cstr_String(navLinkLabel_Gempub(d->sourceGempub, 0))),
SDLK_RIGHT,
0,
format_CStr("!open url:%s",
cstr_String(navLinkUrl_Gempub(d->sourceGempub, 0))) });
}
makeFooterButtons_DocumentWidget_(d, constData_Array(items), size_Array(items));
}
else {
makeFooterButtons_DocumentWidget_(
d,
(iMenuItem[]){ { book_Icon " ${menu.save.downloads.open}",
SDLK_s,
KMOD_PRIMARY | KMOD_SHIFT,
"document.save open:1" },
{ download_Icon " " saveToDownloads_Label,
SDLK_s,
KMOD_PRIMARY,
"document.save" } },
2);
}
if (preloadCoverImage_Gempub(d->sourceGempub, d->view.doc)) {
redoLayout_GmDocument(d->view.doc);
updateVisible_DocumentView_(&d->view);
invalidate_DocumentWidget_(d);
}
SDL_Texture *tex = imageTexture_Media(media_GmDocument(d->view->doc), mediaId_GmRun(run));
const iRect dst = moved_Rect(run->visBounds, origin);
if (tex) {
fillRect_Paint(&d->paint, dst, tmBackground_ColorId); /* in case the image has alpha */
SDL_RenderCopy(d->paint.dst->render, tex, NULL,
&(SDL_Rect){ dst.pos.x, dst.pos.y, dst.size.x, dst.size.y });
}
else {
drawRect_Paint(&d->paint, dst, tmQuoteIcon_ColorId);
drawCentered_Text(uiLabel_FontId,
dst,
iFalse,
tmQuote_ColorId,
explosion_Icon " Error Loading Image");
}
return;
/* Media UIs are drawn afterwards as a dynamic overlay. */
return;
(run->linkId && d->view->hoverLink && run->linkId == d->view->hoverLink->linkId &&
~run->flags & decoration_GmRunFlag);
/* Preformatted runs can be scrolled. */
runOffset_DocumentView_(d->view, run));
+#if 0
iBool isInlineImageCaption = run->linkId && linkFlags & content_GmLinkFlag &&
~linkFlags & permanent_GmLinkFlag;
if (run->flags & decoration_GmRunFlag && ~run->flags & startOfLine_GmRunFlag) {
/* This is the metadata. */
isInlineImageCaption = iFalse;
}
+#endif
/* While this is consistent, it's a bit excessive to indicate that an inlined image
is open: the image itself is the indication. */
const iBool isInlineImageCaption = iFalse;
if (run->linkId && (linkFlags & isOpen_GmLinkFlag || isInlineImageCaption)) {
/* Open links get a highlighted background. */
int bg = tmBackgroundOpenLink_ColorId;
const int frame = tmFrameOpenLink_ColorId;
const int pad = gap_Text;
iRect wideRect = { init_I2(origin.x - pad, visPos.y),
init_I2(d->docBounds.size.x + 2 * pad,
height_Rect(run->visBounds)) };
adjustEdges_Rect(&wideRect,
run->flags & startOfLine_GmRunFlag ? -pad * 3 / 4 : 0, 0,
run->flags & endOfLine_GmRunFlag ? pad * 3 / 4 : 0, 0);
/* The first line is composed of two runs that may be drawn in either order, so
only draw half of the background. */
if (run->flags & decoration_GmRunFlag) {
wideRect.size.x = right_Rect(visRect) - left_Rect(wideRect);
}
else if (equal_String(d->mod.url, indexPageUrl_Gempub(d->sourceGempub))) {
makeFooterButtons_DocumentWidget_(
d,
(iMenuItem[]){ { format_CStr(book_Icon " %s",
cstr_String(property_Gempub(d->sourceGempub,
title_GempubProperty))),
SDLK_LEFT,
0,
format_CStr("!open url:%s",
cstr_String(coverPageUrl_Gempub(d->sourceGempub))) } },
1);
else if (run->flags & startOfLine_GmRunFlag) {
wideRect.size.x = right_Rect(wideRect) - left_Rect(visRect);
wideRect.pos.x = left_Rect(visRect);
}
else {
/* Navigation buttons. */
iArray *items = collectNew_Array(sizeof(iMenuItem));
const size_t navIndex = navIndex_Gempub(d->sourceGempub, d->mod.url);
if (navIndex != iInvalidPos) {
if (navIndex < navSize_Gempub(d->sourceGempub) - 1) {
pushBack_Array(
items,
&(iMenuItem){
format_CStr(forwardArrow_Icon " %s",
cstr_String(navLinkLabel_Gempub(d->sourceGempub, navIndex + 1))),
SDLK_RIGHT,
0,
format_CStr("!open url:%s",
cstr_String(navLinkUrl_Gempub(d->sourceGempub, navIndex + 1))) });
}
if (navIndex > 0) {
pushBack_Array(
items,
&(iMenuItem){
format_CStr(backArrow_Icon " %s",
cstr_String(navLinkLabel_Gempub(d->sourceGempub, navIndex - 1))),
SDLK_LEFT,
0,
format_CStr("!open url:%s",
cstr_String(navLinkUrl_Gempub(d->sourceGempub, navIndex - 1))) });
}
else if (!equalCase_String(d->mod.url, indexPageUrl_Gempub(d->sourceGempub))) {
pushBack_Array(
items,
&(iMenuItem){
format_CStr(book_Icon " %s",
cstr_String(property_Gempub(d->sourceGempub, title_GempubProperty))),
SDLK_LEFT,
0,
format_CStr("!open url:%s",
cstr_String(coverPageUrl_Gempub(d->sourceGempub))) });
}
}
if (!isEmpty_Array(items)) {
makeFooterButtons_DocumentWidget_(d, constData_Array(items), size_Array(items));
}
fillRect_Paint(&d->paint, wideRect, bg);
}
else {
/* Normal background for other runs. There are cases when runs get drawn multiple times,
e.g., at the buffer boundary, and there are slightly overlapping characters in
monospace blocks. Clearing the background here ensures a cleaner visual appearance
since only one glyph is visible at any given point. */
fillRect_Paint(&d->paint, visRect, tmBackground_ColorId);
}
if (run->flags & decoration_GmRunFlag && run->flags & startOfLine_GmRunFlag) {
/* Link icon. */
if (linkFlags & content_GmLinkFlag) {
fg = linkColor_GmDocument(doc, run->linkId, textHover_GmLinkPart);
}
if (!isCached && prefs_App()->pinSplit &&
equal_String(d->mod.url, indexPageUrl_Gempub(d->sourceGempub))) {
const iString *navStart = navStartLinkUrl_Gempub(d->sourceGempub);
if (navStart) {
iWindow *win = get_Window();
/* Auto-split to show index and the first navigation link. */
if (numRoots_Window(win) == 2) {
/* This document is showing the index page. */
iRoot *other = otherRoot_Window(win, w->root);
postCommandf_Root(other, "open url:%s", cstr_String(navStart));
if (prefs_App()->pinSplit == 1 && w->root == win->roots[1]) {
/* On the wrong side. */
postCommand_App("ui.split swap:1");
}
}
else {
postCommandf_App(
"open splitmode:1 newtab:%d url:%s", otherRoot_OpenTabFlag, cstr_String(navStart));
}
}
}
else if (~run->flags & decoration_GmRunFlag) {
fg = linkColor_GmDocument(doc, run->linkId, isHover ? textHover_GmLinkPart : text_GmLinkPart);
if (linkFlags & content_GmLinkFlag) {
fg = linkColor_GmDocument(doc, run->linkId, textHover_GmLinkPart); /* link is inactive */
}
}
}
-}
-static void updateWidth_DocumentView_(iDocumentView *d) {
-}
-static void updateWidthAndRedoLayout_DocumentView_(iDocumentView *d) {
-}
-static void updateDocument_DocumentWidget_(iDocumentWidget *d,
const iGmResponse *response,
iGmDocument *cachedDoc,
const iBool isInitialUpdate) {
return;
const iInt2 margin = preRunMargin_GmDocument(doc, preId_GmRun(run));
fillRect_Paint(&d->paint, (iRect){ visPos, run->visBounds.size }, tmBackgroundAltText_ColorId);
drawRect_Paint(&d->paint, (iRect){ visPos, run->visBounds.size }, tmFrameAltText_ColorId);
drawWrapRange_Text(run->font,
add_I2(visPos, margin),
run->visBounds.size.x - 2 * margin.x,
run->color,
run->text);
}
that does not try to cache the glyph bitmaps. */
iBool setSource = iTrue;
iString str;
invalidate_DocumentWidget_(d);
if (document_App() == d) {
updateTheme_DocumentWidget_(d);
}
clear_String(&d->sourceMime);
d->sourceTime = response->when;
d->view.drawBufs->flags |= updateTimestampBuf_DrawBufsFlag;
initBlock_String(&str, &response->body); /* Note: Body may be megabytes in size. */
if (isSuccess_GmStatusCode(statusCode)) {
/* Check the MIME type. */
iRangecc charset = range_CStr("utf-8");
enum iSourceFormat docFormat = undefined_SourceFormat;
const iString *mimeStr = collect_String(lower_String(&response->meta)); /* for convenience */
set_String(&d->sourceMime, mimeStr);
iRangecc mime = range_String(mimeStr);
iRangecc seg = iNullRange;
while (nextSplit_Rangecc(mime, ";", &seg)) {
iRangecc param = seg;
trim_Rangecc(¶m);
/* Detect fontpacks even if the server doesn't use the right media type. */
if (isRequestFinished && equal_Rangecc(param, "application/octet-stream")) {
if (detect_FontPack(&response->body)) {
param = range_CStr(mimeType_FontPack);
}
}
if (equal_Rangecc(param, "text/gemini")) {
docFormat = gemini_SourceFormat;
setRange_String(&d->sourceMime, param);
}
else if (equal_Rangecc(param, "text/markdown")) {
docFormat = markdown_SourceFormat;
setRange_String(&d->sourceMime, param);
}
else if (startsWith_Rangecc(param, "text/") ||
equal_Rangecc(param, "application/json") ||
equal_Rangecc(param, "application/x-pem-file") ||
equal_Rangecc(param, "application/pem-certificate-chain")) {
docFormat = plainText_SourceFormat;
setRange_String(&d->sourceMime, param);
}
else if (isRequestFinished && equal_Rangecc(param, "font/ttf")) {
clear_String(&str);
docFormat = gemini_SourceFormat;
setRange_String(&d->sourceMime, param);
format_String(&str, "# TrueType Font\n");
iString *decUrl = collect_String(urlDecode_String(d->mod.url));
iRangecc name = baseName_Path(decUrl);
iBool isInstalled = iFalse;
if (startsWith_String(collect_String(localFilePathFromUrl_String(d->mod.url)),
cstr_String(dataDir_App()))) {
isInstalled = iTrue;
}
appendCStr_String(&str, "## ");
appendRange_String(&str, name);
appendCStr_String(&str, "\n\n");
appendCStr_String(
&str, cstr_Lang(isInstalled ? "truetype.help.installed" : "truetype.help"));
appendCStr_String(&str, "\n");
if (!isInstalled) {
makeFooterButtons_DocumentWidget_(
d,
(iMenuItem[]){
{ add_Icon " ${fontpack.install.ttf}",
SDLK_RETURN,
0,
format_CStr("!fontpack.install ttf:1 name:%s",
cstr_Rangecc(name)) },
{ folder_Icon " ${fontpack.open.fontsdir}",
SDLK_d,
0,
format_CStr("!open url:%s/fonts",
cstrCollect_String(makeFileUrl_String(dataDir_App())))
}
}, 2);
}
}
else if (isRequestFinished &&
(equal_Rangecc(param, "application/zip") ||
(startsWith_Rangecc(param, "application/") &&
endsWithCase_Rangecc(param, "+zip")))) {
clear_String(&str);
docFormat = gemini_SourceFormat;
setRange_String(&d->sourceMime, param);
if (equal_Rangecc(param, mimeType_FontPack)) {
/* Show some information about fontpacks, and set up footer actions. */
iArchive *zip = iClob(new_Archive());
if (openData_Archive(zip, &response->body)) {
iFontPack *fp = new_FontPack();
setUrl_FontPack(fp, d->mod.url);
setStandalone_FontPack(fp, iTrue);
if (loadArchive_FontPack(fp, zip)) {
appendFormat_String(&str, "# " fontpack_Icon "%s\n%s",
cstr_String(id_FontPack(fp).id),
cstrCollect_String(infoText_FontPack(fp)));
}
appendCStr_String(&str, "\n");
appendCStr_String(&str, cstr_Lang("fontpack.help"));
appendCStr_String(&str, "\n");
const iArray *actions = actions_FontPack(fp, iTrue);
makeFooterButtons_DocumentWidget_(d, constData_Array(actions),
size_Array(actions));
delete_FontPack(fp);
}
}
else {
format_String(&str, "# %s\n", zipPageHeading_(param));
appendFormat_String(&str,
cstr_Lang("doc.archive"),
cstr_Rangecc(baseName_Path(d->mod.url)));
appendCStr_String(&str, "\n");
}
appendCStr_String(&str, "\n");
iString *localPath = localFilePathFromUrl_String(d->mod.url);
if (!localPath) {
iString *key = collectNew_String();
toString_Sym(SDLK_s, KMOD_PRIMARY, key);
appendFormat_String(&str, "%s\n\n",
format_CStr(cstr_Lang("error.unsupported.suggestsave"),
cstr_String(key),
saveToDownloads_Label));
}
delete_String(localPath);
if (equalCase_Rangecc(urlScheme_String(d->mod.url), "file")) {
appendFormat_String(&str, "=> %s/ " folder_Icon " ${doc.archive.view}\n",
cstr_String(withSpacesEncoded_String(d->mod.url)));
}
translate_Lang(&str);
}
else if (startsWith_Rangecc(param, "image/") ||
startsWith_Rangecc(param, "audio/")) {
const iBool isAudio = startsWith_Rangecc(param, "audio/");
/* Make a simple document with an image or audio player. */
clear_String(&str);
docFormat = gemini_SourceFormat;
setRange_String(&d->sourceMime, param);
const iGmLinkId imgLinkId = 1; /* there's only the one link */
/* TODO: Do the image loading in `postProcessRequestContent_DocumentWidget_()` */
if ((isAudio && isInitialUpdate) || (!isAudio && isRequestFinished)) {
const char *linkTitle = cstr_Lang(
startsWith_String(mimeStr, "image/") ? "media.untitled.image"
: "media.untitled.audio");
iUrl parts;
init_Url(&parts, d->mod.url);
if (!isEmpty_Range(&parts.path)) {
linkTitle =
baseName_Path(collect_String(newRange_String(parts.path))).start;
}
format_String(&str, "=> %s %s\n",
cstr_String(canonicalUrl_String(d->mod.url)),
linkTitle);
setData_Media(media_GmDocument(d->view.doc),
imgLinkId,
mimeStr,
&response->body,
!isRequestFinished ? partialData_MediaFlag : 0);
redoLayout_GmDocument(d->view.doc);
}
else if (isAudio && !isInitialUpdate) {
/* Update the audio content. */
setData_Media(media_GmDocument(d->view.doc),
imgLinkId,
mimeStr,
&response->body,
!isRequestFinished ? partialData_MediaFlag : 0);
refresh_Widget(d);
setSource = iFalse;
}
else {
clear_String(&str);
}
}
else if (startsWith_Rangecc(param, "charset=")) {
charset = (iRangecc){ param.start + 8, param.end };
/* Remove whitespace and quotes. */
trim_Rangecc(&charset);
if (*charset.start == '"' && *charset.end == '"') {
charset.start++;
charset.end--;
}
}
}
if (docFormat == undefined_SourceFormat) {
if (isRequestFinished) {
d->flags &= ~drawDownloadCounter_DocumentWidgetFlag;
showErrorPage_DocumentWidget_(d, unsupportedMimeType_GmStatusCode, &response->meta);
deinit_String(&str);
return;
}
d->flags |= drawDownloadCounter_DocumentWidgetFlag;
clear_PtrSet(d->view.invalidRuns);
deinit_String(&str);
return;
}
setFormat_GmDocument(d->view.doc, docFormat);
/* Convert the source to UTF-8 if needed. */
if (!equalCase_Rangecc(charset, "utf-8")) {
set_String(&str,
collect_String(decode_Block(&str.chars, cstr_Rangecc(charset))));
}
}
if (cachedDoc) {
replaceDocument_DocumentWidget_(d, cachedDoc);
updateWidth_DocumentView_(&d->view);
}
else if (setSource) {
setSource_DocumentWidget(d, &str);
}
deinit_String(&str);
-}
-static void fetch_DocumentWidget_(iDocumentWidget *d) {
iRelease(d->request);
d->request = NULL;
"document.request.started doc:%p url:%s",
d,
cstr_String(d->mod.url));
-}
-static void updateTrust_DocumentWidget_(iDocumentWidget *d, const iGmResponse *response) {
d->certFlags = response->certFlags;
d->certExpiry = response->certValidUntil;
set_Block(d->certFingerprint, &response->certFingerprint);
set_String(d->certSubject, &response->certSubject);
setFlags_Widget(as_Widget(lock), disabled_WidgetFlag, iTrue);
updateTextCStr_LabelWidget(lock, gray50_ColorEscape openLock_Icon);
return;
~d->certFlags & trusted_GmCertFlag) {
updateTextCStr_LabelWidget(lock, red_ColorEscape warning_Icon);
updateTextCStr_LabelWidget(lock, isDarkMode ? orange_ColorEscape warning_Icon
: black_ColorEscape warning_Icon);
updateTextCStr_LabelWidget(lock, green_ColorEscape closedLock_Icon);
-}
-static void parseUser_DocumentWidget_(iDocumentWidget *d) {
-}
-static void cacheRunGlyphs_(void *data, const iGmRun *run) {
cache_Text(run->font, run->text);
-}
-static void cacheDocumentGlyphs_DocumentWidget_(const iDocumentWidget *d) {
~d->flags & animationPlaceholder_DocumentWidgetFlag) {
/* Just cache the top of the document, since this is what we usually need. */
int maxY = height_Widget(&d->widget) * 2;
if (maxY == 0) {
maxY = size_GmDocument(d->view.doc).y;
}
render_GmDocument(d->view.doc, (iRangei){ 0, maxY }, cacheRunGlyphs_, NULL);
-}
-static void addBannerWarnings_DocumentWidget_(iDocumentWidget *d) {
numItems_Banner(d->banner) == 0) {
iString *title = collectNewCStr_String(cstr_Lang("dlg.certwarn.title"));
iString *str = collectNew_String();
if (certFlags & timeVerified_GmCertFlag && certFlags & domainVerified_GmCertFlag) {
iUrl parts;
init_Url(&parts, d->mod.url);
const iTime oldUntil =
domainValidUntil_GmCerts(certs_App(), parts.host, port_Url(&parts));
iDate exp;
init_Date(&exp, &oldUntil);
iTime now;
initCurrent_Time(&now);
const int days = secondsSince_Time(&oldUntil, &now) / 3600 / 24;
if (days <= 30) {
appendCStr_String(str,
format_CStr(cstrCount_Lang("dlg.certwarn.mayberenewed.n", days),
cstrCollect_String(format_Date(&exp, "%Y-%m-%d")),
days));
}
else {
appendCStr_String(str, cstr_Lang("dlg.certwarn.different"));
if (d->showLinkNumbers && run->linkId && run->flags & decoration_GmRunFlag) {
const size_t ord = visibleLinkOrdinal_DocumentView_(d->view, run->linkId);
if (ord >= d->view->owner->ordinalBase) {
const iChar ordChar =
linkOrdinalChar_DocumentWidget_(d->view->owner, ord - d->view->owner->ordinalBase);
if (ordChar) {
const char *circle = "\u25ef"; /* Large Circle */
const int circleFont = FONT_ID(default_FontId, regular_FontStyle, contentRegular_FontSize);
iRect nbArea = { init_I2(d->viewPos.x - gap_UI / 3, visPos.y),
init_I2(3.95f * gap_Text, 1.0f * lineHeight_Text(circleFont)) };
drawRange_Text(
circleFont, topLeft_Rect(nbArea), tmQuote_ColorId, range_CStr(circle));
iRect circleArea = visualBounds_Text(circleFont, range_CStr(circle));
addv_I2(&circleArea.pos, topLeft_Rect(nbArea));
drawCentered_Text(FONT_ID(default_FontId, regular_FontStyle, contentSmall_FontSize),
circleArea,
iTrue,
tmQuote_ColorId,
"%lc",
(int) ordChar);
goto runDrawn;
}
}
}
else if (certFlags & domainVerified_GmCertFlag) {
setCStr_String(title, get_GmError(tlsServerCertificateExpired_GmStatusCode)->title);
appendFormat_String(str, cstr_Lang("dlg.certwarn.expired"),
cstrCollect_String(format_Date(&d->certExpiry, "%Y-%m-%d")));
}
else if (certFlags & timeVerified_GmCertFlag) {
appendFormat_String(str, cstr_Lang("dlg.certwarn.domain"),
cstr_String(d->certSubject));
}
else {
appendCStr_String(str, cstr_Lang("dlg.certwarn.domain.expired"));
}
add_Banner(d->banner, warning_BannerType, none_GmStatusCode, title, str);
value_SiteSpec(collectNewRange_String(urlRoot_String(d->mod.url)),
dismissWarnings_SiteSpecKey) |
(!prefs_App()->warnAboutMissingGlyphs ? missingGlyphs_GmDocumentWarning : 0);
add_Banner(d->banner, warning_BannerType, missingGlyphs_GmStatusCode, NULL, NULL);
/* TODO: List one or more of the missing characters and/or their Unicode blocks? */
add_Banner(d->banner, warning_BannerType, ansiEscapes_GmStatusCode, NULL, NULL);
-}
-static void updateFromCachedResponse_DocumentWidget_(iDocumentWidget *d, float normScrollY,
const iGmResponse *resp, iGmDocument *cachedDoc) {
-// iAssert(width_Widget(d) > 0); /* must be laid out by now */
d->initNormScrollY = normScrollY;
/* Use the cached response data. */
updateTrust_DocumentWidget_(d, resp);
d->sourceTime = resp->when;
d->sourceStatus = success_GmStatusCode;
format_String(&d->sourceHeader, cstr_Lang("pageinfo.header.cached"));
set_Block(&d->sourceContent, &resp->body);
if (!cachedDoc) {
updateWidthAndRedoLayout_DocumentView_(&d->view);
if (run->flags & quoteBorder_GmRunFlag) {
drawVLine_Paint(&d->paint,
addX_I2(visPos,
!run->isRTL
? -gap_Text * 5 / 2
: (width_Rect(run->visBounds) + gap_Text * 5 / 2)),
height_Rect(run->visBounds),
tmQuoteIcon_ColorId);
}
updateDocument_DocumentWidget_(d, resp, cachedDoc, iTrue);
clear_Banner(d->banner);
updateBanner_DocumentWidget_(d);
addBannerWarnings_DocumentWidget_(d);
as_Widget(d)->root, "document.changed doc:%p url:%s", d, cstr_String(d->mod.url));
-}
-static iBool updateFromHistory_DocumentWidget_(iDocumentWidget *d) {
iChangeFlags(d->flags,
openedFromSidebar_DocumentWidgetFlag,
recent->flags.openedFromSidebar);
updateFromCachedResponse_DocumentWidget_(
d, recent->normScrollY, recent->cachedResponse, recent->cachedDoc);
if (!recent->cachedDoc) {
/* We have a cached copy now. */
setCachedDocument_History(d->mod.history, d->view.doc, iFalse);
/* Base attributes. */ {
int f, c;
runBaseAttributes_GmDocument(doc, run, &f, &c);
setBaseAttributes_Text(f, c);
}
return iTrue;
fetch_DocumentWidget_(d);
/* Retain scroll position in refetched content as well. */
d->initNormScrollY = recent->normScrollY;
drawBoundRange_Text(run->font,
visPos,
(run->isRTL ? -1 : 1) * width_Rect(run->visBounds),
fg,
run->text);
setBaseAttributes_Text(-1, -1);
}
-}
-static void refreshWhileScrolling_DocumentWidget_(iAny *ptr) {
for (const iGmRun *r = view->animWideRunRange.start; r != view->animWideRunRange.end; r++) {
insert_PtrSet(view->invalidRuns, r);
const int metaFont = paragraph_FontId;
/* TODO: Show status of an ongoing media request. */
const int flags = linkFlags;
const iRect linkRect = moved_Rect(run->visBounds, origin);
iMediaRequest *mr = NULL;
/* Show metadata about inline content. */
if (flags & content_GmLinkFlag && run->flags & endOfLine_GmRunFlag) {
fg = linkColor_GmDocument(doc, run->linkId, textHover_GmLinkPart);
iString text;
init_String(&text);
const iMediaId linkMedia = findMediaForLink_Media(constMedia_GmDocument(doc),
run->linkId, none_MediaType);
iAssert(linkMedia.type != none_MediaType);
iGmMediaInfo info;
info_Media(constMedia_GmDocument(doc), linkMedia, &info);
switch (linkMedia.type) {
case image_MediaType: {
/* There's a separate decorative GmRun for the metadata. */
break;
}
case audio_MediaType:
format_String(&text, "%s", info.type);
break;
case download_MediaType:
format_String(&text, "%s", info.type);
break;
default:
break;
}
if (linkMedia.type != download_MediaType && /* can't cancel downloads currently */
linkMedia.type != image_MediaType &&
findMediaRequest_DocumentWidget_(d->view->owner, run->linkId)) {
appendFormat_String(
&text, " %s" close_Icon, isHover ? escape_Color(tmLinkText_ColorId) : "");
}
const iInt2 size = measureRange_Text(metaFont, range_String(&text)).bounds.size;
if (size.x) {
fillRect_Paint(
&d->paint,
(iRect){ add_I2(origin, addX_I2(topRight_Rect(run->bounds), -size.x - gap_UI)),
addX_I2(size, 2 * gap_UI) },
tmBackground_ColorId);
drawAlign_Text(metaFont,
add_I2(topRight_Rect(run->bounds), origin),
fg,
right_Alignment,
"%s", cstr_String(&text));
}
deinit_String(&text);
}
view->animWideRunId = 0;
addTicker_App(refreshWhileScrolling_DocumentWidget_, d);
iChangeFlags(d->flags, noHoverWhileScrolling_DocumentWidgetFlag, iFalse);
updateHover_DocumentView_(view, mouseCoord_Window(get_Window(), 0));
-}
-static void scrollBegan_DocumentWidget_(iAnyObject *any, int offset, uint32_t duration) {
setLinkNumberMode_DocumentWidget_(d, iFalse);
invalidateVisibleLinks_DocumentView_(&d->view);
const float normPos = normScrollPos_DocumentView_(&d->view);
if (prefs_App()->hideToolbarOnScroll && iAbs(offset) > 5 && normPos >= 0) {
showToolbar_Root(as_Widget(d)->root, offset < 0);
else if (run->flags & endOfLine_GmRunFlag &&
(mr = findMediaRequest_DocumentWidget_(d->view->owner, run->linkId)) != NULL) {
if (!isFinished_GmRequest(mr->req)) {
draw_Text(metaFont,
topRight_Rect(linkRect),
tmInlineContentMetadata_ColorId,
translateCStr_Lang(" \u2014 ${doc.fetching}\u2026 (%.1f ${mb})"),
(float) bodySize_GmRequest(mr->req) / 1.0e6f);
}
}
}
iChangeFlags(d->flags, noHoverWhileScrolling_DocumentWidgetFlag, iTrue);
addTicker_App(refreshWhileScrolling_DocumentWidget_, d);
drawRect_Paint(&d->paint, (iRect){ visPos, run->bounds.size }, green_ColorId);
drawRect_Paint(&d->paint, (iRect){ visPos, run->visBounds.size }, red_ColorId);
}
}
-static void clampScroll_DocumentView_(iDocumentView *d) {
+static int drawSideRect_(iPaint *p, iRect rect) {
bg = tmBannerIcon_ColorId;
fg = tmBannerBackground_ColorId;
}
-static void immediateScroll_DocumentView_(iDocumentView *d, int offset) {
+static int sideElementAvailWidth_DocumentView_(const iDocumentView *d) {
left_Rect(bounds_Widget(constAs_Widget(d->owner))) - 2 * d->pageMargin * gap_UI;
}
-static void smoothScroll_DocumentView_(iDocumentView *d, int offset, int duration) {
+static iBool isSideHeadingVisible_DocumentView_(const iDocumentView *d) {
}
-static void scrollTo_DocumentView_(iDocumentView *d, int documentY, iBool centered) {
documentY += height_Banner(d->owner->banner) + documentTopPad_DocumentView_(d);
documentY += documentTopPad_DocumentView_(d) + d->pageMargin * gap_UI;
+static void updateSideIconBuf_DocumentView_(const iDocumentView *d) {
return;
}
documentY - (centered ? documentBounds_DocumentView_(d).size.y / 2
: lineHeight_Text(paragraph_FontId)));
-}
-static void scrollToHeading_DocumentView_(iDocumentView *d, const char *heading) {
const iGmHeading *head = h.value;
if (startsWithCase_Rangecc(head->text, heading)) {
postCommandf_Root(as_Widget(d->owner)->root, "document.goto loc:%p", head->text.start);
break;
}
SDL_DestroyTexture(dbuf->sideIconBuf);
dbuf->sideIconBuf = NULL;
}
-}
-static iBool scrollWideBlock_DocumentView_(iDocumentView *d, iInt2 mousePos, int delta,
int duration) {
return iFalse;
return;
}
const iGmRun *run = i.ptr;
if (docPos.y >= top_Rect(run->bounds) && docPos.y <= bottom_Rect(run->bounds)) {
/* We can scroll this run. First find out how much is allowed. */
const iGmRunRange range = findPreformattedRange_GmDocument(d->doc, run);
int maxWidth = 0;
for (const iGmRun *r = range.start; r != range.end; r++) {
maxWidth = iMax(maxWidth, width_Rect(r->visBounds));
}
const int maxOffset = maxWidth - documentWidth_DocumentView_(d) + d->pageMargin * gap_UI;
if (size_Array(&d->wideRunOffsets) <= preId_GmRun(run)) {
resize_Array(&d->wideRunOffsets, preId_GmRun(run) + 1);
}
int *offset = at_Array(&d->wideRunOffsets, preId_GmRun(run) - 1);
const int oldOffset = *offset;
*offset = iClamp(*offset + delta, 0, maxOffset);
/* Make sure the whole block gets redraw. */
if (oldOffset != *offset) {
for (const iGmRun *r = range.start; r != range.end; r++) {
insert_PtrSet(d->invalidRuns, r);
}
refresh_Widget(d);
d->owner->selectMark = iNullRange;
d->owner->foundMark = iNullRange;
}
if (duration) {
if (d->animWideRunId != preId_GmRun(run) || isFinished_Anim(&d->animWideRunOffset)) {
d->animWideRunId = preId_GmRun(run);
init_Anim(&d->animWideRunOffset, oldOffset);
}
setValueEased_Anim(&d->animWideRunOffset, *offset, duration);
d->animWideRunRange = range;
addTicker_App(refreshWhileScrolling_DocumentWidget_, d);
}
else {
d->animWideRunId = 0;
init_Anim(&d->animWideRunOffset, 0);
}
return iTrue;
const iInt2 headingSize = measureWrapRange_Text(sideHeadingFont, avail,
currentHeading_DocumentView_(d)).bounds.size;
if (headingSize.x > 0) {
bufSize.y += gap_Text + headingSize.y;
bufSize.x = iMax(bufSize.x, headingSize.x);
}
else {
isHeadingVisible = iFalse;
}
}
-}
-static void togglePreFold_DocumentWidget_(iDocumentWidget *d, uint16_t preId) {
SDL_PIXELFORMAT_RGBA4444,
SDL_TEXTUREACCESS_STATIC | SDL_TEXTUREACCESS_TARGET,
bufSize.x, bufSize.y);
iRangecc text = currentHeading_DocumentView_(d);
iInt2 pos = addY_I2(bottomLeft_Rect(iconRect), gap_Text);
const int font = sideHeadingFont;
drawWrapRange_Text(font, pos, avail, tmBannerSideTitle_ColorId, text);
}
-static iString *makeQueryUrl_DocumentWidget_(const iDocumentWidget *d,
const iString *userEnteredText) {
remove_Block(&url->chars, qPos, iInvalidSize);
trimEnd_String(cleaned); /* autocorrect may insert an extra space */
if (isEmpty_String(cleaned)) {
set_String(cleaned, userEnteredText); /* user wanted just spaces? */
+static void drawSideElements_DocumentView_(const iDocumentView *d) {
const iInt2 texSize = size_SDLTexture(dbuf->sideIconBuf);
if (avail > texSize.x) {
const int minBannerSize = lineHeight_Text(banner_FontId) * 2;
iInt2 pos = addY_I2(add_I2(topLeft_Rect(bounds), init_I2(margin, 0)),
height_Rect(bounds) / 2 - minBannerSize / 2 -
(texSize.y > minBannerSize
? (gap_Text + lineHeight_Text(heading3_FontId)) / 2
: 0));
SDL_SetTextureAlphaMod(dbuf->sideIconBuf, 255 * opacity);
SDL_RenderCopy(renderer_Window(get_Window()),
dbuf->sideIconBuf, NULL,
&(SDL_Rect){ pos.x, pos.y, texSize.x, texSize.y });
}
}
-}
-static void inputQueryValidator_(iInputWidget *input, void *context) {
iString *trunc = copy_String(text_InputWidget(input));
truncate_String(trunc, 1024);
setText_InputWidget(input, trunc);
delete_String(trunc);
draw_TextBuf(
dbuf->timestampBuf,
add_I2(
bottomLeft_Rect(bounds),
init_I2(margin,
-margin + -dbuf->timestampBuf->size.y +
iMax(0, d->scrollY.max - pos_SmoothScroll(&d->scrollY)))),
tmQuoteIcon_ColorId);
}
avail < 0 ? uiTextCaution_ColorId :
avail < 128 ? uiTextStrong_ColorId
: uiTextDim_ColorId);
}
-static const char *humanReadableStatusCode_(enum iGmStatusCode code) {
return "";
+static void drawMedia_DocumentView_(const iDocumentView *d, iPaint *p) {
const iGmRun * run = i.ptr;
if (run->mediaType == audio_MediaType) {
iPlayerUI ui;
init_PlayerUI(&ui,
audioPlayer_Media(media_GmDocument(d->doc), mediaId_GmRun(run)),
runRect_DocumentView_(d, run));
draw_PlayerUI(&ui, p);
}
else if (run->mediaType == download_MediaType) {
iDownloadUI ui;
init_DownloadUI(&ui, constMedia_GmDocument(d->doc), run->mediaId,
runRect_DocumentView_(d, run));
draw_DownloadUI(&ui, p);
}
}
}
-static void checkResponse_DocumentWidget_(iDocumentWidget *d) {
return;
return;
+static void extend_GmRunRange_(iGmRunRange *runs) {
runs->start--;
runs->end++;
}
d->state = receivedPartialResponse_RequestState;
d->flags &= ~fromCache_DocumentWidgetFlag;
updateTrust_DocumentWidget_(d, resp);
if (isSuccess_GmStatusCode(statusCode)) {
clear_Banner(d->banner);
updateTheme_DocumentWidget_(d);
}
if (~d->certFlags & trusted_GmCertFlag &&
isSuccess_GmStatusCode(statusCode) &&
equalCase_Rangecc(urlScheme_String(d->mod.url), "gemini")) {
statusCode = tlsServerCertificateNotVerified_GmStatusCode;
}
init_Anim(&d->view.sideOpacity, 0);
init_Anim(&d->view.altTextOpacity, 0);
format_String(&d->sourceHeader,
"%s%s",
humanReadableStatusCode_(statusCode),
isEmpty_String(&resp->meta) && !isSuccess_GmStatusCode(statusCode)
? get_GmError(statusCode)->title
: cstr_String(&resp->meta));
d->sourceStatus = statusCode;
switch (category_GmStatusCode(statusCode)) {
case categoryInput_GmStatusCode: {
/* Let the navigation history know that we have been to this URL even though
it is only displayed as an input dialog. */
visitUrl_Visited(visited_App(), d->mod.url, transient_VisitedUrlFlag);
iUrl parts;
init_Url(&parts, d->mod.url);
iWidget *dlg = makeValueInput_Widget(
as_Widget(d),
NULL,
format_CStr(uiHeading_ColorEscape "%s", cstr_Rangecc(parts.host)),
isEmpty_String(&resp->meta)
? format_CStr(cstr_Lang("dlg.input.prompt"), cstr_Rangecc(parts.path))
: cstr_String(&resp->meta),
uiTextCaution_ColorEscape "${dlg.input.send}",
format_CStr("!document.input.submit doc:%p", d));
iWidget *buttons = findChild_Widget(dlg, "dialogbuttons");
iLabelWidget *lineBreak = NULL;
if (statusCode != sensitiveInput_GmStatusCode) {
/* The line break and URL length counters are positioned differently on mobile.
There is no line breaks in sensitive input. */
if (deviceType_App() == desktop_AppDeviceType) {
iString *keyStr = collectNew_String();
toString_Sym(SDLK_RETURN,
lineBreakKeyMod_ReturnKeyBehavior(prefs_App()->returnKey),
keyStr);
lineBreak = new_LabelWidget(
format_CStr("${dlg.input.linebreak}" uiTextAction_ColorEscape " %s",
cstr_String(keyStr)),
NULL);
insertChildAfter_Widget(buttons, iClob(lineBreak), 0);
}
else {
-#if !defined (iPlatformAppleMobile)
lineBreak = new_LabelWidget("${dlg.input.linebreak}", "text.insert arg:10");
-#endif
}
if (lineBreak) {
setFlags_Widget(as_Widget(lineBreak), frameless_WidgetFlag, iTrue);
setTextColor_LabelWidget(lineBreak, uiTextDim_ColorId);
+}
+static iBool render_DocumentView_(const iDocumentView *d, iDrawContext *ctx, iBool prerenderExtra) {
init_Rect(0,
0,
width_Rect(bounds) - constAs_Widget(d->owner->scroll)->rect.size.x,
height_Rect(bounds));
iPaint *p = &ctx->paint;
init_Paint(p);
iForIndices(i, visBuf->buffers) {
iVisBufTexture *buf = &visBuf->buffers[i];
iVisBufMeta *meta = buf->user;
const iRangei bufRange = intersect_Rangei(bufferRange_VisBuf(visBuf, i), full);
const iRangei bufVisRange = intersect_Rangei(bufRange, vis);
ctx->widgetBounds = moved_Rect(ctxWidgetBounds, init_I2(0, -buf->origin));
ctx->viewPos = init_I2(left_Rect(ctx->docBounds) - left_Rect(bounds), -buf->origin);
// printf(" buffer %zu: buf vis range %d...%d\n", i, bufVisRange.start, bufVisRange.end);
if (!prerenderExtra && !isEmpty_Range(&bufVisRange)) {
didDraw = iTrue;
if (isEmpty_Rangei(buf->validRange)) {
/* Fill the required currently visible range (vis). */
const iRangei bufVisRange = intersect_Rangei(bufRange, vis);
if (!isEmpty_Range(&bufVisRange)) {
beginTarget_Paint(p, buf->texture);
fillRect_Paint(p, (iRect){ zero_I2(), visBuf->texSize }, tmBackground_ColorId);
iZap(ctx->runsDrawn);
render_GmDocument(d->doc, bufVisRange, drawRun_DrawContext_, ctx);
meta->runsDrawn = ctx->runsDrawn;
extend_GmRunRange_(&meta->runsDrawn);
buf->validRange = bufVisRange;
// printf(" buffer %zu valid %d...%d\n", i, bufRange.start, bufRange.end);
}
}
iWidget *counter = (iWidget *) new_LabelWidget("", NULL);
setId_Widget(counter, "valueinput.counter");
setFlags_Widget(counter, frameless_WidgetFlag | resizeToParentHeight_WidgetFlag, iTrue);
if (deviceType_App() == desktop_AppDeviceType) {
addChildPos_Widget(buttons, iClob(counter), front_WidgetAddPos);
}
else {
insertChildAfter_Widget(buttons, iClob(counter), 1);
}
if (lineBreak && deviceType_App() != desktop_AppDeviceType) {
addChildPos_Widget(buttons, iClob(lineBreak), front_WidgetAddPos);
}
/* Menu for additional actions, past entries. */ {
iMenuItem items[] = { { "${menu.input.precedingline}",
SDLK_v,
KMOD_PRIMARY | KMOD_SHIFT,
format_CStr("!valueinput.set ptr:%p text:%s",
buttons,
cstr_String(&d->linePrecedingLink)) } };
iLabelWidget *menu = makeMenuButton_LabelWidget(midEllipsis_Icon, items, 1);
if (deviceType_App() == desktop_AppDeviceType) {
addChildPos_Widget(buttons, iClob(menu), front_WidgetAddPos);
/* Progressively fill the required runs. */
if (meta->runsDrawn.start) {
beginTarget_Paint(p, buf->texture);
meta->runsDrawn.start = renderProgressive_GmDocument(d->doc, meta->runsDrawn.start,
-1, iInvalidSize,
bufVisRange,
drawRun_DrawContext_,
ctx);
buf->validRange.start = bufVisRange.start;
}
else {
insertChildAfterFlags_Widget(buttons, iClob(menu), 0,
frameless_WidgetFlag | noBackground_WidgetFlag);
setFont_LabelWidget(menu, font_LabelWidget((iLabelWidget *) lastChild_Widget(buttons)));
setTextColor_LabelWidget(menu, uiTextAction_ColorId);
if (meta->runsDrawn.end) {
beginTarget_Paint(p, buf->texture);
meta->runsDrawn.end = renderProgressive_GmDocument(d->doc, meta->runsDrawn.end,
+1, iInvalidSize,
bufVisRange,
drawRun_DrawContext_,
ctx);
buf->validRange.end = bufVisRange.end;
}
}
setValidator_InputWidget(findChild_Widget(dlg, "input"), inputQueryValidator_, d);
setSensitiveContent_InputWidget(findChild_Widget(dlg, "input"),
statusCode == sensitiveInput_GmStatusCode);
if (document_App() != d) {
postCommandf_App("tabs.switch page:%p", d);
}
else {
updateTheme_DocumentWidget_(d);
}
break;
}
case categorySuccess_GmStatusCode:
if (d->flags & urlChanged_DocumentWidgetFlag) {
/* Keep scroll position when reloading the same page. */
resetScroll_DocumentView_(&d->view);
}
d->view.scrollY.pullActionTriggered = 0;
pauseAllPlayers_Media(media_GmDocument(d->view.doc), iTrue);
iReleasePtr(&d->view.doc); /* new content incoming */
delete_Gempub(d->sourceGempub);
d->sourceGempub = NULL;
destroy_Widget(d->footerButtons);
d->footerButtons = NULL;
d->view.doc = new_GmDocument();
resetWideRuns_DocumentView_(&d->view);
updateDocument_DocumentWidget_(d, resp, NULL, iTrue);
break;
case categoryRedirect_GmStatusCode:
if (isEmpty_String(&resp->meta)) {
showErrorPage_DocumentWidget_(d, invalidRedirect_GmStatusCode, NULL);
/* Progressively draw the rest of the buffer if it isn't fully valid. */
if (prerenderExtra && !equal_Rangei(bufRange, buf->validRange)) {
const iGmRun *next;
// printf("%zu: prerenderExtra (start:%p end:%p)\n", i, meta->runsDrawn.start, meta->runsDrawn.end);
if (meta->runsDrawn.start == NULL) {
/* Haven't drawn anything yet in this buffer, so let's try seeding it. */
const int rh = lineHeight_Text(paragraph_FontId);
const int y = i >= iElemCount(visBuf->buffers) / 2 ? bufRange.start : (bufRange.end - rh);
beginTarget_Paint(p, buf->texture);
fillRect_Paint(p, (iRect){ zero_I2(), visBuf->texSize }, tmBackground_ColorId);
buf->validRange = (iRangei){ y, y + rh };
iZap(ctx->runsDrawn);
render_GmDocument(d->doc, buf->validRange, drawRun_DrawContext_, ctx);
meta->runsDrawn = ctx->runsDrawn;
extend_GmRunRange_(&meta->runsDrawn);
// printf("%zu: seeded, next %p:%p\n", i, meta->runsDrawn.start, meta->runsDrawn.end);
didDraw = iTrue;
}
else {
/* Only accept redirects that use gemini scheme. */
const iString *dstUrl = absoluteUrl_String(d->mod.url, &resp->meta);
const iRangecc srcScheme = urlScheme_String(d->mod.url);
const iRangecc dstScheme = urlScheme_String(dstUrl);
if (d->redirectCount >= 5) {
showErrorPage_DocumentWidget_(d, tooManyRedirects_GmStatusCode, dstUrl);
}
/* Redirects with the same scheme are automatic, and switching automatically
between "gemini" and "titan" is allowed. */
else if (equalRangeCase_Rangecc(dstScheme, srcScheme) ||
(equalCase_Rangecc(srcScheme, "titan") &&
equalCase_Rangecc(dstScheme, "gemini")) ||
(equalCase_Rangecc(srcScheme, "gemini") &&
equalCase_Rangecc(dstScheme, "titan"))) {
visitUrl_Visited(visited_App(), d->mod.url, transient_VisitedUrlFlag);
postCommandf_Root(as_Widget(d)->root,
"open doc:%p redirect:%d url:%s", d, d->redirectCount + 1, cstr_String(dstUrl));
if (meta->runsDrawn.start) {
const iRangei upper = intersect_Rangei(bufRange, (iRangei){ full.start, buf->validRange.start });
if (upper.end > upper.start) {
beginTarget_Paint(p, buf->texture);
next = renderProgressive_GmDocument(d->doc, meta->runsDrawn.start,
-1, 1, upper,
drawRun_DrawContext_,
ctx);
if (next && meta->runsDrawn.start != next) {
meta->runsDrawn.start = next;
buf->validRange.start = bottom_Rect(next->visBounds);
didDraw = iTrue;
}
else {
buf->validRange.start = bufRange.start;
}
}
}
else {
/* Scheme changes must be manually approved. */
showErrorPage_DocumentWidget_(d, schemeChangeRedirect_GmStatusCode, dstUrl);
if (!didDraw && meta->runsDrawn.end) {
const iRangei lower = intersect_Rangei(bufRange, (iRangei){ buf->validRange.end, full.end });
if (lower.end > lower.start) {
beginTarget_Paint(p, buf->texture);
next = renderProgressive_GmDocument(d->doc, meta->runsDrawn.end,
+1, 1, lower,
drawRun_DrawContext_,
ctx);
if (next && meta->runsDrawn.end != next) {
meta->runsDrawn.end = next;
buf->validRange.end = top_Rect(next->visBounds);
didDraw = iTrue;
}
else {
buf->validRange.end = bufRange.end;
}
}
}
unlockResponse_GmRequest(d->request);
iReleasePtr(&d->request);
}
break;
default:
if (isDefined_GmError(statusCode)) {
showErrorPage_DocumentWidget_(d, statusCode, &resp->meta);
}
else if (category_GmStatusCode(statusCode) ==
categoryTemporaryFailure_GmStatusCode) {
showErrorPage_DocumentWidget_(
d, temporaryFailure_GmStatusCode, &resp->meta);
}
else if (category_GmStatusCode(statusCode) ==
categoryPermanentFailure_GmStatusCode) {
showErrorPage_DocumentWidget_(
d, permanentFailure_GmStatusCode, &resp->meta);
}
/* Draw any invalidated runs that fall within this buffer. */
if (!prerenderExtra) {
const iRangei bufRange = { buf->origin, buf->origin + visBuf->texSize.y };
/* Clear full-width backgrounds first in case there are any dynamic elements. */ {
iConstForEach(PtrSet, r, d->invalidRuns) {
const iGmRun *run = *r.value;
if (isOverlapping_Rangei(bufRange, ySpan_Rect(run->visBounds))) {
beginTarget_Paint(p, buf->texture);
fillRect_Paint(p,
init_Rect(0,
run->visBounds.pos.y - buf->origin,
visBuf->texSize.x,
run->visBounds.size.y),
tmBackground_ColorId);
}
}
}
else {
showErrorPage_DocumentWidget_(d, unknownStatusCode_GmStatusCode, &resp->meta);
setAnsiFlags_Text(ansiEscapes_GmDocument(d->doc));
iConstForEach(PtrSet, r, d->invalidRuns) {
const iGmRun *run = *r.value;
if (isOverlapping_Rangei(bufRange, ySpan_Rect(run->visBounds))) {
beginTarget_Paint(p, buf->texture);
drawRun_DrawContext_(ctx, run);
}
}
setAnsiFlags_Text(allowAll_AnsiFlag);
}
endTarget_Paint(p);
if (prerenderExtra && didDraw) {
/* Just a run at a time. */
break;
}
}
d->flags &= ~fromCache_DocumentWidgetFlag;
switch (category_GmStatusCode(statusCode)) {
case categorySuccess_GmStatusCode:
/* More content available. */
updateDocument_DocumentWidget_(d, resp, NULL, iFalse);
break;
default:
break;
if (!prerenderExtra) {
clear_PtrSet(d->invalidRuns);
}
}
}
-static iRangecc sourceLoc_DocumentView_(const iDocumentView *d, iInt2 pos) {
+static void draw_DocumentView_(const iDocumentView *d) {
As we're now drawing a document, ensure that the right palette is in effect.
Document theme colors can be used elsewhere, too, but first a document's palette
must be made global. */
updateTimestampBuf_DocumentView_(d);
updateSideIconBuf_DocumentView_(d);
}
.view = d,
.docBounds = docBounds,
.vis = vis,
.showLinkNumbers = (d->owner->flags & showLinkNumbers_DocumentWidgetFlag) != 0,
};
const int docBgColor = isDocEmpty ? tmBannerBackground_ColorId : tmBackground_ColorId;
setClip_Paint(&ctx.paint, clipBounds);
if (!isDocEmpty) {
draw_VisBuf(d->visBuf, init_I2(bounds.pos.x, yTop), ySpan_Rect(bounds));
}
/* Text markers. */
if (!isEmpty_Range(&d->owner->foundMark) || !isEmpty_Range(&d->owner->selectMark)) {
SDL_Renderer *render = renderer_Window(get_Window());
ctx.firstMarkRect = zero_Rect();
ctx.lastMarkRect = zero_Rect();
SDL_SetRenderDrawBlendMode(render,
isDark_ColorTheme(colorTheme_App()) ? SDL_BLENDMODE_ADD
: SDL_BLENDMODE_BLEND);
ctx.viewPos = topLeft_Rect(docBounds);
/* Marker starting outside the visible range? */
if (d->visibleRuns.start) {
if (!isEmpty_Range(&d->owner->selectMark) &&
d->owner->selectMark.start < d->visibleRuns.start->text.start &&
d->owner->selectMark.end > d->visibleRuns.start->text.start) {
ctx.inSelectMark = iTrue;
}
if (isEmpty_Range(&d->owner->foundMark) &&
d->owner->foundMark.start < d->visibleRuns.start->text.start &&
d->owner->foundMark.end > d->visibleRuns.start->text.start) {
ctx.inFoundMark = iTrue;
}
}
render_GmDocument(d->doc, vis, drawMark_DrawContext_, &ctx);
SDL_SetRenderDrawBlendMode(render, SDL_BLENDMODE_NONE);
/* Selection range pins. */
if (isTouchSelecting) {
drawPin_Paint(&ctx.paint, ctx.firstMarkRect, 0, tmQuote_ColorId);
drawPin_Paint(&ctx.paint, ctx.lastMarkRect, 1, tmQuote_ColorId);
}
}
drawMedia_DocumentView_(d, &ctx.paint);
/* Fill the top and bottom, in case the document is short. */
if (yTop > top_Rect(bounds)) {
fillRect_Paint(&ctx.paint,
(iRect){ bounds.pos, init_I2(bounds.size.x, yTop - top_Rect(bounds)) },
!isEmpty_Banner(banner) ? tmBannerBackground_ColorId
: docBgColor);
}
/* Banner. */
if (!isDocEmpty || numItems_Banner(banner) > 0) {
/* Fill the part between the banner and the top of the document. */
fillRect_Paint(&ctx.paint,
(iRect){ init_I2(left_Rect(bounds),
top_Rect(docBounds) + viewPos_DocumentView_(d) -
documentTopPad_DocumentView_(d)),
init_I2(bounds.size.x, documentTopPad_DocumentView_(d)) },
docBgColor);
setPos_Banner(banner, addY_I2(topLeft_Rect(docBounds),
-pos_SmoothScroll(&d->scrollY)));
draw_Banner(banner);
}
const int yBottom = yTop + size_GmDocument(d->doc).y;
if (yBottom < bottom_Rect(bounds)) {
fillRect_Paint(&ctx.paint,
init_Rect(bounds.pos.x, yBottom, bounds.size.x, bottom_Rect(bounds) - yBottom),
!isDocEmpty ? docBgColor : tmBannerBackground_ColorId);
}
unsetClip_Paint(&ctx.paint);
drawSideElements_DocumentView_(d);
/* Alt text. */
const float altTextOpacity = value_Anim(&d->altTextOpacity) * 6 - 5;
if (d->hoverAltPre && altTextOpacity > 0) {
const iGmPreMeta *meta = preMeta_GmDocument(d->doc, preId_GmRun(d->hoverAltPre));
if (meta->flags & topLeft_GmPreMetaFlag && ~meta->flags & decoration_GmRunFlag &&
!isEmpty_Range(&meta->altText)) {
const int margin = 3 * gap_UI / 2;
const int altFont = uiLabel_FontId;
const int wrap = docBounds.size.x - 2 * margin;
iInt2 pos = addY_I2(add_I2(docBounds.pos, meta->pixelRect.pos),
viewPos_DocumentView_(d));
const iInt2 textSize = measureWrapRange_Text(altFont, wrap, meta->altText).bounds.size;
pos.y -= textSize.y + gap_UI;
pos.y = iMax(pos.y, top_Rect(bounds));
const iRect altRect = { pos, init_I2(docBounds.size.x, textSize.y) };
ctx.paint.alpha = altTextOpacity * 255;
if (altTextOpacity < 1) {
SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_BLEND);
}
fillRect_Paint(&ctx.paint, altRect, tmBackgroundAltText_ColorId);
drawRect_Paint(&ctx.paint, altRect, tmFrameAltText_ColorId);
setOpacity_Text(altTextOpacity);
drawWrapRange_Text(altFont, addX_I2(pos, margin), wrap,
tmQuote_ColorId, meta->altText);
SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_NONE);
setOpacity_Text(1.0f);
}
}
/* Touch selection indicator. */
if (isTouchSelecting) {
iRect rect = { topLeft_Rect(bounds),
init_I2(width_Rect(bounds), lineHeight_Text(uiLabelBold_FontId)) };
fillRect_Paint(&ctx.paint, rect, uiTextAction_ColorId);
const iRangecc mark = selectMark_DocumentWidget_(d->owner);
drawCentered_Text(uiLabelBold_FontId,
rect,
iFalse,
uiBackground_ColorId,
"%zu bytes selected", /* TODO: i18n */
size_Range(&mark));
}
}
-iDeclareType(MiddleRunParams)
+/----------------------------------------------------------------------------------------------/
-struct Impl_MiddleRunParams {
-};
+static void enableActions_DocumentWidget_(iDocumentWidget *d, iBool enable) {
if (isAction_Widget(i.object)) {
setFlags_Widget(i.object, disabled_WidgetFlag, !enable);
}
+}
-static void find_MiddleRunParams_(void *params, const iGmRun *run) {
return;
+static void setLinkNumberMode_DocumentWidget_(iDocumentWidget *d, iBool set) {
setFlags_Widget(d->menu, disabled_WidgetFlag, set);
}
d->closest = run;
d->distance = distance;
+}
+static void requestUpdated_DocumentWidget_(iAnyObject *obj) {
postCommand_Widget(obj,
"document.request.updated doc:%p reqid:%u request:%p",
d,
id_GmRequest(d->request),
d->request);
}
}
-static const iGmRun *middleRun_DocumentView_(const iDocumentView *d) {
+static void requestFinished_DocumentWidget_(iAnyObject *obj) {
"document.request.finished doc:%p reqid:%u request:%p",
d,
id_GmRequest(d->request),
d->request);
}
-static void removeMediaRequest_DocumentWidget_(iDocumentWidget *d, iGmLinkId linkId) {
iMediaRequest *req = (iMediaRequest *) i.object;
if (req->linkId == linkId) {
remove_ObjectListIterator(&i);
break;
}
+static void animate_DocumentWidget_(void *ticker) {
(d->linkInfo && !isFinished_Anim(&d->linkInfo->opacity))) {
addTicker_App(animate_DocumentWidget_, d);
}
}
-static iMediaRequest *findMediaRequest_DocumentWidget_(const iDocumentWidget *d, iGmLinkId linkId) {
const iMediaRequest *req = (const iMediaRequest *) i.object;
if (req->linkId == linkId) {
return iConstCast(iMediaRequest *, req);
}
+static uint32_t mediaUpdateInterval_DocumentWidget_(const iDocumentWidget *d) {
return 0;
}
-}
-static iBool requestMedia_DocumentWidget_(iDocumentWidget *d, iGmLinkId linkId, iBool enableFilters) {
const iString *mediaUrl = absoluteUrl_String(d->mod.url, linkUrl_GmDocument(d->view.doc, linkId));
pushBack_ObjectList(d->media, iClob(new_MediaRequest(d, linkId, mediaUrl, enableFilters)));
invalidate_DocumentWidget_(d);
return iTrue;
return 0;
}
const iGmRun *run = i.ptr;
if (run->mediaType == audio_MediaType) {
iPlayer *plr = audioPlayer_Media(media_GmDocument(d->view.doc), mediaId_GmRun(run));
if (flags_Player(plr) & adjustingVolume_PlayerFlag ||
(isStarted_Player(plr) && !isPaused_Player(plr))) {
interval = iMin(interval, 1000 / 15);
}
}
else if (run->mediaType == download_MediaType) {
interval = iMin(interval, 1000);
}
}
-static iBool isDownloadRequest_DocumentWidget(const iDocumentWidget *d, const iMediaRequest *req) {
+static uint32_t postMediaUpdate_DocumentWidget_(uint32_t interval, void *context) {
}
-static iBool handleMediaCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) {
if (m.object == req) {
isOurRequest = iTrue;
break;
}
return iFalse;
/* Pass new data to media players. */
const enum iGmStatusCode code = status_GmRequest(req->req);
if (isSuccess_GmStatusCode(code)) {
iGmResponse *resp = lockResponse_GmRequest(req->req);
if (isDownloadRequest_DocumentWidget(d, req) ||
startsWith_String(&resp->meta, "audio/")) {
/* TODO: Use a helper? This is same as below except for the partialData flag. */
if (setData_Media(media_GmDocument(d->view.doc),
req->linkId,
&resp->meta,
&resp->body,
partialData_MediaFlag | allowHide_MediaFlag)) {
redoLayout_GmDocument(d->view.doc);
+static void updateMedia_DocumentWidget_(iDocumentWidget *d) {
refresh_Widget(d);
iConstForEach(PtrArray, i, &d->view.visibleMedia) {
const iGmRun *run = i.ptr;
if (run->mediaType == audio_MediaType) {
iPlayer *plr = audioPlayer_Media(media_GmDocument(d->view.doc), mediaId_GmRun(run));
if (idleTimeMs_Player(plr) > 3000 && ~flags_Player(plr) & volumeGrabbed_PlayerFlag &&
flags_Player(plr) & adjustingVolume_PlayerFlag) {
setFlags_Player(plr, adjustingVolume_PlayerFlag, iFalse);
}
updateVisible_DocumentView_(&d->view);
invalidate_DocumentWidget_(d);
refresh_Widget(as_Widget(d));
}
unlockResponse_GmRequest(req->req);
}
/* Update the link's progress. */
invalidateLink_DocumentView_(&d->view, req->linkId);
refresh_Widget(d);
return iTrue;
}
const enum iGmStatusCode code = status_GmRequest(req->req);
/* Give the media to the document for presentation. */
if (isSuccess_GmStatusCode(code)) {
if (isDownloadRequest_DocumentWidget(d, req) ||
startsWith_String(meta_GmRequest(req->req), "image/") ||
startsWith_String(meta_GmRequest(req->req), "audio/")) {
setData_Media(media_GmDocument(d->view.doc),
req->linkId,
meta_GmRequest(req->req),
body_GmRequest(req->req),
allowHide_MediaFlag);
redoLayout_GmDocument(d->view.doc);
iZap(d->view.visibleRuns); /* pointers invalidated */
updateVisible_DocumentView_(&d->view);
invalidate_DocumentWidget_(d);
refresh_Widget(as_Widget(d));
}
}
else {
const iGmError *err = get_GmError(code);
makeSimpleMessage_Widget(format_CStr(uiTextCaution_ColorEscape "%s", err->title), err->info);
removeMediaRequest_DocumentWidget_(d, req->linkId);
}
return iTrue;
SDL_RemoveTimer(d->mediaTimer);
d->mediaTimer = 0;
}
}
-static void allocVisBuffer_DocumentView_(const iDocumentView *d) {
alloc_VisBuf(d->visBuf, size, 1);
+static void animateMedia_DocumentWidget_(iDocumentWidget *d) {
if (d->mediaTimer) {
SDL_RemoveTimer(d->mediaTimer);
d->mediaTimer = 0;
}
return;
}
dealloc_VisBuf(d->visBuf);
d->mediaTimer = SDL_AddTimer(interval, postMediaUpdate_DocumentWidget_, d);
}
}
-static iBool fetchNextUnfetchedImage_DocumentWidget_(iDocumentWidget *d) {
const iGmRun *run = i.ptr;
if (run->linkId && run->mediaType == none_MediaType &&
~run->flags & decoration_GmRunFlag) {
const int linkFlags = linkFlags_GmDocument(d->view.doc, run->linkId);
if (isMediaLink_GmDocument(d->view.doc, run->linkId) &&
linkFlags & imageFileExtension_GmLinkFlag &&
~linkFlags & content_GmLinkFlag && ~linkFlags & permanent_GmLinkFlag ) {
if (requestMedia_DocumentWidget_(d, run->linkId, iTrue)) {
return iTrue;
}
+static void updateWindowTitle_DocumentWidget_(const iDocumentWidget *d) {
"doctabs"), d);
/* Not part of the UI at the moment. */
return;
pushBack_StringArray(title, title_GmDocument(d->view.doc));
pushBack_StringArray(title, d->titleUser);
iUrl parts;
init_Url(&parts, d->mod.url);
if (equalCase_Rangecc(parts.scheme, "about")) {
if (!findWidget_App("winbar")) {
pushBackCStr_StringArray(title, "Lagrange");
}
}
else if (!isEmpty_Range(&parts.host)) {
pushBackRange_StringArray(title, parts.host);
}
}
-}
-static const iString *saveToDownloads_(const iString *url, const iString *mime, const iBlock *content,
iBool showDialog) {
iFile *f = new_File(savePath);
if (open_File(f, writeOnly_FileMode)) {
write_File(f, content);
close_File(f);
const size_t size = size_Block(content);
const iBool isMega = size >= 1000000;
-#if defined (iPlatformAppleMobile)
exportDownloadedFile_iOS(savePath);
-#else
if (showDialog) {
const iMenuItem items[2] = {
{ "${dlg.save.opendownload}", 0, 0,
format_CStr("!open url:%s", cstrCollect_String(makeFileUrl_String(savePath))) },
{ "${dlg.message.ok}", 0, 0, "message.ok" },
};
makeMessage_Widget(uiHeading_ColorEscape "${heading.save}",
format_CStr("%s\n${dlg.save.size} %.3f %s",
cstr_String(path_File(f)),
isMega ? size / 1.0e6f : (size / 1.0e3f),
isMega ? "${mb}" : "${kb}"),
items,
iElemCount(items));
pushBackCStr_StringArray(title, "Lagrange");
iString *text = collect_String(joinCStr_StringArray(title, " \u2014 "));
if (setWindow) {
/* Longest version for the window title, and omit the icon. */
setTitle_MainWindow(get_MainWindow(), text);
setWindow = iFalse;
}
const iChar siteIcon = siteIcon_GmDocument(d->view.doc);
if (siteIcon) {
if (!isEmpty_String(text)) {
prependCStr_String(text, " " restore_ColorEscape);
}
-#endif
return savePath;
prependChar_String(text, siteIcon);
prependCStr_String(text, escape_Color(uiIcon_ColorId));
}
else {
makeSimpleMessage_Widget(uiTextCaution_ColorEscape "${heading.save.error}",
strerror(errno));
const int width = measureRange_Text(font, range_String(text)).advance.x;
const int ellipsisWidth = measure_Text(font, "...").advance.x;
setTextColor_LabelWidget(tabButton, none_ColorId);
iWidget *tabCloseButton = child_Widget(as_Widget(tabButton), 0);
setFlags_Widget(tabCloseButton, visibleOnParentHover_WidgetFlag,
avail > width_Widget(tabCloseButton));
if (width <= avail || isEmpty_StringArray(title)) {
updateText_LabelWidget(tabButton, text);
break;
}
if (size_StringArray(title) == 1) {
/* Just truncate to fit. */
if (siteIcon && avail <= 4 * ellipsisWidth) {
updateText_LabelWidget(tabButton, collect_String(newUnicodeN_String(&siteIcon, 1)));
setTextColor_LabelWidget(tabButton, uiIcon_ColorId);
break;
}
const char *endPos;
tryAdvanceNoWrap_Text(font,
range_String(text),
avail - ellipsisWidth,
&endPos);
updateText_LabelWidget(
tabButton,
collectNewFormat_String(
"%s...", cstr_Rangecc((iRangecc){ constBegin_String(text), endPos })));
break;
}
iRelease(f);
remove_StringArray(title, size_StringArray(title) - 1);
}
}
-static void addAllLinks_(void *context, const iGmRun *run) {
pushBack_PtrArray(links, run);
+static void invalidate_DocumentWidget_(iDocumentWidget *d) {
return;
}
-}
-static size_t visibleLinkOrdinal_DocumentView_(const iDocumentView *d, iGmLinkId linkId) {
const iGmRun *run = i.ptr;
if (top_Rect(run->visBounds) >= visRange.start + gap_UI * d->pageMargin * 4 / 5) {
if (run->flags & decoration_GmRunFlag && run->linkId) {
if (run->linkId == linkId) return ord;
ord++;
}
}
return;
}
d->flags |= invalidationPending_DocumentWidgetFlag;
return;
+// printf("[%p] '%s' invalidated\n", d, cstr_String(id_Widget(as_Widget(d))));
}
-/* Sorted by proximity to F and J. */
-static const int homeRowKeys_[] = {
-};
+static iRangecc siteText_DocumentWidget_(const iDocumentWidget *d) {
: range_String(d->titleUser);
+}
-static iBool updateDocumentWidthRetainingScrollPosition_DocumentView_(iDocumentView *d,
iBool keepCenter) {
+static iBool isPinned_DocumentWidget_(const iDocumentWidget *d) {
return iFalse;
}
of the visible area fixed. */
/* Keep the first visible run visible at the same position. */
/* TODO: First *fully* visible run? */
voffset = visibleRange_DocumentView_(d).start - top_Rect(run->visBounds);
return iTrue;
}
run = findRunAtLoc_GmDocument(d->doc, runLoc);
if (run) {
scrollTo_DocumentView_(
d, top_Rect(run->visBounds) + lineHeight_Text(paragraph_FontId) + voffset, iFalse);
}
return iFalse;
}
run = findRunAtLoc_GmDocument(d->doc, runLoc);
if (run) {
scrollTo_DocumentView_(d, mid_Rect(run->bounds).y, iTrue);
(prefs->pinSplit == 2 && w->root == win->roots[1]);
+}
+static void showOrHidePinningIndicator_DocumentWidget_(iDocumentWidget *d) {
isPinned_DocumentWidget_(d));
+}
+static void documentWasChanged_DocumentWidget_(iDocumentWidget *d) {
const iBookmark *bm = get_Bookmarks(bookmarks_App(), bmid);
if (bm->flags & linkSplit_BookmarkFlag) {
d->flags |= otherRootByDefault_DocumentWidgetFlag;
}
}
setCachedDocument_History(d->mod.history,
d->view.doc, /* keeps a ref */
(d->flags & openedFromSidebar_DocumentWidgetFlag) != 0);
}
-static iBool handlePinch_DocumentWidget_(iDocumentWidget *d, const char *cmd) {
d->pinchZoomInitial = d->pinchZoomPosted = prefs_App()->zoomPercent;
d->flags |= pinchZoom_DocumentWidgetFlag;
refresh_Widget(d);
+static void replaceDocument_DocumentWidget_(iDocumentWidget *d, iGmDocument *newDoc) {
+}
+static void updateBanner_DocumentWidget_(iDocumentWidget *d) {
+}
+static void updateTheme_DocumentWidget_(iDocumentWidget *d) {
return;
+}
+static void makeFooterButtons_DocumentWidget_(iDocumentWidget *d, const iMenuItem *items, size_t count) {
return;
}
const float rel = argf_Command(cmd);
int zoom = iRound(d->pinchZoomInitial * rel / 5.0f) * 5;
zoom = iClamp(zoom, 50, 200);
if (d->pinchZoomPosted != zoom) {
-#if defined (iPlatformAppleMobile)
if (zoom == 100) {
playHapticEffect_iOS(tap_HapticEffect);
unhittable_WidgetFlag | arrangeVertical_WidgetFlag |
resizeWidthOfChildren_WidgetFlag | arrangeHeight_WidgetFlag |
fixedPosition_WidgetFlag | resizeToParentWidth_WidgetFlag,
iTrue);
iLabelWidget *button = addChildFlags_Widget(
d->footerButtons,
iClob(newKeyMods_LabelWidget(
items[i].label, items[i].key, items[i].kmods, items[i].command)),
alignLeft_WidgetFlag | drawKey_WidgetFlag | extraPadding_WidgetFlag);
setPadding1_Widget(as_Widget(button), gap_UI / 2);
checkIcon_LabelWidget(button);
setFont_LabelWidget(button, uiContent_FontId);
setBackgroundColor_Widget(as_Widget(button), uiBackgroundSidebar_ColorId);
+}
+static void showErrorPage_DocumentWidget_(iDocumentWidget *d, enum iGmStatusCode code,
const iString *meta) {
switch (code) {
case schemeChangeRedirect_GmStatusCode:
case tooManyRedirects_GmStatusCode:
appendFormat_String(src, "=> %s\n", cstr_String(meta));
break;
case tlsFailure_GmStatusCode:
+// useBanner = iFalse; /* valid data wasn't received from host */
+// appendFormat_String(src, ">%s\n", cstr_String(meta));
break;
case tlsServerCertificateExpired_GmStatusCode:
makeFooterButtons_DocumentWidget_(
d,
(iMenuItem[]){ { rightArrowhead_Icon " ${menu.unexpire}",
SDLK_RETURN, 0, "server.unexpire"
},
{ info_Icon " ${menu.pageinfo}",
SDLK_i,
KMOD_PRIMARY,
"document.info" } },
2);
break;
case tlsServerCertificateNotVerified_GmStatusCode:
makeFooterButtons_DocumentWidget_(
d,
(iMenuItem[]){ { info_Icon " ${menu.pageinfo}",
SDLK_i,
KMOD_PRIMARY,
"document.info" } },
1);
break;
case failedToOpenFile_GmStatusCode:
case certificateNotValid_GmStatusCode:
+// appendFormat_String(src, "%s", cstr_String(meta));
break;
case unsupportedMimeType_GmStatusCode: {
iString *key = collectNew_String();
toString_Sym(SDLK_s, KMOD_PRIMARY, key);
+// appendFormat_String(src, "\n\n%s\n
\n", cstr_String(meta));
const char *mtype = mediaTypeFromFileExtension_String(d->mod.url);
iArray items;
init_Array(&items, sizeof(iMenuItem));
if (iCmpStr(mtype, "application/octet-stream")) {
pushBack_Array(
&items,
&(iMenuItem){ translateCStr_Lang(format_CStr("View as \"%s\"", mtype)),
SDLK_RETURN,
0,
format_CStr("document.setmediatype mime:%s", mtype) });
}
pushBack_Array(
&items,
&(iMenuItem){ translateCStr_Lang(download_Icon " " saveToDownloads_Label),
0,
0,
"document.save" });
makeFooterButtons_DocumentWidget_(d, data_Array(&items), size_Array(&items));
deinit_Array(&items);
serverErrorMsg = collectNewFormat_String("%s (%s)", msg->title, cstr_String(meta));
break;
}
-#endif
d->pinchZoomPosted = zoom;
postCommandf_App("zoom.set arg:%d", zoom);
default:
if (!isEmpty_String(meta)) {
serverErrorMsg = meta;
}
break;
}
}
d->flags &= ~pinchZoom_DocumentWidgetFlag;
refresh_Widget(d);
makeFooterButtons_DocumentWidget_(
d,
(iMenuItem[]){
{ leftHalf_Icon " ${menu.show.identities}",
'4',
KMOD_PRIMARY,
deviceType_App() == desktop_AppDeviceType ? "sidebar.mode arg:3 show:1"
: "preferences idents:1" },
{ person_Icon " ${menu.identity.new}", newIdentity_KeyShortcut, "ident.new" } },
2);
}
-}
-static void swap_DocumentView_(iDocumentView *d, iDocumentView *swapBuffersWith) {
}
-static void swap_DocumentWidget_(iDocumentWidget *d, iGmDocument *doc,
iDocumentWidget *swapBuffersWith) {
iAssert(isInstance_Object(doc, &Class_GmDocument));
replaceDocument_DocumentWidget_(d, doc);
iSwap(iBanner *, d->banner, swapBuffersWith->banner);
setOwner_Banner(d->banner, d);
setOwner_Banner(swapBuffersWith->banner, swapBuffersWith);
swap_DocumentView_(&d->view, &swapBuffersWith->view);
-// invalidate_DocumentWidget_(swapBuffersWith);
+static void updateFetchProgress_DocumentWidget_(iDocumentWidget *d) {
updateText_LabelWidget(prog,
collectNewFormat_String("%s%.3f ${mb}",
isFinished_GmRequest(d->request)
? uiHeading_ColorEscape
: uiTextCaution_ColorEscape,
dlSize / 1.0e6f));
}
}
-static iWidget *swipeParent_DocumentWidget_(iDocumentWidget *d) {
-}
-static void setUrl_DocumentWidget_(iDocumentWidget *d, const iString *url) {
d->flags |= urlChanged_DocumentWidgetFlag;
set_String(d->mod.url, url);
+static const char *zipPageHeading_(const iRangecc mime) {
return book_Icon " Gempub";
}
-}
-static void setupSwipeOverlay_DocumentWidget_(iDocumentWidget *d, iWidget *overlay) {
innerToWindow_Widget
does not apply visual offset. */-// swap_DocumentWidget_(target, d->doc, d);
overlay
animates off the screen to the right. */ setVisualOffset_Widget(overlay, toPos, 250, easeOut_AnimFlag | softer_AnimFlag);
return fontpack_Icon " Fontpack";
}
const float devFactor = (deviceType_App() == phone_AppDeviceType ? 1.0f : 2.0f);
float swipe = iClamp(d->swipeSpeed, devFactor * 400, devFactor * 1000) * gap_UI;
uint32_t span = ((toPos - fromPos) / swipe) * 1000;
setVisualOffset_Widget(overlay, toPos, span, deviceType_App() == tablet_AppDeviceType ?
easeOut_AnimFlag : 0);
type.start += 2;
}
}
-static iBool handleSwipe_DocumentWidget_(iDocumentWidget *d, const char *cmd) {
If DocumentWidget is refactored to split the document presentation from state
and request management (a new DocumentView class), plain views could be used for this
animation without having to mess with the complete state of the DocumentWidget. That
seems like a less error-prone approach -- the current implementation will likely break
down (again) if anything is changed in the document internals.
+static void postProcessRequestContent_DocumentWidget_(iDocumentWidget *d, iBool isCached) {
iWidget *w = as_Widget(d);
GmDocument content and temporary underlay/overlay DocumentWidgets. Depending on the
swipe direction, the DocumentWidget `d` may wait until the finger is released to actually
perform the navigation action. */
//printf("[%p] responds to edgeswipe.moved\n", d);
as_Widget(d)->offsetRef = NULL;
const int side = argLabel_Command(cmd, "side");
const int offset = arg_Command(cmd);
if (side == 1) { /* left edge */
if (atOldest_History(d->mod.history)) {
return iTrue;
/* TODO: move this to gempub.c */
delete_Gempub(d->sourceGempub);
d->sourceGempub = NULL;
if (!cmpCase_String(&d->sourceMime, "application/octet-stream") ||
!cmpCase_String(&d->sourceMime, mimeType_Gempub) ||
endsWithCase_String(d->mod.url, ".gpub")) {
iGempub *gempub = new_Gempub();
if (open_Gempub(gempub, &d->sourceContent)) {
setBaseUrl_Gempub(gempub, d->mod.url);
setSource_DocumentWidget(d, collect_String(coverPageSource_Gempub(gempub)));
setCStr_String(&d->sourceMime, mimeType_Gempub);
d->sourceGempub = gempub;
}
iWidget *swipeParent = swipeParent_DocumentWidget_(d);
if (findChild_Widget(swipeParent, "swipeout")) {
return iTrue; /* too fast, previous animation hasn't finished */
else {
delete_Gempub(gempub);
}
}
if (!d->sourceGempub) {
const iString *localPath = collect_String(localFilePathFromUrl_String(d->mod.url));
iBool isInside = iFalse;
if (localPath && !fileExists_FileInfo(localPath)) {
/* This URL may refer to a file inside the archive. */
localPath = findContainerArchive_Path(localPath);
isInside = iTrue;
}
if (localPath && equal_CStr(mediaType_Path(localPath), mimeType_Gempub)) {
iGempub *gempub = new_Gempub();
if (openFile_Gempub(gempub, localPath)) {
setBaseUrl_Gempub(gempub, collect_String(makeFileUrl_String(localPath)));
if (!isInside) {
setSource_DocumentWidget(d, collect_String(coverPageSource_Gempub(gempub)));
setCStr_String(&d->sourceMime, mimeType_Gempub);
}
d->sourceGempub = gempub;
}
else {
delete_Gempub(gempub);
}
}
}
if (d->sourceGempub) {
if (equal_String(d->mod.url, coverPageUrl_Gempub(d->sourceGempub))) {
if (!isRemote_Gempub(d->sourceGempub)) {
iArray *items = collectNew_Array(sizeof(iMenuItem));
pushBack_Array(
items,
&(iMenuItem){ book_Icon " ${gempub.cover.view}",
0,
0,
format_CStr("!open url:%s",
cstr_String(indexPageUrl_Gempub(d->sourceGempub))) });
if (navSize_Gempub(d->sourceGempub) > 0) {
pushBack_Array(
items,
&(iMenuItem){
format_CStr(forwardArrow_Icon " %s",
cstr_String(navLinkLabel_Gempub(d->sourceGempub, 0))),
SDLK_RIGHT,
0,
format_CStr("!open url:%s",
cstr_String(navLinkUrl_Gempub(d->sourceGempub, 0))) });
}
makeFooterButtons_DocumentWidget_(d, constData_Array(items), size_Array(items));
}
else {
makeFooterButtons_DocumentWidget_(
d,
(iMenuItem[]){ { book_Icon " ${menu.save.downloads.open}",
SDLK_s,
KMOD_PRIMARY | KMOD_SHIFT,
"document.save open:1" },
{ download_Icon " " saveToDownloads_Label,
SDLK_s,
KMOD_PRIMARY,
"document.save" } },
2);
}
if (preloadCoverImage_Gempub(d->sourceGempub, d->view.doc)) {
redoLayout_GmDocument(d->view.doc);
updateVisible_DocumentView_(&d->view);
invalidate_DocumentWidget_(d);
}
}
else if (equal_String(d->mod.url, indexPageUrl_Gempub(d->sourceGempub))) {
makeFooterButtons_DocumentWidget_(
d,
(iMenuItem[]){ { format_CStr(book_Icon " %s",
cstr_String(property_Gempub(d->sourceGempub,
title_GempubProperty))),
SDLK_LEFT,
0,
format_CStr("!open url:%s",
cstr_String(coverPageUrl_Gempub(d->sourceGempub))) } },
1);
}
else {
/* Navigation buttons. */
iArray *items = collectNew_Array(sizeof(iMenuItem));
const size_t navIndex = navIndex_Gempub(d->sourceGempub, d->mod.url);
if (navIndex != iInvalidPos) {
if (navIndex < navSize_Gempub(d->sourceGempub) - 1) {
pushBack_Array(
items,
&(iMenuItem){
format_CStr(forwardArrow_Icon " %s",
cstr_String(navLinkLabel_Gempub(d->sourceGempub, navIndex + 1))),
SDLK_RIGHT,
0,
format_CStr("!open url:%s",
cstr_String(navLinkUrl_Gempub(d->sourceGempub, navIndex + 1))) });
}
if (navIndex > 0) {
pushBack_Array(
items,
&(iMenuItem){
format_CStr(backArrow_Icon " %s",
cstr_String(navLinkLabel_Gempub(d->sourceGempub, navIndex - 1))),
SDLK_LEFT,
0,
format_CStr("!open url:%s",
cstr_String(navLinkUrl_Gempub(d->sourceGempub, navIndex - 1))) });
}
else if (!equalCase_String(d->mod.url, indexPageUrl_Gempub(d->sourceGempub))) {
pushBack_Array(
items,
&(iMenuItem){
format_CStr(book_Icon " %s",
cstr_String(property_Gempub(d->sourceGempub, title_GempubProperty))),
SDLK_LEFT,
0,
format_CStr("!open url:%s",
cstr_String(coverPageUrl_Gempub(d->sourceGempub))) });
}
}
if (!isEmpty_Array(items)) {
makeFooterButtons_DocumentWidget_(d, constData_Array(items), size_Array(items));
}
}
/* The temporary "swipein" will display the previous page until the finger is lifted. */
iDocumentWidget *swipeIn = findChild_Widget(swipeParent, "swipein");
if (!swipeIn) {
swipeIn = new_DocumentWidget();
swipeIn->flags |= animationPlaceholder_DocumentWidgetFlag;
setId_Widget(as_Widget(swipeIn), "swipein");
setFlags_Widget(as_Widget(swipeIn),
disabled_WidgetFlag | refChildrenOffset_WidgetFlag |
fixedPosition_WidgetFlag | fixedSize_WidgetFlag, iTrue);
setFlags_Widget(findChild_Widget(as_Widget(swipeIn), "scroll"), hidden_WidgetFlag, iTrue);
swipeIn->widget.rect.pos = windowToInner_Widget(swipeParent, localToWindow_Widget(w, w->rect.pos));
swipeIn->widget.rect.size = d->widget.rect.size;
swipeIn->widget.offsetRef = parent_Widget(w);
/* Use a cached document for the layer underneath. */ {
lock_History(d->mod.history);
iRecentUrl *recent = precedingLocked_History(d->mod.history);
if (recent && recent->cachedResponse) {
setUrl_DocumentWidget_(swipeIn, &recent->url);
updateFromCachedResponse_DocumentWidget_(swipeIn,
recent->normScrollY,
recent->cachedResponse,
recent->cachedDoc);
parseUser_DocumentWidget_(swipeIn);
updateBanner_DocumentWidget_(swipeIn);
if (!isCached && prefs_App()->pinSplit &&
equal_String(d->mod.url, indexPageUrl_Gempub(d->sourceGempub))) {
const iString *navStart = navStartLinkUrl_Gempub(d->sourceGempub);
if (navStart) {
iWindow *win = get_Window();
/* Auto-split to show index and the first navigation link. */
if (numRoots_Window(win) == 2) {
/* This document is showing the index page. */
iRoot *other = otherRoot_Window(win, w->root);
postCommandf_Root(other, "open url:%s", cstr_String(navStart));
if (prefs_App()->pinSplit == 1 && w->root == win->roots[1]) {
/* On the wrong side. */
postCommand_App("ui.split swap:1");
}
}
else {
setUrlAndSource_DocumentWidget(swipeIn, &recent->url,
collectNewCStr_String("text/gemini"),
collect_Block(new_Block(0)));
postCommandf_App(
"open splitmode:1 newtab:%d url:%s", otherRoot_OpenTabFlag, cstr_String(navStart));
}
unlock_History(d->mod.history);
}
addChildPos_Widget(swipeParent, iClob(swipeIn), front_WidgetAddPos);
}
}
if (side == 2) { /* right edge */
if (offset < -get_Window()->pixelRatio * 10) {
int animSpan = 10;
if (!atNewest_History(d->mod.history) && ~flags_Widget(w) & dragged_WidgetFlag) {
iWidget *swipeParent = swipeParent_DocumentWidget_(d);
if (findChild_Widget(swipeParent, "swipeout")) {
return iTrue; /* too fast, previous animation hasn't finished */
}
/* Setup the drag. `d` will be moving with the finger. */
animSpan = 0;
postCommand_Widget(d, "navigate.forward");
setFlags_Widget(w, dragged_WidgetFlag, iTrue);
/* Set up the swipe dummy. */
iDocumentWidget *target = new_DocumentWidget();
target->flags |= animationPlaceholder_DocumentWidgetFlag;
setId_Widget(as_Widget(target), "swipeout");
/* "swipeout" takes `d`'s document and goes underneath. */
target->widget.rect.pos = windowToInner_Widget(swipeParent, localToWindow_Widget(w, w->rect.pos));
target->widget.rect.size = d->widget.rect.size;
setFlags_Widget(as_Widget(target), fixedPosition_WidgetFlag | fixedSize_WidgetFlag, iTrue);
swap_DocumentWidget_(target, d->view.doc, d);
addChildPos_Widget(swipeParent, iClob(target), front_WidgetAddPos);
setFlags_Widget(as_Widget(target), refChildrenOffset_WidgetFlag, iTrue);
as_Widget(target)->offsetRef = parent_Widget(w);
/* Mark it for deletion after animation finishes. */
destroy_Widget(as_Widget(target));
/* The `d` document will now navigate forward and be replaced with a cached
copy. However, if a cached response isn't available, we'll need to show a
blank page. */
setUrlAndSource_DocumentWidget(d,
collectNewCStr_String("about:blank"),
collectNewCStr_String("text/gemini"),
collect_Block(new_Block(0)));
+}
+static void updateDocument_DocumentWidget_(iDocumentWidget *d,
const iGmResponse *response,
iGmDocument *cachedDoc,
const iBool isInitialUpdate) {
return;
that does not try to cache the glyph bitmaps. */
iBool setSource = iTrue;
iString str;
invalidate_DocumentWidget_(d);
if (document_App() == d) {
updateTheme_DocumentWidget_(d);
}
clear_String(&d->sourceMime);
d->sourceTime = response->when;
d->view.drawBufs->flags |= updateTimestampBuf_DrawBufsFlag;
initBlock_String(&str, &response->body); /* Note: Body may be megabytes in size. */
if (isSuccess_GmStatusCode(statusCode)) {
/* Check the MIME type. */
iRangecc charset = range_CStr("utf-8");
enum iSourceFormat docFormat = undefined_SourceFormat;
const iString *mimeStr = collect_String(lower_String(&response->meta)); /* for convenience */
set_String(&d->sourceMime, mimeStr);
iRangecc mime = range_String(mimeStr);
iRangecc seg = iNullRange;
while (nextSplit_Rangecc(mime, ";", &seg)) {
iRangecc param = seg;
trim_Rangecc(¶m);
/* Detect fontpacks even if the server doesn't use the right media type. */
if (isRequestFinished && equal_Rangecc(param, "application/octet-stream")) {
if (detect_FontPack(&response->body)) {
param = range_CStr(mimeType_FontPack);
}
}
if (equal_Rangecc(param, "text/gemini")) {
docFormat = gemini_SourceFormat;
setRange_String(&d->sourceMime, param);
}
else if (equal_Rangecc(param, "text/markdown")) {
docFormat = markdown_SourceFormat;
setRange_String(&d->sourceMime, param);
}
else if (startsWith_Rangecc(param, "text/") ||
equal_Rangecc(param, "application/json") ||
equal_Rangecc(param, "application/x-pem-file") ||
equal_Rangecc(param, "application/pem-certificate-chain")) {
docFormat = plainText_SourceFormat;
setRange_String(&d->sourceMime, param);
}
else if (isRequestFinished && equal_Rangecc(param, "font/ttf")) {
clear_String(&str);
docFormat = gemini_SourceFormat;
setRange_String(&d->sourceMime, param);
format_String(&str, "# TrueType Font\n");
iString *decUrl = collect_String(urlDecode_String(d->mod.url));
iRangecc name = baseName_Path(decUrl);
iBool isInstalled = iFalse;
if (startsWith_String(collect_String(localFilePathFromUrl_String(d->mod.url)),
cstr_String(dataDir_App()))) {
isInstalled = iTrue;
}
appendCStr_String(&str, "## ");
appendRange_String(&str, name);
appendCStr_String(&str, "\n\n");
appendCStr_String(
&str, cstr_Lang(isInstalled ? "truetype.help.installed" : "truetype.help"));
appendCStr_String(&str, "\n");
if (!isInstalled) {
makeFooterButtons_DocumentWidget_(
d,
(iMenuItem[]){
{ add_Icon " ${fontpack.install.ttf}",
SDLK_RETURN,
0,
format_CStr("!fontpack.install ttf:1 name:%s",
cstr_Rangecc(name)) },
{ folder_Icon " ${fontpack.open.fontsdir}",
SDLK_d,
0,
format_CStr("!open url:%s/fonts",
cstrCollect_String(makeFileUrl_String(dataDir_App())))
}
}, 2);
}
}
else if (isRequestFinished &&
(equal_Rangecc(param, "application/zip") ||
(startsWith_Rangecc(param, "application/") &&
endsWithCase_Rangecc(param, "+zip")))) {
clear_String(&str);
docFormat = gemini_SourceFormat;
setRange_String(&d->sourceMime, param);
if (equal_Rangecc(param, mimeType_FontPack)) {
/* Show some information about fontpacks, and set up footer actions. */
iArchive *zip = iClob(new_Archive());
if (openData_Archive(zip, &response->body)) {
iFontPack *fp = new_FontPack();
setUrl_FontPack(fp, d->mod.url);
setStandalone_FontPack(fp, iTrue);
if (loadArchive_FontPack(fp, zip)) {
appendFormat_String(&str, "# " fontpack_Icon "%s\n%s",
cstr_String(id_FontPack(fp).id),
cstrCollect_String(infoText_FontPack(fp)));
}
appendCStr_String(&str, "\n");
appendCStr_String(&str, cstr_Lang("fontpack.help"));
appendCStr_String(&str, "\n");
const iArray *actions = actions_FontPack(fp, iTrue);
makeFooterButtons_DocumentWidget_(d, constData_Array(actions),
size_Array(actions));
delete_FontPack(fp);
}
}
else {
format_String(&str, "# %s\n", zipPageHeading_(param));
appendFormat_String(&str,
cstr_Lang("doc.archive"),
cstr_Rangecc(baseName_Path(d->mod.url)));
appendCStr_String(&str, "\n");
}
appendCStr_String(&str, "\n");
iString *localPath = localFilePathFromUrl_String(d->mod.url);
if (!localPath) {
iString *key = collectNew_String();
toString_Sym(SDLK_s, KMOD_PRIMARY, key);
appendFormat_String(&str, "%s\n\n",
format_CStr(cstr_Lang("error.unsupported.suggestsave"),
cstr_String(key),
saveToDownloads_Label));
}
delete_String(localPath);
if (equalCase_Rangecc(urlScheme_String(d->mod.url), "file")) {
appendFormat_String(&str, "=> %s/ " folder_Icon " ${doc.archive.view}\n",
cstr_String(withSpacesEncoded_String(d->mod.url)));
}
translate_Lang(&str);
}
else if (startsWith_Rangecc(param, "image/") ||
startsWith_Rangecc(param, "audio/")) {
const iBool isAudio = startsWith_Rangecc(param, "audio/");
/* Make a simple document with an image or audio player. */
clear_String(&str);
docFormat = gemini_SourceFormat;
setRange_String(&d->sourceMime, param);
const iGmLinkId imgLinkId = 1; /* there's only the one link */
/* TODO: Do the image loading in `postProcessRequestContent_DocumentWidget_()` */
if ((isAudio && isInitialUpdate) || (!isAudio && isRequestFinished)) {
const char *linkTitle = cstr_Lang(
startsWith_String(mimeStr, "image/") ? "media.untitled.image"
: "media.untitled.audio");
iUrl parts;
init_Url(&parts, d->mod.url);
if (!isEmpty_Range(&parts.path)) {
linkTitle =
baseName_Path(collect_String(newRange_String(parts.path))).start;
}
format_String(&str, "=> %s %s\n",
cstr_String(canonicalUrl_String(d->mod.url)),
linkTitle);
setData_Media(media_GmDocument(d->view.doc),
imgLinkId,
mimeStr,
&response->body,
!isRequestFinished ? partialData_MediaFlag : 0);
redoLayout_GmDocument(d->view.doc);
}
else if (isAudio && !isInitialUpdate) {
/* Update the audio content. */
setData_Media(media_GmDocument(d->view.doc),
imgLinkId,
mimeStr,
&response->body,
!isRequestFinished ? partialData_MediaFlag : 0);
refresh_Widget(d);
setSource = iFalse;
}
else {
clear_String(&str);
}
}
if (flags_Widget(w) & dragged_WidgetFlag) {
setVisualOffset_Widget(w, width_Widget(w) +
width_Widget(d) * offset / size_Root(w->root).x,
animSpan, 0);
else if (startsWith_Rangecc(param, "charset=")) {
charset = (iRangecc){ param.start + 8, param.end };
/* Remove whitespace and quotes. */
trim_Rangecc(&charset);
if (*charset.start == '"' && *charset.end == '"') {
charset.start++;
charset.end--;
}
}
else {
setVisualOffset_Widget(w, offset / 4, animSpan, 0);
}
if (docFormat == undefined_SourceFormat) {
if (isRequestFinished) {
d->flags &= ~drawDownloadCounter_DocumentWidgetFlag;
showErrorPage_DocumentWidget_(d, unsupportedMimeType_GmStatusCode, &response->meta);
deinit_String(&str);
return;
}
d->flags |= drawDownloadCounter_DocumentWidgetFlag;
clear_PtrSet(d->view.invalidRuns);
deinit_String(&str);
return;
}
return iTrue;
}
if (argLabel_Command(cmd, "abort") && flags_Widget(w) & dragged_WidgetFlag) {
setFlags_Widget(w, dragged_WidgetFlag, iFalse);
postCommand_Widget(d, "navigate.back");
/* We must now undo the swap that was done when the drag started. */
/* TODO: Currently not animated! What exactly is the appropriate thing to do here? */
iWidget *swipeParent = swipeParent_DocumentWidget_(d);
iDocumentWidget *swipeOut = findChild_Widget(swipeParent, "swipeout");
swap_DocumentWidget_(d, swipeOut->view.doc, swipeOut);
-// const int visOff = visualOffsetByReference_Widget(w);
w->offsetRef = NULL;
-// setVisualOffset_Widget(w, visOff, 0, 0);
-// setVisualOffset_Widget(w, 0, 150, 0);
setVisualOffset_Widget(w, 0, 0, 0);
/* Make it an overlay instead. */
-// removeChild_Widget(swipeParent, swipeOut);
-// addChildPos_Widget(swipeParent, iClob(swipeOut), back_WidgetAddPos);
-// setupSwipeOverlay_DocumentWidget_(d, as_Widget(swipeOut));
return iTrue;
}
iAssert(~d->flags & animationPlaceholder_DocumentWidgetFlag);
setFlags_Widget(w, dragged_WidgetFlag, iFalse);
setVisualOffset_Widget(w, 0, 250, easeOut_AnimFlag | softer_AnimFlag);
return iTrue;
iWidget *swipeParent = swipeParent_DocumentWidget_(d);
iDocumentWidget *swipeIn = findChild_Widget(swipeParent, "swipein");
d->swipeSpeed = argLabel_Command(cmd, "speed") / gap_UI;
/* "swipe.back" will soon follow. The `d` document will do the actual back navigation,
switching immediately to a cached page. However, if one is not available, we'll need
to show a blank page for a while. */
if (swipeIn) {
if (!argLabel_Command(cmd, "abort")) {
iWidget *swipeParent = swipeParent_DocumentWidget_(d);
/* What was being shown in the `d` document is now being swapped to
the outgoing page animation. */
iDocumentWidget *target = new_DocumentWidget();
target->flags |= animationPlaceholder_DocumentWidgetFlag;
addChildPos_Widget(swipeParent, iClob(target), back_WidgetAddPos);
setId_Widget(as_Widget(target), "swipeout");
setFlags_Widget(as_Widget(target), disabled_WidgetFlag, iTrue);
swap_DocumentWidget_(target, d->view.doc, d);
setUrlAndSource_DocumentWidget(d,
swipeIn->mod.url,
collectNewCStr_String("text/gemini"),
collect_Block(new_Block(0)));
as_Widget(swipeIn)->offsetRef = NULL;
setFormat_GmDocument(d->view.doc, docFormat);
/* Convert the source to UTF-8 if needed. */
if (!equalCase_Rangecc(charset, "utf-8")) {
set_String(&str,
collect_String(decode_Block(&str.chars, cstr_Rangecc(charset))));
}
destroy_Widget(as_Widget(swipeIn));
}
iWidget *swipeParent = swipeParent_DocumentWidget_(d);
iDocumentWidget *target = findChild_Widget(swipeParent, "swipeout");
if (atOldest_History(d->mod.history)) {
setVisualOffset_Widget(w, 0, 100, 0);
if (target) {
destroy_Widget(as_Widget(target)); /* didn't need it after all */
}
return iTrue;
if (cachedDoc) {
replaceDocument_DocumentWidget_(d, cachedDoc);
updateWidth_DocumentView_(&d->view);
}
setupSwipeOverlay_DocumentWidget_(d, as_Widget(target));
destroy_Widget(as_Widget(target)); /* will be actually deleted after animation finishes */
postCommand_Widget(d, "navigate.back");
return iTrue;
else if (setSource) {
setSource_DocumentWidget(d, &str);
}
deinit_String(&str);
}
}
-static iBool cancelRequest_DocumentWidget_(iDocumentWidget *d, iBool postBack) {
+static void fetch_DocumentWidget_(iDocumentWidget *d) {
if (d->request) {
iWidget *w = as_Widget(d);
postCommandf_Root(w->root,
"document.request.cancelled doc:%p url:%s", d, cstr_String(d->mod.url));
iReleasePtr(&d->request);
if (d->state != ready_RequestState) {
d->state = ready_RequestState;
if (postBack) {
postCommand_Root(w->root, "navigate.back");
}
}
updateFetchProgress_DocumentWidget_(d);
return iTrue;
iRelease(d->request);
d->request = NULL;
}
"document.request.started doc:%p url:%s",
d,
cstr_String(d->mod.url));
}
-static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) {
if (d->flags & animationPlaceholder_DocumentWidgetFlag) {
return iFalse;
}
/* When any tab changes its document URL, update the open link indicators. */
if (updateOpenURLs_GmDocument(d->view.doc)) {
invalidate_DocumentWidget_(d);
refresh_Widget(d);
}
return iFalse;
updateVisitedLinks_GmDocument(d->view.doc);
invalidateVisibleLinks_DocumentView_(&d->view);
return iFalse;
Periodic
makes direct dispatch to here */ {-// printf("%u: document.render\n", SDL_GetTicks());
if (SDL_GetTicks() - d->view.drawBufs->lastRenderTime > 150) {
remove_Periodic(periodic_App(), d);
/* Scrolling has stopped, begin filling up the buffer. */
if (d->view.visBuf->buffers[0].texture) {
addTicker_App(prerender_DocumentWidget_, d);
}
}
return iTrue;
equal_Command(cmd, "keyroot.changed")) {
if (equal_Command(cmd, "font.changed")) {
invalidateCachedLayout_History(d->mod.history);
}
/* Alt/Option key may be involved in window size changes. */
setLinkNumberMode_DocumentWidget_(d, iFalse);
d->phoneToolbar = findWidget_App("toolbar");
const iBool keepCenter = equal_Command(cmd, "font.changed");
updateDocumentWidthRetainingScrollPosition_DocumentView_(&d->view, keepCenter);
d->view.drawBufs->flags |= updateSideBuf_DrawBufsFlag;
updateVisible_DocumentView_(&d->view);
invalidate_DocumentWidget_(d);
dealloc_VisBuf(d->view.visBuf);
updateWindowTitle_DocumentWidget_(d);
showOrHidePinningIndicator_DocumentWidget_(d);
refresh_Widget(w);
if (d->flags & showLinkNumbers_DocumentWidgetFlag) {
setLinkNumberMode_DocumentWidget_(d, iFalse);
invalidateVisibleLinks_DocumentView_(&d->view);
refresh_Widget(w);
}
return iFalse;
return iFalse;
invalidateTheme_History(d->mod.history); /* forget cached color palettes */
if (document_App() == d) {
updateTheme_DocumentWidget_(d);
updateVisible_DocumentView_(&d->view);
updateTrust_DocumentWidget_(d, NULL);
d->view.drawBufs->flags |= updateSideBuf_DrawBufsFlag;
invalidate_DocumentWidget_(d);
refresh_Widget(w);
}
if (argLabel_Command(cmd, "redo")) {
redoLayout_GmDocument(d->view.doc);
}
updateSize_DocumentWidget(d);
+static void updateTrust_DocumentWidget_(iDocumentWidget *d, const iGmResponse *response) {
d->certFlags = response->certFlags;
d->certExpiry = response->certValidUntil;
set_Block(d->certFingerprint, &response->certFingerprint);
set_String(d->certSubject, &response->certSubject);
}
postCommand_App("document.update.pin"); /* prefs value not set yet */
return iFalse;
setFlags_Widget(as_Widget(lock), disabled_WidgetFlag, iTrue);
updateTextCStr_LabelWidget(lock, gray50_ColorEscape openLock_Icon);
return;
}
showOrHidePinningIndicator_DocumentWidget_(d);
return iFalse;
~d->certFlags & trusted_GmCertFlag) {
updateTextCStr_LabelWidget(lock, red_ColorEscape warning_Icon);
}
setLinkNumberMode_DocumentWidget_(d, iFalse);
if (cmp_String(id_Widget(w), suffixPtr_Command(cmd, "id")) == 0) {
/* Set palette for our document. */
updateTheme_DocumentWidget_(d);
updateTrust_DocumentWidget_(d, NULL);
updateSize_DocumentWidget(d);
showOrHidePinningIndicator_DocumentWidget_(d);
updateFetchProgress_DocumentWidget_(d);
updateHover_Window(window_Widget(w));
}
init_Anim(&d->view.sideOpacity, 0);
init_Anim(&d->view.altTextOpacity, 0);
updateSideOpacity_DocumentView_(&d->view, iFalse);
updateWindowTitle_DocumentWidget_(d);
allocVisBuffer_DocumentView_(&d->view);
animateMedia_DocumentWidget_(d);
remove_Periodic(periodic_App(), d);
removeTicker_App(prerender_DocumentWidget_, d);
return iFalse;
updateTextCStr_LabelWidget(lock, isDarkMode ? orange_ColorEscape warning_Icon
: black_ColorEscape warning_Icon);
}
/* Space for tab buttons has changed. */
updateWindowTitle_DocumentWidget_(d);
return iFalse;
updateTextCStr_LabelWidget(lock, green_ColorEscape closedLock_Icon);
}
/* Touch selection mode. */
if (!arg_Command(cmd)) {
d->selectMark = iNullRange;
setFlags_Widget(w, touchDrag_WidgetFlag, iFalse);
setFadeEnabled_ScrollWidget(d->scroll, iTrue);
}
else {
setFlags_Widget(w, touchDrag_WidgetFlag, iTrue);
d->flags |= movingSelectMarkEnd_DocumentWidgetFlag |
selectWords_DocumentWidgetFlag; /* finger-based selection is imprecise */
d->flags &= ~selectLines_DocumentWidgetFlag;
setFadeEnabled_ScrollWidget(d->scroll, iFalse);
d->selectMark = sourceLoc_DocumentView_(&d->view, d->contextPos);
extendRange_Rangecc(&d->selectMark, range_String(source_GmDocument(d->view.doc)),
word_RangeExtension | bothStartAndEnd_RangeExtension);
d->initialSelectMark = d->selectMark;
}
return iTrue;
+}
+static void parseUser_DocumentWidget_(iDocumentWidget *d) {
+}
+static void cacheRunGlyphs_(void *data, const iGmRun *run) {
cache_Text(run->font, run->text);
}
const char *unchecked = red_ColorEscape "\u2610";
const char *checked = green_ColorEscape "\u2611";
const iBool haveFingerprint = (d->certFlags & haveFingerprint_GmCertFlag) != 0;
const int requiredForTrust = (available_GmCertFlag | haveFingerprint_GmCertFlag |
timeVerified_GmCertFlag);
const iBool canTrust = ~d->certFlags & trusted_GmCertFlag &&
((d->certFlags & requiredForTrust) == requiredForTrust);
const iRecentUrl *recent = constMostRecentUrl_History(d->mod.history);
const iString *meta = &d->sourceMime;
if (recent && recent->cachedResponse) {
meta = &recent->cachedResponse->meta;
}
iString *msg = collectNew_String();
if (isEmpty_String(&d->sourceHeader)) {
appendFormat_String(msg,
"%s\n%s\n",
cstr_String(meta),
formatCStrs_Lang("num.bytes.n", size_Block(&d->sourceContent)));
+}
+static void cacheDocumentGlyphs_DocumentWidget_(const iDocumentWidget *d) {
~d->flags & animationPlaceholder_DocumentWidgetFlag) {
/* Just cache the top of the document, since this is what we usually need. */
int maxY = height_Widget(&d->widget) * 2;
if (maxY == 0) {
maxY = size_GmDocument(d->view.doc).y;
}
else {
appendFormat_String(msg, "%s\n", cstr_String(&d->sourceHeader));
if (size_Block(&d->sourceContent)) {
appendFormat_String(
msg, "%s\n", formatCStrs_Lang("num.bytes.n", size_Block(&d->sourceContent)));
render_GmDocument(d->view.doc, (iRangei){ 0, maxY }, cacheRunGlyphs_, NULL);
+}
+static void addBannerWarnings_DocumentWidget_(iDocumentWidget *d) {
numItems_Banner(d->banner) == 0) {
iString *title = collectNewCStr_String(cstr_Lang("dlg.certwarn.title"));
iString *str = collectNew_String();
if (certFlags & timeVerified_GmCertFlag && certFlags & domainVerified_GmCertFlag) {
iUrl parts;
init_Url(&parts, d->mod.url);
const iTime oldUntil =
domainValidUntil_GmCerts(certs_App(), parts.host, port_Url(&parts));
iDate exp;
init_Date(&exp, &oldUntil);
iTime now;
initCurrent_Time(&now);
const int days = secondsSince_Time(&oldUntil, &now) / 3600 / 24;
if (days <= 30) {
appendCStr_String(str,
format_CStr(cstrCount_Lang("dlg.certwarn.mayberenewed.n", days),
cstrCollect_String(format_Date(&exp, "%Y-%m-%d")),
days));
}
else {
appendCStr_String(str, cstr_Lang("dlg.certwarn.different"));
}
}
/* TODO: On mobile, omit the CA status. */
appendFormat_String(
msg,
"\n%s${pageinfo.cert.status}\n"
"%s%s %s\n"
"%s%s %s%s\n"
"%s%s %s (%04d-%02d-%02d %02d:%02d:%02d)\n"
"%s%s %s",
uiHeading_ColorEscape,
d->certFlags & authorityVerified_GmCertFlag ? checked
: uiTextAction_ColorEscape "\u2610",
uiText_ColorEscape,
d->certFlags & authorityVerified_GmCertFlag ? "${pageinfo.cert.ca.verified}"
: "${pageinfo.cert.ca.unverified}",
d->certFlags & domainVerified_GmCertFlag ? checked : unchecked,
uiText_ColorEscape,
d->certFlags & domainVerified_GmCertFlag ? "${pageinfo.domain.match}"
: "${pageinfo.domain.mismatch}",
~d->certFlags & domainVerified_GmCertFlag
? format_CStr(" (%s)", cstr_String(d->certSubject))
: "",
d->certFlags & timeVerified_GmCertFlag ? checked : unchecked,
uiText_ColorEscape,
d->certFlags & timeVerified_GmCertFlag ? "${pageinfo.cert.notexpired}"
: "${pageinfo.cert.expired}",
d->certExpiry.year,
d->certExpiry.month,
d->certExpiry.day,
d->certExpiry.hour,
d->certExpiry.minute,
d->certExpiry.second,
d->certFlags & trusted_GmCertFlag ? checked : unchecked,
uiText_ColorEscape,
d->certFlags & trusted_GmCertFlag ? "${pageinfo.cert.trusted}"
: "${pageinfo.cert.untrusted}");
setFocus_Widget(NULL);
iArray *items = new_Array(sizeof(iMenuItem));
if (canTrust) {
pushBack_Array(items,
&(iMenuItem){ uiTextCaution_ColorEscape "${dlg.cert.trust}",
SDLK_u,
KMOD_PRIMARY | KMOD_SHIFT,
"server.trustcert" });
}
if (haveFingerprint) {
pushBack_Array(items, &(iMenuItem){ "${dlg.cert.fingerprint}", 0, 0, "server.copycert" });
else if (certFlags & domainVerified_GmCertFlag) {
setCStr_String(title, get_GmError(tlsServerCertificateExpired_GmStatusCode)->title);
appendFormat_String(str, cstr_Lang("dlg.certwarn.expired"),
cstrCollect_String(format_Date(&d->certExpiry, "%Y-%m-%d")));
}
if (!isEmpty_Array(items)) {
pushBack_Array(items, &(iMenuItem){ "---", 0, 0, 0 });
else if (certFlags & timeVerified_GmCertFlag) {
appendFormat_String(str, cstr_Lang("dlg.certwarn.domain"),
cstr_String(d->certSubject));
}
pushBack_Array(items, &(iMenuItem){ "${close}", 0, 0, "message.ok" });
iWidget *dlg = makeQuestion_Widget(uiHeading_ColorEscape "${heading.pageinfo}",
cstr_String(msg),
data_Array(items),
size_Array(items));
delete_Array(items);
/* Enforce a minimum size. */
-// iWidget *sizer = new_Widget();
-// setFixedSize_Widget(sizer, init_I2(gap_UI * 65, 1));
-// addChildFlags_Widget(dlg, iClob(sizer), frameless_WidgetFlag);
-// setFlags_Widget(dlg, centerHorizontal_WidgetFlag, iFalse);
if (deviceType_App() == desktop_AppDeviceType) {
const iWidget *lockButton = findWidget_Root("navbar.lock");
setPos_Widget(dlg, windowToLocal_Widget(dlg, bottomLeft_Rect(bounds_Widget(lockButton))));
else {
appendCStr_String(str, cstr_Lang("dlg.certwarn.domain.expired"));
}
arrange_Widget(dlg);
addAction_Widget(dlg, SDLK_ESCAPE, 0, "message.ok");
addAction_Widget(dlg, SDLK_SPACE, 0, "message.ok");
return iTrue;
add_Banner(d->banner, warning_BannerType, none_GmStatusCode, title, str);
}
const iRangecc host = urlHost_String(d->mod.url);
const uint16_t port = urlPort_String(d->mod.url);
if (!isEmpty_Block(d->certFingerprint) && !isEmpty_Range(&host)) {
iTime expiry;
initCurrent_Time(&expiry);
iTime oneHour; /* One hour is long enough for a single visit (?). */
initSeconds_Time(&oneHour, 3600);
add_Time(&expiry, &oneHour);
iDate expDate;
init_Date(&expDate, &expiry);
setTrusted_GmCerts(certs_App(), host, port, d->certFingerprint, &expDate);
postCommand_Widget(w, "navigate.reload");
}
return iTrue;
value_SiteSpec(collectNewRange_String(urlRoot_String(d->mod.url)),
dismissWarnings_SiteSpecKey) |
(!prefs_App()->warnAboutMissingGlyphs ? missingGlyphs_GmDocumentWarning : 0);
add_Banner(d->banner, warning_BannerType, missingGlyphs_GmStatusCode, NULL, NULL);
/* TODO: List one or more of the missing characters and/or their Unicode blocks? */
}
const iRangecc host = urlHost_String(d->mod.url);
const uint16_t port = urlPort_String(d->mod.url);
if (!isEmpty_Block(d->certFingerprint) && !isEmpty_Range(&host)) {
setTrusted_GmCerts(certs_App(), host, port, d->certFingerprint, &d->certExpiry);
postCommand_Widget(w, "navigate.reload");
add_Banner(d->banner, warning_BannerType, ansiEscapes_GmStatusCode, NULL, NULL);
+}
+static void updateFromCachedResponse_DocumentWidget_(iDocumentWidget *d, float normScrollY,
const iGmResponse *resp, iGmDocument *cachedDoc) {
+// iAssert(width_Widget(d) > 0); /* must be laid out by now */
d->initNormScrollY = normScrollY;
/* Use the cached response data. */
updateTrust_DocumentWidget_(d, resp);
d->sourceTime = resp->when;
d->sourceStatus = success_GmStatusCode;
format_String(&d->sourceHeader, cstr_Lang("pageinfo.header.cached"));
set_Block(&d->sourceContent, &resp->body);
if (!cachedDoc) {
updateWidthAndRedoLayout_DocumentView_(&d->view);
}
return iTrue;
SDL_SetClipboardText(cstrCollect_String(hexEncode_Block(d->certFingerprint)));
return iTrue;
updateDocument_DocumentWidget_(d, resp, cachedDoc, iTrue);
clear_Banner(d->banner);
updateBanner_DocumentWidget_(d);
addBannerWarnings_DocumentWidget_(d);
}
iString *copied;
if (d->selectMark.start) {
iRangecc mark = d->selectMark;
if (mark.start > mark.end) {
iSwap(const char *, mark.start, mark.end);
}
copied = newRange_String(mark);
}
else {
/* Full document. */
copied = copy_String(source_GmDocument(d->view.doc));
}
SDL_SetClipboardText(cstr_String(copied));
delete_String(copied);
if (flags_Widget(w) & touchDrag_WidgetFlag) {
postCommand_Widget(w, "document.select arg:0");
as_Widget(d)->root, "document.changed doc:%p url:%s", d, cstr_String(d->mod.url));
+}
+static iBool updateFromHistory_DocumentWidget_(iDocumentWidget *d) {
iChangeFlags(d->flags,
openedFromSidebar_DocumentWidgetFlag,
recent->flags.openedFromSidebar);
updateFromCachedResponse_DocumentWidget_(
d, recent->normScrollY, recent->cachedResponse, recent->cachedDoc);
if (!recent->cachedDoc) {
/* We have a cached copy now. */
setCachedDocument_History(d->mod.history, d->view.doc, iFalse);
}
return iTrue;
}
if (d->contextLink) {
SDL_SetClipboardText(cstr_String(canonicalUrl_String(absoluteUrl_String(
d->mod.url, linkUrl_GmDocument(d->view.doc, d->contextLink->linkId)))));
}
else {
SDL_SetClipboardText(cstr_String(canonicalUrl_String(d->mod.url)));
}
return iTrue;
fetch_DocumentWidget_(d);
}
if (d->contextLink) {
const iGmLinkId linkId = d->contextLink->linkId;
setUrl_Media(media_GmDocument(d->view.doc),
linkId,
download_MediaType,
linkUrl_GmDocument(d->view.doc, linkId));
requestMedia_DocumentWidget_(d, linkId, iFalse /* no filters */);
redoLayout_GmDocument(d->view.doc); /* inline downloader becomes visible */
updateVisible_DocumentView_(&d->view);
invalidate_DocumentWidget_(d);
refresh_Widget(w);
}
return iTrue;
/* Retain scroll position in refetched content as well. */
d->initNormScrollY = recent->normScrollY;
}
postCommandf_Root(w->root,
/* use the `redirect:1` argument to cause the input query URL to be
replaced in History; we don't want to navigate onto it */
"open redirect:1 url:%s",
cstrCollect_String(makeQueryUrl_DocumentWidget_
(d, collect_String(suffix_Command(cmd, "value")))));
return iTrue;
+}
+static void refreshWhileScrolling_DocumentWidget_(iAny *ptr) {
for (const iGmRun *r = view->animWideRunRange.start; r != view->animWideRunRange.end; r++) {
insert_PtrSet(view->invalidRuns, r);
}
}
equal_Rangecc(range_Command(cmd, "id"), "document.input.submit") && document_App() == d) {
postCommand_Root(get_Root(), "navigate.back");
return iTrue;
view->animWideRunId = 0;
}
id_GmRequest(d->request) == argU32Label_Command(cmd, "reqid")) {
-// set_Block(&d->sourceContent, &lockResponse_GmRequest(d->request)->body);
-// unlockResponse_GmRequest(d->request);
if (document_App() == d) {
updateFetchProgress_DocumentWidget_(d);
}
checkResponse_DocumentWidget_(d);
set_Atomic(&d->isRequestUpdated, iFalse); /* ready to be notified again */
return iFalse;
addTicker_App(refreshWhileScrolling_DocumentWidget_, d);
}
id_GmRequest(d->request) == argU32Label_Command(cmd, "reqid")) {
d->flags &= ~fromCache_DocumentWidgetFlag;
set_Block(&d->sourceContent, body_GmRequest(d->request));
if (!isSuccess_GmStatusCode(status_GmRequest(d->request))) {
/* TODO: Why is this here? Can it be removed? */
format_String(&d->sourceHeader,
"%s%s",
humanReadableStatusCode_(status_GmRequest(d->request)),
cstr_String(meta_GmRequest(d->request)));
}
updateFetchProgress_DocumentWidget_(d);
checkResponse_DocumentWidget_(d);
if (category_GmStatusCode(status_GmRequest(d->request)) == categorySuccess_GmStatusCode) {
init_Anim(&d->view.scrollY.pos, d->initNormScrollY * pageHeight_DocumentView_(&d->view));
/* TODO: unless user already scrolled! */
}
addBannerWarnings_DocumentWidget_(d);
iChangeFlags(d->flags,
urlChanged_DocumentWidgetFlag | drawDownloadCounter_DocumentWidgetFlag,
iFalse);
d->state = ready_RequestState;
postProcessRequestContent_DocumentWidget_(d, iFalse);
/* The response may be cached. */
if (d->request) {
iAssert(~d->flags & animationPlaceholder_DocumentWidgetFlag);
iAssert(~d->flags & fromCache_DocumentWidgetFlag);
if (!equal_Rangecc(urlScheme_String(d->mod.url), "about") &&
(startsWithCase_String(meta_GmRequest(d->request), "text/") ||
!cmp_String(&d->sourceMime, mimeType_Gempub))) {
setCachedResponse_History(d->mod.history, lockResponse_GmRequest(d->request));
unlockResponse_GmRequest(d->request);
}
}
iReleasePtr(&d->request);
updateVisible_DocumentView_(&d->view);
d->view.drawBufs->flags |= updateSideBuf_DrawBufsFlag;
postCommandf_Root(w->root,
"document.changed doc:%p status:%d url:%s",
d,
d->sourceStatus,
cstr_String(d->mod.url));
/* Check for a pending goto. */
if (!isEmpty_String(&d->pendingGotoHeading)) {
scrollToHeading_DocumentView_(&d->view, cstr_String(&d->pendingGotoHeading));
clear_String(&d->pendingGotoHeading);
}
cacheDocumentGlyphs_DocumentWidget_(d);
return iFalse;
iChangeFlags(d->flags, noHoverWhileScrolling_DocumentWidgetFlag, iFalse);
updateHover_DocumentView_(view, mouseCoord_Window(get_Window(), 0));
}
if (!d->translation) {
d->translation = new_Translation(d);
if (isUsingPanelLayout_Mobile()) {
const iRect safe = safeRect_Root(w->root);
d->translation->dlg->rect.pos = windowToLocal_Widget(w, zero_I2());
d->translation->dlg->rect.size = safe.size;
}
}
return iTrue;
+}
+static void scrollBegan_DocumentWidget_(iAnyObject *any, int offset, uint32_t duration) {
setLinkNumberMode_DocumentWidget_(d, iFalse);
invalidateVisibleLinks_DocumentView_(&d->view);
}
const iBool wasHandled = handleCommand_Translation(d->translation, cmd);
if (isFinished_Translation(d->translation)) {
delete_Translation(d->translation);
d->translation = NULL;
const float normPos = normScrollPos_DocumentView_(&d->view);
if (prefs_App()->hideToolbarOnScroll && iAbs(offset) > 5 && normPos >= 0) {
showToolbar_Root(as_Widget(d)->root, offset < 0);
}
return wasHandled;
}
if (findChild_Widget(root_Widget(w), "upload")) {
return iTrue; /* already open */
}
const iBool isGemini = equalCase_Rangecc(urlScheme_String(d->mod.url), "gemini");
if (isGemini || equalCase_Rangecc(urlScheme_String(d->mod.url), "titan")) {
iUploadWidget *upload = new_UploadWidget();
setUrl_UploadWidget(upload, d->mod.url);
setResponseViewer_UploadWidget(upload, d);
addChild_Widget(get_Root()->widget, iClob(upload));
setupSheetTransition_Mobile(as_Widget(upload), iTrue);
postRefresh_App();
}
return iTrue;
iChangeFlags(d->flags, noHoverWhileScrolling_DocumentWidgetFlag, iTrue);
addTicker_App(refreshWhileScrolling_DocumentWidget_, d);
}
return handleMediaCommand_DocumentWidget_(d, cmd);
+}
+static void togglePreFold_DocumentWidget_(iDocumentWidget *d, uint16_t preId) {
+}
+static iString *makeQueryUrl_DocumentWidget_(const iDocumentWidget *d,
const iString *userEnteredText) {
remove_Block(&url->chars, qPos, iInvalidSize);
}
/* When one media player starts, pause the others that may be playing. */
const iPlayer *startedPlr = pointerLabel_Command(cmd, "player");
const iMedia * media = media_GmDocument(d->view.doc);
const size_t num = numAudio_Media(media);
for (size_t id = 1; id <= num; id++) {
iPlayer *plr = audioPlayer_Media(media, (iMediaId){ audio_MediaType, id });
if (plr != startedPlr) {
setPaused_Player(plr, iTrue);
}
trimEnd_String(cleaned); /* autocorrect may insert an extra space */
if (isEmpty_String(cleaned)) {
set_String(cleaned, userEnteredText); /* user wanted just spaces? */
}
}
updateMedia_DocumentWidget_(d);
return iFalse;
+}
+static void inputQueryValidator_(iInputWidget *input, void *context) {
iString *trunc = copy_String(text_InputWidget(input));
truncate_String(trunc, 1024);
setText_InputWidget(input, trunc);
delete_String(trunc);
}
if (cancelRequest_DocumentWidget_(d, iTrue /* navigate back */)) {
return iTrue;
}
avail < 0 ? uiTextCaution_ColorId :
avail < 128 ? uiTextStrong_ColorId
: uiTextDim_ColorId);
+}
+static const char *humanReadableStatusCode_(enum iGmStatusCode code) {
return "";
}
const iGmLinkId linkId = argLabel_Command(cmd, "link");
const iMediaRequest *media = findMediaRequest_DocumentWidget_(d, linkId);
if (media) {
saveToDownloads_(url_GmRequest(media->req), meta_GmRequest(media->req),
body_GmRequest(media->req), iTrue);
}
+}
+static void checkResponse_DocumentWidget_(iDocumentWidget *d) {
return;
}
if (d->request) {
makeSimpleMessage_Widget(uiTextCaution_ColorEscape "${heading.save.incomplete}",
"${dlg.save.incomplete}");
}
else if (!isEmpty_Block(&d->sourceContent)) {
const iBool doOpen = argLabel_Command(cmd, "open");
const iString *savePath = saveToDownloads_(d->mod.url, &d->sourceMime,
&d->sourceContent, !doOpen);
if (!isEmpty_String(savePath) && doOpen) {
postCommandf_Root(
w->root, "!open url:%s", cstrCollect_String(makeFileUrl_String(savePath)));
}
}
return iTrue;
return;
}
d->initNormScrollY = normScrollPos_DocumentView_(&d->view);
if (equalCase_Rangecc(urlScheme_String(d->mod.url), "titan")) {
/* Reopen so the Upload dialog gets shown. */
postCommandf_App("open url:%s", cstr_String(d->mod.url));
return iTrue;
d->state = receivedPartialResponse_RequestState;
d->flags &= ~fromCache_DocumentWidgetFlag;
updateTrust_DocumentWidget_(d, resp);
if (isSuccess_GmStatusCode(statusCode)) {
clear_Banner(d->banner);
updateTheme_DocumentWidget_(d);
}
fetch_DocumentWidget_(d);
return iTrue;
if (argLabel_Command(cmd, "release")) {
setLinkNumberMode_DocumentWidget_(d, iFalse);
if (~d->certFlags & trusted_GmCertFlag &&
isSuccess_GmStatusCode(statusCode) &&
equalCase_Rangecc(urlScheme_String(d->mod.url), "gemini")) {
statusCode = tlsServerCertificateNotVerified_GmStatusCode;
}
else if (argLabel_Command(cmd, "more")) {
if (d->flags & showLinkNumbers_DocumentWidgetFlag &&
d->ordinalMode == homeRow_DocumentLinkOrdinalMode) {
const size_t numKeys = iElemCount(homeRowKeys_);
const iGmRun *last = lastVisibleLink_DocumentView_(&d->view);
if (!last) {
d->ordinalBase = 0;
init_Anim(&d->view.sideOpacity, 0);
init_Anim(&d->view.altTextOpacity, 0);
format_String(&d->sourceHeader,
"%s%s",
humanReadableStatusCode_(statusCode),
isEmpty_String(&resp->meta) && !isSuccess_GmStatusCode(statusCode)
? get_GmError(statusCode)->title
: cstr_String(&resp->meta));
d->sourceStatus = statusCode;
switch (category_GmStatusCode(statusCode)) {
case categoryInput_GmStatusCode: {
/* Let the navigation history know that we have been to this URL even though
it is only displayed as an input dialog. */
visitUrl_Visited(visited_App(), d->mod.url, transient_VisitedUrlFlag);
iUrl parts;
init_Url(&parts, d->mod.url);
iWidget *dlg = makeValueInput_Widget(
as_Widget(d),
NULL,
format_CStr(uiHeading_ColorEscape "%s", cstr_Rangecc(parts.host)),
isEmpty_String(&resp->meta)
? format_CStr(cstr_Lang("dlg.input.prompt"), cstr_Rangecc(parts.path))
: cstr_String(&resp->meta),
uiTextCaution_ColorEscape "${dlg.input.send}",
format_CStr("!document.input.submit doc:%p", d));
iWidget *buttons = findChild_Widget(dlg, "dialogbuttons");
iLabelWidget *lineBreak = NULL;
if (statusCode != sensitiveInput_GmStatusCode) {
/* The line break and URL length counters are positioned differently on mobile.
There is no line breaks in sensitive input. */
if (deviceType_App() == desktop_AppDeviceType) {
iString *keyStr = collectNew_String();
toString_Sym(SDLK_RETURN,
lineBreakKeyMod_ReturnKeyBehavior(prefs_App()->returnKey),
keyStr);
lineBreak = new_LabelWidget(
format_CStr("${dlg.input.linebreak}" uiTextAction_ColorEscape " %s",
cstr_String(keyStr)),
NULL);
insertChildAfter_Widget(buttons, iClob(lineBreak), 0);
}
else {
+#if !defined (iPlatformAppleMobile)
lineBreak = new_LabelWidget("${dlg.input.linebreak}", "text.insert arg:10");
+#endif
}
if (lineBreak) {
setFlags_Widget(as_Widget(lineBreak), frameless_WidgetFlag, iTrue);
setTextColor_LabelWidget(lineBreak, uiTextDim_ColorId);
}
}
iWidget *counter = (iWidget *) new_LabelWidget("", NULL);
setId_Widget(counter, "valueinput.counter");
setFlags_Widget(counter, frameless_WidgetFlag | resizeToParentHeight_WidgetFlag, iTrue);
if (deviceType_App() == desktop_AppDeviceType) {
addChildPos_Widget(buttons, iClob(counter), front_WidgetAddPos);
}
else {
d->ordinalBase += numKeys;
if (visibleLinkOrdinal_DocumentView_(&d->view, last->linkId) < d->ordinalBase) {
d->ordinalBase = 0;
insertChildAfter_Widget(buttons, iClob(counter), 1);
}
if (lineBreak && deviceType_App() != desktop_AppDeviceType) {
addChildPos_Widget(buttons, iClob(lineBreak), front_WidgetAddPos);
}
/* Menu for additional actions, past entries. */ {
iMenuItem items[] = { { "${menu.input.precedingline}",
SDLK_v,
KMOD_PRIMARY | KMOD_SHIFT,
format_CStr("!valueinput.set ptr:%p text:%s",
buttons,
cstr_String(&d->linePrecedingLink)) } };
iLabelWidget *menu = makeMenuButton_LabelWidget(midEllipsis_Icon, items, 1);
if (deviceType_App() == desktop_AppDeviceType) {
addChildPos_Widget(buttons, iClob(menu), front_WidgetAddPos);
}
else {
insertChildAfterFlags_Widget(buttons, iClob(menu), 0,
frameless_WidgetFlag | noBackground_WidgetFlag);
setFont_LabelWidget(menu, font_LabelWidget((iLabelWidget *) lastChild_Widget(buttons)));
setTextColor_LabelWidget(menu, uiTextAction_ColorId);
}
}
setValidator_InputWidget(findChild_Widget(dlg, "input"), inputQueryValidator_, d);
setSensitiveContent_InputWidget(findChild_Widget(dlg, "input"),
statusCode == sensitiveInput_GmStatusCode);
if (document_App() != d) {
postCommandf_App("tabs.switch page:%p", d);
}
else {
updateTheme_DocumentWidget_(d);
}
break;
}
else if (~d->flags & showLinkNumbers_DocumentWidgetFlag) {
d->ordinalMode = homeRow_DocumentLinkOrdinalMode;
d->ordinalBase = 0;
setLinkNumberMode_DocumentWidget_(d, iTrue);
}
}
else {
d->ordinalMode = arg_Command(cmd);
d->ordinalBase = 0;
setLinkNumberMode_DocumentWidget_(d, iTrue);
iChangeFlags(d->flags, setHoverViaKeys_DocumentWidgetFlag,
argLabel_Command(cmd, "hover") != 0);
iChangeFlags(d->flags, newTabViaHomeKeys_DocumentWidgetFlag,
argLabel_Command(cmd, "newtab") != 0);
case categorySuccess_GmStatusCode:
if (d->flags & urlChanged_DocumentWidgetFlag) {
/* Keep scroll position when reloading the same page. */
resetScroll_DocumentView_(&d->view);
}
d->view.scrollY.pullActionTriggered = 0;
pauseAllPlayers_Media(media_GmDocument(d->view.doc), iTrue);
iReleasePtr(&d->view.doc); /* new content incoming */
delete_Gempub(d->sourceGempub);
d->sourceGempub = NULL;
destroy_Widget(d->footerButtons);
d->footerButtons = NULL;
d->view.doc = new_GmDocument();
resetWideRuns_DocumentView_(&d->view);
updateDocument_DocumentWidget_(d, resp, NULL, iTrue);
break;
case categoryRedirect_GmStatusCode:
if (isEmpty_String(&resp->meta)) {
showErrorPage_DocumentWidget_(d, invalidRedirect_GmStatusCode, NULL);
}
else {
/* Only accept redirects that use gemini scheme. */
const iString *dstUrl = absoluteUrl_String(d->mod.url, &resp->meta);
const iRangecc srcScheme = urlScheme_String(d->mod.url);
const iRangecc dstScheme = urlScheme_String(dstUrl);
if (d->redirectCount >= 5) {
showErrorPage_DocumentWidget_(d, tooManyRedirects_GmStatusCode, dstUrl);
}
/* Redirects with the same scheme are automatic, and switching automatically
between "gemini" and "titan" is allowed. */
else if (equalRangeCase_Rangecc(dstScheme, srcScheme) ||
(equalCase_Rangecc(srcScheme, "titan") &&
equalCase_Rangecc(dstScheme, "gemini")) ||
(equalCase_Rangecc(srcScheme, "gemini") &&
equalCase_Rangecc(dstScheme, "titan"))) {
visitUrl_Visited(visited_App(), d->mod.url, transient_VisitedUrlFlag);
postCommandf_Root(as_Widget(d)->root,
"open doc:%p redirect:%d url:%s", d, d->redirectCount + 1, cstr_String(dstUrl));
}
else {
/* Scheme changes must be manually approved. */
showErrorPage_DocumentWidget_(d, schemeChangeRedirect_GmStatusCode, dstUrl);
}
unlockResponse_GmRequest(d->request);
iReleasePtr(&d->request);
}
break;
default:
if (isDefined_GmError(statusCode)) {
showErrorPage_DocumentWidget_(d, statusCode, &resp->meta);
}
else if (category_GmStatusCode(statusCode) ==
categoryTemporaryFailure_GmStatusCode) {
showErrorPage_DocumentWidget_(
d, temporaryFailure_GmStatusCode, &resp->meta);
}
else if (category_GmStatusCode(statusCode) ==
categoryPermanentFailure_GmStatusCode) {
showErrorPage_DocumentWidget_(
d, permanentFailure_GmStatusCode, &resp->meta);
}
else {
showErrorPage_DocumentWidget_(d, unknownStatusCode_GmStatusCode, &resp->meta);
}
break;
}
invalidateVisibleLinks_DocumentView_(&d->view);
refresh_Widget(d);
return iTrue;
}
if (d->request) {
postCommandf_Root(w->root,
"document.request.cancelled doc:%p url:%s", d, cstr_String(d->mod.url));
iReleasePtr(&d->request);
updateFetchProgress_DocumentWidget_(d);
d->flags &= ~fromCache_DocumentWidgetFlag;
switch (category_GmStatusCode(statusCode)) {
case categorySuccess_GmStatusCode:
/* More content available. */
updateDocument_DocumentWidget_(d, resp, NULL, iFalse);
break;
default:
break;
}
goBack_History(d->mod.history);
return iTrue;
goForward_History(d->mod.history);
return iTrue;
}
iUrl parts;
init_Url(&parts, d->mod.url);
/* Remove the last path segment. */
if (size_Range(&parts.path) > 1) {
if (parts.path.end[-1] == '/') {
parts.path.end--;
}
while (parts.path.end > parts.path.start) {
if (parts.path.end[-1] == '/') break;
parts.path.end--;
}
postCommandf_Root(w->root,
"open url:%s",
cstr_Rangecc((iRangecc){ constBegin_String(d->mod.url), parts.path.end }));
+}
+static void removeMediaRequest_DocumentWidget_(iDocumentWidget *d, iGmLinkId linkId) {
iMediaRequest *req = (iMediaRequest *) i.object;
if (req->linkId == linkId) {
remove_ObjectListIterator(&i);
break;
}
return iTrue;
}
postCommandf_Root(w->root, "open url:%s/", cstr_Rangecc(urlRoot_String(d->mod.url)));
return iTrue;
init_Anim(&d->view.scrollY.pos, arg_Command(cmd));
updateVisible_DocumentView_(&d->view);
+}
+static iBool requestMedia_DocumentWidget_(iDocumentWidget *d, iGmLinkId linkId, iBool enableFilters) {
const iString *mediaUrl = absoluteUrl_String(d->mod.url, linkUrl_GmDocument(d->view.doc, linkId));
pushBack_ObjectList(d->media, iClob(new_MediaRequest(d, linkId, mediaUrl, enableFilters)));
invalidate_DocumentWidget_(d);
return iTrue;
}
const int dir = arg_Command(cmd);
if (dir > 0 && !argLabel_Command(cmd, "repeat") &&
prefs_App()->loadImageInsteadOfScrolling &&
fetchNextUnfetchedImage_DocumentWidget_(d)) {
return iTrue;
+}
+static iBool isDownloadRequest_DocumentWidget(const iDocumentWidget *d, const iMediaRequest *req) {
+}
+static iBool handleMediaCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) {
if (m.object == req) {
isOurRequest = iTrue;
break;
}
const float amount = argLabel_Command(cmd, "full") != 0 ? 1.0f : 0.5f;
smoothScroll_DocumentView_(&d->view,
dir * amount *
height_Rect(documentBounds_DocumentView_(&d->view)),
smoothDuration_DocumentWidget_(keyboard_ScrollType));
return iTrue;
init_Anim(&d->view.scrollY.pos, 0);
invalidate_VisBuf(d->view.visBuf);
clampScroll_DocumentView_(&d->view);
updateVisible_DocumentView_(&d->view);
refresh_Widget(w);
return iTrue;
}
updateScrollMax_DocumentView_(&d->view); /* scrollY.max might not be fully updated */
init_Anim(&d->view.scrollY.pos, d->view.scrollY.max);
invalidate_VisBuf(d->view.visBuf);
clampScroll_DocumentView_(&d->view);
updateVisible_DocumentView_(&d->view);
refresh_Widget(w);
return iTrue;
return iFalse;
}
const int dir = arg_Command(cmd);
if (dir > 0 && !argLabel_Command(cmd, "repeat") &&
prefs_App()->loadImageInsteadOfScrolling &&
fetchNextUnfetchedImage_DocumentWidget_(d)) {
return iTrue;
/* Pass new data to media players. */
const enum iGmStatusCode code = status_GmRequest(req->req);
if (isSuccess_GmStatusCode(code)) {
iGmResponse *resp = lockResponse_GmRequest(req->req);
if (isDownloadRequest_DocumentWidget(d, req) ||
startsWith_String(&resp->meta, "audio/")) {
/* TODO: Use a helper? This is same as below except for the partialData flag. */
if (setData_Media(media_GmDocument(d->view.doc),
req->linkId,
&resp->meta,
&resp->body,
partialData_MediaFlag | allowHide_MediaFlag)) {
redoLayout_GmDocument(d->view.doc);
}
updateVisible_DocumentView_(&d->view);
invalidate_DocumentWidget_(d);
refresh_Widget(as_Widget(d));
}
unlockResponse_GmRequest(req->req);
}
smoothScroll_DocumentView_(&d->view,
3 * lineHeight_Text(paragraph_FontId) * dir,
smoothDuration_DocumentWidget_(keyboard_ScrollType));
/* Update the link's progress. */
invalidateLink_DocumentView_(&d->view, req->linkId);
refresh_Widget(d);
return iTrue;
}
const char *heading = suffixPtr_Command(cmd, "heading");
if (heading) {
if (isRequestOngoing_DocumentWidget(d)) {
/* Scroll position set when request finishes. */
setCStr_String(&d->pendingGotoHeading, heading);
return iTrue;
const enum iGmStatusCode code = status_GmRequest(req->req);
/* Give the media to the document for presentation. */
if (isSuccess_GmStatusCode(code)) {
if (isDownloadRequest_DocumentWidget(d, req) ||
startsWith_String(meta_GmRequest(req->req), "image/") ||
startsWith_String(meta_GmRequest(req->req), "audio/")) {
setData_Media(media_GmDocument(d->view.doc),
req->linkId,
meta_GmRequest(req->req),
body_GmRequest(req->req),
allowHide_MediaFlag);
redoLayout_GmDocument(d->view.doc);
iZap(d->view.visibleRuns); /* pointers invalidated */
updateVisible_DocumentView_(&d->view);
invalidate_DocumentWidget_(d);
refresh_Widget(as_Widget(d));
}
scrollToHeading_DocumentView_(&d->view, heading);
return iTrue;
}
const char *loc = pointerLabel_Command(cmd, "loc");
const iGmRun *run = findRunAtLoc_GmDocument(d->view.doc, loc);
if (run) {
scrollTo_DocumentView_(&d->view, run->visBounds.pos.y, iFalse);
else {
const iGmError *err = get_GmError(code);
makeSimpleMessage_Widget(format_CStr(uiTextCaution_ColorEscape "%s", err->title), err->info);
removeMediaRequest_DocumentWidget_(d, req->linkId);
}
return iTrue;
}
document_App() == d) {
const int dir = equal_Command(cmd, "find.next") ? +1 : -1;
iRangecc (*finder)(const iGmDocument *, const iString *, const char *) =
dir > 0 ? findText_GmDocument : findTextBefore_GmDocument;
iInputWidget *find = findWidget_App("find.input");
if (isEmpty_String(text_InputWidget(find))) {
d->foundMark = iNullRange;
}
else {
const iBool wrap = d->foundMark.start != NULL;
d->foundMark = finder(d->view.doc, text_InputWidget(find), dir > 0 ? d->foundMark.end
: d->foundMark.start);
if (!d->foundMark.start && wrap) {
/* Wrap around. */
d->foundMark = finder(d->view.doc, text_InputWidget(find), NULL);
}
if (d->foundMark.start) {
const iGmRun *found;
if ((found = findRunAtLoc_GmDocument(d->view.doc, d->foundMark.start)) != NULL) {
scrollTo_DocumentView_(&d->view, mid_Rect(found->bounds).y, iTrue);
+}
+static iBool fetchNextUnfetchedImage_DocumentWidget_(iDocumentWidget *d) {
const iGmRun *run = i.ptr;
if (run->linkId && run->mediaType == none_MediaType &&
~run->flags & decoration_GmRunFlag) {
const int linkFlags = linkFlags_GmDocument(d->view.doc, run->linkId);
if (isMediaLink_GmDocument(d->view.doc, run->linkId) &&
linkFlags & imageFileExtension_GmLinkFlag &&
~linkFlags & content_GmLinkFlag && ~linkFlags & permanent_GmLinkFlag ) {
if (requestMedia_DocumentWidget_(d, run->linkId, iTrue)) {
return iTrue;
}
}
}
if (flags_Widget(w) & touchDrag_WidgetFlag) {
postCommand_Root(w->root, "document.select arg:0"); /* we can't handle both at the same time */
+}
+static const iString *saveToDownloads_(const iString *url, const iString *mime, const iBlock *content,
iBool showDialog) {
iFile *f = new_File(savePath);
if (open_File(f, writeOnly_FileMode)) {
write_File(f, content);
close_File(f);
const size_t size = size_Block(content);
const iBool isMega = size >= 1000000;
+#if defined (iPlatformAppleMobile)
exportDownloadedFile_iOS(savePath);
+#else
if (showDialog) {
const iMenuItem items[2] = {
{ "${dlg.save.opendownload}", 0, 0,
format_CStr("!open url:%s", cstrCollect_String(makeFileUrl_String(savePath))) },
{ "${dlg.message.ok}", 0, 0, "message.ok" },
};
makeMessage_Widget(uiHeading_ColorEscape "${heading.save}",
format_CStr("%s\n${dlg.save.size} %.3f %s",
cstr_String(path_File(f)),
isMega ? size / 1.0e6f : (size / 1.0e3f),
isMega ? "${mb}" : "${kb}"),
items,
iElemCount(items));
}
+#endif
return savePath;
}
else {
makeSimpleMessage_Widget(uiTextCaution_ColorEscape "${heading.save.error}",
strerror(errno));
}
invalidateWideRunsWithNonzeroOffset_DocumentView_(&d->view); /* markers don't support offsets */
resetWideRuns_DocumentView_(&d->view);
refresh_Widget(w);
return iTrue;
iRelease(f);
}
if (d->foundMark.start) {
d->foundMark = iNullRange;
refresh_Widget(w);
}
return iTrue;
+}
+static void addAllLinks_(void *context, const iGmRun *run) {
pushBack_PtrArray(links, run);
}
iPtrArray *links = collectNew_PtrArray();
render_GmDocument(d->view.doc, (iRangei){ 0, size_GmDocument(d->view.doc).y }, addAllLinks_, links);
/* Find links that aren't already bookmarked. */
iForEach(PtrArray, i, links) {
const iGmRun *run = i.ptr;
uint32_t bmid;
if ((bmid = findUrl_Bookmarks(bookmarks_App(),
linkUrl_GmDocument(d->view.doc, run->linkId))) != 0) {
const iBookmark *bm = get_Bookmarks(bookmarks_App(), bmid);
/* We can import local copies of remote bookmarks. */
if (~bm->flags & remote_BookmarkFlag) {
remove_PtrArrayIterator(&i);
}
+}
+static iBool handlePinch_DocumentWidget_(iDocumentWidget *d, const char *cmd) {
d->pinchZoomInitial = d->pinchZoomPosted = prefs_App()->zoomPercent;
d->flags |= pinchZoom_DocumentWidgetFlag;
refresh_Widget(d);
const float rel = argf_Command(cmd);
int zoom = iRound(d->pinchZoomInitial * rel / 5.0f) * 5;
zoom = iClamp(zoom, 50, 200);
if (d->pinchZoomPosted != zoom) {
+#if defined (iPlatformAppleMobile)
if (zoom == 100) {
playHapticEffect_iOS(tap_HapticEffect);
}
+#endif
d->pinchZoomPosted = zoom;
postCommandf_App("zoom.set arg:%d", zoom);
}
if (!isEmpty_PtrArray(links)) {
if (argLabel_Command(cmd, "confirm")) {
const size_t count = size_PtrArray(links);
makeQuestion_Widget(
uiHeading_ColorEscape "${heading.import.bookmarks}",
formatCStrs_Lang("dlg.import.found.n", count),
(iMenuItem[]){ { "${cancel}" },
{ format_CStr(cstrCount_Lang("dlg.import.add.n", (int) count),
uiTextAction_ColorEscape,
count),
0,
0,
"bookmark.links" } },
2);
d->flags &= ~pinchZoom_DocumentWidgetFlag;
refresh_Widget(d);
+}
+static void swap_DocumentWidget_(iDocumentWidget *d, iGmDocument *doc,
iDocumentWidget *swapBuffersWith) {
iAssert(isInstance_Object(doc, &Class_GmDocument));
replaceDocument_DocumentWidget_(d, doc);
iSwap(iBanner *, d->banner, swapBuffersWith->banner);
setOwner_Banner(d->banner, d);
setOwner_Banner(swapBuffersWith->banner, swapBuffersWith);
swap_DocumentView_(&d->view, &swapBuffersWith->view);
+// invalidate_DocumentWidget_(swapBuffersWith);
+}
+static iWidget *swipeParent_DocumentWidget_(iDocumentWidget *d) {
+}
+static void setUrl_DocumentWidget_(iDocumentWidget *d, const iString *url) {
d->flags |= urlChanged_DocumentWidgetFlag;
set_String(d->mod.url, url);
+}
+static void setupSwipeOverlay_DocumentWidget_(iDocumentWidget *d, iWidget *overlay) {
innerToWindow_Widget
does not apply visual offset. */+// swap_DocumentWidget_(target, d->doc, d);
overlay
animates off the screen to the right. */ setVisualOffset_Widget(overlay, toPos, 250, easeOut_AnimFlag | softer_AnimFlag);
const float devFactor = (deviceType_App() == phone_AppDeviceType ? 1.0f : 2.0f);
float swipe = iClamp(d->swipeSpeed, devFactor * 400, devFactor * 1000) * gap_UI;
uint32_t span = ((toPos - fromPos) / swipe) * 1000;
setVisualOffset_Widget(overlay, toPos, span, deviceType_App() == tablet_AppDeviceType ?
easeOut_AnimFlag : 0);
+}
+static iBool handleSwipe_DocumentWidget_(iDocumentWidget *d, const char *cmd) {
If DocumentWidget is refactored to split the document presentation from state
and request management (a new DocumentView class), plain views could be used for this
animation without having to mess with the complete state of the DocumentWidget. That
seems like a less error-prone approach -- the current implementation will likely break
down (again) if anything is changed in the document internals.
GmDocument content and temporary underlay/overlay DocumentWidgets. Depending on the
swipe direction, the DocumentWidget `d` may wait until the finger is released to actually
perform the navigation action. */
//printf("[%p] responds to edgeswipe.moved\n", d);
as_Widget(d)->offsetRef = NULL;
const int side = argLabel_Command(cmd, "side");
const int offset = arg_Command(cmd);
if (side == 1) { /* left edge */
if (atOldest_History(d->mod.history)) {
return iTrue;
}
else {
iConstForEach(PtrArray, j, links) {
const iGmRun *run = j.ptr;
add_Bookmarks(bookmarks_App(),
linkUrl_GmDocument(d->view.doc, run->linkId),
collect_String(newRange_String(run->text)),
NULL,
0x1f588 /* pin */);
iWidget *swipeParent = swipeParent_DocumentWidget_(d);
if (findChild_Widget(swipeParent, "swipeout")) {
return iTrue; /* too fast, previous animation hasn't finished */
}
/* The temporary "swipein" will display the previous page until the finger is lifted. */
iDocumentWidget *swipeIn = findChild_Widget(swipeParent, "swipein");
if (!swipeIn) {
swipeIn = new_DocumentWidget();
swipeIn->flags |= animationPlaceholder_DocumentWidgetFlag;
setId_Widget(as_Widget(swipeIn), "swipein");
setFlags_Widget(as_Widget(swipeIn),
disabled_WidgetFlag | refChildrenOffset_WidgetFlag |
fixedPosition_WidgetFlag | fixedSize_WidgetFlag, iTrue);
setFlags_Widget(findChild_Widget(as_Widget(swipeIn), "scroll"), hidden_WidgetFlag, iTrue);
swipeIn->widget.rect.pos = windowToInner_Widget(swipeParent, localToWindow_Widget(w, w->rect.pos));
swipeIn->widget.rect.size = d->widget.rect.size;
swipeIn->widget.offsetRef = parent_Widget(w);
/* Use a cached document for the layer underneath. */ {
lock_History(d->mod.history);
iRecentUrl *recent = precedingLocked_History(d->mod.history);
if (recent && recent->cachedResponse) {
setUrl_DocumentWidget_(swipeIn, &recent->url);
updateFromCachedResponse_DocumentWidget_(swipeIn,
recent->normScrollY,
recent->cachedResponse,
recent->cachedDoc);
parseUser_DocumentWidget_(swipeIn);
updateBanner_DocumentWidget_(swipeIn);
}
else {
setUrlAndSource_DocumentWidget(swipeIn, &recent->url,
collectNewCStr_String("text/gemini"),
collect_Block(new_Block(0)));
}
unlock_History(d->mod.history);
}
postCommand_App("bookmarks.changed");
addChildPos_Widget(swipeParent, iClob(swipeIn), front_WidgetAddPos);
}
}
else {
makeSimpleMessage_Widget(uiHeading_ColorEscape "${heading.import.bookmarks}",
"${dlg.import.notnew}");
}
return iTrue;
updateHover_DocumentView_(&d->view, mouseCoord_Window(get_Window(), 0));
if (d->mod.reloadInterval) {
if (!isValid_Time(&d->sourceTime) || elapsedSeconds_Time(&d->sourceTime) >=
seconds_ReloadInterval_(d->mod.reloadInterval)) {
postCommand_Widget(w, "document.reload");
if (side == 2) { /* right edge */
if (offset < -get_Window()->pixelRatio * 10) {
int animSpan = 10;
if (!atNewest_History(d->mod.history) && ~flags_Widget(w) & dragged_WidgetFlag) {
iWidget *swipeParent = swipeParent_DocumentWidget_(d);
if (findChild_Widget(swipeParent, "swipeout")) {
return iTrue; /* too fast, previous animation hasn't finished */
}
/* Setup the drag. `d` will be moving with the finger. */
animSpan = 0;
postCommand_Widget(d, "navigate.forward");
setFlags_Widget(w, dragged_WidgetFlag, iTrue);
/* Set up the swipe dummy. */
iDocumentWidget *target = new_DocumentWidget();
target->flags |= animationPlaceholder_DocumentWidgetFlag;
setId_Widget(as_Widget(target), "swipeout");
/* "swipeout" takes `d`'s document and goes underneath. */
target->widget.rect.pos = windowToInner_Widget(swipeParent, localToWindow_Widget(w, w->rect.pos));
target->widget.rect.size = d->widget.rect.size;
setFlags_Widget(as_Widget(target), fixedPosition_WidgetFlag | fixedSize_WidgetFlag, iTrue);
swap_DocumentWidget_(target, d->view.doc, d);
addChildPos_Widget(swipeParent, iClob(target), front_WidgetAddPos);
setFlags_Widget(as_Widget(target), refChildrenOffset_WidgetFlag, iTrue);
as_Widget(target)->offsetRef = parent_Widget(w);
/* Mark it for deletion after animation finishes. */
destroy_Widget(as_Widget(target));
/* The `d` document will now navigate forward and be replaced with a cached
copy. However, if a cached response isn't available, we'll need to show a
blank page. */
setUrlAndSource_DocumentWidget(d,
collectNewCStr_String("about:blank"),
collectNewCStr_String("text/gemini"),
collect_Block(new_Block(0)));
}
if (flags_Widget(w) & dragged_WidgetFlag) {
setVisualOffset_Widget(w, width_Widget(w) +
width_Widget(d) * offset / size_Root(w->root).x,
animSpan, 0);
}
else {
setVisualOffset_Widget(w, offset / 4, animSpan, 0);
}
}
return iTrue;
}
}
iArray *items = collectNew_Array(sizeof(iMenuItem));
for (int i = 0; i < max_ReloadInterval; ++i) {
pushBack_Array(items, &(iMenuItem){
format_CStr("%s%s", ((int) d->mod.reloadInterval == i ? "&" : "*"),
label_ReloadInterval_(i)),
0,
0,
format_CStr("document.autoreload.set arg:%d", i) });
}
pushBack_Array(items, &(iMenuItem){ "${cancel}", 0, 0, NULL });
makeQuestion_Widget(uiTextAction_ColorEscape "${heading.autoreload}",
"${dlg.autoreload}",
constData_Array(items), size_Array(items));
return iTrue;
d->mod.reloadInterval = arg_Command(cmd);
const iString *site = collectNewRange_String(urlRoot_String(d->mod.url));
const int dismissed = value_SiteSpec(site, dismissWarnings_SiteSpecKey);
const int arg = argLabel_Command(cmd, "warning");
setValue_SiteSpec(site, dismissWarnings_SiteSpecKey, dismissed | arg);
if (arg == ansiEscapes_GmDocumentWarning) {
remove_Banner(d->banner, ansiEscapes_GmStatusCode);
refresh_Widget(w);
}
return iTrue;
return handlePinch_DocumentWidget_(d, cmd);
document_App() == d) {
return handleSwipe_DocumentWidget_(d, cmd);
if (!isRequestOngoing_DocumentWidget(d)) {
setUrlAndSource_DocumentWidget(d, d->mod.url, string_Command(cmd, "mime"),
&d->sourceContent);
}
return iTrue;
if (argLabel_Command(cmd, "ttf")) {
iAssert(!cmp_String(&d->sourceMime, "font/ttf"));
installFontFile_Fonts(collect_String(suffix_Command(cmd, "name")), &d->sourceContent);
postCommand_App("open url:about:fonts");
}
else {
const iString *id = idFromUrl_FontPack(d->mod.url);
install_Fonts(id, &d->sourceContent);
postCommandf_App("open gotoheading:%s url:about:fonts", cstr_String(id));
if (argLabel_Command(cmd, "abort") && flags_Widget(w) & dragged_WidgetFlag) {
setFlags_Widget(w, dragged_WidgetFlag, iFalse);
postCommand_Widget(d, "navigate.back");
/* We must now undo the swap that was done when the drag started. */
/* TODO: Currently not animated! What exactly is the appropriate thing to do here? */
iWidget *swipeParent = swipeParent_DocumentWidget_(d);
iDocumentWidget *swipeOut = findChild_Widget(swipeParent, "swipeout");
swap_DocumentWidget_(d, swipeOut->view.doc, swipeOut);
+// const int visOff = visualOffsetByReference_Widget(w);
w->offsetRef = NULL;
+// setVisualOffset_Widget(w, visOff, 0, 0);
+// setVisualOffset_Widget(w, 0, 150, 0);
setVisualOffset_Widget(w, 0, 0, 0);
/* Make it an overlay instead. */
+// removeChild_Widget(swipeParent, swipeOut);
+// addChildPos_Widget(swipeParent, iClob(swipeOut), back_WidgetAddPos);
+// setupSwipeOverlay_DocumentWidget_(d, as_Widget(swipeOut));
return iTrue;
}
iAssert(~d->flags & animationPlaceholder_DocumentWidgetFlag);
setFlags_Widget(w, dragged_WidgetFlag, iFalse);
setVisualOffset_Widget(w, 0, 250, easeOut_AnimFlag | softer_AnimFlag);
return iTrue;
}
-}
-static iRect runRect_DocumentView_(const iDocumentView *d, const iGmRun *run) {
-}
-static void setGrabbedPlayer_DocumentWidget_(iDocumentWidget *d, const iGmRun *run) {
iPlayer *plr = audioPlayer_Media(media_GmDocument(d->view.doc), mediaId_GmRun(run));
setFlags_Player(plr, volumeGrabbed_PlayerFlag, iTrue);
d->grabbedStartVolume = volume_Player(plr);
d->grabbedPlayer = run;
refresh_Widget(d);
setFlags_Player(
audioPlayer_Media(media_GmDocument(d->view.doc), mediaId_GmRun(d->grabbedPlayer)),
volumeGrabbed_PlayerFlag,
iFalse);
d->grabbedPlayer = NULL;
refresh_Widget(d);
iAssert(iFalse);
-}
-static iBool processMediaEvents_DocumentWidget_(iDocumentWidget *d, const SDL_Event *ev) {
ev->type != SDL_MOUSEMOTION) {
return iFalse;
if (ev->button.button != SDL_BUTTON_LEFT) {
return iFalse;
iWidget *swipeParent = swipeParent_DocumentWidget_(d);
iDocumentWidget *swipeIn = findChild_Widget(swipeParent, "swipein");
d->swipeSpeed = argLabel_Command(cmd, "speed") / gap_UI;
/* "swipe.back" will soon follow. The `d` document will do the actual back navigation,
switching immediately to a cached page. However, if one is not available, we'll need
to show a blank page for a while. */
if (swipeIn) {
if (!argLabel_Command(cmd, "abort")) {
iWidget *swipeParent = swipeParent_DocumentWidget_(d);
/* What was being shown in the `d` document is now being swapped to
the outgoing page animation. */
iDocumentWidget *target = new_DocumentWidget();
target->flags |= animationPlaceholder_DocumentWidgetFlag;
addChildPos_Widget(swipeParent, iClob(target), back_WidgetAddPos);
setId_Widget(as_Widget(target), "swipeout");
setFlags_Widget(as_Widget(target), disabled_WidgetFlag, iTrue);
swap_DocumentWidget_(target, d->view.doc, d);
setUrlAndSource_DocumentWidget(d,
swipeIn->mod.url,
collectNewCStr_String("text/gemini"),
collect_Block(new_Block(0)));
as_Widget(swipeIn)->offsetRef = NULL;
}
destroy_Widget(as_Widget(swipeIn));
}
}
/* Updated in the drag. */
return iFalse;
const iGmRun *run = i.ptr;
if (run->mediaType != audio_MediaType) {
continue;
}
/* TODO: move this to mediaui.c */
const iRect rect = runRect_DocumentView_(&d->view, run);
iPlayer * plr = audioPlayer_Media(media_GmDocument(d->view.doc), mediaId_GmRun(run));
if (contains_Rect(rect, mouse)) {
iPlayerUI ui;
init_PlayerUI(&ui, plr, rect);
if (ev->type == SDL_MOUSEBUTTONDOWN && flags_Player(plr) & adjustingVolume_PlayerFlag &&
contains_Rect(adjusted_Rect(ui.volumeAdjustRect,
zero_I2(),
init_I2(-height_Rect(ui.volumeAdjustRect), 0)),
mouse)) {
setGrabbedPlayer_DocumentWidget_(d, run);
processEvent_Click(&d->click, ev);
/* The rest is done in the DocumentWidget click responder. */
refresh_Widget(d);
return iTrue;
}
else if (ev->type == SDL_MOUSEBUTTONDOWN || ev->type == SDL_MOUSEMOTION) {
refresh_Widget(d);
return iTrue;
}
if (contains_Rect(ui.playPauseRect, mouse)) {
setPaused_Player(plr, !isPaused_Player(plr));
animateMedia_DocumentWidget_(d);
return iTrue;
}
else if (contains_Rect(ui.rewindRect, mouse)) {
if (isStarted_Player(plr) && time_Player(plr) > 0.5f) {
stop_Player(plr);
start_Player(plr);
setPaused_Player(plr, iTrue);
}
refresh_Widget(d);
return iTrue;
}
else if (contains_Rect(ui.volumeRect, mouse)) {
setFlags_Player(plr,
adjustingVolume_PlayerFlag,
!(flags_Player(plr) & adjustingVolume_PlayerFlag));
animateMedia_DocumentWidget_(d);
refresh_Widget(d);
return iTrue;
}
else if (contains_Rect(ui.menuRect, mouse)) {
/* TODO: Add menu items for:
- output device
- Save to Downloads
*/
if (d->playerMenu) {
destroy_Widget(d->playerMenu);
d->playerMenu = NULL;
return iTrue;
}
d->playerMenu = makeMenu_Widget(
as_Widget(d),
(iMenuItem[]){
{ cstrCollect_String(metadataLabel_Player(plr)) },
},
1);
openMenu_Widget(d->playerMenu, bottomLeft_Rect(ui.menuRect));
return iTrue;
iWidget *swipeParent = swipeParent_DocumentWidget_(d);
iDocumentWidget *target = findChild_Widget(swipeParent, "swipeout");
if (atOldest_History(d->mod.history)) {
setVisualOffset_Widget(w, 0, 100, 0);
if (target) {
destroy_Widget(as_Widget(target)); /* didn't need it after all */
}
return iTrue;
}
setupSwipeOverlay_DocumentWidget_(d, as_Widget(target));
destroy_Widget(as_Widget(target)); /* will be actually deleted after animation finishes */
postCommand_Widget(d, "navigate.back");
return iTrue;
}
return iFalse;
}
-static size_t linkOrdinalFromKey_DocumentWidget_(const iDocumentWidget *d, int key) {
if (key >= '1' && key <= '9') {
return key - '1';
}
if (key < 'a' || key > 'z') {
return iInvalidPos;
}
ord = key - 'a' + 9;
-#if defined (iPlatformApple)
/* Skip keys that would conflict with default system shortcuts: hide, minimize, quit, close. */
if (key == 'h' || key == 'm' || key == 'q' || key == 'w') {
return iInvalidPos;
}
if (key > 'h') ord--;
if (key > 'm') ord--;
if (key > 'q') ord--;
if (key > 'w') ord--;
-#endif
iForIndices(i, homeRowKeys_) {
if (homeRowKeys_[i] == key) {
return i;
+static iBool cancelRequest_DocumentWidget_(iDocumentWidget *d, iBool postBack) {
iWidget *w = as_Widget(d);
postCommandf_Root(w->root,
"document.request.cancelled doc:%p url:%s", d, cstr_String(d->mod.url));
iReleasePtr(&d->request);
if (d->state != ready_RequestState) {
d->state = ready_RequestState;
if (postBack) {
postCommand_Root(w->root, "navigate.back");
}
}
updateFetchProgress_DocumentWidget_(d);
return iTrue;
}
}
-static iChar linkOrdinalChar_DocumentWidget_(const iDocumentWidget *d, size_t ord) {
if (ord < 9) {
return '1' + ord;
}
-#if defined (iPlatformApple)
if (ord < 9 + 22) {
int key = 'a' + ord - 9;
if (key >= 'h') key++;
if (key >= 'm') key++;
if (key >= 'q') key++;
if (key >= 'w') key++;
return 'A' + key - 'a';
+static const int smoothDuration_DocumentWidget_(enum iScrollType type) {
+}
+static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) {
if (d->flags & animationPlaceholder_DocumentWidgetFlag) {
return iFalse;
}
-#else
if (ord < 9 + 26) {
return 'A' + ord - 9;
/* When any tab changes its document URL, update the open link indicators. */
if (updateOpenURLs_GmDocument(d->view.doc)) {
invalidate_DocumentWidget_(d);
refresh_Widget(d);
}
-#endif
return iFalse;
}
if (ord < iElemCount(homeRowKeys_)) {
return 'A' + homeRowKeys_[ord] - 'a';
}
updateVisitedLinks_GmDocument(d->view.doc);
invalidateVisibleLinks_DocumentView_(&d->view);
return iFalse;
}
-}
-static void beginMarkingSelection_DocumentWidget_(iDocumentWidget *d, iInt2 pos) {
-}
-static void interactingWithLink_DocumentWidget_(iDocumentWidget *d, iGmLinkId id) {
clear_String(&d->linePrecedingLink);
return;
Periodic
makes direct dispatch to here */ {+// printf("%u: document.render\n", SDL_GetTicks());
if (SDL_GetTicks() - d->view.drawBufs->lastRenderTime > 150) {
remove_Periodic(periodic_App(), d);
/* Scrolling has stopped, begin filling up the buffer. */
if (d->view.visBuf->buffers[0].texture) {
addTicker_App(prerender_DocumentWidget_, d);
}
}
return iTrue;
}
loc.start--;
equal_Command(cmd, "keyroot.changed")) {
if (equal_Command(cmd, "font.changed")) {
invalidateCachedLayout_History(d->mod.history);
}
/* Alt/Option key may be involved in window size changes. */
setLinkNumberMode_DocumentWidget_(d, iFalse);
d->phoneToolbar = findWidget_App("toolbar");
const iBool keepCenter = equal_Command(cmd, "font.changed");
updateDocumentWidthRetainingScrollPosition_DocumentView_(&d->view, keepCenter);
d->view.drawBufs->flags |= updateSideBuf_DrawBufsFlag;
updateVisible_DocumentView_(&d->view);
invalidate_DocumentWidget_(d);
dealloc_VisBuf(d->view.visBuf);
updateWindowTitle_DocumentWidget_(d);
showOrHidePinningIndicator_DocumentWidget_(d);
refresh_Widget(w);
}
loc.start--;
if (d->flags & showLinkNumbers_DocumentWidgetFlag) {
setLinkNumberMode_DocumentWidget_(d, iFalse);
invalidateVisibleLinks_DocumentView_(&d->view);
refresh_Widget(w);
}
return iFalse;
}
loc.start--;
return iFalse;
}
loc.start++; /* Start of the preceding line. */
invalidateTheme_History(d->mod.history); /* forget cached color palettes */
if (document_App() == d) {
updateTheme_DocumentWidget_(d);
updateVisible_DocumentView_(&d->view);
updateTrust_DocumentWidget_(d, NULL);
d->view.drawBufs->flags |= updateSideBuf_DrawBufsFlag;
invalidate_DocumentWidget_(d);
refresh_Widget(w);
}
}
-}
-iLocalDef int wheelSwipeSide_DocumentWidget_(const iDocumentWidget *d) {
: d->flags & leftWheelSwipe_DocumentWidgetFlag ? 1
: 0);
-}
-static void finishWheelSwipe_DocumentWidget_(iDocumentWidget *d) {
d->wheelSwipeState == direct_WheelSwipeState) {
const int side = wheelSwipeSide_DocumentWidget_(d);
int abort = ((side == 1 && d->swipeSpeed < 0) || (side == 2 && d->swipeSpeed > 0));
if (iAbs(d->wheelSwipeDistance) < width_Widget(d) / 4 && iAbs(d->swipeSpeed) < 4 * gap_UI) {
abort = 1;
if (argLabel_Command(cmd, "redo")) {
redoLayout_GmDocument(d->view.doc);
}
postCommand_Widget(d, "edgeswipe.ended side:%d abort:%d", side, abort);
d->flags &= ~eitherWheelSwipe_DocumentWidgetFlag;
updateSize_DocumentWidget(d);
}
-}
-static iBool handleWheelSwipe_DocumentWidget_(iDocumentWidget *d, const SDL_MouseWheelEvent *ev) {
postCommand_App("document.update.pin"); /* prefs value not set yet */
return iFalse;
}
showOrHidePinningIndicator_DocumentWidget_(d);
return iFalse;
}
-// printf("STATE:%d wheel x:%d inert:%d end:%d\n", d->wheelSwipeState,
-// ev->x, isInertia_MouseWheelEvent(ev),
-// isScrollFinished_MouseWheelEvent(ev));
-// fflush(stdout);
case none_WheelSwipeState:
/* A new swipe starts. */
if (!isInertia_MouseWheelEvent(ev) && !isScrollFinished_MouseWheelEvent(ev)) {
int side = ev->x > 0 ? 1 : 2;
d->wheelSwipeDistance = ev->x * 2;
d->flags &= ~eitherWheelSwipe_DocumentWidgetFlag;
d->flags |= (side == 1 ? leftWheelSwipe_DocumentWidgetFlag
: rightWheelSwipe_DocumentWidgetFlag);
// printf("swipe starts at %d, side %d\n", d->wheelSwipeDistance, side);
d->wheelSwipeState = direct_WheelSwipeState;
d->swipeSpeed = 0;
postCommand_Widget(d, "edgeswipe.moved arg:%d side:%d", d->wheelSwipeDistance, side);
return iTrue;
}
break;
case direct_WheelSwipeState:
if (isInertia_MouseWheelEvent(ev) || isScrollFinished_MouseWheelEvent(ev)) {
finishWheelSwipe_DocumentWidget_(d);
d->wheelSwipeState = none_WheelSwipeState;
}
else {
int step = ev->x * 2;
d->wheelSwipeDistance += step;
/* Remember the maximum speed. */
if (d->swipeSpeed < 0 && step < 0) {
d->swipeSpeed = iMin(d->swipeSpeed, step);
}
else if (d->swipeSpeed > 0 && step > 0) {
d->swipeSpeed = iMax(d->swipeSpeed, step);
}
else {
d->swipeSpeed = step;
}
switch (wheelSwipeSide_DocumentWidget_(d)) {
case 1:
d->wheelSwipeDistance = iMax(0, d->wheelSwipeDistance);
d->wheelSwipeDistance = iMin(width_Widget(d), d->wheelSwipeDistance);
break;
case 2:
d->wheelSwipeDistance = iMin(0, d->wheelSwipeDistance);
d->wheelSwipeDistance = iMax(-width_Widget(d), d->wheelSwipeDistance);
break;
}
/* TODO: calculate speed, rememeber direction */
//printf("swipe moved to %d, side %d\n", d->wheelSwipeDistance, side);
postCommand_Widget(d, "edgeswipe.moved arg:%d side:%d", d->wheelSwipeDistance,
wheelSwipeSide_DocumentWidget_(d));
}
return iTrue;
setLinkNumberMode_DocumentWidget_(d, iFalse);
if (cmp_String(id_Widget(w), suffixPtr_Command(cmd, "id")) == 0) {
/* Set palette for our document. */
updateTheme_DocumentWidget_(d);
updateTrust_DocumentWidget_(d, NULL);
updateSize_DocumentWidget(d);
showOrHidePinningIndicator_DocumentWidget_(d);
updateFetchProgress_DocumentWidget_(d);
updateHover_Window(window_Widget(w));
}
init_Anim(&d->view.sideOpacity, 0);
init_Anim(&d->view.altTextOpacity, 0);
updateSideOpacity_DocumentView_(&d->view, iFalse);
updateWindowTitle_DocumentWidget_(d);
allocVisBuffer_DocumentView_(&d->view);
animateMedia_DocumentWidget_(d);
remove_Periodic(periodic_App(), d);
removeTicker_App(prerender_DocumentWidget_, d);
return iFalse;
}
-}
-static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *ev) {
updateSize_DocumentWidget(d);
/* Space for tab buttons has changed. */
updateWindowTitle_DocumentWidget_(d);
return iFalse;
}
/* Touch selection mode. */
if (!arg_Command(cmd)) {
d->selectMark = iNullRange;
setFlags_Widget(w, touchDrag_WidgetFlag, iFalse);
setFadeEnabled_ScrollWidget(d->scroll, iTrue);
}
else {
setFlags_Widget(w, touchDrag_WidgetFlag, iTrue);
d->flags |= movingSelectMarkEnd_DocumentWidgetFlag |
selectWords_DocumentWidgetFlag; /* finger-based selection is imprecise */
d->flags &= ~selectLines_DocumentWidgetFlag;
setFadeEnabled_ScrollWidget(d->scroll, iFalse);
d->selectMark = sourceLoc_DocumentView_(&d->view, d->contextPos);
extendRange_Rangecc(&d->selectMark, range_String(source_GmDocument(d->view.doc)),
word_RangeExtension | bothStartAndEnd_RangeExtension);
d->initialSelectMark = d->selectMark;
}
return iTrue;
}
if (isCommand_Widget(w, ev, "pullaction")) {
postCommand_Widget(w, "navigate.reload");
return iTrue;
const char *unchecked = red_ColorEscape "\u2610";
const char *checked = green_ColorEscape "\u2611";
const iBool haveFingerprint = (d->certFlags & haveFingerprint_GmCertFlag) != 0;
const int requiredForTrust = (available_GmCertFlag | haveFingerprint_GmCertFlag |
timeVerified_GmCertFlag);
const iBool canTrust = ~d->certFlags & trusted_GmCertFlag &&
((d->certFlags & requiredForTrust) == requiredForTrust);
const iRecentUrl *recent = constMostRecentUrl_History(d->mod.history);
const iString *meta = &d->sourceMime;
if (recent && recent->cachedResponse) {
meta = &recent->cachedResponse->meta;
}
if (!handleCommand_DocumentWidget_(d, command_UserEvent(ev))) {
/* Base class commands. */
return processEvent_Widget(w, ev);
iString *msg = collectNew_String();
if (isEmpty_String(&d->sourceHeader)) {
appendFormat_String(msg,
"%s\n%s\n",
cstr_String(meta),
formatCStrs_Lang("num.bytes.n", size_Block(&d->sourceContent)));
}
else {
appendFormat_String(msg, "%s\n", cstr_String(&d->sourceHeader));
if (size_Block(&d->sourceContent)) {
appendFormat_String(
msg, "%s\n", formatCStrs_Lang("num.bytes.n", size_Block(&d->sourceContent)));
}
}
/* TODO: On mobile, omit the CA status. */
appendFormat_String(
msg,
"\n%s${pageinfo.cert.status}\n"
"%s%s %s\n"
"%s%s %s%s\n"
"%s%s %s (%04d-%02d-%02d %02d:%02d:%02d)\n"
"%s%s %s",
uiHeading_ColorEscape,
d->certFlags & authorityVerified_GmCertFlag ? checked
: uiTextAction_ColorEscape "\u2610",
uiText_ColorEscape,
d->certFlags & authorityVerified_GmCertFlag ? "${pageinfo.cert.ca.verified}"
: "${pageinfo.cert.ca.unverified}",
d->certFlags & domainVerified_GmCertFlag ? checked : unchecked,
uiText_ColorEscape,
d->certFlags & domainVerified_GmCertFlag ? "${pageinfo.domain.match}"
: "${pageinfo.domain.mismatch}",
~d->certFlags & domainVerified_GmCertFlag
? format_CStr(" (%s)", cstr_String(d->certSubject))
: "",
d->certFlags & timeVerified_GmCertFlag ? checked : unchecked,
uiText_ColorEscape,
d->certFlags & timeVerified_GmCertFlag ? "${pageinfo.cert.notexpired}"
: "${pageinfo.cert.expired}",
d->certExpiry.year,
d->certExpiry.month,
d->certExpiry.day,
d->certExpiry.hour,
d->certExpiry.minute,
d->certExpiry.second,
d->certFlags & trusted_GmCertFlag ? checked : unchecked,
uiText_ColorEscape,
d->certFlags & trusted_GmCertFlag ? "${pageinfo.cert.trusted}"
: "${pageinfo.cert.untrusted}");
setFocus_Widget(NULL);
iArray *items = new_Array(sizeof(iMenuItem));
if (canTrust) {
pushBack_Array(items,
&(iMenuItem){ uiTextCaution_ColorEscape "${dlg.cert.trust}",
SDLK_u,
KMOD_PRIMARY | KMOD_SHIFT,
"server.trustcert" });
}
if (haveFingerprint) {
pushBack_Array(items, &(iMenuItem){ "${dlg.cert.fingerprint}", 0, 0, "server.copycert" });
}
if (!isEmpty_Array(items)) {
pushBack_Array(items, &(iMenuItem){ "---", 0, 0, 0 });
}
pushBack_Array(items, &(iMenuItem){ "${close}", 0, 0, "message.ok" });
iWidget *dlg = makeQuestion_Widget(uiHeading_ColorEscape "${heading.pageinfo}",
cstr_String(msg),
data_Array(items),
size_Array(items));
delete_Array(items);
/* Enforce a minimum size. */
+// iWidget *sizer = new_Widget();
+// setFixedSize_Widget(sizer, init_I2(gap_UI * 65, 1));
+// addChildFlags_Widget(dlg, iClob(sizer), frameless_WidgetFlag);
+// setFlags_Widget(dlg, centerHorizontal_WidgetFlag, iFalse);
if (deviceType_App() == desktop_AppDeviceType) {
const iWidget *lockButton = findWidget_Root("navbar.lock");
setPos_Widget(dlg, windowToLocal_Widget(dlg, bottomLeft_Rect(bounds_Widget(lockButton))));
}
arrange_Widget(dlg);
addAction_Widget(dlg, SDLK_ESCAPE, 0, "message.ok");
addAction_Widget(dlg, SDLK_SPACE, 0, "message.ok");
return iTrue;
}
const int key = ev->key.keysym.sym;
if ((d->flags & showLinkNumbers_DocumentWidgetFlag) &&
((key >= '1' && key <= '9') || (key >= 'a' && key <= 'z'))) {
const size_t ord = linkOrdinalFromKey_DocumentWidget_(d, key) + d->ordinalBase;
iConstForEach(PtrArray, i, &d->view.visibleLinks) {
if (ord == iInvalidPos) break;
const iGmRun *run = i.ptr;
if (run->flags & decoration_GmRunFlag &&
visibleLinkOrdinal_DocumentView_(view, run->linkId) == ord) {
if (d->flags & setHoverViaKeys_DocumentWidgetFlag) {
view->hoverLink = run;
}
else {
postCommandf_Root(
w->root,
"open newtab:%d url:%s",
(isPinned_DocumentWidget_(d) ? otherRoot_OpenTabFlag : 0) ^
(d->ordinalMode == numbersAndAlphabet_DocumentLinkOrdinalMode
? openTabMode_Sym(modState_Keys())
: (d->flags & newTabViaHomeKeys_DocumentWidgetFlag ? 1 : 0)),
cstr_String(absoluteUrl_String(
d->mod.url, linkUrl_GmDocument(view->doc, run->linkId))));
interactingWithLink_DocumentWidget_(d, run->linkId);
}
setLinkNumberMode_DocumentWidget_(d, iFalse);
invalidateVisibleLinks_DocumentView_(view);
refresh_Widget(d);
return iTrue;
}
}
const iRangecc host = urlHost_String(d->mod.url);
const uint16_t port = urlPort_String(d->mod.url);
if (!isEmpty_Block(d->certFingerprint) && !isEmpty_Range(&host)) {
iTime expiry;
initCurrent_Time(&expiry);
iTime oneHour; /* One hour is long enough for a single visit (?). */
initSeconds_Time(&oneHour, 3600);
add_Time(&expiry, &oneHour);
iDate expDate;
init_Date(&expDate, &expiry);
setTrusted_GmCerts(certs_App(), host, port, d->certFingerprint, &expDate);
postCommand_Widget(w, "navigate.reload");
}
switch (key) {
case SDLK_ESCAPE:
if (d->flags & showLinkNumbers_DocumentWidgetFlag && document_App() == d) {
setLinkNumberMode_DocumentWidget_(d, iFalse);
invalidateVisibleLinks_DocumentView_(view);
refresh_Widget(d);
return iTrue;
}
break;
-#if !defined (NDEBUG)
case SDLK_KP_1:
case '`': {
iBlock *seed = new_Block(64);
for (size_t i = 0; i < 64; ++i) {
setByte_Block(seed, i, iRandom(0, 256));
}
setThemeSeed_GmDocument(view->doc, seed);
delete_Block(seed);
invalidate_DocumentWidget_(d);
refresh_Widget(w);
break;
}
-#endif
-#if 0
case '0': {
extern int enableHalfPixelGlyphs_Text;
enableHalfPixelGlyphs_Text = !enableHalfPixelGlyphs_Text;
refresh_Widget(w);
printf("halfpixel: %d\n", enableHalfPixelGlyphs_Text);
fflush(stdout);
break;
}
-#endif
-#if 0
case '0': {
extern int enableKerning_Text;
enableKerning_Text = !enableKerning_Text;
invalidate_DocumentWidget_(d);
refresh_Widget(w);
printf("kerning: %d\n", enableKerning_Text);
fflush(stdout);
break;
}
-#endif
return iTrue;
const iRangecc host = urlHost_String(d->mod.url);
const uint16_t port = urlPort_String(d->mod.url);
if (!isEmpty_Block(d->certFingerprint) && !isEmpty_Range(&host)) {
setTrusted_GmCerts(certs_App(), host, port, d->certFingerprint, &d->certExpiry);
postCommand_Widget(w, "navigate.reload");
}
return iTrue;
}
-#if defined (iPlatformAppleDesktop)
ev->wheel.y == 0 &&
d->wheelSwipeState == direct_WheelSwipeState &&
handleWheelSwipe_DocumentWidget_(d, &ev->wheel)) {
SDL_SetClipboardText(cstrCollect_String(hexEncode_Block(d->certFingerprint)));
return iTrue;
}
-#endif
const iInt2 mouseCoord = coord_MouseWheelEvent(&ev->wheel);
if (isPerPixel_MouseWheelEvent(&ev->wheel)) {
const iInt2 wheel = init_I2(ev->wheel.x, ev->wheel.y);
stop_Anim(&d->view.scrollY.pos);
immediateScroll_DocumentView_(view, -wheel.y);
if (!scrollWideBlock_DocumentView_(view, mouseCoord, -wheel.x, 0) &&
wheel.x) {
handleWheelSwipe_DocumentWidget_(d, &ev->wheel);
iString *copied;
if (d->selectMark.start) {
iRangecc mark = d->selectMark;
if (mark.start > mark.end) {
iSwap(const char *, mark.start, mark.end);
}
copied = newRange_String(mark);
}
else {
/* Traditional mouse wheel. */
const int amount = ev->wheel.y;
if (keyMods_Sym(modState_Keys()) == KMOD_PRIMARY) {
postCommandf_App("zoom.delta arg:%d", amount > 0 ? 10 : -10);
return iTrue;
}
smoothScroll_DocumentView_(view,
-3 * amount * lineHeight_Text(paragraph_FontId),
smoothDuration_DocumentWidget_(mouse_ScrollType));
scrollWideBlock_DocumentView_(
view, mouseCoord, -3 * ev->wheel.x * lineHeight_Text(paragraph_FontId), 167);
/* Full document. */
copied = copy_String(source_GmDocument(d->view.doc));
}
SDL_SetClipboardText(cstr_String(copied));
delete_String(copied);
if (flags_Widget(w) & touchDrag_WidgetFlag) {
postCommand_Widget(w, "document.select arg:0");
}
iChangeFlags(d->flags, noHoverWhileScrolling_DocumentWidgetFlag, iTrue);
return iTrue;
}
if (ev->motion.which != SDL_TOUCH_MOUSEID) {
iChangeFlags(d->flags, noHoverWhileScrolling_DocumentWidgetFlag, iFalse);
}
const iInt2 mpos = init_I2(ev->motion.x, ev->motion.y);
if (isVisible_Widget(d->menu)) {
setCursor_Window(get_Window(), SDL_SYSTEM_CURSOR_ARROW);
}
-#if 0
else if (contains_Rect(siteBannerRect_DocumentWidget_(d), mpos)) {
setCursor_Window(get_Window(), SDL_SYSTEM_CURSOR_HAND);
if (d->contextLink) {
SDL_SetClipboardText(cstr_String(canonicalUrl_String(absoluteUrl_String(
d->mod.url, linkUrl_GmDocument(d->view.doc, d->contextLink->linkId)))));
}
-#endif
else {
if (value_Anim(&view->altTextOpacity) < 0.833f) {
setValue_Anim(&view->altTextOpacity, 0, 0); /* keep it hidden while moving */
}
updateHover_DocumentView_(view, mpos);
SDL_SetClipboardText(cstr_String(canonicalUrl_String(d->mod.url)));
}
iChangeFlags(d->flags, noHoverWhileScrolling_DocumentWidgetFlag, iFalse);
return iTrue;
}
if (ev->button.button == SDL_BUTTON_X1) {
postCommand_Root(w->root, "navigate.back");
return iTrue;
}
if (ev->button.button == SDL_BUTTON_X2) {
postCommand_Root(w->root, "navigate.forward");
return iTrue;
if (d->contextLink) {
const iGmLinkId linkId = d->contextLink->linkId;
setUrl_Media(media_GmDocument(d->view.doc),
linkId,
download_MediaType,
linkUrl_GmDocument(d->view.doc, linkId));
requestMedia_DocumentWidget_(d, linkId, iFalse /* no filters */);
redoLayout_GmDocument(d->view.doc); /* inline downloader becomes visible */
updateVisible_DocumentView_(&d->view);
invalidate_DocumentWidget_(d);
refresh_Widget(w);
}
if (ev->button.button == SDL_BUTTON_MIDDLE && view->hoverLink) {
interactingWithLink_DocumentWidget_(d, view->hoverLink->linkId);
postCommandf_Root(w->root, "open newtab:%d url:%s",
(isPinned_DocumentWidget_(d) ? otherRoot_OpenTabFlag : 0) |
(modState_Keys() & KMOD_SHIFT ? new_OpenTabFlag : newBackground_OpenTabFlag),
cstr_String(linkUrl_GmDocument(view->doc, view->hoverLink->linkId)));
return iTrue;
return iTrue;
postCommandf_Root(w->root,
/* use the `redirect:1` argument to cause the input query URL to be
replaced in History; we don't want to navigate onto it */
"open redirect:1 url:%s",
cstrCollect_String(makeQueryUrl_DocumentWidget_
(d, collect_String(suffix_Command(cmd, "value")))));
return iTrue;
equal_Rangecc(range_Command(cmd, "id"), "document.input.submit") && document_App() == d) {
postCommand_Root(get_Root(), "navigate.back");
return iTrue;
id_GmRequest(d->request) == argU32Label_Command(cmd, "reqid")) {
+// set_Block(&d->sourceContent, &lockResponse_GmRequest(d->request)->body);
+// unlockResponse_GmRequest(d->request);
if (document_App() == d) {
updateFetchProgress_DocumentWidget_(d);
}
if (ev->button.button == SDL_BUTTON_RIGHT &&
contains_Widget(w, init_I2(ev->button.x, ev->button.y))) {
if (!isVisible_Widget(d->menu)) {
d->contextLink = view->hoverLink;
d->contextPos = init_I2(ev->button.x, ev->button.y);
if (d->menu) {
destroy_Widget(d->menu);
d->menu = NULL;
}
setFocus_Widget(NULL);
iArray items;
init_Array(&items, sizeof(iMenuItem));
if (d->contextLink) {
/* Context menu for a link. */
interactingWithLink_DocumentWidget_(d, d->contextLink->linkId); /* perhaps will be triggered */
const iString *linkUrl = linkUrl_GmDocument(view->doc, d->contextLink->linkId);
-// const int linkFlags = linkFlags_GmDocument(d->doc, d->contextLink->linkId);
const iRangecc scheme = urlScheme_String(linkUrl);
const iBool isGemini = equalCase_Rangecc(scheme, "gemini");
iBool isNative = iFalse;
if (deviceType_App() != desktop_AppDeviceType) {
/* Show the link as the first, non-interactive item. */
pushBack_Array(&items, &(iMenuItem){
format_CStr("```%s", cstr_String(linkUrl)),
0, 0, NULL });
}
if (willUseProxy_App(scheme) || isGemini ||
equalCase_Rangecc(scheme, "file") ||
equalCase_Rangecc(scheme, "finger") ||
equalCase_Rangecc(scheme, "gopher")) {
isNative = iTrue;
/* Regular links that we can open. */
pushBackN_Array(
&items,
(iMenuItem[]){ { openTab_Icon " ${link.newtab}",
0,
0,
format_CStr("!open newtab:1 origin:%s url:%s",
cstr_String(id_Widget(w)),
cstr_String(linkUrl)) },
{ openTabBg_Icon " ${link.newtab.background}",
0,
0,
format_CStr("!open newtab:2 origin:%s url:%s",
cstr_String(id_Widget(w)),
cstr_String(linkUrl)) },
{ "${link.side}",
0,
0,
format_CStr("!open newtab:4 origin:%s url:%s",
cstr_String(id_Widget(w)),
cstr_String(linkUrl)) },
{ "${link.side.newtab}",
0,
0,
format_CStr("!open newtab:5 origin:%s url:%s",
cstr_String(id_Widget(w)),
cstr_String(linkUrl)) } },
4);
if (deviceType_App() == phone_AppDeviceType) {
removeN_Array(&items, size_Array(&items) - 2, iInvalidSize);
}
}
else if (!willUseProxy_App(scheme)) {
pushBack_Array(
&items,
&(iMenuItem){ openExt_Icon " ${link.browser}",
0,
0,
format_CStr("!open default:1 url:%s", cstr_String(linkUrl)) });
}
if (willUseProxy_App(scheme)) {
pushBackN_Array(
&items,
(iMenuItem[]){
{ "---" },
{ isGemini ? "${link.noproxy}" : openExt_Icon " ${link.browser}",
0,
0,
format_CStr("!open origin:%s noproxy:1 url:%s",
cstr_String(id_Widget(w)),
cstr_String(linkUrl)) } },
2);
}
iString *linkLabel = collectNewRange_String(
linkLabel_GmDocument(view->doc, d->contextLink->linkId));
urlEncodeSpaces_String(linkLabel);
pushBackN_Array(&items,
(iMenuItem[]){ { "---" },
{ "${link.copy}", 0, 0, "document.copylink" },
{ bookmark_Icon " ${link.bookmark}",
0,
0,
format_CStr("!bookmark.add title:%s url:%s",
cstr_String(linkLabel),
cstr_String(linkUrl)) },
},
3);
if (isNative && d->contextLink->mediaType != download_MediaType) {
pushBackN_Array(&items, (iMenuItem[]){
{ "---" },
{ download_Icon " ${link.download}", 0, 0, "document.downloadlink" },
}, 2);
}
iMediaRequest *mediaReq;
if ((mediaReq = findMediaRequest_DocumentWidget_(d, d->contextLink->linkId)) != NULL &&
d->contextLink->mediaType != download_MediaType) {
if (isFinished_GmRequest(mediaReq->req)) {
pushBack_Array(&items,
&(iMenuItem){ download_Icon " " saveToDownloads_Label,
0,
0,
format_CStr("document.media.save link:%u",
d->contextLink->linkId) });
}
}
if (equalCase_Rangecc(scheme, "file")) {
/* Local files may be deleted. */
pushBack_Array(
&items,
&(iMenuItem){ delete_Icon " " uiTextCaution_ColorEscape
"${link.file.delete}",
0,
0,
format_CStr("!file.delete confirm:1 path:%s",
cstrCollect_String(
localFilePathFromUrl_String(linkUrl))) });
}
}
else if (deviceType_App() == desktop_AppDeviceType) {
if (!isEmpty_Range(&d->selectMark)) {
pushBackN_Array(&items,
(iMenuItem[]){ { "${menu.copy}", 0, 0, "copy" },
{ "---", 0, 0, NULL } },
2);
}
pushBackN_Array(
&items,
(iMenuItem[]){
{ backArrow_Icon " ${menu.back}", navigateBack_KeyShortcut, "navigate.back" },
{ forwardArrow_Icon " ${menu.forward}", navigateForward_KeyShortcut, "navigate.forward" },
{ upArrow_Icon " ${menu.parent}", navigateParent_KeyShortcut, "navigate.parent" },
{ upArrowBar_Icon " ${menu.root}", navigateRoot_KeyShortcut, "navigate.root" },
{ "---" },
{ reload_Icon " ${menu.reload}", reload_KeyShortcut, "navigate.reload" },
{ timer_Icon " ${menu.autoreload}", 0, 0, "document.autoreload.menu" },
{ "---" },
{ bookmark_Icon " ${menu.page.bookmark}", SDLK_d, KMOD_PRIMARY, "bookmark.add" },
{ star_Icon " ${menu.page.subscribe}", subscribeToPage_KeyModifier, "feeds.subscribe" },
{ "---" },
{ book_Icon " ${menu.page.import}", 0, 0, "bookmark.links confirm:1" },
{ globe_Icon " ${menu.page.translate}", 0, 0, "document.translate" },
{ upload_Icon " ${menu.page.upload}", 0, 0, "document.upload" },
{ "---" },
{ "${menu.page.copyurl}", 0, 0, "document.copylink" } },
16);
if (isEmpty_Range(&d->selectMark)) {
pushBackN_Array(
&items,
(iMenuItem[]){
{ "${menu.page.copysource}", 'c', KMOD_PRIMARY, "copy" },
{ download_Icon " " saveToDownloads_Label, SDLK_s, KMOD_PRIMARY, "document.save" } },
2);
}
}
else {
/* Mobile text selection menu. */
-#if 0
pushBackN_Array(
&items,
(iMenuItem[]){
{ "${menu.select}", 0, 0, "document.select arg:1" },
{ "${menu.select.word}", 0, 0, "document.select arg:2" },
{ "${menu.select.par}", 0, 0, "document.select arg:3" },
},
3);
-#endif
postCommand_Root(w->root, "document.select arg:1");
return iTrue;
}
d->menu = makeMenu_Widget(w, data_Array(&items), size_Array(&items));
deinit_Array(&items);
setMenuItemDisabled_Widget(
d->menu,
"document.upload",
!equalCase_Rangecc(urlScheme_String(d->mod.url), "gemini") &&
!equalCase_Rangecc(urlScheme_String(d->mod.url), "titan"));
checkResponse_DocumentWidget_(d);
set_Atomic(&d->isRequestUpdated, iFalse); /* ready to be notified again */
return iFalse;
id_GmRequest(d->request) == argU32Label_Command(cmd, "reqid")) {
d->flags &= ~fromCache_DocumentWidgetFlag;
set_Block(&d->sourceContent, body_GmRequest(d->request));
if (!isSuccess_GmStatusCode(status_GmRequest(d->request))) {
/* TODO: Why is this here? Can it be removed? */
format_String(&d->sourceHeader,
"%s%s",
humanReadableStatusCode_(status_GmRequest(d->request)),
cstr_String(meta_GmRequest(d->request)));
}
updateFetchProgress_DocumentWidget_(d);
checkResponse_DocumentWidget_(d);
if (category_GmStatusCode(status_GmRequest(d->request)) == categorySuccess_GmStatusCode) {
init_Anim(&d->view.scrollY.pos, d->initNormScrollY * pageHeight_DocumentView_(&d->view));
/* TODO: unless user already scrolled! */
}
addBannerWarnings_DocumentWidget_(d);
iChangeFlags(d->flags,
urlChanged_DocumentWidgetFlag | drawDownloadCounter_DocumentWidgetFlag,
iFalse);
d->state = ready_RequestState;
postProcessRequestContent_DocumentWidget_(d, iFalse);
/* The response may be cached. */
if (d->request) {
iAssert(~d->flags & animationPlaceholder_DocumentWidgetFlag);
iAssert(~d->flags & fromCache_DocumentWidgetFlag);
if (!equal_Rangecc(urlScheme_String(d->mod.url), "about") &&
(startsWithCase_String(meta_GmRequest(d->request), "text/") ||
!cmp_String(&d->sourceMime, mimeType_Gempub))) {
setCachedResponse_History(d->mod.history, lockResponse_GmRequest(d->request));
unlockResponse_GmRequest(d->request);
}
processContextMenuEvent_Widget(d->menu, ev, {});
}
iReleasePtr(&d->request);
updateVisible_DocumentView_(&d->view);
d->view.drawBufs->flags |= updateSideBuf_DrawBufsFlag;
postCommandf_Root(w->root,
"document.changed doc:%p status:%d url:%s",
d,
d->sourceStatus,
cstr_String(d->mod.url));
/* Check for a pending goto. */
if (!isEmpty_String(&d->pendingGotoHeading)) {
scrollToHeading_DocumentView_(&d->view, cstr_String(&d->pendingGotoHeading));
clear_String(&d->pendingGotoHeading);
}
cacheDocumentGlyphs_DocumentWidget_(d);
return iFalse;
}
if (!d->translation) {
d->translation = new_Translation(d);
if (isUsingPanelLayout_Mobile()) {
const iRect safe = safeRect_Root(w->root);
d->translation->dlg->rect.pos = windowToLocal_Widget(w, zero_I2());
d->translation->dlg->rect.size = safe.size;
}
}
return iTrue;
}
const iBool wasHandled = handleCommand_Translation(d->translation, cmd);
if (isFinished_Translation(d->translation)) {
delete_Translation(d->translation);
d->translation = NULL;
}
return wasHandled;
if (findChild_Widget(root_Widget(w), "upload")) {
return iTrue; /* already open */
}
const iBool isGemini = equalCase_Rangecc(urlScheme_String(d->mod.url), "gemini");
if (isGemini || equalCase_Rangecc(urlScheme_String(d->mod.url), "titan")) {
iUploadWidget *upload = new_UploadWidget();
setUrl_UploadWidget(upload, d->mod.url);
setResponseViewer_UploadWidget(upload, d);
addChild_Widget(get_Root()->widget, iClob(upload));
setupSheetTransition_Mobile(as_Widget(upload), iTrue);
postRefresh_App();
}
return iTrue;
}
case started_ClickResult:
if (d->grabbedPlayer) {
return iTrue;
}
/* Enable hover state now that scrolling has surely finished. */
if (d->flags & noHoverWhileScrolling_DocumentWidgetFlag) {
d->flags &= ~noHoverWhileScrolling_DocumentWidgetFlag;
updateHover_DocumentView_(view, mouseCoord_Window(get_Window(), ev->button.which));
}
if (~flags_Widget(w) & touchDrag_WidgetFlag) {
iChangeFlags(d->flags, selecting_DocumentWidgetFlag, iFalse);
iChangeFlags(d->flags, selectWords_DocumentWidgetFlag, d->click.count == 2);
iChangeFlags(d->flags, selectLines_DocumentWidgetFlag, d->click.count >= 3);
/* Double/triple clicks marks the selection immediately. */
if (d->click.count >= 2) {
beginMarkingSelection_DocumentWidget_(d, d->click.startPos);
extendRange_Rangecc(
&d->selectMark,
range_String(source_GmDocument(view->doc)),
bothStartAndEnd_RangeExtension |
(d->click.count == 2 ? word_RangeExtension : line_RangeExtension));
d->initialSelectMark = d->selectMark;
refresh_Widget(w);
}
else {
d->initialSelectMark = iNullRange;
}
return handleMediaCommand_DocumentWidget_(d, cmd);
/* When one media player starts, pause the others that may be playing. */
const iPlayer *startedPlr = pointerLabel_Command(cmd, "player");
const iMedia * media = media_GmDocument(d->view.doc);
const size_t num = numAudio_Media(media);
for (size_t id = 1; id <= num; id++) {
iPlayer *plr = audioPlayer_Media(media, (iMediaId){ audio_MediaType, id });
if (plr != startedPlr) {
setPaused_Player(plr, iTrue);
}
}
updateMedia_DocumentWidget_(d);
return iFalse;
if (cancelRequest_DocumentWidget_(d, iTrue /* navigate back */)) {
return iTrue;
case drag_ClickResult: {
if (d->grabbedPlayer) {
iPlayer *plr =
audioPlayer_Media(media_GmDocument(view->doc), mediaId_GmRun(d->grabbedPlayer));
iPlayerUI ui;
init_PlayerUI(&ui, plr, runRect_DocumentView_(view, d->grabbedPlayer));
float off = (float) delta_Click(&d->click).x / (float) width_Rect(ui.volumeSlider);
setVolume_Player(plr, d->grabbedStartVolume + off);
refresh_Widget(w);
return iTrue;
}
/* Fold/unfold a preformatted block. */
if (~d->flags & selecting_DocumentWidgetFlag && view->hoverPre &&
preIsFolded_GmDocument(view->doc, preId_GmRun(view->hoverPre))) {
return iTrue;
}
/* Begin selecting a range of text. */
if (~d->flags & selecting_DocumentWidgetFlag) {
beginMarkingSelection_DocumentWidget_(d, d->click.startPos);
}
iRangecc loc = sourceLoc_DocumentView_(view, pos_Click(&d->click));
if (d->selectMark.start == NULL) {
d->selectMark = loc;
}
const iGmLinkId linkId = argLabel_Command(cmd, "link");
const iMediaRequest *media = findMediaRequest_DocumentWidget_(d, linkId);
if (media) {
saveToDownloads_(url_GmRequest(media->req), meta_GmRequest(media->req),
body_GmRequest(media->req), iTrue);
}
if (d->request) {
makeSimpleMessage_Widget(uiTextCaution_ColorEscape "${heading.save.incomplete}",
"${dlg.save.incomplete}");
}
else if (!isEmpty_Block(&d->sourceContent)) {
const iBool doOpen = argLabel_Command(cmd, "open");
const iString *savePath = saveToDownloads_(d->mod.url, &d->sourceMime,
&d->sourceContent, !doOpen);
if (!isEmpty_String(savePath) && doOpen) {
postCommandf_Root(
w->root, "!open url:%s", cstrCollect_String(makeFileUrl_String(savePath)));
}
else if (loc.end) {
if (flags_Widget(w) & touchDrag_WidgetFlag) {
/* Choose which end to move. */
if (!(d->flags & (movingSelectMarkStart_DocumentWidgetFlag |
movingSelectMarkEnd_DocumentWidgetFlag))) {
const iRangecc mark = selectMark_DocumentWidget_(d);
const char * midMark = mark.start + size_Range(&mark) / 2;
const iRangecc loc = sourceLoc_DocumentView_(view, pos_Click(&d->click));
const iBool isCloserToStart = d->selectMark.start > d->selectMark.end ?
(loc.start > midMark) : (loc.start < midMark);
iChangeFlags(d->flags, movingSelectMarkStart_DocumentWidgetFlag, isCloserToStart);
iChangeFlags(d->flags, movingSelectMarkEnd_DocumentWidgetFlag, !isCloserToStart);
}
/* Move the start or the end depending on which is nearer. */
if (d->flags & movingSelectMarkStart_DocumentWidgetFlag) {
d->selectMark.start = loc.start;
}
else {
d->selectMark.end = (d->selectMark.end > d->selectMark.start ? loc.end : loc.start);
}
}
return iTrue;
d->initNormScrollY = normScrollPos_DocumentView_(&d->view);
if (equalCase_Rangecc(urlScheme_String(d->mod.url), "titan")) {
/* Reopen so the Upload dialog gets shown. */
postCommandf_App("open url:%s", cstr_String(d->mod.url));
return iTrue;
}
fetch_DocumentWidget_(d);
return iTrue;
if (argLabel_Command(cmd, "release")) {
setLinkNumberMode_DocumentWidget_(d, iFalse);
}
else if (argLabel_Command(cmd, "more")) {
if (d->flags & showLinkNumbers_DocumentWidgetFlag &&
d->ordinalMode == homeRow_DocumentLinkOrdinalMode) {
const size_t numKeys = iElemCount(homeRowKeys_);
const iGmRun *last = lastVisibleLink_DocumentView_(&d->view);
if (!last) {
d->ordinalBase = 0;
}
else {
d->selectMark.end = loc.end;// (d->selectMark.end > d->selectMark.start ? loc.end : loc.start);
if (loc.start < d->initialSelectMark.start) {
d->selectMark.end = loc.start;
}
if (isEmpty_Range(&d->selectMark)) {
d->selectMark = d->initialSelectMark;
d->ordinalBase += numKeys;
if (visibleLinkOrdinal_DocumentView_(&d->view, last->linkId) < d->ordinalBase) {
d->ordinalBase = 0;
}
}
}
iAssert((!d->selectMark.start && !d->selectMark.end) ||
( d->selectMark.start && d->selectMark.end));
/* Extend to full words/paragraphs. */
if (d->flags & (selectWords_DocumentWidgetFlag | selectLines_DocumentWidgetFlag)) {
extendRange_Rangecc(
&d->selectMark,
range_String(source_GmDocument(view->doc)),
(d->flags & movingSelectMarkStart_DocumentWidgetFlag ? moveStart_RangeExtension
: moveEnd_RangeExtension) |
(d->flags & selectWords_DocumentWidgetFlag ? word_RangeExtension
: line_RangeExtension));
if (d->flags & movingSelectMarkStart_DocumentWidgetFlag) {
d->initialSelectMark.start =
d->initialSelectMark.end = d->selectMark.start;
}
else if (~d->flags & showLinkNumbers_DocumentWidgetFlag) {
d->ordinalMode = homeRow_DocumentLinkOrdinalMode;
d->ordinalBase = 0;
setLinkNumberMode_DocumentWidget_(d, iTrue);
}
}
else {
d->ordinalMode = arg_Command(cmd);
d->ordinalBase = 0;
setLinkNumberMode_DocumentWidget_(d, iTrue);
iChangeFlags(d->flags, setHoverViaKeys_DocumentWidgetFlag,
argLabel_Command(cmd, "hover") != 0);
iChangeFlags(d->flags, newTabViaHomeKeys_DocumentWidgetFlag,
argLabel_Command(cmd, "newtab") != 0);
}
invalidateVisibleLinks_DocumentView_(&d->view);
refresh_Widget(d);
return iTrue;
if (d->request) {
postCommandf_Root(w->root,
"document.request.cancelled doc:%p url:%s", d, cstr_String(d->mod.url));
iReleasePtr(&d->request);
updateFetchProgress_DocumentWidget_(d);
}
goBack_History(d->mod.history);
return iTrue;
goForward_History(d->mod.history);
return iTrue;
iUrl parts;
init_Url(&parts, d->mod.url);
/* Remove the last path segment. */
if (size_Range(&parts.path) > 1) {
if (parts.path.end[-1] == '/') {
parts.path.end--;
}
if (d->initialSelectMark.start) {
if (d->selectMark.end > d->selectMark.start) {
d->selectMark.start = d->initialSelectMark.start;
}
else if (d->selectMark.end < d->selectMark.start) {
d->selectMark.start = d->initialSelectMark.end;
}
while (parts.path.end > parts.path.start) {
if (parts.path.end[-1] == '/') break;
parts.path.end--;
}
-// printf("mark %zu ... %zu\n", d->selectMark.start - cstr_String(source_GmDocument(d->doc)),
-// d->selectMark.end - cstr_String(source_GmDocument(d->doc)));
-// fflush(stdout);
refresh_Widget(w);
postCommandf_Root(w->root,
"open url:%s",
cstr_Rangecc((iRangecc){ constBegin_String(d->mod.url), parts.path.end }));
}
return iTrue;
postCommandf_Root(w->root, "open url:%s/", cstr_Rangecc(urlRoot_String(d->mod.url)));
return iTrue;
init_Anim(&d->view.scrollY.pos, arg_Command(cmd));
updateVisible_DocumentView_(&d->view);
return iTrue;
const int dir = arg_Command(cmd);
if (dir > 0 && !argLabel_Command(cmd, "repeat") &&
prefs_App()->loadImageInsteadOfScrolling &&
fetchNextUnfetchedImage_DocumentWidget_(d)) {
return iTrue;
}
case finished_ClickResult:
if (d->grabbedPlayer) {
setGrabbedPlayer_DocumentWidget_(d, NULL);
const float amount = argLabel_Command(cmd, "full") != 0 ? 1.0f : 0.5f;
smoothScroll_DocumentView_(&d->view,
dir * amount *
height_Rect(documentBounds_DocumentView_(&d->view)),
smoothDuration_DocumentWidget_(keyboard_ScrollType));
return iTrue;
init_Anim(&d->view.scrollY.pos, 0);
invalidate_VisBuf(d->view.visBuf);
clampScroll_DocumentView_(&d->view);
updateVisible_DocumentView_(&d->view);
refresh_Widget(w);
return iTrue;
updateScrollMax_DocumentView_(&d->view); /* scrollY.max might not be fully updated */
init_Anim(&d->view.scrollY.pos, d->view.scrollY.max);
invalidate_VisBuf(d->view.visBuf);
clampScroll_DocumentView_(&d->view);
updateVisible_DocumentView_(&d->view);
refresh_Widget(w);
return iTrue;
const int dir = arg_Command(cmd);
if (dir > 0 && !argLabel_Command(cmd, "repeat") &&
prefs_App()->loadImageInsteadOfScrolling &&
fetchNextUnfetchedImage_DocumentWidget_(d)) {
return iTrue;
}
smoothScroll_DocumentView_(&d->view,
3 * lineHeight_Text(paragraph_FontId) * dir,
smoothDuration_DocumentWidget_(keyboard_ScrollType));
return iTrue;
const char *heading = suffixPtr_Command(cmd, "heading");
if (heading) {
if (isRequestOngoing_DocumentWidget(d)) {
/* Scroll position set when request finishes. */
setCStr_String(&d->pendingGotoHeading, heading);
return iTrue;
}
if (isVisible_Widget(d->menu)) {
closeMenu_Widget(d->menu);
scrollToHeading_DocumentView_(&d->view, heading);
return iTrue;
}
const char *loc = pointerLabel_Command(cmd, "loc");
const iGmRun *run = findRunAtLoc_GmDocument(d->view.doc, loc);
if (run) {
scrollTo_DocumentView_(&d->view, run->visBounds.pos.y, iFalse);
}
return iTrue;
document_App() == d) {
const int dir = equal_Command(cmd, "find.next") ? +1 : -1;
iRangecc (*finder)(const iGmDocument *, const iString *, const char *) =
dir > 0 ? findText_GmDocument : findTextBefore_GmDocument;
iInputWidget *find = findWidget_App("find.input");
if (isEmpty_String(text_InputWidget(find))) {
d->foundMark = iNullRange;
}
else {
const iBool wrap = d->foundMark.start != NULL;
d->foundMark = finder(d->view.doc, text_InputWidget(find), dir > 0 ? d->foundMark.end
: d->foundMark.start);
if (!d->foundMark.start && wrap) {
/* Wrap around. */
d->foundMark = finder(d->view.doc, text_InputWidget(find), NULL);
}
d->flags &= ~(movingSelectMarkStart_DocumentWidgetFlag |
movingSelectMarkEnd_DocumentWidgetFlag);
if (!isMoved_Click(&d->click)) {
setFocus_Widget(NULL);
/* Tap in tap selection mode. */
if (flags_Widget(w) & touchDrag_WidgetFlag) {
const iRangecc tapLoc = sourceLoc_DocumentView_(view, pos_Click(&d->click));
/* Tapping on the selection will show a menu. */
const iRangecc mark = selectMark_DocumentWidget_(d);
if (tapLoc.start >= mark.start && tapLoc.end <= mark.end) {
if (d->copyMenu) {
closeMenu_Widget(d->copyMenu);
destroy_Widget(d->copyMenu);
d->copyMenu = NULL;
}
d->copyMenu = makeMenu_Widget(w, (iMenuItem[]){
{ clipCopy_Icon " ${menu.copy}", 0, 0, "copy" },
{ "---" },
{ close_Icon " ${menu.select.clear}", 0, 0, "document.select arg:0" },
}, 3);
setFlags_Widget(d->copyMenu, noFadeBackground_WidgetFlag, iTrue);
openMenu_Widget(d->copyMenu, pos_Click(&d->click));
return iTrue;
}
else {
/* Tapping elsewhere exits selection mode. */
postCommand_Widget(d, "document.select arg:0");
return iTrue;
}
}
if (view->hoverPre) {
togglePreFold_DocumentWidget_(d, preId_GmRun(view->hoverPre));
return iTrue;
if (d->foundMark.start) {
const iGmRun *found;
if ((found = findRunAtLoc_GmDocument(d->view.doc, d->foundMark.start)) != NULL) {
scrollTo_DocumentView_(&d->view, mid_Rect(found->bounds).y, iTrue);
}
if (view->hoverLink) {
/* TODO: Move this to a method. */
const iGmLinkId linkId = view->hoverLink->linkId;
const iMediaId linkMedia = mediaId_GmRun(view->hoverLink);
const int linkFlags = linkFlags_GmDocument(view->doc, linkId);
iAssert(linkId);
/* Media links are opened inline by default. */
if (isMediaLink_GmDocument(view->doc, linkId)) {
if (linkFlags & content_GmLinkFlag && linkFlags & permanent_GmLinkFlag) {
/* We have the content and it cannot be dismissed, so nothing
further to do. */
return iTrue;
}
if (!requestMedia_DocumentWidget_(d, linkId, iTrue)) {
if (linkFlags & content_GmLinkFlag) {
/* Dismiss shown content on click. */
setData_Media(media_GmDocument(view->doc),
linkId,
NULL,
NULL,
allowHide_MediaFlag);
/* Cancel a partially received request. */ {
iMediaRequest *req = findMediaRequest_DocumentWidget_(d, linkId);
if (!isFinished_GmRequest(req->req)) {
cancel_GmRequest(req->req);
removeMediaRequest_DocumentWidget_(d, linkId);
/* Note: Some of the audio IDs have changed now, layout must
be redone. */
}
}
redoLayout_GmDocument(view->doc);
view->hoverLink = NULL;
clampScroll_DocumentView_(view);
updateVisible_DocumentView_(view);
invalidate_DocumentWidget_(d);
refresh_Widget(w);
return iTrue;
}
else {
/* Show the existing content again if we have it. */
iMediaRequest *req = findMediaRequest_DocumentWidget_(d, linkId);
if (req) {
setData_Media(media_GmDocument(view->doc),
linkId,
meta_GmRequest(req->req),
body_GmRequest(req->req),
allowHide_MediaFlag);
redoLayout_GmDocument(view->doc);
updateVisible_DocumentView_(view);
invalidate_DocumentWidget_(d);
refresh_Widget(w);
return iTrue;
}
}
}
refresh_Widget(w);
}
else if (linkMedia.type == download_MediaType ||
findMediaRequest_DocumentWidget_(d, linkId)) {
/* TODO: What should be done when clicking on an inline download?
Maybe dismiss if finished? */
return iTrue;
}
else if (linkFlags & supportedScheme_GmLinkFlag) {
int tabMode = openTabMode_Sym(modState_Keys());
if (isPinned_DocumentWidget_(d)) {
tabMode ^= otherRoot_OpenTabFlag;
}
interactingWithLink_DocumentWidget_(d, linkId);
postCommandf_Root(w->root, "open newtab:%d url:%s",
tabMode,
cstr_String(absoluteUrl_String(
d->mod.url, linkUrl_GmDocument(view->doc, linkId))));
}
else {
const iString *url = absoluteUrl_String(
d->mod.url, linkUrl_GmDocument(view->doc, linkId));
makeQuestion_Widget(
uiTextCaution_ColorEscape "${heading.openlink}",
format_CStr(
cstr_Lang("dlg.openlink.confirm"),
uiTextAction_ColorEscape,
cstr_String(url)),
(iMenuItem[]){
{ "${cancel}" },
{ uiTextCaution_ColorEscape "${dlg.openlink}",
0, 0, format_CStr("!open default:1 url:%s", cstr_String(url)) } },
2);
}
}
}
if (flags_Widget(w) & touchDrag_WidgetFlag) {
postCommand_Root(w->root, "document.select arg:0"); /* we can't handle both at the same time */
}
invalidateWideRunsWithNonzeroOffset_DocumentView_(&d->view); /* markers don't support offsets */
resetWideRuns_DocumentView_(&d->view);
refresh_Widget(w);
return iTrue;
if (d->foundMark.start) {
d->foundMark = iNullRange;
refresh_Widget(w);
}
return iTrue;
iPtrArray *links = collectNew_PtrArray();
render_GmDocument(d->view.doc, (iRangei){ 0, size_GmDocument(d->view.doc).y }, addAllLinks_, links);
/* Find links that aren't already bookmarked. */
iForEach(PtrArray, i, links) {
const iGmRun *run = i.ptr;
uint32_t bmid;
if ((bmid = findUrl_Bookmarks(bookmarks_App(),
linkUrl_GmDocument(d->view.doc, run->linkId))) != 0) {
const iBookmark *bm = get_Bookmarks(bookmarks_App(), bmid);
/* We can import local copies of remote bookmarks. */
if (~bm->flags & remote_BookmarkFlag) {
remove_PtrArrayIterator(&i);
}
if (d->selectMark.start && !(d->flags & (selectLines_DocumentWidgetFlag |
selectWords_DocumentWidgetFlag))) {
d->selectMark = iNullRange;
refresh_Widget(w);
}
}
if (!isEmpty_PtrArray(links)) {
if (argLabel_Command(cmd, "confirm")) {
const size_t count = size_PtrArray(links);
makeQuestion_Widget(
uiHeading_ColorEscape "${heading.import.bookmarks}",
formatCStrs_Lang("dlg.import.found.n", count),
(iMenuItem[]){ { "${cancel}" },
{ format_CStr(cstrCount_Lang("dlg.import.add.n", (int) count),
uiTextAction_ColorEscape,
count),
0,
0,
"bookmark.links" } },
2);
}
else {
iConstForEach(PtrArray, j, links) {
const iGmRun *run = j.ptr;
add_Bookmarks(bookmarks_App(),
linkUrl_GmDocument(d->view.doc, run->linkId),
collect_String(newRange_String(run->text)),
NULL,
0x1f588 /* pin */);
}
postCommand_App("bookmarks.changed");
}
return iTrue;
case aborted_ClickResult:
if (d->grabbedPlayer) {
setGrabbedPlayer_DocumentWidget_(d, NULL);
return iTrue;
}
else {
makeSimpleMessage_Widget(uiHeading_ColorEscape "${heading.import.bookmarks}",
"${dlg.import.notnew}");
}
return iTrue;
updateHover_DocumentView_(&d->view, mouseCoord_Window(get_Window(), 0));
if (d->mod.reloadInterval) {
if (!isValid_Time(&d->sourceTime) || elapsedSeconds_Time(&d->sourceTime) >=
seconds_ReloadInterval_(d->mod.reloadInterval)) {
postCommand_Widget(w, "document.reload");
}
return iTrue;
default:
break;
}
}
-}
-/----------------------------------------------------------------------------------------------/
-iDeclareType(DrawContext)
-struct Impl_DrawContext {
-};
-static void fillRange_DrawContext_(iDrawContext *d, const iGmRun *run, enum iColorId color,
iRangecc mark, iBool *isInside) {
/* Selection may be done in either direction. */
iSwap(const char *, mark.start, mark.end);
iArray *items = collectNew_Array(sizeof(iMenuItem));
for (int i = 0; i < max_ReloadInterval; ++i) {
pushBack_Array(items, &(iMenuItem){
format_CStr("%s%s", ((int) d->mod.reloadInterval == i ? "&" : "*"),
label_ReloadInterval_(i)),
0,
0,
format_CStr("document.autoreload.set arg:%d", i) });
}
pushBack_Array(items, &(iMenuItem){ "${cancel}", 0, 0, NULL });
makeQuestion_Widget(uiTextAction_ColorEscape "${heading.autoreload}",
"${dlg.autoreload}",
constData_Array(items), size_Array(items));
return iTrue;
}
contains_Range(&mark, run->text.start))) {
int x = 0;
if (!*isInside) {
x = measureRange_Text(run->font,
(iRangecc){ run->text.start, iMax(run->text.start, mark.start) })
.advance.x;
d->mod.reloadInterval = arg_Command(cmd);
const iString *site = collectNewRange_String(urlRoot_String(d->mod.url));
const int dismissed = value_SiteSpec(site, dismissWarnings_SiteSpecKey);
const int arg = argLabel_Command(cmd, "warning");
setValue_SiteSpec(site, dismissWarnings_SiteSpecKey, dismissed | arg);
if (arg == ansiEscapes_GmDocumentWarning) {
remove_Banner(d->banner, ansiEscapes_GmStatusCode);
refresh_Widget(w);
}
int w = width_Rect(run->visBounds) - x;
if (contains_Range(&run->text, mark.end) || mark.end < run->text.start) {
iRangecc mk = !*isInside ? mark
: (iRangecc){ run->text.start, iMax(run->text.start, mark.end) };
mk.start = iMax(mk.start, run->text.start);
w = measureRange_Text(run->font, mk).advance.x;
*isInside = iFalse;
return iTrue;
return handlePinch_DocumentWidget_(d, cmd);
document_App() == d) {
return handleSwipe_DocumentWidget_(d, cmd);
if (!isRequestOngoing_DocumentWidget(d)) {
setUrlAndSource_DocumentWidget(d, d->mod.url, string_Command(cmd, "mime"),
&d->sourceContent);
}
return iTrue;
if (argLabel_Command(cmd, "ttf")) {
iAssert(!cmp_String(&d->sourceMime, "font/ttf"));
installFontFile_Fonts(collect_String(suffix_Command(cmd, "name")), &d->sourceContent);
postCommand_App("open url:about:fonts");
}
else {
*isInside = iTrue; /* at least until the next run */
const iString *id = idFromUrl_FontPack(d->mod.url);
install_Fonts(id, &d->sourceContent);
postCommandf_App("open gotoheading:%s url:about:fonts", cstr_String(id));
}
if (w > width_Rect(run->visBounds) - x) {
w = width_Rect(run->visBounds) - x;
return iTrue;
+}
+static void setGrabbedPlayer_DocumentWidget_(iDocumentWidget *d, const iGmRun *run) {
iPlayer *plr = audioPlayer_Media(media_GmDocument(d->view.doc), mediaId_GmRun(run));
setFlags_Player(plr, volumeGrabbed_PlayerFlag, iTrue);
d->grabbedStartVolume = volume_Player(plr);
d->grabbedPlayer = run;
refresh_Widget(d);
setFlags_Player(
audioPlayer_Media(media_GmDocument(d->view.doc), mediaId_GmRun(d->grabbedPlayer)),
volumeGrabbed_PlayerFlag,
iFalse);
d->grabbedPlayer = NULL;
refresh_Widget(d);
iAssert(iFalse);
+}
+static iBool processMediaEvents_DocumentWidget_(iDocumentWidget *d, const SDL_Event *ev) {
ev->type != SDL_MOUSEMOTION) {
return iFalse;
if (ev->button.button != SDL_BUTTON_LEFT) {
return iFalse;
}
if (~run->flags & decoration_GmRunFlag) {
const iInt2 visPos =
add_I2(run->bounds.pos, addY_I2(d->viewPos, viewPos_DocumentView_(d->view)));
const iRect rangeRect = { addX_I2(visPos, x), init_I2(w, height_Rect(run->bounds)) };
if (rangeRect.size.x) {
fillRect_Paint(&d->paint, rangeRect, color);
/* Keep track of the first and last marked rects. */
if (d->firstMarkRect.size.x == 0) {
d->firstMarkRect = rangeRect;
/* Updated in the drag. */
return iFalse;
const iGmRun *run = i.ptr;
if (run->mediaType != audio_MediaType) {
continue;
}
/* TODO: move this to mediaui.c */
const iRect rect = runRect_DocumentView_(&d->view, run);
iPlayer * plr = audioPlayer_Media(media_GmDocument(d->view.doc), mediaId_GmRun(run));
if (contains_Rect(rect, mouse)) {
iPlayerUI ui;
init_PlayerUI(&ui, plr, rect);
if (ev->type == SDL_MOUSEBUTTONDOWN && flags_Player(plr) & adjustingVolume_PlayerFlag &&
contains_Rect(adjusted_Rect(ui.volumeAdjustRect,
zero_I2(),
init_I2(-height_Rect(ui.volumeAdjustRect), 0)),
mouse)) {
setGrabbedPlayer_DocumentWidget_(d, run);
processEvent_Click(&d->click, ev);
/* The rest is done in the DocumentWidget click responder. */
refresh_Widget(d);
return iTrue;
}
else if (ev->type == SDL_MOUSEBUTTONDOWN || ev->type == SDL_MOUSEMOTION) {
refresh_Widget(d);
return iTrue;
}
if (contains_Rect(ui.playPauseRect, mouse)) {
setPaused_Player(plr, !isPaused_Player(plr));
animateMedia_DocumentWidget_(d);
return iTrue;
}
else if (contains_Rect(ui.rewindRect, mouse)) {
if (isStarted_Player(plr) && time_Player(plr) > 0.5f) {
stop_Player(plr);
start_Player(plr);
setPaused_Player(plr, iTrue);
}
d->lastMarkRect = rangeRect;
refresh_Widget(d);
return iTrue;
}
else if (contains_Rect(ui.volumeRect, mouse)) {
setFlags_Player(plr,
adjustingVolume_PlayerFlag,
!(flags_Player(plr) & adjustingVolume_PlayerFlag));
animateMedia_DocumentWidget_(d);
refresh_Widget(d);
return iTrue;
}
else if (contains_Rect(ui.menuRect, mouse)) {
/* TODO: Add menu items for:
- output device
- Save to Downloads
*/
if (d->playerMenu) {
destroy_Widget(d->playerMenu);
d->playerMenu = NULL;
return iTrue;
}
d->playerMenu = makeMenu_Widget(
as_Widget(d),
(iMenuItem[]){
{ cstrCollect_String(metadataLabel_Player(plr)) },
},
1);
openMenu_Widget(d->playerMenu, bottomLeft_Rect(ui.menuRect));
return iTrue;
}
}
}
these ranges as a special case. */
const iRangecc url = linkUrlRange_GmDocument(d->view->doc, run->linkId);
if (contains_Range(&url, mark.start) &&
(contains_Range(&url, mark.end) || url.end == mark.end)) {
fillRect_Paint(
&d->paint,
moved_Rect(run->visBounds, addY_I2(d->viewPos, viewPos_DocumentView_(d->view))),
color);
}
}
-static void drawMark_DrawContext_(void *context, const iGmRun *run) {
fillRange_DrawContext_(d, run, uiMatching_ColorId, d->view->owner->foundMark, &d->inFoundMark);
fillRange_DrawContext_(d, run, uiMarked_ColorId, d->view->owner->selectMark, &d->inSelectMark);
+static void beginMarkingSelection_DocumentWidget_(iDocumentWidget *d, iInt2 pos) {
}
-static void drawRun_DrawContext_(void *context, const iGmRun *run) {
if (!d->runsDrawn.start || run < d->runsDrawn.start) {
d->runsDrawn.start = run;
}
if (!d->runsDrawn.end || run > d->runsDrawn.end) {
d->runsDrawn.end = run;
}
SDL_Texture *tex = imageTexture_Media(media_GmDocument(d->view->doc), mediaId_GmRun(run));
const iRect dst = moved_Rect(run->visBounds, origin);
if (tex) {
fillRect_Paint(&d->paint, dst, tmBackground_ColorId); /* in case the image has alpha */
SDL_RenderCopy(d->paint.dst->render, tex, NULL,
&(SDL_Rect){ dst.pos.x, dst.pos.y, dst.size.x, dst.size.y });
}
else {
drawRect_Paint(&d->paint, dst, tmQuoteIcon_ColorId);
drawCentered_Text(uiLabel_FontId,
dst,
iFalse,
tmQuote_ColorId,
explosion_Icon " Error Loading Image");
}
+static void interactingWithLink_DocumentWidget_(iDocumentWidget *d, iGmLinkId id) {
clear_String(&d->linePrecedingLink);
return;
}
/* Media UIs are drawn afterwards as a dynamic overlay. */
return;
loc.start--;
}
(run->linkId && d->view->hoverLink && run->linkId == d->view->hoverLink->linkId &&
~run->flags & decoration_GmRunFlag);
/* Preformatted runs can be scrolled. */
runOffset_DocumentView_(d->view, run));
-#if 0
iBool isInlineImageCaption = run->linkId && linkFlags & content_GmLinkFlag &&
~linkFlags & permanent_GmLinkFlag;
if (run->flags & decoration_GmRunFlag && ~run->flags & startOfLine_GmRunFlag) {
/* This is the metadata. */
isInlineImageCaption = iFalse;
}
-#endif
/* While this is consistent, it's a bit excessive to indicate that an inlined image
is open: the image itself is the indication. */
const iBool isInlineImageCaption = iFalse;
if (run->linkId && (linkFlags & isOpen_GmLinkFlag || isInlineImageCaption)) {
/* Open links get a highlighted background. */
int bg = tmBackgroundOpenLink_ColorId;
const int frame = tmFrameOpenLink_ColorId;
const int pad = gap_Text;
iRect wideRect = { init_I2(origin.x - pad, visPos.y),
init_I2(d->docBounds.size.x + 2 * pad,
height_Rect(run->visBounds)) };
adjustEdges_Rect(&wideRect,
run->flags & startOfLine_GmRunFlag ? -pad * 3 / 4 : 0, 0,
run->flags & endOfLine_GmRunFlag ? pad * 3 / 4 : 0, 0);
/* The first line is composed of two runs that may be drawn in either order, so
only draw half of the background. */
if (run->flags & decoration_GmRunFlag) {
wideRect.size.x = right_Rect(visRect) - left_Rect(wideRect);
}
else if (run->flags & startOfLine_GmRunFlag) {
wideRect.size.x = right_Rect(wideRect) - left_Rect(visRect);
wideRect.pos.x = left_Rect(visRect);
}
fillRect_Paint(&d->paint, wideRect, bg);
}
else {
/* Normal background for other runs. There are cases when runs get drawn multiple times,
e.g., at the buffer boundary, and there are slightly overlapping characters in
monospace blocks. Clearing the background here ensures a cleaner visual appearance
since only one glyph is visible at any given point. */
fillRect_Paint(&d->paint, visRect, tmBackground_ColorId);
}
loc.start--;
}
if (run->flags & decoration_GmRunFlag && run->flags & startOfLine_GmRunFlag) {
/* Link icon. */
if (linkFlags & content_GmLinkFlag) {
fg = linkColor_GmDocument(doc, run->linkId, textHover_GmLinkPart);
}
}
else if (~run->flags & decoration_GmRunFlag) {
fg = linkColor_GmDocument(doc, run->linkId, isHover ? textHover_GmLinkPart : text_GmLinkPart);
if (linkFlags & content_GmLinkFlag) {
fg = linkColor_GmDocument(doc, run->linkId, textHover_GmLinkPart); /* link is inactive */
}
loc.start--;
loc.start++; /* Start of the preceding line. */
+}
+iLocalDef int wheelSwipeSide_DocumentWidget_(const iDocumentWidget *d) {
: d->flags & leftWheelSwipe_DocumentWidgetFlag ? 1
: 0);
+}
+static void finishWheelSwipe_DocumentWidget_(iDocumentWidget *d) {
d->wheelSwipeState == direct_WheelSwipeState) {
const int side = wheelSwipeSide_DocumentWidget_(d);
int abort = ((side == 1 && d->swipeSpeed < 0) || (side == 2 && d->swipeSpeed > 0));
if (iAbs(d->wheelSwipeDistance) < width_Widget(d) / 4 && iAbs(d->swipeSpeed) < 4 * gap_UI) {
abort = 1;
}
postCommand_Widget(d, "edgeswipe.ended side:%d abort:%d", side, abort);
d->flags &= ~eitherWheelSwipe_DocumentWidgetFlag;
}
const iInt2 margin = preRunMargin_GmDocument(doc, preId_GmRun(run));
fillRect_Paint(&d->paint, (iRect){ visPos, run->visBounds.size }, tmBackgroundAltText_ColorId);
drawRect_Paint(&d->paint, (iRect){ visPos, run->visBounds.size }, tmFrameAltText_ColorId);
drawWrapRange_Text(run->font,
add_I2(visPos, margin),
run->visBounds.size.x - 2 * margin.x,
run->color,
run->text);
+}
+static iBool handleWheelSwipe_DocumentWidget_(iDocumentWidget *d, const SDL_MouseWheelEvent *ev) {
return iFalse;
}
if (d->showLinkNumbers && run->linkId && run->flags & decoration_GmRunFlag) {
const size_t ord = visibleLinkOrdinal_DocumentView_(d->view, run->linkId);
if (ord >= d->view->owner->ordinalBase) {
const iChar ordChar =
linkOrdinalChar_DocumentWidget_(d->view->owner, ord - d->view->owner->ordinalBase);
if (ordChar) {
const char *circle = "\u25ef"; /* Large Circle */
const int circleFont = FONT_ID(default_FontId, regular_FontStyle, contentRegular_FontSize);
iRect nbArea = { init_I2(d->viewPos.x - gap_UI / 3, visPos.y),
init_I2(3.95f * gap_Text, 1.0f * lineHeight_Text(circleFont)) };
drawRange_Text(
circleFont, topLeft_Rect(nbArea), tmQuote_ColorId, range_CStr(circle));
iRect circleArea = visualBounds_Text(circleFont, range_CStr(circle));
addv_I2(&circleArea.pos, topLeft_Rect(nbArea));
drawCentered_Text(FONT_ID(default_FontId, regular_FontStyle, contentSmall_FontSize),
circleArea,
iTrue,
tmQuote_ColorId,
"%lc",
(int) ordChar);
goto runDrawn;
return iFalse;
+// printf("STATE:%d wheel x:%d inert:%d end:%d\n", d->wheelSwipeState,
+// ev->x, isInertia_MouseWheelEvent(ev),
+// isScrollFinished_MouseWheelEvent(ev));
+// fflush(stdout);
case none_WheelSwipeState:
/* A new swipe starts. */
if (!isInertia_MouseWheelEvent(ev) && !isScrollFinished_MouseWheelEvent(ev)) {
int side = ev->x > 0 ? 1 : 2;
d->wheelSwipeDistance = ev->x * 2;
d->flags &= ~eitherWheelSwipe_DocumentWidgetFlag;
d->flags |= (side == 1 ? leftWheelSwipe_DocumentWidgetFlag
: rightWheelSwipe_DocumentWidgetFlag);
// printf("swipe starts at %d, side %d\n", d->wheelSwipeDistance, side);
d->wheelSwipeState = direct_WheelSwipeState;
d->swipeSpeed = 0;
postCommand_Widget(d, "edgeswipe.moved arg:%d side:%d", d->wheelSwipeDistance, side);
return iTrue;
}
break;
case direct_WheelSwipeState:
if (isInertia_MouseWheelEvent(ev) || isScrollFinished_MouseWheelEvent(ev)) {
finishWheelSwipe_DocumentWidget_(d);
d->wheelSwipeState = none_WheelSwipeState;
}
else {
int step = ev->x * 2;
d->wheelSwipeDistance += step;
/* Remember the maximum speed. */
if (d->swipeSpeed < 0 && step < 0) {
d->swipeSpeed = iMin(d->swipeSpeed, step);
}
else if (d->swipeSpeed > 0 && step > 0) {
d->swipeSpeed = iMax(d->swipeSpeed, step);
}
else {
d->swipeSpeed = step;
}
switch (wheelSwipeSide_DocumentWidget_(d)) {
case 1:
d->wheelSwipeDistance = iMax(0, d->wheelSwipeDistance);
d->wheelSwipeDistance = iMin(width_Widget(d), d->wheelSwipeDistance);
break;
case 2:
d->wheelSwipeDistance = iMin(0, d->wheelSwipeDistance);
d->wheelSwipeDistance = iMax(-width_Widget(d), d->wheelSwipeDistance);
break;
}
/* TODO: calculate speed, rememeber direction */
//printf("swipe moved to %d, side %d\n", d->wheelSwipeDistance, side);
postCommand_Widget(d, "edgeswipe.moved arg:%d side:%d", d->wheelSwipeDistance,
wheelSwipeSide_DocumentWidget_(d));
}
return iTrue;
+}
+static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *ev) {
updateSize_DocumentWidget(d);
return iTrue;
if (isCommand_Widget(w, ev, "pullaction")) {
postCommand_Widget(w, "navigate.reload");
return iTrue;
}
if (run->flags & quoteBorder_GmRunFlag) {
drawVLine_Paint(&d->paint,
addX_I2(visPos,
!run->isRTL
? -gap_Text * 5 / 2
: (width_Rect(run->visBounds) + gap_Text * 5 / 2)),
height_Rect(run->visBounds),
tmQuoteIcon_ColorId);
}
/* Base attributes. */ {
int f, c;
runBaseAttributes_GmDocument(doc, run, &f, &c);
setBaseAttributes_Text(f, c);
if (!handleCommand_DocumentWidget_(d, command_UserEvent(ev))) {
/* Base class commands. */
return processEvent_Widget(w, ev);
}
drawBoundRange_Text(run->font,
visPos,
(run->isRTL ? -1 : 1) * width_Rect(run->visBounds),
fg,
run->text);
setBaseAttributes_Text(-1, -1);
return iTrue;
}
const int metaFont = paragraph_FontId;
/* TODO: Show status of an ongoing media request. */
const int flags = linkFlags;
const iRect linkRect = moved_Rect(run->visBounds, origin);
iMediaRequest *mr = NULL;
/* Show metadata about inline content. */
if (flags & content_GmLinkFlag && run->flags & endOfLine_GmRunFlag) {
fg = linkColor_GmDocument(doc, run->linkId, textHover_GmLinkPart);
iString text;
init_String(&text);
const iMediaId linkMedia = findMediaForLink_Media(constMedia_GmDocument(doc),
run->linkId, none_MediaType);
iAssert(linkMedia.type != none_MediaType);
iGmMediaInfo info;
info_Media(constMedia_GmDocument(doc), linkMedia, &info);
switch (linkMedia.type) {
case image_MediaType: {
/* There's a separate decorative GmRun for the metadata. */
break;
const int key = ev->key.keysym.sym;
if ((d->flags & showLinkNumbers_DocumentWidgetFlag) &&
((key >= '1' && key <= '9') || (key >= 'a' && key <= 'z'))) {
const size_t ord = linkOrdinalFromKey_DocumentWidget_(d, key) + d->ordinalBase;
iConstForEach(PtrArray, i, &d->view.visibleLinks) {
if (ord == iInvalidPos) break;
const iGmRun *run = i.ptr;
if (run->flags & decoration_GmRunFlag &&
visibleLinkOrdinal_DocumentView_(view, run->linkId) == ord) {
if (d->flags & setHoverViaKeys_DocumentWidgetFlag) {
view->hoverLink = run;
}
else {
postCommandf_Root(
w->root,
"open newtab:%d url:%s",
(isPinned_DocumentWidget_(d) ? otherRoot_OpenTabFlag : 0) ^
(d->ordinalMode == numbersAndAlphabet_DocumentLinkOrdinalMode
? openTabMode_Sym(modState_Keys())
: (d->flags & newTabViaHomeKeys_DocumentWidgetFlag ? 1 : 0)),
cstr_String(absoluteUrl_String(
d->mod.url, linkUrl_GmDocument(view->doc, run->linkId))));
interactingWithLink_DocumentWidget_(d, run->linkId);
}
setLinkNumberMode_DocumentWidget_(d, iFalse);
invalidateVisibleLinks_DocumentView_(view);
refresh_Widget(d);
return iTrue;
}
case audio_MediaType:
format_String(&text, "%s", info.type);
break;
case download_MediaType:
format_String(&text, "%s", info.type);
break;
default:
break;
}
if (linkMedia.type != download_MediaType && /* can't cancel downloads currently */
linkMedia.type != image_MediaType &&
findMediaRequest_DocumentWidget_(d->view->owner, run->linkId)) {
appendFormat_String(
&text, " %s" close_Icon, isHover ? escape_Color(tmLinkText_ColorId) : "");
}
switch (key) {
case SDLK_ESCAPE:
if (d->flags & showLinkNumbers_DocumentWidgetFlag && document_App() == d) {
setLinkNumberMode_DocumentWidget_(d, iFalse);
invalidateVisibleLinks_DocumentView_(view);
refresh_Widget(d);
return iTrue;
}
break;
+#if !defined (NDEBUG)
case SDLK_KP_1:
case '`': {
iBlock *seed = new_Block(64);
for (size_t i = 0; i < 64; ++i) {
setByte_Block(seed, i, iRandom(0, 256));
}
setThemeSeed_GmDocument(view->doc, seed);
delete_Block(seed);
invalidate_DocumentWidget_(d);
refresh_Widget(w);
break;
}
const iInt2 size = measureRange_Text(metaFont, range_String(&text)).bounds.size;
if (size.x) {
fillRect_Paint(
&d->paint,
(iRect){ add_I2(origin, addX_I2(topRight_Rect(run->bounds), -size.x - gap_UI)),
addX_I2(size, 2 * gap_UI) },
tmBackground_ColorId);
drawAlign_Text(metaFont,
add_I2(topRight_Rect(run->bounds), origin),
fg,
right_Alignment,
"%s", cstr_String(&text));
+#endif
+#if 0
case '0': {
extern int enableHalfPixelGlyphs_Text;
enableHalfPixelGlyphs_Text = !enableHalfPixelGlyphs_Text;
refresh_Widget(w);
printf("halfpixel: %d\n", enableHalfPixelGlyphs_Text);
fflush(stdout);
break;
}
deinit_String(&text);
}
else if (run->flags & endOfLine_GmRunFlag &&
(mr = findMediaRequest_DocumentWidget_(d->view->owner, run->linkId)) != NULL) {
if (!isFinished_GmRequest(mr->req)) {
draw_Text(metaFont,
topRight_Rect(linkRect),
tmInlineContentMetadata_ColorId,
translateCStr_Lang(" \u2014 ${doc.fetching}\u2026 (%.1f ${mb})"),
(float) bodySize_GmRequest(mr->req) / 1.0e6f);
+#endif
+#if 0
case '0': {
extern int enableKerning_Text;
enableKerning_Text = !enableKerning_Text;
invalidate_DocumentWidget_(d);
refresh_Widget(w);
printf("kerning: %d\n", enableKerning_Text);
fflush(stdout);
break;
}
+#endif
}
}
drawRect_Paint(&d->paint, (iRect){ visPos, run->bounds.size }, green_ColorId);
drawRect_Paint(&d->paint, (iRect){ visPos, run->visBounds.size }, red_ColorId);
-}
-static int drawSideRect_(iPaint *p, iRect rect) {
bg = tmBannerIcon_ColorId;
fg = tmBannerBackground_ColorId;
-}
-static int sideElementAvailWidth_DocumentView_(const iDocumentView *d) {
left_Rect(bounds_Widget(constAs_Widget(d->owner))) - 2 * d->pageMargin * gap_UI;
-}
-static iBool isSideHeadingVisible_DocumentView_(const iDocumentView *d) {
-}
-static void updateSideIconBuf_DocumentView_(const iDocumentView *d) {
return;
SDL_DestroyTexture(dbuf->sideIconBuf);
dbuf->sideIconBuf = NULL;
+#if defined (iPlatformAppleDesktop)
ev->wheel.y == 0 &&
d->wheelSwipeState == direct_WheelSwipeState &&
handleWheelSwipe_DocumentWidget_(d, &ev->wheel)) {
return iTrue;
}
-// const iGmRun *banner = siteBanner_GmDocument(d->doc);
return;
+#endif
const iInt2 mouseCoord = coord_MouseWheelEvent(&ev->wheel);
if (isPerPixel_MouseWheelEvent(&ev->wheel)) {
const iInt2 wheel = init_I2(ev->wheel.x, ev->wheel.y);
stop_Anim(&d->view.scrollY.pos);
immediateScroll_DocumentView_(view, -wheel.y);
if (!scrollWideBlock_DocumentView_(view, mouseCoord, -wheel.x, 0) &&
wheel.x) {
handleWheelSwipe_DocumentWidget_(d, &ev->wheel);
}
}
else {
/* Traditional mouse wheel. */
const int amount = ev->wheel.y;
if (keyMods_Sym(modState_Keys()) == KMOD_PRIMARY) {
postCommandf_App("zoom.delta arg:%d", amount > 0 ? 10 : -10);
return iTrue;
}
smoothScroll_DocumentView_(view,
-3 * amount * lineHeight_Text(paragraph_FontId),
smoothDuration_DocumentWidget_(mouse_ScrollType));
scrollWideBlock_DocumentView_(
view, mouseCoord, -3 * ev->wheel.x * lineHeight_Text(paragraph_FontId), 167);
}
iChangeFlags(d->flags, noHoverWhileScrolling_DocumentWidgetFlag, iTrue);
return iTrue;
}
const iInt2 headingSize = measureWrapRange_Text(sideHeadingFont, avail,
currentHeading_DocumentView_(d)).bounds.size;
if (headingSize.x > 0) {
bufSize.y += gap_Text + headingSize.y;
bufSize.x = iMax(bufSize.x, headingSize.x);
if (ev->motion.which != SDL_TOUCH_MOUSEID) {
iChangeFlags(d->flags, noHoverWhileScrolling_DocumentWidgetFlag, iFalse);
}
const iInt2 mpos = init_I2(ev->motion.x, ev->motion.y);
if (isVisible_Widget(d->menu)) {
setCursor_Window(get_Window(), SDL_SYSTEM_CURSOR_ARROW);
}
+#if 0
else if (contains_Rect(siteBannerRect_DocumentWidget_(d), mpos)) {
setCursor_Window(get_Window(), SDL_SYSTEM_CURSOR_HAND);
}
+#endif
else {
isHeadingVisible = iFalse;
if (value_Anim(&view->altTextOpacity) < 0.833f) {
setValue_Anim(&view->altTextOpacity, 0, 0); /* keep it hidden while moving */
}
updateHover_DocumentView_(view, mpos);
}
}
SDL_PIXELFORMAT_RGBA4444,
SDL_TEXTUREACCESS_STATIC | SDL_TEXTUREACCESS_TARGET,
bufSize.x, bufSize.y);
iRangecc text = currentHeading_DocumentView_(d);
iInt2 pos = addY_I2(bottomLeft_Rect(iconRect), gap_Text);
const int font = sideHeadingFont;
drawWrapRange_Text(font, pos, avail, tmBannerSideTitle_ColorId, text);
iChangeFlags(d->flags, noHoverWhileScrolling_DocumentWidgetFlag, iFalse);
return iTrue;
}
-}
-static void drawSideElements_DocumentView_(const iDocumentView *d) {
const iInt2 texSize = size_SDLTexture(dbuf->sideIconBuf);
if (avail > texSize.x) {
const int minBannerSize = lineHeight_Text(banner_FontId) * 2;
iInt2 pos = addY_I2(add_I2(topLeft_Rect(bounds), init_I2(margin, 0)),
height_Rect(bounds) / 2 - minBannerSize / 2 -
(texSize.y > minBannerSize
? (gap_Text + lineHeight_Text(heading3_FontId)) / 2
: 0));
SDL_SetTextureAlphaMod(dbuf->sideIconBuf, 255 * opacity);
SDL_RenderCopy(renderer_Window(get_Window()),
dbuf->sideIconBuf, NULL,
&(SDL_Rect){ pos.x, pos.y, texSize.x, texSize.y });
if (ev->button.button == SDL_BUTTON_X1) {
postCommand_Root(w->root, "navigate.back");
return iTrue;
}
if (ev->button.button == SDL_BUTTON_X2) {
postCommand_Root(w->root, "navigate.forward");
return iTrue;
}
if (ev->button.button == SDL_BUTTON_MIDDLE && view->hoverLink) {
interactingWithLink_DocumentWidget_(d, view->hoverLink->linkId);
postCommandf_Root(w->root, "open newtab:%d url:%s",
(isPinned_DocumentWidget_(d) ? otherRoot_OpenTabFlag : 0) |
(modState_Keys() & KMOD_SHIFT ? new_OpenTabFlag : newBackground_OpenTabFlag),
cstr_String(linkUrl_GmDocument(view->doc, view->hoverLink->linkId)));
return iTrue;
}
if (ev->button.button == SDL_BUTTON_RIGHT &&
contains_Widget(w, init_I2(ev->button.x, ev->button.y))) {
if (!isVisible_Widget(d->menu)) {
d->contextLink = view->hoverLink;
d->contextPos = init_I2(ev->button.x, ev->button.y);
if (d->menu) {
destroy_Widget(d->menu);
d->menu = NULL;
}
setFocus_Widget(NULL);
iArray items;
init_Array(&items, sizeof(iMenuItem));
if (d->contextLink) {
/* Context menu for a link. */
interactingWithLink_DocumentWidget_(d, d->contextLink->linkId); /* perhaps will be triggered */
const iString *linkUrl = linkUrl_GmDocument(view->doc, d->contextLink->linkId);
+// const int linkFlags = linkFlags_GmDocument(d->doc, d->contextLink->linkId);
const iRangecc scheme = urlScheme_String(linkUrl);
const iBool isGemini = equalCase_Rangecc(scheme, "gemini");
iBool isNative = iFalse;
if (deviceType_App() != desktop_AppDeviceType) {
/* Show the link as the first, non-interactive item. */
pushBack_Array(&items, &(iMenuItem){
format_CStr("```%s", cstr_String(linkUrl)),
0, 0, NULL });
}
if (willUseProxy_App(scheme) || isGemini ||
equalCase_Rangecc(scheme, "file") ||
equalCase_Rangecc(scheme, "finger") ||
equalCase_Rangecc(scheme, "gopher")) {
isNative = iTrue;
/* Regular links that we can open. */
pushBackN_Array(
&items,
(iMenuItem[]){ { openTab_Icon " ${link.newtab}",
0,
0,
format_CStr("!open newtab:1 origin:%s url:%s",
cstr_String(id_Widget(w)),
cstr_String(linkUrl)) },
{ openTabBg_Icon " ${link.newtab.background}",
0,
0,
format_CStr("!open newtab:2 origin:%s url:%s",
cstr_String(id_Widget(w)),
cstr_String(linkUrl)) },
{ "${link.side}",
0,
0,
format_CStr("!open newtab:4 origin:%s url:%s",
cstr_String(id_Widget(w)),
cstr_String(linkUrl)) },
{ "${link.side.newtab}",
0,
0,
format_CStr("!open newtab:5 origin:%s url:%s",
cstr_String(id_Widget(w)),
cstr_String(linkUrl)) } },
4);
if (deviceType_App() == phone_AppDeviceType) {
removeN_Array(&items, size_Array(&items) - 2, iInvalidSize);
}
}
else if (!willUseProxy_App(scheme)) {
pushBack_Array(
&items,
&(iMenuItem){ openExt_Icon " ${link.browser}",
0,
0,
format_CStr("!open default:1 url:%s", cstr_String(linkUrl)) });
}
if (willUseProxy_App(scheme)) {
pushBackN_Array(
&items,
(iMenuItem[]){
{ "---" },
{ isGemini ? "${link.noproxy}" : openExt_Icon " ${link.browser}",
0,
0,
format_CStr("!open origin:%s noproxy:1 url:%s",
cstr_String(id_Widget(w)),
cstr_String(linkUrl)) } },
2);
}
iString *linkLabel = collectNewRange_String(
linkLabel_GmDocument(view->doc, d->contextLink->linkId));
urlEncodeSpaces_String(linkLabel);
pushBackN_Array(&items,
(iMenuItem[]){ { "---" },
{ "${link.copy}", 0, 0, "document.copylink" },
{ bookmark_Icon " ${link.bookmark}",
0,
0,
format_CStr("!bookmark.add title:%s url:%s",
cstr_String(linkLabel),
cstr_String(linkUrl)) },
},
3);
if (isNative && d->contextLink->mediaType != download_MediaType) {
pushBackN_Array(&items, (iMenuItem[]){
{ "---" },
{ download_Icon " ${link.download}", 0, 0, "document.downloadlink" },
}, 2);
}
iMediaRequest *mediaReq;
if ((mediaReq = findMediaRequest_DocumentWidget_(d, d->contextLink->linkId)) != NULL &&
d->contextLink->mediaType != download_MediaType) {
if (isFinished_GmRequest(mediaReq->req)) {
pushBack_Array(&items,
&(iMenuItem){ download_Icon " " saveToDownloads_Label,
0,
0,
format_CStr("document.media.save link:%u",
d->contextLink->linkId) });
}
}
if (equalCase_Rangecc(scheme, "file")) {
/* Local files may be deleted. */
pushBack_Array(
&items,
&(iMenuItem){ delete_Icon " " uiTextCaution_ColorEscape
"${link.file.delete}",
0,
0,
format_CStr("!file.delete confirm:1 path:%s",
cstrCollect_String(
localFilePathFromUrl_String(linkUrl))) });
}
}
else if (deviceType_App() == desktop_AppDeviceType) {
if (!isEmpty_Range(&d->selectMark)) {
pushBackN_Array(&items,
(iMenuItem[]){ { "${menu.copy}", 0, 0, "copy" },
{ "---", 0, 0, NULL } },
2);
}
pushBackN_Array(
&items,
(iMenuItem[]){
{ backArrow_Icon " ${menu.back}", navigateBack_KeyShortcut, "navigate.back" },
{ forwardArrow_Icon " ${menu.forward}", navigateForward_KeyShortcut, "navigate.forward" },
{ upArrow_Icon " ${menu.parent}", navigateParent_KeyShortcut, "navigate.parent" },
{ upArrowBar_Icon " ${menu.root}", navigateRoot_KeyShortcut, "navigate.root" },
{ "---" },
{ reload_Icon " ${menu.reload}", reload_KeyShortcut, "navigate.reload" },
{ timer_Icon " ${menu.autoreload}", 0, 0, "document.autoreload.menu" },
{ "---" },
{ bookmark_Icon " ${menu.page.bookmark}", SDLK_d, KMOD_PRIMARY, "bookmark.add" },
{ star_Icon " ${menu.page.subscribe}", subscribeToPage_KeyModifier, "feeds.subscribe" },
{ "---" },
{ book_Icon " ${menu.page.import}", 0, 0, "bookmark.links confirm:1" },
{ globe_Icon " ${menu.page.translate}", 0, 0, "document.translate" },
{ upload_Icon " ${menu.page.upload}", 0, 0, "document.upload" },
{ "---" },
{ "${menu.page.copyurl}", 0, 0, "document.copylink" } },
16);
if (isEmpty_Range(&d->selectMark)) {
pushBackN_Array(
&items,
(iMenuItem[]){
{ "${menu.page.copysource}", 'c', KMOD_PRIMARY, "copy" },
{ download_Icon " " saveToDownloads_Label, SDLK_s, KMOD_PRIMARY, "document.save" } },
2);
}
}
else {
/* Mobile text selection menu. */
+#if 0
pushBackN_Array(
&items,
(iMenuItem[]){
{ "${menu.select}", 0, 0, "document.select arg:1" },
{ "${menu.select.word}", 0, 0, "document.select arg:2" },
{ "${menu.select.par}", 0, 0, "document.select arg:3" },
},
3);
+#endif
postCommand_Root(w->root, "document.select arg:1");
return iTrue;
}
d->menu = makeMenu_Widget(w, data_Array(&items), size_Array(&items));
deinit_Array(&items);
setMenuItemDisabled_Widget(
d->menu,
"document.upload",
!equalCase_Rangecc(urlScheme_String(d->mod.url), "gemini") &&
!equalCase_Rangecc(urlScheme_String(d->mod.url), "titan"));
}
processContextMenuEvent_Widget(d->menu, ev, {});
}
}
draw_TextBuf(
dbuf->timestampBuf,
add_I2(
bottomLeft_Rect(bounds),
init_I2(margin,
-margin + -dbuf->timestampBuf->size.y +
iMax(0, d->scrollY.max - pos_SmoothScroll(&d->scrollY)))),
tmQuoteIcon_ColorId);
-}
-static void drawMedia_DocumentView_(const iDocumentView *d, iPaint *p) {
const iGmRun * run = i.ptr;
if (run->mediaType == audio_MediaType) {
iPlayerUI ui;
init_PlayerUI(&ui,
audioPlayer_Media(media_GmDocument(d->doc), mediaId_GmRun(run)),
runRect_DocumentView_(d, run));
draw_PlayerUI(&ui, p);
}
else if (run->mediaType == download_MediaType) {
iDownloadUI ui;
init_DownloadUI(&ui, constMedia_GmDocument(d->doc), run->mediaId,
runRect_DocumentView_(d, run));
draw_DownloadUI(&ui, p);
}
return iTrue;
}
-}
-static void extend_GmRunRange_(iGmRunRange *runs) {
runs->start--;
runs->end++;
return iTrue;
}
-}
-static iBool render_DocumentView_(const iDocumentView *d, iDrawContext *ctx, iBool prerenderExtra) {
init_Rect(0,
0,
width_Rect(bounds) - constAs_Widget(d->owner->scroll)->rect.size.x,
height_Rect(bounds));
iPaint *p = &ctx->paint;
init_Paint(p);
iForIndices(i, visBuf->buffers) {
iVisBufTexture *buf = &visBuf->buffers[i];
iVisBufMeta *meta = buf->user;
const iRangei bufRange = intersect_Rangei(bufferRange_VisBuf(visBuf, i), full);
const iRangei bufVisRange = intersect_Rangei(bufRange, vis);
ctx->widgetBounds = moved_Rect(ctxWidgetBounds, init_I2(0, -buf->origin));
ctx->viewPos = init_I2(left_Rect(ctx->docBounds) - left_Rect(bounds), -buf->origin);
-// printf(" buffer %zu: buf vis range %d...%d\n", i, bufVisRange.start, bufVisRange.end);
if (!prerenderExtra && !isEmpty_Range(&bufVisRange)) {
didDraw = iTrue;
if (isEmpty_Rangei(buf->validRange)) {
/* Fill the required currently visible range (vis). */
const iRangei bufVisRange = intersect_Rangei(bufRange, vis);
if (!isEmpty_Range(&bufVisRange)) {
beginTarget_Paint(p, buf->texture);
fillRect_Paint(p, (iRect){ zero_I2(), visBuf->texSize }, tmBackground_ColorId);
iZap(ctx->runsDrawn);
render_GmDocument(d->doc, bufVisRange, drawRun_DrawContext_, ctx);
meta->runsDrawn = ctx->runsDrawn;
extend_GmRunRange_(&meta->runsDrawn);
buf->validRange = bufVisRange;
case started_ClickResult:
if (d->grabbedPlayer) {
return iTrue;
}
/* Enable hover state now that scrolling has surely finished. */
if (d->flags & noHoverWhileScrolling_DocumentWidgetFlag) {
d->flags &= ~noHoverWhileScrolling_DocumentWidgetFlag;
updateHover_DocumentView_(view, mouseCoord_Window(get_Window(), ev->button.which));
}
if (~flags_Widget(w) & touchDrag_WidgetFlag) {
iChangeFlags(d->flags, selecting_DocumentWidgetFlag, iFalse);
iChangeFlags(d->flags, selectWords_DocumentWidgetFlag, d->click.count == 2);
iChangeFlags(d->flags, selectLines_DocumentWidgetFlag, d->click.count >= 3);
/* Double/triple clicks marks the selection immediately. */
if (d->click.count >= 2) {
beginMarkingSelection_DocumentWidget_(d, d->click.startPos);
extendRange_Rangecc(
&d->selectMark,
range_String(source_GmDocument(view->doc)),
bothStartAndEnd_RangeExtension |
(d->click.count == 2 ? word_RangeExtension : line_RangeExtension));
d->initialSelectMark = d->selectMark;
refresh_Widget(w);
}
else {
d->initialSelectMark = iNullRange;
}
}
return iTrue;
case drag_ClickResult: {
if (d->grabbedPlayer) {
iPlayer *plr =
audioPlayer_Media(media_GmDocument(view->doc), mediaId_GmRun(d->grabbedPlayer));
iPlayerUI ui;
init_PlayerUI(&ui, plr, runRect_DocumentView_(view, d->grabbedPlayer));
float off = (float) delta_Click(&d->click).x / (float) width_Rect(ui.volumeSlider);
setVolume_Player(plr, d->grabbedStartVolume + off);
refresh_Widget(w);
return iTrue;
}
/* Fold/unfold a preformatted block. */
if (~d->flags & selecting_DocumentWidgetFlag && view->hoverPre &&
preIsFolded_GmDocument(view->doc, preId_GmRun(view->hoverPre))) {
return iTrue;
}
/* Begin selecting a range of text. */
if (~d->flags & selecting_DocumentWidgetFlag) {
beginMarkingSelection_DocumentWidget_(d, d->click.startPos);
}
iRangecc loc = sourceLoc_DocumentView_(view, pos_Click(&d->click));
if (d->selectMark.start == NULL) {
d->selectMark = loc;
}
else if (loc.end) {
if (flags_Widget(w) & touchDrag_WidgetFlag) {
/* Choose which end to move. */
if (!(d->flags & (movingSelectMarkStart_DocumentWidgetFlag |
movingSelectMarkEnd_DocumentWidgetFlag))) {
const iRangecc mark = selectMark_DocumentWidget_(d);
const char * midMark = mark.start + size_Range(&mark) / 2;
const iRangecc loc = sourceLoc_DocumentView_(view, pos_Click(&d->click));
const iBool isCloserToStart = d->selectMark.start > d->selectMark.end ?
(loc.start > midMark) : (loc.start < midMark);
iChangeFlags(d->flags, movingSelectMarkStart_DocumentWidgetFlag, isCloserToStart);
iChangeFlags(d->flags, movingSelectMarkEnd_DocumentWidgetFlag, !isCloserToStart);
}
/* Move the start or the end depending on which is nearer. */
if (d->flags & movingSelectMarkStart_DocumentWidgetFlag) {
d->selectMark.start = loc.start;
}
else {
d->selectMark.end = (d->selectMark.end > d->selectMark.start ? loc.end : loc.start);
}
}
else {
/* Progressively fill the required runs. */
if (meta->runsDrawn.start) {
beginTarget_Paint(p, buf->texture);
meta->runsDrawn.start = renderProgressive_GmDocument(d->doc, meta->runsDrawn.start,
-1, iInvalidSize,
bufVisRange,
drawRun_DrawContext_,
ctx);
buf->validRange.start = bufVisRange.start;
d->selectMark.end = loc.end;// (d->selectMark.end > d->selectMark.start ? loc.end : loc.start);
if (loc.start < d->initialSelectMark.start) {
d->selectMark.end = loc.start;
}
if (isEmpty_Range(&d->selectMark)) {
d->selectMark = d->initialSelectMark;
}
}
}
iAssert((!d->selectMark.start && !d->selectMark.end) ||
( d->selectMark.start && d->selectMark.end));
/* Extend to full words/paragraphs. */
if (d->flags & (selectWords_DocumentWidgetFlag | selectLines_DocumentWidgetFlag)) {
extendRange_Rangecc(
&d->selectMark,
range_String(source_GmDocument(view->doc)),
(d->flags & movingSelectMarkStart_DocumentWidgetFlag ? moveStart_RangeExtension
: moveEnd_RangeExtension) |
(d->flags & selectWords_DocumentWidgetFlag ? word_RangeExtension
: line_RangeExtension));
if (d->flags & movingSelectMarkStart_DocumentWidgetFlag) {
d->initialSelectMark.start =
d->initialSelectMark.end = d->selectMark.start;
}
}
if (d->initialSelectMark.start) {
if (d->selectMark.end > d->selectMark.start) {
d->selectMark.start = d->initialSelectMark.start;
}
else if (d->selectMark.end < d->selectMark.start) {
d->selectMark.start = d->initialSelectMark.end;
}
}
+// printf("mark %zu ... %zu\n", d->selectMark.start - cstr_String(source_GmDocument(d->doc)),
+// d->selectMark.end - cstr_String(source_GmDocument(d->doc)));
+// fflush(stdout);
refresh_Widget(w);
return iTrue;
}
case finished_ClickResult:
if (d->grabbedPlayer) {
setGrabbedPlayer_DocumentWidget_(d, NULL);
return iTrue;
}
if (isVisible_Widget(d->menu)) {
closeMenu_Widget(d->menu);
}
d->flags &= ~(movingSelectMarkStart_DocumentWidgetFlag |
movingSelectMarkEnd_DocumentWidgetFlag);
if (!isMoved_Click(&d->click)) {
setFocus_Widget(NULL);
/* Tap in tap selection mode. */
if (flags_Widget(w) & touchDrag_WidgetFlag) {
const iRangecc tapLoc = sourceLoc_DocumentView_(view, pos_Click(&d->click));
/* Tapping on the selection will show a menu. */
const iRangecc mark = selectMark_DocumentWidget_(d);
if (tapLoc.start >= mark.start && tapLoc.end <= mark.end) {
if (d->copyMenu) {
closeMenu_Widget(d->copyMenu);
destroy_Widget(d->copyMenu);
d->copyMenu = NULL;
}
d->copyMenu = makeMenu_Widget(w, (iMenuItem[]){
{ clipCopy_Icon " ${menu.copy}", 0, 0, "copy" },
{ "---" },
{ close_Icon " ${menu.select.clear}", 0, 0, "document.select arg:0" },
}, 3);
setFlags_Widget(d->copyMenu, noFadeBackground_WidgetFlag, iTrue);
openMenu_Widget(d->copyMenu, pos_Click(&d->click));
return iTrue;
}
if (meta->runsDrawn.end) {
beginTarget_Paint(p, buf->texture);
meta->runsDrawn.end = renderProgressive_GmDocument(d->doc, meta->runsDrawn.end,
+1, iInvalidSize,
bufVisRange,
drawRun_DrawContext_,
ctx);
buf->validRange.end = bufVisRange.end;
else {
/* Tapping elsewhere exits selection mode. */
postCommand_Widget(d, "document.select arg:0");
return iTrue;
}
}
}
/* Progressively draw the rest of the buffer if it isn't fully valid. */
if (prerenderExtra && !equal_Rangei(bufRange, buf->validRange)) {
const iGmRun *next;
-// printf("%zu: prerenderExtra (start:%p end:%p)\n", i, meta->runsDrawn.start, meta->runsDrawn.end);
if (meta->runsDrawn.start == NULL) {
/* Haven't drawn anything yet in this buffer, so let's try seeding it. */
const int rh = lineHeight_Text(paragraph_FontId);
const int y = i >= iElemCount(visBuf->buffers) / 2 ? bufRange.start : (bufRange.end - rh);
beginTarget_Paint(p, buf->texture);
fillRect_Paint(p, (iRect){ zero_I2(), visBuf->texSize }, tmBackground_ColorId);
buf->validRange = (iRangei){ y, y + rh };
iZap(ctx->runsDrawn);
render_GmDocument(d->doc, buf->validRange, drawRun_DrawContext_, ctx);
meta->runsDrawn = ctx->runsDrawn;
extend_GmRunRange_(&meta->runsDrawn);
-// printf("%zu: seeded, next %p:%p\n", i, meta->runsDrawn.start, meta->runsDrawn.end);
didDraw = iTrue;
if (view->hoverPre) {
togglePreFold_DocumentWidget_(d, preId_GmRun(view->hoverPre));
return iTrue;
}
else {
if (meta->runsDrawn.start) {
const iRangei upper = intersect_Rangei(bufRange, (iRangei){ full.start, buf->validRange.start });
if (upper.end > upper.start) {
beginTarget_Paint(p, buf->texture);
next = renderProgressive_GmDocument(d->doc, meta->runsDrawn.start,
-1, 1, upper,
drawRun_DrawContext_,
ctx);
if (next && meta->runsDrawn.start != next) {
meta->runsDrawn.start = next;
buf->validRange.start = bottom_Rect(next->visBounds);
didDraw = iTrue;
}
else {
buf->validRange.start = bufRange.start;
}
if (view->hoverLink) {
/* TODO: Move this to a method. */
const iGmLinkId linkId = view->hoverLink->linkId;
const iMediaId linkMedia = mediaId_GmRun(view->hoverLink);
const int linkFlags = linkFlags_GmDocument(view->doc, linkId);
iAssert(linkId);
/* Media links are opened inline by default. */
if (isMediaLink_GmDocument(view->doc, linkId)) {
if (linkFlags & content_GmLinkFlag && linkFlags & permanent_GmLinkFlag) {
/* We have the content and it cannot be dismissed, so nothing
further to do. */
return iTrue;
}
}
if (!didDraw && meta->runsDrawn.end) {
const iRangei lower = intersect_Rangei(bufRange, (iRangei){ buf->validRange.end, full.end });
if (lower.end > lower.start) {
beginTarget_Paint(p, buf->texture);
next = renderProgressive_GmDocument(d->doc, meta->runsDrawn.end,
+1, 1, lower,
drawRun_DrawContext_,
ctx);
if (next && meta->runsDrawn.end != next) {
meta->runsDrawn.end = next;
buf->validRange.end = top_Rect(next->visBounds);
didDraw = iTrue;
if (!requestMedia_DocumentWidget_(d, linkId, iTrue)) {
if (linkFlags & content_GmLinkFlag) {
/* Dismiss shown content on click. */
setData_Media(media_GmDocument(view->doc),
linkId,
NULL,
NULL,
allowHide_MediaFlag);
/* Cancel a partially received request. */ {
iMediaRequest *req = findMediaRequest_DocumentWidget_(d, linkId);
if (!isFinished_GmRequest(req->req)) {
cancel_GmRequest(req->req);
removeMediaRequest_DocumentWidget_(d, linkId);
/* Note: Some of the audio IDs have changed now, layout must
be redone. */
}
}
redoLayout_GmDocument(view->doc);
view->hoverLink = NULL;
clampScroll_DocumentView_(view);
updateVisible_DocumentView_(view);
invalidate_DocumentWidget_(d);
refresh_Widget(w);
return iTrue;
}
else {
buf->validRange.end = bufRange.end;
/* Show the existing content again if we have it. */
iMediaRequest *req = findMediaRequest_DocumentWidget_(d, linkId);
if (req) {
setData_Media(media_GmDocument(view->doc),
linkId,
meta_GmRequest(req->req),
body_GmRequest(req->req),
allowHide_MediaFlag);
redoLayout_GmDocument(view->doc);
updateVisible_DocumentView_(view);
invalidate_DocumentWidget_(d);
refresh_Widget(w);
return iTrue;
}
}
}
refresh_Widget(w);
}
}
}
/* Draw any invalidated runs that fall within this buffer. */
if (!prerenderExtra) {
const iRangei bufRange = { buf->origin, buf->origin + visBuf->texSize.y };
/* Clear full-width backgrounds first in case there are any dynamic elements. */ {
iConstForEach(PtrSet, r, d->invalidRuns) {
const iGmRun *run = *r.value;
if (isOverlapping_Rangei(bufRange, ySpan_Rect(run->visBounds))) {
beginTarget_Paint(p, buf->texture);
fillRect_Paint(p,
init_Rect(0,
run->visBounds.pos.y - buf->origin,
visBuf->texSize.x,
run->visBounds.size.y),
tmBackground_ColorId);
}
}
}
setAnsiFlags_Text(ansiEscapes_GmDocument(d->doc));
iConstForEach(PtrSet, r, d->invalidRuns) {
const iGmRun *run = *r.value;
if (isOverlapping_Rangei(bufRange, ySpan_Rect(run->visBounds))) {
beginTarget_Paint(p, buf->texture);
drawRun_DrawContext_(ctx, run);
else if (linkMedia.type == download_MediaType ||
findMediaRequest_DocumentWidget_(d, linkId)) {
/* TODO: What should be done when clicking on an inline download?
Maybe dismiss if finished? */
return iTrue;
}
else if (linkFlags & supportedScheme_GmLinkFlag) {
int tabMode = openTabMode_Sym(modState_Keys());
if (isPinned_DocumentWidget_(d)) {
tabMode ^= otherRoot_OpenTabFlag;
}
interactingWithLink_DocumentWidget_(d, linkId);
postCommandf_Root(w->root, "open newtab:%d url:%s",
tabMode,
cstr_String(absoluteUrl_String(
d->mod.url, linkUrl_GmDocument(view->doc, linkId))));
}
else {
const iString *url = absoluteUrl_String(
d->mod.url, linkUrl_GmDocument(view->doc, linkId));
makeQuestion_Widget(
uiTextCaution_ColorEscape "${heading.openlink}",
format_CStr(
cstr_Lang("dlg.openlink.confirm"),
uiTextAction_ColorEscape,
cstr_String(url)),
(iMenuItem[]){
{ "${cancel}" },
{ uiTextCaution_ColorEscape "${dlg.openlink}",
0, 0, format_CStr("!open default:1 url:%s", cstr_String(url)) } },
2);
}
}
setAnsiFlags_Text(allowAll_AnsiFlag);
}
endTarget_Paint(p);
if (prerenderExtra && didDraw) {
/* Just a run at a time. */
break;
}
}
if (!prerenderExtra) {
clear_PtrSet(d->invalidRuns);
}
-}
-static void prerender_DocumentWidget_(iAny *context) {
/* The widget has probably been removed from the widget tree, pending destruction.
Tickers are not cancelled until the widget is actually destroyed. */
return;
.view = &d->view,
.docBounds = documentBounds_DocumentView_(&d->view),
.vis = visibleRange_DocumentView_(&d->view),
.showLinkNumbers = (d->flags & showLinkNumbers_DocumentWidgetFlag) != 0
-// printf("%u prerendering\n", SDL_GetTicks());
makePaletteGlobal_GmDocument(d->view.doc);
if (render_DocumentView_(&d->view, &ctx, iTrue /* just fill up progressively */)) {
/* Something was drawn, should check later if there is still more to do. */
addTicker_App(prerender_DocumentWidget_, context);
}
-}
-static void checkPendingInvalidation_DocumentWidget_(const iDocumentWidget *d) {
!isAffectedByVisualOffset_Widget(constAs_Widget(d))) {
-// printf("%p visoff: %d\n", d, left_Rect(bounds_Widget(w)) - left_Rect(boundsWithoutVisualOffset_Widget(w)));
iDocumentWidget *m = (iDocumentWidget *) d; /* Hrrm, not const... */
m->flags &= ~invalidationPending_DocumentWidgetFlag;
invalidate_DocumentWidget_(m);
-}
-static void draw_DocumentView_(const iDocumentView *d) {
As we're now drawing a document, ensure that the right palette is in effect.
Document theme colors can be used elsewhere, too, but first a document's palette
must be made global. */
updateTimestampBuf_DocumentView_(d);
updateSideIconBuf_DocumentView_(d);
.view = d,
.docBounds = docBounds,
.vis = vis,
.showLinkNumbers = (d->owner->flags & showLinkNumbers_DocumentWidgetFlag) != 0,
const int docBgColor = isDocEmpty ? tmBannerBackground_ColorId : tmBackground_ColorId;
setClip_Paint(&ctx.paint, clipBounds);
if (!isDocEmpty) {
draw_VisBuf(d->visBuf, init_I2(bounds.pos.x, yTop), ySpan_Rect(bounds));
}
/* Text markers. */
if (!isEmpty_Range(&d->owner->foundMark) || !isEmpty_Range(&d->owner->selectMark)) {
SDL_Renderer *render = renderer_Window(get_Window());
ctx.firstMarkRect = zero_Rect();
ctx.lastMarkRect = zero_Rect();
SDL_SetRenderDrawBlendMode(render,
isDark_ColorTheme(colorTheme_App()) ? SDL_BLENDMODE_ADD
: SDL_BLENDMODE_BLEND);
ctx.viewPos = topLeft_Rect(docBounds);
/* Marker starting outside the visible range? */
if (d->visibleRuns.start) {
if (!isEmpty_Range(&d->owner->selectMark) &&
d->owner->selectMark.start < d->visibleRuns.start->text.start &&
d->owner->selectMark.end > d->visibleRuns.start->text.start) {
ctx.inSelectMark = iTrue;
}
if (isEmpty_Range(&d->owner->foundMark) &&
d->owner->foundMark.start < d->visibleRuns.start->text.start &&
d->owner->foundMark.end > d->visibleRuns.start->text.start) {
ctx.inFoundMark = iTrue;
if (d->selectMark.start && !(d->flags & (selectLines_DocumentWidgetFlag |
selectWords_DocumentWidgetFlag))) {
d->selectMark = iNullRange;
refresh_Widget(w);
}
}
render_GmDocument(d->doc, vis, drawMark_DrawContext_, &ctx);
SDL_SetRenderDrawBlendMode(render, SDL_BLENDMODE_NONE);
/* Selection range pins. */
if (isTouchSelecting) {
drawPin_Paint(&ctx.paint, ctx.firstMarkRect, 0, tmQuote_ColorId);
drawPin_Paint(&ctx.paint, ctx.lastMarkRect, 1, tmQuote_ColorId);
}
}
drawMedia_DocumentView_(d, &ctx.paint);
/* Fill the top and bottom, in case the document is short. */
if (yTop > top_Rect(bounds)) {
fillRect_Paint(&ctx.paint,
(iRect){ bounds.pos, init_I2(bounds.size.x, yTop - top_Rect(bounds)) },
!isEmpty_Banner(banner) ? tmBannerBackground_ColorId
: docBgColor);
}
/* Banner. */
if (!isDocEmpty || numItems_Banner(banner) > 0) {
/* Fill the part between the banner and the top of the document. */
fillRect_Paint(&ctx.paint,
(iRect){ init_I2(left_Rect(bounds),
top_Rect(docBounds) + viewPos_DocumentView_(d) -
documentTopPad_DocumentView_(d)),
init_I2(bounds.size.x, documentTopPad_DocumentView_(d)) },
docBgColor);
setPos_Banner(banner, addY_I2(topLeft_Rect(docBounds),
-pos_SmoothScroll(&d->scrollY)));
draw_Banner(banner);
}
const int yBottom = yTop + size_GmDocument(d->doc).y;
if (yBottom < bottom_Rect(bounds)) {
fillRect_Paint(&ctx.paint,
init_Rect(bounds.pos.x, yBottom, bounds.size.x, bottom_Rect(bounds) - yBottom),
!isDocEmpty ? docBgColor : tmBannerBackground_ColorId);
}
unsetClip_Paint(&ctx.paint);
drawSideElements_DocumentView_(d);
/* Alt text. */
const float altTextOpacity = value_Anim(&d->altTextOpacity) * 6 - 5;
if (d->hoverAltPre && altTextOpacity > 0) {
const iGmPreMeta *meta = preMeta_GmDocument(d->doc, preId_GmRun(d->hoverAltPre));
if (meta->flags & topLeft_GmPreMetaFlag && ~meta->flags & decoration_GmRunFlag &&
!isEmpty_Range(&meta->altText)) {
const int margin = 3 * gap_UI / 2;
const int altFont = uiLabel_FontId;
const int wrap = docBounds.size.x - 2 * margin;
iInt2 pos = addY_I2(add_I2(docBounds.pos, meta->pixelRect.pos),
viewPos_DocumentView_(d));
const iInt2 textSize = measureWrapRange_Text(altFont, wrap, meta->altText).bounds.size;
pos.y -= textSize.y + gap_UI;
pos.y = iMax(pos.y, top_Rect(bounds));
const iRect altRect = { pos, init_I2(docBounds.size.x, textSize.y) };
ctx.paint.alpha = altTextOpacity * 255;
if (altTextOpacity < 1) {
SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_BLEND);
}
fillRect_Paint(&ctx.paint, altRect, tmBackgroundAltText_ColorId);
drawRect_Paint(&ctx.paint, altRect, tmFrameAltText_ColorId);
setOpacity_Text(altTextOpacity);
drawWrapRange_Text(altFont, addX_I2(pos, margin), wrap,
tmQuote_ColorId, meta->altText);
SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_NONE);
setOpacity_Text(1.0f);
return iTrue;
case aborted_ClickResult:
if (d->grabbedPlayer) {
setGrabbedPlayer_DocumentWidget_(d, NULL);
return iTrue;
}
}
/* Touch selection indicator. */
if (isTouchSelecting) {
iRect rect = { topLeft_Rect(bounds),
init_I2(width_Rect(bounds), lineHeight_Text(uiLabelBold_FontId)) };
fillRect_Paint(&ctx.paint, rect, uiTextAction_ColorId);
const iRangecc mark = selectMark_DocumentWidget_(d->owner);
drawCentered_Text(uiLabelBold_FontId,
rect,
iFalse,
uiBackground_ColorId,
"%zu bytes selected", /* TODO: i18n */
size_Range(&mark));
return iTrue;
default:
break;
+}
+static void checkPendingInvalidation_DocumentWidget_(const iDocumentWidget *d) {
!isAffectedByVisualOffset_Widget(constAs_Widget(d))) {
// printf("%p visoff: %d\n", d, left_Rect(bounds_Widget(w)) - left_Rect(boundsWithoutVisualOffset_Widget(w)));
iDocumentWidget *m = (iDocumentWidget *) d; /* Hrrm, not const... */
m->flags &= ~invalidationPending_DocumentWidgetFlag;
invalidate_DocumentWidget_(m);
+}
+static void prerender_DocumentWidget_(iAny *context) {
/* The widget has probably been removed from the widget tree, pending destruction.
Tickers are not cancelled until the widget is actually destroyed. */
return;
.view = &d->view,
.docBounds = documentBounds_DocumentView_(&d->view),
.vis = visibleRange_DocumentView_(&d->view),
.showLinkNumbers = (d->flags & showLinkNumbers_DocumentWidgetFlag) != 0
makePaletteGlobal_GmDocument(d->view.doc);
if (render_DocumentView_(&d->view, &ctx, iTrue /* just fill up progressively */)) {
/* Something was drawn, should check later if there is still more to do. */
addTicker_App(prerender_DocumentWidget_, context);
}
}
}
@@ -5554,6 +5436,128 @@ static void draw_DocumentWidget_(const iDocumentWidget *d) {
/----------------------------------------------------------------------------------------------/
+void init_DocumentWidget(iDocumentWidget *d) {
+#if defined (iPlatformAppleDesktop)
+#else
+#endif
setFlags_Widget(w, leftEdgeDraggable_WidgetFlag | rightEdgeDraggable_WidgetFlag |
horizontalOffset_WidgetFlag, iTrue);
iClob(new_IndicatorWidget()),
resizeToParentWidth_WidgetFlag | resizeToParentHeight_WidgetFlag);
+#if !defined (iPlatformAppleDesktop) /* in system menu */
+#endif
+}
+void cancelAllRequests_DocumentWidget(iDocumentWidget *d) {
iMediaRequest *mr = i.object;
cancel_GmRequest(mr->req);
cancel_GmRequest(d->request);
+}
+void deinit_DocumentWidget(iDocumentWidget *d) {
SDL_RemoveTimer(d->mediaTimer);
+}
+void setSource_DocumentWidget(iDocumentWidget *d, const iString *source) {
source,
docWidth,
width_Widget(d),
isFinished_GmRequest(d->request) ? final_GmDocumentUpdate
: partial_GmDocumentUpdate);
+}
iHistory *history_DocumentWidget(iDocumentWidget *d) {
return d->mod.history;
}
--
2.25.1
text/plain
This content has been proxied by September (3851b).