From edf1c0bb8b112879433f2e31fd9750c30e2d5144 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sat, 28 Nov 2020 14:23:18 +0200 Subject: Scrolling wide preformatted blocks horizontally Not entirely glitch-free but should be good enough for now. IssueID #44 --- src/gmdocument.c | 43 +++++++++++++++++++---- src/gmdocument.h | 13 +++++-- src/ui/documentwidget.c | 92 +++++++++++++++++++++++++++++++++++++++---------- src/ui/text.c | 11 +++--- 4 files changed, 126 insertions(+), 33 deletions(-) (limited to 'src') diff --git a/src/gmdocument.c b/src/gmdocument.c index 2f3a006f..1ff085c7 100644 --- a/src/gmdocument.c +++ b/src/gmdocument.c @@ -304,6 +304,7 @@ static void doLayout_GmDocument_(iGmDocument *d) { iBool isPreformat = iFalse; iRangecc preAltText = iNullRange; int preFont = preformatted_FontId; + uint16_t preId = 0; iBool enableIndents = iFalse; iBool addSiteBanner = d->siteBannerEnabled; enum iGmLineType prevType = text_GmLineType; @@ -313,12 +314,7 @@ static void doLayout_GmDocument_(iGmDocument *d) { } while (nextSplit_Rangecc(content, "\n", &contentLine)) { iRangecc line = contentLine; /* `line` will be trimmed later; would confuse nextSplit */ - iGmRun run; - run.flags = 0; - run.color = white_ColorId; - run.linkId = 0; - run.imageId = 0; - run.audioId = 0; + iGmRun run = { .color = white_ColorId }; enum iGmLineType type; int indent = 0; /* Detect the type of the line. */ @@ -330,6 +326,7 @@ static void doLayout_GmDocument_(iGmDocument *d) { indent = indents[type]; if (type == preformatted_GmLineType) { isPreformat = iTrue; + preId++; preFont = preformatted_FontId; /* Use a smaller font if the block contents are wide. */ if (measurePreformattedBlock_GmDocument_(d, line.start, preFont).x > @@ -370,6 +367,7 @@ static void doLayout_GmDocument_(iGmDocument *d) { addSiteBanner = iFalse; /* overrides the banner */ continue; } + run.preId = preId; run.font = (d->format == plainText_GmDocumentFormat ? regularMonospace_FontId : preFont); indent = indents[type]; } @@ -509,8 +507,9 @@ static void doLayout_GmDocument_(iGmDocument *d) { } run.bounds.pos = addX_I2(pos, indent * gap_Text); const char *contPos; - const int avail = d->size.x - run.bounds.pos.x; + const int avail = isPreformat ? 0 : (d->size.x - run.bounds.pos.x); 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.y = dims.y; run.visBounds = run.bounds; @@ -596,6 +595,19 @@ static void doLayout_GmDocument_(iGmDocument *d) { prevType = type; } d->size.y = pos.y; + /* Go over the preformatted blocks and mark them wide if at least one run is wide. */ { + iForEach(Array, i, &d->layout) { + iGmRun *run = i.value; + if (run->preId && run->flags & wide_GmRunFlag) { + iGmRunRange block = findPreformattedRange_GmDocument(d, run); + for (const iGmRun *j = block.start; j != block.end; j++) { + iConstCast(iGmRun *, j)->flags |= wide_GmRunFlag; + } + /* Skip to the end of the block. */ + i.pos = block.end - (const iGmRun *) constData_Array(&d->layout) - 1; + } + } + } } void init_GmDocument(iGmDocument *d) { @@ -1237,6 +1249,23 @@ iRangecc findTextBefore_GmDocument(const iGmDocument *d, const iString *text, co return found; } +iGmRunRange findPreformattedRange_GmDocument(const iGmDocument *d, const iGmRun *run) { + iAssert(run->preId); + iGmRunRange range = { run, run }; + /* Find the beginning. */ + while (range.start > (const iGmRun *) constData_Array(&d->layout)) { + const iGmRun *prev = range.start - 1; + if (prev->preId != run->preId) break; + range.start = prev; + } + /* Find the ending. */ + while (range.end < (const iGmRun *) constEnd_Array(&d->layout)) { + if (range.end->preId != run->preId) break; + range.end++; + } + return range; +} + const iGmRun *findRun_GmDocument(const iGmDocument *d, iInt2 pos) { /* TODO: Perf optimization likely needed; use a block map? */ const iGmRun *last = NULL; diff --git a/src/gmdocument.h b/src/gmdocument.h index c2a4b272..6804d772 100644 --- a/src/gmdocument.h +++ b/src/gmdocument.h @@ -90,11 +90,19 @@ struct Impl_GmRun { uint8_t flags; iRect bounds; /* used for hit testing, may extend to edges */ iRect visBounds; /* actual visual bounds */ + uint16_t preId; /* preformatted block ID (sequential) */ iGmLinkId linkId; /* zero for non-links */ uint16_t imageId; /* zero if not an image */ uint16_t audioId; /* zero if not audio */ }; +iDeclareType(GmRunRange) + +struct Impl_GmRunRange { + const iGmRun *start; + const iGmRun *end; +}; + const char * findLoc_GmRun (const iGmRun *, iInt2 pos); iDeclareClass(GmDocument) @@ -130,8 +138,9 @@ const iString * bannerText_GmDocument (const iGmDocument *); const iArray * headings_GmDocument (const iGmDocument *); /* array of GmHeadings */ const iString * source_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); +iRangecc findText_GmDocument (const iGmDocument *, const iString *text, const char *start); +iRangecc findTextBefore_GmDocument (const iGmDocument *, const iString *text, const char *before); +iGmRunRange findPreformattedRange_GmDocument (const iGmDocument *, const iGmRun *run); enum iGmLinkPart { icon_GmLinkPart, diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c index 4de25b58..a1b26e7f 100644 --- a/src/ui/documentwidget.c +++ b/src/ui/documentwidget.c @@ -157,6 +157,8 @@ struct Impl_DocumentWidget { iRangecc foundMark; int pageMargin; iPtrArray visibleLinks; + iPtrArray visibleWideRuns; /* scrollable blocks */ + iArray wideRunOffsets; iPtrArray visiblePlayers; /* currently playing audio */ const iGmRun * grabbedPlayer; /* currently adjusting volume in a player */ float grabbedStartVolume; @@ -218,6 +220,8 @@ void init_DocumentWidget(iDocumentWidget *d) { init_Block(&d->sourceContent, 0); iZap(d->sourceTime); init_PtrArray(&d->visibleLinks); + init_PtrArray(&d->visibleWideRuns); + init_Array(&d->wideRunOffsets, sizeof(int)); init_PtrArray(&d->visiblePlayers); d->grabbedPlayer = NULL; d->playerTimer = 0; @@ -256,7 +260,9 @@ void deinit_DocumentWidget(iDocumentWidget *d) { if (d->playerTimer) { SDL_RemoveTimer(d->playerTimer); } + deinit_Array(&d->wideRunOffsets); deinit_PtrArray(&d->visiblePlayers); + deinit_PtrArray(&d->visibleWideRuns); deinit_PtrArray(&d->visibleLinks); delete_Block(d->certFingerprint); delete_String(d->certSubject); @@ -317,20 +323,6 @@ static iRect siteBannerRect_DocumentWidget_(const iDocumentWidget *d) { return moved_Rect(banner->visBounds, origin); } -#if 0 -static int forceBreakWidth_DocumentWidget_(const iDocumentWidget *d) { - if (equalCase_Rangecc(urlScheme_String(d->mod.url), "gopher")) { - return documentWidth_DocumentWidget_(d); - } - if (forceLineWrap_App()) { - const iRect bounds = bounds_Widget(constAs_Widget(d)); - const iRect docBounds = documentBounds_DocumentWidget_(d); - return right_Rect(bounds) - left_Rect(docBounds) - gap_UI * d->pageMargin; - } - return 0; -} -#endif - static iInt2 documentPos_DocumentWidget_(const iDocumentWidget *d, iInt2 pos) { return addY_I2(sub_I2(pos, topLeft_Rect(documentBounds_DocumentWidget_(d))), value_Anim(&d->scrollY)); @@ -351,6 +343,9 @@ 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->audioId) { pushBack_PtrArray(&d->visiblePlayers, run); } @@ -538,6 +533,7 @@ static void updateVisible_DocumentWidget_(iDocumentWidget *d) { value_Anim(&d->scrollY), docSize > 0 ? height_Rect(bounds) * size_Range(&visRange) / docSize : 0); clear_PtrArray(&d->visibleLinks); + clear_PtrArray(&d->visibleWideRuns); clear_PtrArray(&d->visiblePlayers); const iRangecc oldHeading = currentHeading_DocumentWidget_(d); /* Scan for visible runs. */ { @@ -766,6 +762,7 @@ static void showErrorPage_DocumentWidget_(iDocumentWidget *d, enum iGmStatusCode updateTheme_DocumentWidget_(d); init_Anim(&d->scrollY, 0); init_Anim(&d->sideOpacity, 0); + clear_Array(&d->wideRunOffsets); d->state = ready_RequestState; } @@ -903,7 +900,6 @@ static void fetch_DocumentWidget_(iDocumentWidget *d) { d->request = new_GmRequest(certs_App()); setUrl_GmRequest(d->request, d->mod.url); iConnect(GmRequest, d->request, updated, d, requestUpdated_DocumentWidget_); -// iConnect(GmRequest, d->request, timeout, d, requestTimedOut_DocumentWidget_); iConnect(GmRequest, d->request, finished, d, requestFinished_DocumentWidget_); submit_GmRequest(d->request); } @@ -955,6 +951,7 @@ static iBool updateFromHistory_DocumentWidget_(iDocumentWidget *d) { reset_GmDocument(d->doc); d->state = fetching_RequestState; d->initNormScrollY = recent->normScrollY; + clear_Array(&d->wideRunOffsets); /* Use the cached response data. */ updateTrust_DocumentWidget_(d, resp); d->sourceTime = resp->when; @@ -986,6 +983,9 @@ static void refreshWhileScrolling_DocumentWidget_(iAny *ptr) { } static void smoothScroll_DocumentWidget_(iDocumentWidget *d, int offset, int duration) { + if (offset == 0) { + return; + } /* Get rid of link numbers when scrolling. */ if (offset && d->flags & showLinkNumbers_DocumentWidgetFlag) { d->flags &= ~showLinkNumbers_DocumentWidgetFlag; @@ -1030,6 +1030,39 @@ static void scrollTo_DocumentWidget_(iDocumentWidget *d, int documentY, iBool ce scroll_DocumentWidget_(d, 0); /* clamp it */ } +static void scrollWideBlock_DocumentWidget_(iDocumentWidget *d, iInt2 mousePos, int delta) { + if (delta == 0) { + return; + } + const iInt2 docPos = documentPos_DocumentWidget_(d, mousePos); + iConstForEach(PtrArray, i, &d->visibleWideRuns) { + 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_DocumentWidget_(d) + d->pageMargin * gap_UI; + if (size_Array(&d->wideRunOffsets) <= run->preId) { + resize_Array(&d->wideRunOffsets, run->preId + 1); + } + int *offset = at_Array(&d->wideRunOffsets, run->preId - 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); + } + break; + } + } +} + static void checkResponse_DocumentWidget_(iDocumentWidget *d) { if (!d->request) { return; @@ -1064,6 +1097,7 @@ static void checkResponse_DocumentWidget_(iDocumentWidget *d) { case categorySuccess_GmStatusCode: init_Anim(&d->scrollY, 0); reset_GmDocument(d->doc); /* new content incoming */ + clear_Array(&d->wideRunOffsets); updateDocument_DocumentWidget_(d, resp, iTrue); break; case categoryRedirect_GmStatusCode: @@ -2015,8 +2049,9 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e } else if (ev->type == SDL_MOUSEWHEEL && isHover_Widget(w)) { float acceleration = 1.0f; + const iInt2 mouseCoord = mouseCoord_Window(get_Window()); if (prefs_App()->hoverOutline && - contains_Widget(constAs_Widget(d->scroll), mouseCoord_Window(get_Window()))) { + contains_Widget(constAs_Widget(d->scroll), mouseCoord)) { const int outHeight = outlineHeight_DocumentWidget_(d); if (outHeight > height_Rect(bounds_Widget(w))) { acceleration = (float) size_GmDocument(d->doc).y / (float) outHeight; @@ -2027,7 +2062,15 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e which device is sending the event. */ if (ev->wheel.which == 0) { /* Trackpad with precise scrolling w/inertia. */ stop_Anim(&d->scrollY); - scroll_DocumentWidget_(d, -ev->wheel.y * get_Window()->pixelRatio * acceleration); + iInt2 wheel = init_I2(ev->wheel.x, ev->wheel.y); + if (iAbs(wheel.x) > iAbs(wheel.y)) { + wheel.y = 0; + } + else { + wheel.x = 0; + } + scroll_DocumentWidget_(d, -wheel.y * get_Window()->pixelRatio * acceleration); + scrollWideBlock_DocumentWidget_(d, mouseCoord, wheel.x * get_Window()->pixelRatio); } else #endif @@ -2046,8 +2089,10 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e d, -3 * amount * lineHeight_Text(paragraph_FontId) * acceleration, smoothDuration_DocumentWidget_ * + /* accelerated speed for repeated wheelings */ (!isFinished_Anim(&d->scrollY) && pos_Anim(&d->scrollY) < 0.25f ? 0.5f : 1.0f)); - /* accelerated speed for repeated wheelings */ + scrollWideBlock_DocumentWidget_( + d, mouseCoord, ev->wheel.x * lineHeight_Text(paragraph_FontId)); } iChangeFlags(d->flags, noHoverWhileScrolling_DocumentWidgetFlag, iTrue); return iTrue; @@ -2399,7 +2444,15 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) { const iBool isHover = (run->linkId && d->widget->hoverLink && run->linkId == d->widget->hoverLink->linkId && ~run->flags & decoration_GmRunFlag); - const iInt2 visPos = add_I2(run->visBounds.pos, origin); + iInt2 visPos = add_I2(run->visBounds.pos, origin); + /* Preformatted runs can be scrolled. */ + if (run->preId && run->flags & wide_GmRunFlag) { + const size_t numOffsets = size_Array(&d->widget->wideRunOffsets); + const int *offsets = constData_Array(&d->widget->wideRunOffsets); + if (run->preId <= numOffsets) { + visPos.x -= offsets[run->preId - 1]; + } + } fillRect_Paint(&d->paint, (iRect){ visPos, run->visBounds.size }, tmBackground_ColorId); if (run->linkId && ~run->flags & decoration_GmRunFlag) { fg = linkColor_GmDocument(doc, run->linkId, isHover ? textHover_GmLinkPart : text_GmLinkPart); @@ -2962,6 +3015,7 @@ iBool isRequestOngoing_DocumentWidget(const iDocumentWidget *d) { void updateSize_DocumentWidget(iDocumentWidget *d) { setWidth_GmDocument(d->doc, documentWidth_DocumentWidget_(d)); + clear_Array(&d->wideRunOffsets); updateSideIconBuf_DocumentWidget_(d); updateOutline_DocumentWidget_(d); updateVisible_DocumentWidget_(d); diff --git a/src/ui/text.c b/src/ui/text.c index 0b8c98e7..e047bbce 100644 --- a/src/ui/text.c +++ b/src/ui/text.c @@ -165,6 +165,7 @@ static iText text_; static void initFonts_Text_(iText *d) { const float textSize = fontSize_UI * d->contentFontSize; const float monoSize = fontSize_UI * d->contentFontSize / contentScale_Text_ * 0.866f; + const float smallMonoSize = monoSize * 0.866f; const iBlock *regularFont = &fontNunitoRegular_Embedded; const iBlock *italicFont = &fontNunitoLightItalic_Embedded; const iBlock *h12Font = &fontNunitoExtraBold_Embedded; @@ -218,7 +219,7 @@ static void initFonts_Text_(iText *d) { /* content fonts */ { regularFont, textSize, scaling, symbols_FontId }, { &fontFiraMonoRegular_Embedded, monoSize, 1.0f, monospaceSymbols_FontId }, - { &fontFiraMonoRegular_Embedded, monoSize * 0.750f, 1.0f, monospaceSmallSymbols_FontId }, + { &fontFiraMonoRegular_Embedded, smallMonoSize, 1.0f, monospaceSmallSymbols_FontId }, { regularFont, textSize * 1.200f, scaling, mediumSymbols_FontId }, { h3Font, textSize * 1.333f, h123Scaling, bigSymbols_FontId }, { italicFont, textSize, scaling, symbols_FontId }, @@ -237,7 +238,7 @@ static void initFonts_Text_(iText *d) { { &fontSymbola_Embedded, textSize * 1.666f, 1.0f, largeSymbols_FontId }, { &fontSymbola_Embedded, textSize * 2.000f, 1.0f, hugeSymbols_FontId }, { &fontSymbola_Embedded, monoSize, 1.0f, monospaceSymbols_FontId }, - { &fontSymbola_Embedded, monoSize * 0.750f, 1.0f, monospaceSmallSymbols_FontId }, + { &fontSymbola_Embedded, smallMonoSize, 1.0f, monospaceSmallSymbols_FontId }, /* emoji fonts */ { &fontNotoEmojiRegular_Embedded, fontSize_UI, 1.0f, defaultSymbols_FontId }, { &fontNotoEmojiRegular_Embedded, fontSize_UI * 1.125f, 1.0f, defaultMediumSymbols_FontId }, @@ -248,10 +249,10 @@ static void initFonts_Text_(iText *d) { { &fontNotoEmojiRegular_Embedded, textSize * 1.666f, 1.0f, largeSymbols_FontId }, { &fontNotoEmojiRegular_Embedded, textSize * 2.000f, 1.0f, hugeSymbols_FontId }, { &fontNotoEmojiRegular_Embedded, monoSize, 1.0f, monospaceSymbols_FontId }, - { &fontNotoEmojiRegular_Embedded, monoSize * 0.750f, 1.0f, monospaceSmallSymbols_FontId }, + { &fontNotoEmojiRegular_Embedded, smallMonoSize, 1.0f, monospaceSmallSymbols_FontId }, /* japanese fonts */ { &fontNotoSansJPRegular_Embedded, fontSize_UI, 1.0f, defaultSymbols_FontId }, - { &fontNotoSansJPRegular_Embedded, monoSize * 0.750, 1.0f, monospaceSmallSymbols_FontId }, + { &fontNotoSansJPRegular_Embedded, smallMonoSize, 1.0f, monospaceSmallSymbols_FontId }, { &fontNotoSansJPRegular_Embedded, monoSize, 1.0f, monospaceSymbols_FontId }, { &fontNotoSansJPRegular_Embedded, textSize, 1.0f, symbols_FontId }, { &fontNotoSansJPRegular_Embedded, textSize * 1.200f, 1.0f, mediumSymbols_FontId }, @@ -260,7 +261,7 @@ static void initFonts_Text_(iText *d) { { &fontNotoSansJPRegular_Embedded, textSize * 2.000f, 1.0f, hugeSymbols_FontId }, /* korean fonts */ { &fontNanumGothicRegular_Embedded, fontSize_UI, 1.0f, defaultSymbols_FontId }, - { &fontNanumGothicRegular_Embedded, monoSize * 0.750, 1.0f, monospaceSmallSymbols_FontId }, + { &fontNanumGothicRegular_Embedded, smallMonoSize, 1.0f, monospaceSmallSymbols_FontId }, { &fontNanumGothicRegular_Embedded, monoSize, 1.0f, monospaceSymbols_FontId }, { &fontNanumGothicRegular_Embedded, textSize, 1.0f, symbols_FontId }, { &fontNanumGothicRegular_Embedded, textSize * 1.200f, 1.0f, mediumSymbols_FontId }, -- cgit v1.2.3