From b4e13bb58f644276469a69c4bd93e22381aa0002 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Mon, 25 Oct 2021 22:10:04 +0300 Subject: Document presentation warnings Warn the user about missing glyphs and potentially unsupported ANSI escapes. TODO: Site-specific setting for dismissed warning; fonts preference about missing glyph warnings. --- src/gmdocument.c | 47 +++++++++++++++++++++++++----- src/gmdocument.h | 6 ++++ src/gmutil.c | 8 +++++ src/gmutil.h | 2 ++ src/ui/banner.c | 77 ++++++++++++++++++++++++++++++++++++++++++------- src/ui/documentwidget.c | 27 ++++++++++++----- src/ui/labelwidget.c | 8 +++-- src/ui/text.c | 9 ++++++ src/ui/text.h | 1 + 9 files changed, 156 insertions(+), 29 deletions(-) (limited to 'src') diff --git a/src/gmdocument.c b/src/gmdocument.c index bd3fa788..c7d5501f 100644 --- a/src/gmdocument.c +++ b/src/gmdocument.c @@ -107,6 +107,7 @@ struct Impl_GmDocument { iChar siteIcon; iMedia * media; iStringSet *openURLs; /* currently open URLs for highlighting links */ + int warnings; iBool isPaletteValid; iColor palette[tmMax_ColorId]; /* copy of the color palette */ }; @@ -596,6 +597,8 @@ static void doLayout_GmDocument_(iGmDocument *d) { isPreformat = iTrue; isFirstText = iFalse; } + d->warnings &= ~missingGlyphs_GmDocumentWarning; + checkMissing_Text(); /* clear the flag */ setAnsiFlags_Text(d->theme.ansiEscapes); while (nextSplit_Rangecc(content, "\n", &contentLine)) { iRangecc line = contentLine; /* `line` will be trimmed; modifying would confuse `nextSplit_Rangecc` */ @@ -1045,6 +1048,9 @@ static void doLayout_GmDocument_(iGmDocument *d) { } #endif d->size.y = pos.y; + if (checkMissing_Text()) { + d->warnings |= missingGlyphs_GmDocumentWarning; + } /* Go over the preformatted blocks and mark them wide if at least one run is wide. */ { /* TODO: Store the dimensions and ranges for later access. */ iForEach(Array, i, &d->layout) { @@ -1084,6 +1090,7 @@ void init_GmDocument(iGmDocument *d) { d->siteIcon = 0; d->media = new_Media(); d->openURLs = NULL; + d->warnings = 0; d->isPaletteValid = iFalse; iZap(d->palette); } @@ -1123,16 +1130,28 @@ static void setDerivedThemeColors_(enum iGmDocumentTheme theme) { mix_Color(get_Color(tmBannerTitle_ColorId), get_Color(tmBackground_ColorId), theme == colorfulDark_GmDocumentTheme ? 0.55f : 0)); - const int bannerItemFg = isDark_GmDocumentTheme(currentTheme_()) ? white_ColorId : black_ColorId; - set_Color(tmBannerItemBackground_ColorId, mix_Color(get_Color(tmBannerBackground_ColorId), - get_Color(tmBannerTitle_ColorId), 0.1f)); - set_Color(tmBannerItemFrame_ColorId, mix_Color(get_Color(tmBannerBackground_ColorId), - get_Color(tmBannerTitle_ColorId), 0.4f)); - set_Color(tmBannerItemText_ColorId, mix_Color(get_Color(tmBannerBackground_ColorId), - get_Color(bannerItemFg), 0.75f)); - set_Color(tmBannerItemTitle_ColorId, get_Color(bannerItemFg)); + /* Banner colors. */ + if (theme == highContrast_GmDocumentTheme) { + set_Color(tmBannerItemBackground_ColorId, get_Color(tmBannerBackground_ColorId)); + set_Color(tmBannerItemFrame_ColorId, get_Color(tmBannerIcon_ColorId)); + set_Color(tmBannerItemTitle_ColorId, get_Color(tmBannerTitle_ColorId)); + set_Color(tmBannerItemText_ColorId, get_Color(tmBannerTitle_ColorId)); + } + else { + const int bannerItemFg = isDark_GmDocumentTheme(currentTheme_()) ? white_ColorId : black_ColorId; + set_Color(tmBannerItemBackground_ColorId, mix_Color(get_Color(tmBannerBackground_ColorId), + get_Color(tmBannerTitle_ColorId), 0.1f)); + set_Color(tmBannerItemFrame_ColorId, mix_Color(get_Color(tmBannerBackground_ColorId), + get_Color(tmBannerTitle_ColorId), 0.4f)); + set_Color(tmBannerItemText_ColorId, mix_Color(get_Color(tmBannerTitle_ColorId), + get_Color(bannerItemFg), 0.5f)); + set_Color(tmBannerItemTitle_ColorId, get_Color(bannerItemFg)); + } + /* Modified backgrounds. */ set_Color(tmBackgroundAltText_ColorId, mix_Color(get_Color(tmQuoteIcon_ColorId), get_Color(tmBackground_ColorId), 0.85f)); + set_Color(tmFrameAltText_ColorId, + mix_Color(get_Color(tmQuoteIcon_ColorId), get_Color(tmBackground_ColorId), 0.4f)); set_Color(tmBackgroundOpenLink_ColorId, mix_Color(get_Color(tmLinkText_ColorId), get_Color(tmBackground_ColorId), 0.90f)); set_Color(tmFrameOpenLink_ColorId, @@ -2008,6 +2027,14 @@ void setSource_GmDocument(iGmDocument *d, const iString *source, int width, int /* Normalize and convert to Gemtext if needed. */ set_String(&d->unormSource, source); set_String(&d->source, source); + /* Detect use of ANSI escapes. */ { + iRegExp *ansiEsc = new_RegExp("\x1b[[()]([0-9;AB]*?)m", 0); + iRegExpMatch m; + init_RegExpMatch(&m); + const iBool found = matchString_RegExp(ansiEsc, &d->unormSource, &m); + iChangeFlags(d->warnings, ansiEscapes_GmDocumentWarning, found); + iRelease(ansiEsc); + } if (d->format == gemini_SourceFormat) { d->theme.ansiEscapes = prefs_App()->gemtextAnsiEscapes; } @@ -2157,6 +2184,10 @@ size_t memorySize_GmDocument(const iGmDocument *d) { memorySize_Media(d->media); } +int warnings_GmDocument(const iGmDocument *d) { + return d->warnings; +} + iRangecc findText_GmDocument(const iGmDocument *d, const iString *text, const char *start) { const char * src = constBegin_String(&d->source); const size_t startPos = (start ? start - src : 0); diff --git a/src/gmdocument.h b/src/gmdocument.h index e010ad43..0c24302f 100644 --- a/src/gmdocument.h +++ b/src/gmdocument.h @@ -172,6 +172,11 @@ iRangecc findLoc_GmRun (const iGmRun *, iInt2 pos); iDeclareClass(GmDocument) iDeclareObjectConstruction(GmDocument) +enum iGmDocumentWarning { + ansiEscapes_GmDocumentWarning = iBit(1), + missingGlyphs_GmDocumentWarning = iBit(2), +}; + /* enum iGmDocumentBanner { none_GmDocumentBanner, @@ -221,6 +226,7 @@ iInt2 size_GmDocument (const iGmDocument *); const iArray * headings_GmDocument (const iGmDocument *); /* array of GmHeadings */ const iString * source_GmDocument (const iGmDocument *); size_t memorySize_GmDocument (const iGmDocument *); /* bytes */ +int warnings_GmDocument (const iGmDocument *); iRangecc findText_GmDocument (const iGmDocument *, const iString *text, const char *start); iRangecc findTextBefore_GmDocument (const iGmDocument *, const iString *text, const char *before); diff --git a/src/gmutil.c b/src/gmutil.c index 264e7ac8..22219221 100644 --- a/src/gmutil.c +++ b/src/gmutil.c @@ -755,6 +755,14 @@ static const struct { { 0x1f645, /* no good */ "${error.certverify}", "${error.certverify.msg}" } }, + { ansiEscapes_GmStatusCode, + { 0x1f5b3, /* old computer */ + "${error.ansi}", + "${error.ansi.msg}" } }, + { missingGlyphs_GmStatusCode, + { 0x1f520, /* ABCD */ + "${error.glyphs}", + "${error.glyphs.msg}" } }, { temporaryFailure_GmStatusCode, { 0x1f50c, /* electric plug */ "${error.temporary}", diff --git a/src/gmutil.h b/src/gmutil.h index c65a17e6..6b10b444 100644 --- a/src/gmutil.h +++ b/src/gmutil.h @@ -45,6 +45,8 @@ enum iGmStatusCode { tlsFailure_GmStatusCode, tlsServerCertificateExpired_GmStatusCode, tlsServerCertificateNotVerified_GmStatusCode, + ansiEscapes_GmStatusCode, + missingGlyphs_GmStatusCode, none_GmStatusCode = 0, /* general status code categories */ diff --git a/src/ui/banner.c b/src/ui/banner.c index d95c853b..e817f1bb 100644 --- a/src/ui/banner.c +++ b/src/ui/banner.c @@ -27,6 +27,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "lang.h" #include "paint.h" #include "util.h" +#include "app.h" iDeclareType(BannerItem) @@ -53,27 +54,33 @@ struct Impl_Banner { iRect rect; iString site; iString icon; + int siteHeight; iArray items; iBool isClick; }; iDefineTypeConstruction(Banner) +#define itemGap_Banner_ (3 * gap_UI) +#define itemVPad_Banner_ (2 * gap_UI) +#define itemHPad_Banner_ (3 * gap_UI) +#define bottomPad_Banner_ (4 * gap_UI) + static void updateHeight_Banner_(iBanner *d) { d->rect.size.y = 0; if (!isEmpty_String(&d->site)) { - d->rect.size.y += lineHeight_Text(banner_FontId) * 2; + d->siteHeight = lineHeight_Text(banner_FontId) * 2; + d->rect.size.y += d->siteHeight; } const size_t numItems = size_Array(&d->items); if (numItems) { - const int outerPad = 2 * gap_UI; const int innerPad = gap_UI; iConstForEach(Array, i, &d->items) { const iBannerItem *item = i.value; d->rect.size.y += item->height; } - d->rect.size.y += (numItems - 1) * innerPad; - d->rect.size.y += outerPad; + d->rect.size.y += (numItems - 1) * itemGap_Banner_; + d->rect.size.y += bottomPad_Banner_; } } @@ -99,9 +106,9 @@ void setOwner_Banner(iBanner *d, iDocumentWidget *owner) { static void updateItemHeight_Banner_(const iBanner *d, iBannerItem *item) { item->height = measureWrapRange_Text(uiContent_FontId, - width_Rect(d->rect) - 6 * gap_UI, + width_Rect(d->rect) - 2 * itemHPad_Banner_, range_String(&item->text)) - .bounds.size.y + 4 * gap_UI; + .bounds.size.y + 2 * itemVPad_Banner_; } void setWidth_Banner(iBanner *d, int width) { @@ -186,6 +193,7 @@ void draw_Banner(const iBanner *d) { return; } iRect bounds = d->rect; + /* TODO: use d->siteHeight */ iInt2 pos = addY_I2(topLeft_Rect(bounds), lineHeight_Text(banner_FontId) / 2); iPaint p; init_Paint(&p); @@ -218,16 +226,28 @@ void draw_Banner(const iBanner *d) { setBaseAttributes_Text(uiContent_FontId, tmBannerItemText_ColorId); iWrapText wt = { .text = range_String(&item->text), - .maxWidth = width_Rect(itemRect) - 6 * gap_UI, + .maxWidth = width_Rect(itemRect) - 2 * itemHPad_Banner_, .mode = word_WrapTextMode }; - draw_WrapText(&wt, uiContent_FontId, add_I2(pos, init_I2(3 * gap_UI, 2 * gap_UI)), + draw_WrapText(&wt, uiContent_FontId, add_I2(pos, init_I2(itemHPad_Banner_, itemVPad_Banner_)), tmBannerItemText_ColorId); - pos.y += innerPad; + pos.y += item->height + itemGap_Banner_; } setBaseAttributes_Text(-1, -1); } +static size_t itemAtCoord_Banner_(const iBanner *d, iInt2 coord) { + iInt2 pos = addY_I2(topLeft_Rect(d->rect), lineHeight_Text(banner_FontId) * 2); + iConstForEach(Array, i, &d->items) { + const iBannerItem *item = i.value; + if (contains_Rect((iRect){ pos, init_I2(d->rect.size.x, item->height)}, coord)) { + return index_ArrayConstIterator(&i); + } + pos.y += itemGap_Banner_ + item->height; + } + return iInvalidPos; +} + iBool processEvent_Banner(iBanner *d, const SDL_Event *ev) { iWidget *w = as_Widget(d->doc); switch (ev->type) { @@ -240,14 +260,49 @@ iBool processEvent_Banner(iBanner *d, const SDL_Event *ev) { case SDL_MOUSEBUTTONUP: /* Clicking on the top/side banner navigates to site root. */ if (ev->button.button == SDL_BUTTON_LEFT) { - const iBool isInside = contains_Rect(d->rect, init_I2(ev->button.x, ev->button.y)); + const iInt2 coord = init_I2(ev->button.x, ev->button.y); + const iBool isInside = contains_Rect(d->rect, coord); if (isInside && ev->button.state == SDL_PRESSED) { d->isClick = iTrue; return iTrue; } else if (ev->button.state == SDL_RELEASED) { if (d->isClick && isInside) { - postCommand_Widget(d->doc, "navigate.root"); + const size_t index = itemAtCoord_Banner_(d, coord); + if (index == iInvalidPos) { + if (coord.y < top_Rect(d->rect) + d->siteHeight) { + postCommand_Widget(d->doc, "navigate.root"); + } + } + else { + const iBannerItem *item = constAt_Array(&d->items, index); + if (item->type == error_BannerType) { + postCommand_Widget(d->doc, "document.info"); + } + else { + switch (item->code) { + case missingGlyphs_GmStatusCode: + postCommandf_App("open newtab:1 url:about:fonts"); + break; + case ansiEscapes_GmStatusCode: + makeQuestion_Widget(uiHeading_ColorEscape "${heading.dismiss.warning}", + format_Lang("${dlg.dismiss.ansi}", + format_CStr(uiTextStrong_ColorEscape "%s" + restore_ColorEscape, cstr_Rangecc(urlHost_String(url_DocumentWidget(d->doc))))), + (iMenuItem[]){ { "${cancel}" }, + { uiTextAction_ColorEscape "${dlg.dismiss.warning}", + SDLK_RETURN, 0, + format_CStr("document.dismiss warning:%d", + ansiEscapes_GmDocumentWarning) + } + }, 2); + break; + default: + postCommand_Widget(d->doc, "document.info"); + break; + } + } + } } d->isClick = iFalse; } diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c index 13a8dae7..755cec6a 100644 --- a/src/ui/documentwidget.c +++ b/src/ui/documentwidget.c @@ -934,7 +934,7 @@ static void updateVisible_DocumentWidget_(iDocumentWidget *d) { updateSideOpacity_DocumentWidget_(d, iTrue); animateMedia_DocumentWidget_(d); setPos_Banner(d->banner, addY_I2(topLeft_Rect(documentBounds_DocumentWidget_(d)), - -pos_SmoothScroll(&d->scrollY))); + -pos_SmoothScroll(&d->scrollY))); /*init_I2(documentBounds_DocumentWidget_(d).pos.x, viewPos_DocumentWidget_(d) - documentTopPad_DocumentWidget_(d)));*/ @@ -1291,7 +1291,6 @@ static void showErrorPage_DocumentWidget_(iDocumentWidget *d, enum iGmStatusCode iRelease(errorDoc); clear_Banner(d->banner); add_Banner(d->banner, error_BannerType, code, meta); -// translate_Lang(src); d->state = ready_RequestState; setSource_DocumentWidget(d, src); updateTheme_DocumentWidget_(d); @@ -1798,6 +1797,16 @@ static void cacheDocumentGlyphs_DocumentWidget_(const iDocumentWidget *d) { } } +static void addBannerWarnings_DocumentWidget_(iDocumentWidget *d) { + if (warnings_GmDocument(d->doc) & missingGlyphs_GmDocumentWarning) { + add_Banner(d->banner, warning_BannerType, missingGlyphs_GmStatusCode, NULL); + /* TODO: List one or more of the missing characters and/or their Unicode blocks? */ + } + if (warnings_GmDocument(d->doc) & ansiEscapes_GmDocumentWarning) { + add_Banner(d->banner, warning_BannerType, ansiEscapes_GmStatusCode, NULL); + } +} + static void updateFromCachedResponse_DocumentWidget_(iDocumentWidget *d, float normScrollY, const iGmResponse *resp, iGmDocument *cachedDoc) { setLinkNumberMode_DocumentWidget_(d, iFalse); @@ -1824,6 +1833,7 @@ static void updateFromCachedResponse_DocumentWidget_(iDocumentWidget *d, float n // (d->flags & openedFromSidebar_DocumentWidgetFlag) != 0); clear_Banner(d->banner); updateBanner_DocumentWidget_(d); + addBannerWarnings_DocumentWidget_(d); } d->state = ready_RequestState; postProcessRequestContent_DocumentWidget_(d, iTrue); @@ -2864,10 +2874,10 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) 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); +// 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() != phone_AppDeviceType) { const iWidget *lockButton = findWidget_Root("navbar.lock"); setPos_Widget(dlg, windowToLocal_Widget(dlg, bottomLeft_Rect(bounds_Widget(lockButton)))); @@ -2989,6 +2999,7 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) if (category_GmStatusCode(status_GmRequest(d->request)) == categorySuccess_GmStatusCode) { init_Anim(&d->scrollY.pos, d->initNormScrollY * pageHeight_DocumentWidget_(d)); /* TODO: unless user already scrolled! */ } + addBannerWarnings_DocumentWidget_(d); iChangeFlags(d->flags, urlChanged_DocumentWidgetFlag | drawDownloadCounter_DocumentWidgetFlag, iFalse); @@ -4377,7 +4388,7 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) { if (run->flags & altText_GmRunFlag) { 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 }, tmQuoteIcon_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, @@ -5053,7 +5064,7 @@ static void draw_DocumentWidget_(const iDocumentWidget *d) { SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_BLEND); } fillRect_Paint(&ctx.paint, altRect, tmBackgroundAltText_ColorId); - drawRect_Paint(&ctx.paint, altRect, tmQuoteIcon_ColorId); + drawRect_Paint(&ctx.paint, altRect, tmFrameAltText_ColorId); setOpacity_Text(altTextOpacity); drawWrapRange_Text(altFont, addX_I2(pos, margin), wrap, tmQuote_ColorId, meta->altText); diff --git a/src/ui/labelwidget.c b/src/ui/labelwidget.c index fbcd24b9..daca05d1 100644 --- a/src/ui/labelwidget.c +++ b/src/ui/labelwidget.c @@ -380,8 +380,12 @@ static void draw_LabelWidget_(const iLabelWidget *d) { } if (d->flags.wrap) { const iRect cont = contentBounds_LabelWidget_(d); - drawWrapRange_Text( - d->font, topLeft_Rect(cont), width_Rect(cont), fg, range_String(&d->label)); + iWrapText wt = { + .text = range_String(&d->label), + .maxWidth = width_Rect(cont), + .mode = word_WrapTextMode, + }; + draw_WrapText(&wt, d->font, topLeft_Rect(cont), fg); } else if (flags & alignLeft_WidgetFlag) { draw_Text(d->font, add_I2(bounds.pos, addX_I2(padding_LabelWidget_(d, 0), iconPad)), diff --git a/src/ui/text.c b/src/ui/text.c index 106c55e9..2ef54d39 100644 --- a/src/ui/text.c +++ b/src/ui/text.c @@ -264,6 +264,7 @@ struct Impl_Text { int ansiFlags; int baseFontId; /* base attributes (for restoring via escapes) */ int baseColorId; + iBool missingGlyphs; /* true if a glyph couldn't be found */ }; iDefineTypeConstructionArgs(Text, (SDL_Renderer *render), render) @@ -797,6 +798,7 @@ iLocalDef iFont *characterFont_Font_(iFont *d, iChar ch, uint32_t *glyphIndex) { } #endif // 0 if (!*glyphIndex) { + activeText_->missingGlyphs = iTrue; fprintf(stderr, "failed to find %08x (%lc)\n", ch, (int)ch); fflush(stderr); } return d; @@ -2260,6 +2262,13 @@ iTextMetrics draw_WrapText(iWrapText *d, int fontId, iInt2 pos, int color) { return tm; } +iBool checkMissing_Text(void) { + iText *d = activeText_; + const iBool missing = d->missingGlyphs; + d->missingGlyphs = iFalse; + return missing; +} + SDL_Texture *glyphCache_Text(void) { return activeText_->cache; } diff --git a/src/ui/text.h b/src/ui/text.h index 99ddb704..13a636d4 100644 --- a/src/ui/text.h +++ b/src/ui/text.h @@ -220,6 +220,7 @@ struct Impl_WrapText { iTextMetrics measure_WrapText (iWrapText *, int fontId); iTextMetrics draw_WrapText (iWrapText *, int fontId, iInt2 pos, int color); +iBool checkMissing_Text (void); /* returns the flag, and clears it */ SDL_Texture * glyphCache_Text (void); enum iTextBlockMode { quadrants_TextBlockMode, shading_TextBlockMode }; -- cgit v1.2.3