From de3463f5d9ac3a8b7c0f3e23ff11ade2b19fdb68 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sun, 28 Mar 2021 22:04:24 +0300 Subject: Folding preformatted blocks and showing alt text The alt text of preformatted blocks is shown on mouse hover. The blocks can be clicked to toggle folding. IssueID #180 --- src/gmdocument.c | 137 ++++++++++++++++++++++++++++++++---------------- src/gmdocument.h | 20 ++++++- src/ui/color.h | 3 +- src/ui/documentwidget.c | 119 +++++++++++++++++++++++++++++++++++------ 4 files changed, 215 insertions(+), 64 deletions(-) (limited to 'src') diff --git a/src/gmdocument.c b/src/gmdocument.c index f1471f0f..874a117e 100644 --- a/src/gmdocument.c +++ b/src/gmdocument.c @@ -22,6 +22,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "gmdocument.h" #include "gmutil.h" +#include "lang.h" #include "ui/color.h" #include "ui/text.h" #include "ui/metrics.h" @@ -82,6 +83,7 @@ struct Impl_GmDocument { iString bannerText; iString title; /* the first top-level title */ iArray headings; + iArray preMeta; /* metadata about preformatted blocks */ uint32_t themeSeed; iChar siteIcon; iMedia * media; @@ -143,19 +145,21 @@ static int lastVisibleRunBottom_GmDocument_(const iGmDocument *d) { return 0; } -static iInt2 measurePreformattedBlock_GmDocument_(const iGmDocument *d, const char *start, int font) { +static iInt2 measurePreformattedBlock_GmDocument_(const iGmDocument *d, const char *start, int font, + iRangecc *contents, const char **endPos) { const iRangecc content = { start, constEnd_String(&d->source) }; iRangecc line = iNullRange; nextSplit_Rangecc(content, "\n", &line); iAssert(startsWith_Rangecc(line, "```")); - iRangecc preBlock = { line.end + 1, line.end + 1 }; + *contents = (iRangecc){ line.end + 1, line.end + 1 }; while (nextSplit_Rangecc(content, "\n", &line)) { if (startsWith_Rangecc(line, "```")) { + if (endPos) *endPos = line.end; break; } - preBlock.end = line.end; + contents->end = line.end; } - return measureRange_Text(font, preBlock); + return measureRange_Text(font, *contents); } static iRangecc addLink_GmDocument_(iGmDocument *d, iRangecc line, iGmLinkId *linkId) { @@ -279,8 +283,8 @@ static iBool isForcedMonospace_GmDocument_(const iGmDocument *d) { return iFalse; } -static void linkContentLaidOut_GmDocument_(iGmDocument *d, const iGmMediaInfo *mediaInfo, - uint16_t linkId) { +static void linkContentWasLaidOut_GmDocument_(iGmDocument *d, const iGmMediaInfo *mediaInfo, + uint16_t linkId) { iGmLink *link = at_PtrArray(&d->links, linkId - 1); link->flags |= content_GmLinkFlag; if (mediaInfo && mediaInfo->isPermanent) { @@ -370,6 +374,8 @@ static void doLayout_GmDocument_(iGmDocument *d) { clear_Array(&d->layout); clearLinks_GmDocument_(d); clear_Array(&d->headings); + const iArray *oldPreMeta = collect_Array(copy_Array(&d->preMeta)); /* remember fold states */ + clear_Array(&d->preMeta); clear_String(&d->title); clear_String(&d->bannerText); if (d->size.x <= 0 || isEmpty_String(&d->source)) { @@ -381,7 +387,6 @@ static void doLayout_GmDocument_(iGmDocument *d) { iBool isFirstText = prefs->bigFirstParagraph; iBool addQuoteIcon = prefs->quoteIcon; iBool isPreformat = iFalse; - iRangecc preAltText = iNullRange; /* TODO: alt text is being ignored */ int preFont = preformatted_FontId; uint16_t preId = 0; iBool enableIndents = iFalse; @@ -408,17 +413,26 @@ static void doLayout_GmDocument_(iGmDocument *d) { } indent = indents[type]; if (type == preformatted_GmLineType) { + /* Begin a new preformatted block. */ isPreformat = iTrue; - preId++; + const size_t preIndex = preId++; preFont = preformatted_FontId; /* Use a smaller font if the block contents are wide. */ - if (measurePreformattedBlock_GmDocument_(d, line.start, preFont).x > + iGmPreMeta meta = { .bounds = line }; + meta.pixelRect.size = measurePreformattedBlock_GmDocument_( + d, line.start, preFont, &meta.contents, &meta.bounds.end); + if (meta.pixelRect.size.x > d->size.x /*- indents[preformatted_GmLineType] * gap_Text*/) { preFont = preformattedSmall_FontId; } trimLine_Rangecc(&line, type, isNormalized); - preAltText = line; - /* TODO: store and link the alt text to this run */ + meta.altText = line; /* without the ``` */ + /* Reuse previous state. */ + if (preIndex < size_Array(oldPreMeta)) { + meta.flags = constValue_Array(oldPreMeta, preIndex, iGmPreMeta).flags & + folded_GmPreMetaFlag; + } + pushBack_Array(&d->preMeta, &meta); continue; } else if (type == link_GmLineType) { @@ -446,7 +460,6 @@ static void doLayout_GmDocument_(iGmDocument *d) { if (d->format == gemini_GmDocumentFormat && startsWithSc_Rangecc(line, "```", &iCaseSensitive)) { isPreformat = iFalse; - preAltText = iNullRange; addSiteBanner = iFalse; /* overrides the banner */ continue; } @@ -523,6 +536,30 @@ static void doLayout_GmDocument_(iGmDocument *d) { pos.y += required - delta; } } + /* Folded blocks are represented by a single run with the alt text. */ + if (isPreformat) { + const iGmPreMeta *meta = constAt_Array(&d->preMeta, preId - 1); + if (meta->flags & folded_GmPreMetaFlag) { + const iBool isBlank = isEmpty_Range(&meta->altText); + iGmRun altText = { .font = paragraph_FontId, + .flags = (isBlank ? decoration_GmRunFlag : 0) | altText_GmRunFlag }; + const iInt2 margin = preRunMargin_GmDocument(d, 0); + altText.color = tmQuote_ColorId; + altText.text = isBlank ? range_Lang(range_CStr("doc.pre.nocaption")) + : meta->altText; + iInt2 size = advanceWrapRange_Text(altText.font, d->size.x - 2 * margin.x, + altText.text); + altText.bounds = altText.visBounds = init_Rect(pos.x, pos.y, d->size.x, + size.y + 2 * margin.y); + altText.preId = preId; + pushBack_Array(&d->layout, &altText); + pos.y += height_Rect(altText.bounds); + contentLine = meta->bounds; /* Skip the whole thing. */ + isPreformat = iFalse; + prevType = preformatted_GmLineType; + continue; + } + } /* Save the document title (first high-level heading). */ if ((type == heading1_GmLineType || type == heading2_GmLineType) && isEmpty_String(&d->title)) { @@ -632,14 +669,27 @@ static void doLayout_GmDocument_(iGmDocument *d) { type == quote_GmLineType ? 4 : 0); const iBool isWordWrapped = (d->format == plainText_GmDocumentFormat ? prefs->plainTextWrap : !isPreformat); + if (isPreformat) { + /* Remember the top left coordinates of the block (first line of block). */ + iGmPreMeta *meta = at_Array(&d->preMeta, preId - 1); + if (~meta->flags & topLeft_GmPreMetaFlag) { + meta->pixelRect.pos = pos; //, indent * gap_Text); + meta->flags |= topLeft_GmPreMetaFlag; + } + /* Collapse indentation if too wide. */ + if (width_Rect(meta->pixelRect) > d->size.x - (indent + rightMargin) * gap_Text) { + indent = 0; + } + } iAssert(!isEmpty_Range(&runLine)); /* must have something at this point */ while (!isEmpty_Range(&runLine)) { run.bounds.pos = addX_I2(pos, indent * gap_Text); - const int avail = isWordWrapped ? d->size.x - run.bounds.pos.x - rightMargin * gap_Text : 0; + const int wrapAvail = d->size.x - run.bounds.pos.x - rightMargin * gap_Text; + const int avail = isWordWrapped ? wrapAvail : 0; const char *contPos; const iInt2 dims = tryAdvance_Text(run.font, runLine, avail, &contPos); iChangeFlags(run.flags, wide_GmRunFlag, (isPreformat && dims.x > d->size.x)); - run.bounds.size.x = iMax(avail, dims.x); /* Extends to the right edge for selection. */ + run.bounds.size.x = iMax(wrapAvail, dims.x); /* Extends to the right edge for selection. */ run.bounds.size.y = dims.y; run.visBounds = run.bounds; run.visBounds.size.x = dims.x; @@ -671,7 +721,7 @@ static void doLayout_GmDocument_(iGmDocument *d) { iGmMediaInfo img; imageInfo_Media(d->media, imageId, &img); const iInt2 imgSize = imageSize_Media(d->media, imageId); - linkContentLaidOut_GmDocument_(d, &img, run.linkId); + linkContentWasLaidOut_GmDocument_(d, &img, run.linkId); const int margin = lineHeight_Text(paragraph_FontId) / 2; pos.y += margin; run.bounds.pos = pos; @@ -699,7 +749,7 @@ static void doLayout_GmDocument_(iGmDocument *d) { else if (audioId) { iGmMediaInfo info; audioInfo_Media(d->media, audioId, &info); - linkContentLaidOut_GmDocument_(d, &info, run.linkId); + linkContentWasLaidOut_GmDocument_(d, &info, run.linkId); const int margin = lineHeight_Text(paragraph_FontId) / 2; pos.y += margin; run.bounds.pos = pos; @@ -716,7 +766,7 @@ static void doLayout_GmDocument_(iGmDocument *d) { else if (downloadId) { iGmMediaInfo info; downloadInfo_Media(d->media, downloadId, &info); - linkContentLaidOut_GmDocument_(d, &info, run.linkId); + linkContentWasLaidOut_GmDocument_(d, &info, run.linkId); const int margin = lineHeight_Text(paragraph_FontId) / 2; pos.y += margin; run.bounds.pos = pos; @@ -773,6 +823,7 @@ void init_GmDocument(iGmDocument *d) { init_String(&d->bannerText); init_String(&d->title); init_Array(&d->headings, sizeof(iGmHeading)); + init_Array(&d->preMeta, sizeof(iGmPreMeta)); d->themeSeed = 0; d->siteIcon = 0; d->media = new_Media(); @@ -784,6 +835,7 @@ void deinit_GmDocument(iGmDocument *d) { deinit_String(&d->title); clearLinks_GmDocument_(d); deinit_PtrArray(&d->links); + deinit_Array(&d->preMeta); deinit_Array(&d->headings); deinit_Array(&d->layout); deinit_String(&d->localHost); @@ -804,6 +856,7 @@ void reset_GmDocument(iGmDocument *d) { clearLinks_GmDocument_(d); clear_Array(&d->layout); clear_Array(&d->headings); + clear_Array(&d->preMeta); clear_String(&d->url); clear_String(&d->localHost); d->themeSeed = 0; @@ -815,6 +868,8 @@ static void setDerivedThemeColors_(enum iGmDocumentTheme theme) { set_Color(tmBannerSideTitle_ColorId, mix_Color(get_Color(tmBannerTitle_ColorId), get_Color(tmBackground_ColorId), theme == colorfulDark_GmDocumentTheme ? 0.55f : 0)); + set_Color(tmAltTextBackground_ColorId, mix_Color(get_Color(tmQuoteIcon_ColorId), + get_Color(tmBackground_ColorId), 0.85f)); if (theme == colorfulDark_GmDocumentTheme) { /* Ensure paragraph text and link text aren't too similarly colored. */ if (delta_Color(get_Color(tmLinkText_ColorId), get_Color(tmParagraph_ColorId)) < 100) { @@ -824,35 +879,6 @@ static void setDerivedThemeColors_(enum iGmDocumentTheme theme) { } set_Color(tmLinkCustomIconVisited_ColorId, mix_Color(get_Color(tmLinkIconVisited_ColorId), get_Color(tmLinkIcon_ColorId), 0.5f)); -#if 0 - set_Color(tmOutlineHeadingAbove_ColorId, get_Color(white_ColorId)); - set_Color(tmOutlineHeadingBelow_ColorId, get_Color(black_ColorId)); - switch (theme) { - case colorfulDark_GmDocumentTheme: - set_Color(tmOutlineHeadingBelow_ColorId, get_Color(tmBannerTitle_ColorId)); - if (equal_Color(get_Color(tmOutlineHeadingAbove_ColorId), - get_Color(tmOutlineHeadingBelow_ColorId))) { - set_Color(tmOutlineHeadingBelow_ColorId, get_Color(tmHeading3_ColorId)); - } - break; - case colorfulLight_GmDocumentTheme: - case sepia_GmDocumentTheme: - set_Color(tmOutlineHeadingAbove_ColorId, get_Color(black_ColorId)); - set_Color(tmOutlineHeadingBelow_ColorId, mix_Color(get_Color(tmBackground_ColorId), get_Color(black_ColorId), 0.6f)); - break; - case gray_GmDocumentTheme: - set_Color(tmOutlineHeadingBelow_ColorId, get_Color(gray75_ColorId)); - break; - case white_GmDocumentTheme: - set_Color(tmOutlineHeadingBelow_ColorId, mix_Color(get_Color(tmBannerIcon_ColorId), get_Color(white_ColorId), 0.6f)); - break; - case highContrast_GmDocumentTheme: - set_Color(tmOutlineHeadingAbove_ColorId, get_Color(black_ColorId)); - break; - default: - break; - } -#endif } static void updateIconBasedOnUrl_GmDocument_(iGmDocument *d) { @@ -1406,6 +1432,20 @@ void setSource_GmDocument(iGmDocument *d, const iString *source, int width) { setWidth_GmDocument(d, width); /* re-do layout */ } +void foldPre_GmDocument(iGmDocument *d, uint16_t preId) { + if (preId > 0 && preId <= size_Array(&d->preMeta)) { + iGmPreMeta *meta = at_Array(&d->preMeta, preId - 1); + meta->flags ^= folded_GmPreMetaFlag; + } +} + +const iGmPreMeta *preMeta_GmDocument(const iGmDocument *d, uint16_t preId) { + if (preId > 0 && preId <= size_Array(&d->preMeta)) { + return constAt_Array(&d->preMeta, preId - 1); + } + return NULL; +} + void render_GmDocument(const iGmDocument *d, iRangei visRangeY, iGmDocumentRenderFunc render, void *context) { iBool isInside = iFalse; @@ -1684,4 +1724,9 @@ iRangecc findLoc_GmRun(const iGmRun *d, iInt2 pos) { return loc; } +iInt2 preRunMargin_GmDocument(const iGmDocument *d, uint16_t preId) { + iUnused(d, preId); + return init_I2(3 * gap_Text, 2 * gap_Text); +} + iDefineClass(GmDocument) diff --git a/src/gmdocument.h b/src/gmdocument.h index 57c19e9a..14c5c85d 100644 --- a/src/gmdocument.h +++ b/src/gmdocument.h @@ -32,6 +32,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include iDeclareType(GmHeading) +iDeclareType(GmPreMeta) iDeclareType(GmRun) enum iGmLineType { @@ -89,6 +90,20 @@ struct Impl_GmHeading { int level; /* 0, 1, 2 */ }; +enum iGmPreMetaFlag { + folded_GmPreMetaFlag = 0x1, + topLeft_GmPreMetaFlag = 0x2, +}; + +struct Impl_GmPreMeta { + iRangecc bounds; /* including ``` markers */ + iRangecc altText; /* range in source */ + iRangecc contents; /* just the content lines */ + int flags; + /* TODO: refactor old code to incorporate wide scroll handling here */ + iRect pixelRect; +}; + enum iGmRunFlags { decoration_GmRunFlag = iBit(1), /* not part of the source */ startOfLine_GmRunFlag = iBit(2), @@ -97,6 +112,7 @@ enum iGmRunFlags { quoteBorder_GmRunFlag = iBit(5), wide_GmRunFlag = iBit(6), /* horizontally scrollable */ footer_GmRunFlag = iBit(7), + altText_GmRunFlag = iBit(8), }; enum iGmRunMediaType { @@ -150,6 +166,7 @@ void setWidth_GmDocument (iGmDocument *, int width); void redoLayout_GmDocument (iGmDocument *); void setUrl_GmDocument (iGmDocument *, const iString *url); void setSource_GmDocument (iGmDocument *, const iString *source, int width); +void foldPre_GmDocument (iGmDocument *, uint16_t preId); void reset_GmDocument (iGmDocument *); /* free images */ @@ -194,4 +211,5 @@ const iTime * linkTime_GmDocument (const iGmDocument *, iGmLinkId linkId); iBool isMediaLink_GmDocument (const iGmDocument *, iGmLinkId linkId); const iString * title_GmDocument (const iGmDocument *); iChar siteIcon_GmDocument (const iGmDocument *); - +const iGmPreMeta *preMeta_GmDocument (const iGmDocument *, uint16_t preId); +iInt2 preRunMargin_GmDocument (const iGmDocument *, uint16_t preId); diff --git a/src/ui/color.h b/src/ui/color.h index 057ca42d..cd2f95d3 100644 --- a/src/ui/color.h +++ b/src/ui/color.h @@ -115,7 +115,8 @@ enum iColorId { uiTextAppTitle_ColorId, uiBackgroundSidebar_ColorId, uiBackgroundMenu_ColorId, - tmLinkCustomIconVisited_ColorId, + tmLinkCustomIconVisited_ColorId, /* derived from other theme colors */ + tmAltTextBackground_ColorId, /* derived from other theme colors */ /* content theme colors */ tmFirst_ColorId, diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c index 6bb16a93..93297c70 100644 --- a/src/ui/documentwidget.c +++ b/src/ui/documentwidget.c @@ -202,7 +202,8 @@ struct Impl_DocumentWidget { iRangecc foundMark; int pageMargin; iPtrArray visibleLinks; - iPtrArray visibleWideRuns; /* scrollable blocks */ + iPtrArray visiblePre; + iPtrArray visibleWideRuns; /* scrollable blocks; TODO: merge into `visiblePre` */ iArray wideRunOffsets; iAnim animWideRunOffset; uint16_t animWideRunId; @@ -211,6 +212,8 @@ struct Impl_DocumentWidget { const iGmRun * grabbedPlayer; /* currently adjusting volume in a player */ float grabbedStartVolume; int mediaTimer; + const iGmRun * hoverPre; /* for clicking */ + const iGmRun * hoverAltPre; /* for drawing alt text */ const iGmRun * hoverLink; const iGmRun * contextLink; const iGmRun * firstVisibleRun; @@ -220,6 +223,7 @@ struct Impl_DocumentWidget { float initNormScrollY; iAnim scrollY; iAnim sideOpacity; + iAnim altTextOpacity; iScrollWidget *scroll; iWidget * menu; iWidget * playerMenu; @@ -260,6 +264,8 @@ void init_DocumentWidget(iDocumentWidget *d) { d->selectMark = iNullRange; d->foundMark = iNullRange; d->pageMargin = 5; + d->hoverPre = NULL; + d->hoverAltPre = NULL; d->hoverLink = NULL; d->contextLink = NULL; d->firstVisibleRun = NULL; @@ -267,12 +273,14 @@ void init_DocumentWidget(iDocumentWidget *d) { d->visBuf = new_VisBuf(); d->invalidRuns = new_PtrSet(); init_Anim(&d->sideOpacity, 0); + init_Anim(&d->altTextOpacity, 0); d->sourceStatus = none_GmStatusCode; init_String(&d->sourceHeader); init_String(&d->sourceMime); init_Block(&d->sourceContent, 0); iZap(d->sourceTime); init_PtrArray(&d->visibleLinks); + init_PtrArray(&d->visiblePre); init_PtrArray(&d->visibleWideRuns); init_Array(&d->wideRunOffsets, sizeof(int)); init_PtrArray(&d->visibleMedia); @@ -322,6 +330,7 @@ void deinit_DocumentWidget(iDocumentWidget *d) { deinit_Array(&d->wideRunOffsets); deinit_PtrArray(&d->visibleMedia); deinit_PtrArray(&d->visibleWideRuns); + deinit_PtrArray(&d->visiblePre); deinit_PtrArray(&d->visibleLinks); delete_Block(d->certFingerprint); delete_String(d->certSubject); @@ -419,8 +428,11 @@ static void addVisible_DocumentWidget_(void *context, const iGmRun *run) { } d->lastVisibleRun = run; } - if (run->preId && run->flags & wide_GmRunFlag) { - pushBack_PtrArray(&d->visibleWideRuns, run); + if (run->preId) { + pushBack_PtrArray(&d->visiblePre, run); + if (run->flags & wide_GmRunFlag) { + pushBack_PtrArray(&d->visibleWideRuns, run); + } } if (run->mediaType == audio_GmRunMediaType || run->mediaType == download_GmRunMediaType) { iAssert(run->mediaId); @@ -501,10 +513,18 @@ static void invalidateWideRunsWithNonzeroOffset_DocumentWidget_(iDocumentWidget } } +static void animate_DocumentWidget_(void *ticker) { + iDocumentWidget *d = ticker; + if (!isFinished_Anim(&d->sideOpacity) || !isFinished_Anim(&d->altTextOpacity)) { + addTicker_App(animate_DocumentWidget_, d); + } +} + static void updateHover_DocumentWidget_(iDocumentWidget *d, iInt2 mouse) { const iWidget *w = constAs_Widget(d); const iRect docBounds = documentBounds_DocumentWidget_(d); const iGmRun * oldHoverLink = d->hoverLink; + d->hoverPre = NULL; d->hoverLink = NULL; const iInt2 hoverPos = addY_I2(sub_I2(mouse, topLeft_Rect(docBounds)), value_Anim(&d->scrollY)); if (isHover_Widget(w) && (~d->flags & noHoverWhileScrolling_DocumentWidgetFlag) && @@ -525,11 +545,31 @@ static void updateHover_DocumentWidget_(iDocumentWidget *d, iInt2 mouse) { if (d->hoverLink) { invalidateLink_DocumentWidget_(d, d->hoverLink->linkId); } - refresh_Widget(as_Widget(d)); + refresh_Widget(w); + } + if (isHover_Widget(w)) { + iConstForEach(PtrArray, j, &d->visiblePre) { + const iGmRun *run = j.ptr; + if (contains_Rect(run->bounds, hoverPos)) { + d->hoverPre = run; + d->hoverAltPre = run; + break; + } + } + } + if (!d->hoverPre && targetValue_Anim(&d->altTextOpacity) > 0.5f) { + setValue_Anim(&d->altTextOpacity, 0.0f, 300); + animate_DocumentWidget_(d); + refresh_Widget(w); + } + else if (d->hoverPre && targetValue_Anim(&d->altTextOpacity) < 0.5f) { + setValue_Anim(&d->altTextOpacity, 1.0f, 0); + refresh_Widget(w); } if (isHover_Widget(w) && !contains_Widget(constAs_Widget(d->scroll), mouse)) { setCursor_Window(get_Window(), - d->hoverLink ? SDL_SYSTEM_CURSOR_HAND : SDL_SYSTEM_CURSOR_IBEAM); + d->hoverLink || d->hoverPre ? SDL_SYSTEM_CURSOR_HAND + : SDL_SYSTEM_CURSOR_IBEAM); if (d->hoverLink && linkFlags_GmDocument(d->doc, d->hoverLink->linkId) & permanent_GmLinkFlag) { setCursor_Window(get_Window(), SDL_SYSTEM_CURSOR_ARROW); /* not dismissable */ @@ -537,13 +577,6 @@ static void updateHover_DocumentWidget_(iDocumentWidget *d, iInt2 mouse) { } } -static void animate_DocumentWidget_(void *ticker) { - iDocumentWidget *d = ticker; - if (!isFinished_Anim(&d->sideOpacity)) { - addTicker_App(animate_DocumentWidget_, d); - } -} - static void updateSideOpacity_DocumentWidget_(iDocumentWidget *d, iBool isAnimated) { float opacity = 0.0f; const iGmRun *banner = siteBanner_GmDocument(d->doc); @@ -649,6 +682,7 @@ static void updateVisible_DocumentWidget_(iDocumentWidget *d) { docSize > 0 ? height_Rect(bounds) * size_Range(&visRange) / docSize : 0); clear_PtrArray(&d->visibleLinks); clear_PtrArray(&d->visibleWideRuns); + clear_PtrArray(&d->visiblePre); clear_PtrArray(&d->visibleMedia); const iRangecc oldHeading = currentHeading_DocumentWidget_(d); /* Scan for visible runs. */ { @@ -765,6 +799,8 @@ static iRangecc bannerText_DocumentWidget_(const iDocumentWidget *d) { static void documentRunsInvalidated_DocumentWidget_(iDocumentWidget *d) { d->foundMark = iNullRange; d->selectMark = iNullRange; + d->hoverPre = NULL; + d->hoverAltPre = NULL; d->hoverLink = NULL; d->contextLink = NULL; d->firstVisibleRun = NULL; @@ -1199,6 +1235,18 @@ static void scrollWideBlock_DocumentWidget_(iDocumentWidget *d, iInt2 mousePos, } } +static void togglePreFold_DocumentWidget_(iDocumentWidget *d, uint16_t preId) { + d->hoverPre = NULL; + d->hoverAltPre = NULL; + d->selectMark = iNullRange; + foldPre_GmDocument(d->doc, preId); + redoLayout_GmDocument(d->doc); + scroll_DocumentWidget_(d, 0); + updateHover_DocumentWidget_(d, mouseCoord_Window(get_Window())); + invalidate_DocumentWidget_(d); + refresh_Widget(as_Widget(d)); +} + static void checkResponse_DocumentWidget_(iDocumentWidget *d) { if (!d->request) { return; @@ -2681,6 +2729,10 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e } if (!isMoved_Click(&d->click)) { setFocus_Widget(NULL); + if (d->hoverPre) { + togglePreFold_DocumentWidget_(d, d->hoverPre->preId); + return iTrue; + } if (d->hoverLink) { const iGmLinkId linkId = d->hoverLink->linkId; const int linkFlags = linkFlags_GmDocument(d->doc, linkId); @@ -2994,7 +3046,14 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) { fg = linkColor_GmDocument(doc, run->linkId, textHover_GmLinkPart); /* link is inactive */ } } - if (run->flags & siteBanner_GmRunFlag) { + if (run->flags & altText_GmRunFlag) { + const iInt2 margin = preRunMargin_GmDocument(doc, run->preId); + fillRect_Paint(&d->paint, (iRect){ visPos, run->visBounds.size }, tmAltTextBackground_ColorId); + drawRect_Paint(&d->paint, (iRect){ visPos, run->visBounds.size }, tmQuoteIcon_ColorId); + drawWrapRange_Text(run->font, add_I2(visPos, margin), + run->visBounds.size.x - 2 * margin.x, run->color, run->text); + } + else if (run->flags & siteBanner_GmRunFlag) { /* Banner background. */ iRect bannerBack = initCorners_Rect(topLeft_Rect(d->widgetBounds), init_I2(right_Rect(bounds_Widget(constAs_Widget(d->widget))), @@ -3157,8 +3216,8 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) { } } } -// drawRect_Paint(&d->paint, (iRect){ visPos, run->bounds.size }, green_ColorId); -// drawRect_Paint(&d->paint, (iRect){ visPos, run->visBounds.size }, red_ColorId); + 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) { @@ -3392,7 +3451,7 @@ static void draw_DocumentWidget_(const iDocumentWidget *d) { hasSiteBanner_GmDocument(d->doc) ? tmBannerBackground_ColorId : tmBackground_ColorId); } - const int yBottom = yTop + size_GmDocument(d->doc).y; + const int yBottom = yTop + size_GmDocument(d->doc).y + 1; if (yBottom < bottom_Rect(bounds)) { fillRect_Paint(&ctx.paint, init_Rect(bounds.pos.x, yBottom, bounds.size.x, bottom_Rect(bounds) - yBottom), @@ -3412,6 +3471,34 @@ static void draw_DocumentWidget_(const iDocumentWidget *d) { drawHLine_Paint(&ctx.paint, topLeft_Rect(bounds), width_Rect(bounds), uiSeparator_ColorId); } draw_Widget(w); + /* Alt text. */ + const float altTextOpacity = value_Anim(&d->altTextOpacity); + if (d->hoverAltPre && altTextOpacity > 0) { + const iGmPreMeta *meta = preMeta_GmDocument(d->doc, d->hoverAltPre->preId); + if (meta->flags & topLeft_GmPreMetaFlag && ~meta->flags & decoration_GmRunFlag && + !isEmpty_Range(&meta->altText)) { + const int margin = 2 * gap_UI; + const int altFont = uiLabel_FontId; + const int wrap = docBounds.size.x - 2 * margin; + iInt2 pos = addY_I2(add_I2(docBounds.pos, meta->pixelRect.pos), + -value_Anim(&d->scrollY)); + const iInt2 textSize = advanceWrapRange_Text(altFont, wrap, meta->altText); + 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, tmAltTextBackground_ColorId); + drawRect_Paint(&ctx.paint, altRect, tmQuoteIcon_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); + } + } } /*----------------------------------------------------------------------------------------------*/ -- cgit v1.2.3