Lagrange [work/v1.9]

Scrolling wide preformatted blocks horizontally

=> edf1c0bb8b112879433f2e31fd9750c30e2d5144

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 },
Proxy Information
Original URL
gemini://git.skyjake.fi/lagrange/work%2Fv1.9/cdiff/edf1c0bb8b112879433f2e31fd9750c30e2d5144
Status Code
Success (20)
Meta
text/gemini; charset=utf-8
Capsule Response Time
73.731489 milliseconds
Gemini-to-HTML Time
1.712543 milliseconds

This content has been proxied by September (ba2dc).