Lagrange [work/v1.10]

LinkInfo: Improved link metadata popup

=> 5f7709f0b84e74fde847cdcf02c41990ef037daa

diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c
index afd0070f..1870efd6 100644
--- a/src/ui/documentwidget.c
+++ b/src/ui/documentwidget.c
@@ -41,6 +41,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
 #include "inputwidget.h"
 #include "keys.h"
 #include "labelwidget.h"
+#include "linkinfo.h"
 #include "media.h"
 #include "paint.h"
 #include "periodic.h"
@@ -161,10 +162,10 @@ enum iDrawBufsFlag {
 };
 
 struct Impl_DrawBufs {
-    int            flags;
-    SDL_Texture *  sideIconBuf;
-    iTextBuf *     timestampBuf;
-    uint32_t       lastRenderTime;
+    int          flags;
+    SDL_Texture *sideIconBuf;
+    iTextBuf    *timestampBuf;
+    uint32_t     lastRenderTime;
 };
 
 static void init_DrawBufs(iDrawBufs *d) {
@@ -234,10 +235,11 @@ enum iDocumentWidgetFlag {
     fromCache_DocumentWidgetFlag             = iBit(16), /* don't write anything to cache */
     animationPlaceholder_DocumentWidgetFlag  = iBit(17), /* avoid slow operations */
     invalidationPending_DocumentWidgetFlag   = iBit(18), /* invalidate as soon as convenient */
-    leftWheelSwipe_DocumentWidgetFlag         = iBit(19), /* swipe state flags are used on desktop */
-    rightWheelSwipe_DocumentWidgetFlag        = iBit(20), 
-    eitherWheelSwipe_DocumentWidgetFlag       = leftWheelSwipe_DocumentWidgetFlag | rightWheelSwipe_DocumentWidgetFlag,
-//    wheelSwipeFinished_DocumentWidgetFlag     = iBit(21),
+    leftWheelSwipe_DocumentWidgetFlag        = iBit(19), /* swipe state flags are used on desktop */
+    rightWheelSwipe_DocumentWidgetFlag       = iBit(20), 
+    eitherWheelSwipe_DocumentWidgetFlag      = leftWheelSwipe_DocumentWidgetFlag |
+                                               rightWheelSwipe_DocumentWidgetFlag,
+//    wheelSwipeFinished_DocumentWidgetFlag    = iBit(21),
 };
 
 enum iDocumentLinkOrdinalMode {
@@ -322,6 +324,7 @@ struct Impl_DocumentWidget {
     iVisBufMeta *  visBufMeta;
     iGmRunRange    renderRuns;
     iPtrSet *      invalidRuns;
+    iLinkInfo *    linkInfo;
     
     /* Widget structure: */    
     iScrollWidget *scroll;
@@ -413,6 +416,7 @@ void init_DocumentWidget(iDocumentWidget *d) {
     init_String(&d->pendingGotoHeading);
     init_String(&d->linePrecedingLink);
     init_Click(&d->click, d, SDL_BUTTON_LEFT);
+    d->linkInfo = (deviceType_App() == desktop_AppDeviceType ? new_LinkInfo() : NULL);
     addChild_Widget(w, iClob(d->scroll = new_ScrollWidget()));
     d->menu         = NULL; /* created when clicking */
     d->playerMenu   = NULL;
@@ -457,6 +461,7 @@ void deinit_DocumentWidget(iDocumentWidget *d) {
     delete_VisBuf(d->visBuf);
     free(d->visBufMeta);
     delete_PtrSet(d->invalidRuns);
+    delete_LinkInfo(d->linkInfo);
     iRelease(d->media);
     iRelease(d->request);
     delete_Gempub(d->sourceGempub);
@@ -737,7 +742,8 @@ static void updateHover_DocumentWidget_(iDocumentWidget *d, iInt2 mouse);
 static void animate_DocumentWidget_(void *ticker) {
     iDocumentWidget *d = ticker;
     refresh_Widget(d);
-    if (!isFinished_Anim(&d->sideOpacity) || !isFinished_Anim(&d->altTextOpacity)) {
+    if (!isFinished_Anim(&d->sideOpacity) || !isFinished_Anim(&d->altTextOpacity) ||
+        (d->linkInfo && !isFinished_Anim(&d->linkInfo->opacity))) {
         addTicker_App(animate_DocumentWidget_, d);
     }
 }
@@ -789,6 +795,10 @@ static void updateHover_DocumentWidget_(iDocumentWidget *d, iInt2 mouse) {
         if (d->hoverLink) {
             invalidateLink_DocumentWidget_(d, d->hoverLink->linkId);
         }
+        if (update_LinkInfo(d->linkInfo, d->doc, d->hoverLink ? d->hoverLink->linkId : 0,
+                            width_Widget(w))) {
+            animate_DocumentWidget_(d);            
+        }
         refresh_Widget(w);
     }
     /* Hovering over preformatted blocks. */
@@ -4923,6 +4933,7 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) {
                           (float) bodySize_GmRequest(mr->req) / 1.0e6f);
             }
         }
+#if 0
         else if (isHover) {
             /* TODO: Make this a dynamic overlay, not part of the VisBuf content. */
             const iGmLinkId linkId = d->widget->hoverLink->linkId;
@@ -5002,6 +5013,7 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) {
                 deinit_String(&str);
             }
         }
+#endif
     }
     if (0) {
         drawRect_Paint(&d->paint, (iRect){ visPos, run->bounds.size }, green_ColorId);
@@ -5446,6 +5458,25 @@ static void draw_DocumentWidget_(const iDocumentWidget *d) {
         }
         unsetClip_Paint(&ctx.paint);
         drawSideElements_DocumentWidget_(d);
+        if (deviceType_App() == desktop_AppDeviceType && prefs_App()->hoverLink && d->linkInfo) {
+            const int pad = gap_UI;
+            update_LinkInfo(d->linkInfo,
+                            d->doc,
+                            d->hoverLink ? d->hoverLink->linkId : 0,
+                            width_Rect(bounds) - 2 * pad);
+            const iInt2 infoSize = size_LinkInfo(d->linkInfo);
+            iInt2 infoPos = add_I2(bottomLeft_Rect(bounds), init_I2(pad, -infoSize.y - pad));
+            if (d->hoverLink) {
+                const iRect runRect = runRect_DocumentWidget_(d, d->hoverLink);
+                d->linkInfo->isAltPos =
+                    (bottom_Rect(runRect) >= infoPos.y - lineHeight_Text(paragraph_FontId));
+            }
+            if (d->linkInfo->isAltPos) {
+                infoPos.y = top_Rect(bounds) + pad;
+            }
+            draw_LinkInfo(d->linkInfo, infoPos);
+        }
+#if 0
         if (prefs_App()->hoverLink && d->hoverLink) {
             const int      font     = uiLabel_FontId;
             const iRangecc linkUrl  = range_String(linkUrl_GmDocument(d->doc, d->hoverLink->linkId));
@@ -5455,6 +5486,7 @@ static void draw_DocumentWidget_(const iDocumentWidget *d) {
             fillRect_Paint(&ctx.paint, linkRect, tmBackground_ColorId);
             drawRange_Text(font, addX_I2(topLeft_Rect(linkRect), gap_UI), tmParagraph_ColorId, linkUrl);
         }
+#endif
     }
     if (colorTheme_App() == pureWhite_ColorTheme) {
         drawHLine_Paint(&ctx.paint, topLeft_Rect(bounds), width_Rect(bounds), uiSeparator_ColorId);
diff --git a/src/ui/linkinfo.c b/src/ui/linkinfo.c
index e20c183c..72abea1a 100644
--- a/src/ui/linkinfo.c
+++ b/src/ui/linkinfo.c
@@ -21,12 +21,23 @@ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
 
 #include "linkinfo.h"
+#include "metrics.h"
+#include "paint.h"
+#include "../gmcerts.h"
+#include "../app.h"
 
-#define hPad_LinkInfo_  (2 * gap_UI)
-#define vPad_LinkInfo_  (1 * gap_UI)
+#include 
+
+iDefineTypeConstruction(LinkInfo)
+
+#define minWidth_LinkInfo_  (40 * gap_UI)
+#define hPad_LinkInfo_      (2 * gap_UI)
+#define vPad_LinkInfo_      (1 * gap_UI)
 
 void init_LinkInfo(iLinkInfo *d) {
-    d->buf = NULL;    
+    d->buf = NULL;
+    init_Anim(&d->opacity, 0.0f);
+    d->isAltPos = iFalse;
 }
 
 void deinit_LinkInfo(iLinkInfo *d) {
@@ -37,5 +48,136 @@ iInt2 size_LinkInfo(const iLinkInfo *d) {
     if (!d->buf) {
         return zero_I2();
     }
-    return add_I2(d->buf->size, init_I2(2 * hPad_LinkInfo_, 2 * vPad_LinkInfo_)));
+    return add_I2(d->buf->size, init_I2(2 * hPad_LinkInfo_, 2 * vPad_LinkInfo_));
+}
+
+iBool update_LinkInfo(iLinkInfo *d, const iGmDocument *doc, iGmLinkId linkId, int maxWidth) {
+    if (!d) {
+        return iFalse;
+    }
+    if (d->linkId != linkId || d->maxWidth != maxWidth) {
+        d->linkId   = linkId;
+        d->maxWidth = maxWidth;
+        invalidate_LinkInfo(d);
+        if (linkId) {
+            /* Measure and draw. */
+            if (targetValue_Anim(&d->opacity) < 1) {
+                setValue_Anim(&d->opacity, 1, 75);
+            }
+            const int avail = iMax(minWidth_LinkInfo_, maxWidth) - 2 * hPad_LinkInfo_;
+            const iString *url = linkUrl_GmDocument(doc, linkId);
+            iUrl parts;
+            init_Url(&parts, url);
+            const int flags = linkFlags_GmDocument(doc, linkId);
+            const enum iGmLinkScheme scheme = scheme_GmLinkFlag(flags);
+            const iBool showHost  = (flags & humanReadable_GmLinkFlag &&
+                                    (!isEmpty_Range(&parts.host) ||
+                                     scheme == mailto_GmLinkScheme));
+            const iBool showImage = (flags & imageFileExtension_GmLinkFlag) != 0;
+            const iBool showAudio = (flags & audioFileExtension_GmLinkFlag) != 0;
+//            int fg                = linkColor_GmDocument(doc, linkId, textHover_GmLinkPart);
+            iString str;
+            init_String(&str);
+            /* Identity that will be used. */
+            const iGmIdentity *ident = identityForUrl_GmCerts(certs_App(), url);
+            if (ident) {
+                appendFormat_String(&str, person_Icon " %s",
+                                                             //escape_Color(tmBannerItemTitle_ColorId),
+                                    cstr_String(name_GmIdentity(ident)));                
+            }            /* Show scheme and host. */
+            if ((showHost ||
+                 (flags & (imageFileExtension_GmLinkFlag | audioFileExtension_GmLinkFlag))) &&
+                scheme != mailto_GmLinkScheme) {
+                if (!isEmpty_String(&str)) {
+                    appendCStr_String(&str, "\n");
+                }
+                if (showHost  && scheme != gemini_GmLinkScheme) {
+                    append_String(
+                        &str, collect_String(upper_String(collectNewRange_String(parts.scheme))));
+                    appendCStr_String(&str, " \u2014 ");
+                }
+                if (showHost) {
+                    appendFormat_String(&str, "\x1b[1m%s\x1b[0m", cstr_Rangecc(parts.host));
+                }
+                if (showImage || showAudio) {
+                    appendFormat_String(
+                        &str,
+                        "%s%s%s",
+    //                    showHost ? (scheme == mailto_GmLinkScheme ? cstr_String(url)
+    //                                : scheme != gemini_GmLinkScheme
+    //                                    ? format_CStr("%s \u2014 %s",
+    //                                                  cstrCollect_String(upper_String(
+    //                                                      collectNewRange_String(parts.scheme))),
+    //                                                  cstr_Rangecc(parts.host))
+    //                                    : cstr_Rangecc(parts.host))
+    //                             : "",
+    //                    showHost ? format_CStr("\x1b[1m%s\x1b[0m", cstr_Rangecc(parts.host)) : "",
+                        showHost && (showImage || showAudio) ? " \u2014" : "",
+                        showImage || showAudio
+                            ? ""
+                            : "", // escape_Color(linkColor_GmDocument(doc, linkId, domain_GmLinkPart)),
+                        showImage || showAudio
+                            ? format_CStr(showImage ? photo_Icon " %s " : "\U0001f3b5 %s",
+                                          cstr_Lang(showImage ? "link.hint.image" : "link.hint.audio"))
+                            : "");
+                }
+            }
+            if (flags & visited_GmLinkFlag) {
+                iDate date;
+                init_Date(&date, linkTime_GmDocument(doc, linkId));
+                if (!isEmpty_String(&str)) {
+                    appendCStr_String(&str, " \u2014 ");
+                }
+//                appendCStr_String(&str, escape_Color(tmQuoteIcon_ColorId));
+                iString *dateStr = format_Date(&date, "%b %d");
+                append_String(&str, dateStr);
+                delete_String(dateStr);
+            }
+            if (!isEmpty_String(&str)) {
+                appendCStr_String(&str, "\n");
+            }
+            appendRange_String(&str, range_String(url));
+            /* Draw the text. */            
+            iWrapText wt = { .text = range_String(&str), .maxWidth = avail, .mode = word_WrapTextMode };
+            d->buf = new_TextBuf(&wt, uiLabel_FontId, tmQuote_ColorId);
+            deinit_String(&str);
+        }
+        else {
+            if (targetValue_Anim(&d->opacity) > 0) {
+                setValue_Anim(&d->opacity, 0, 150);
+            }            
+        }
+        return iTrue;
+    }
+    return iFalse;
+}
+
+void invalidate_LinkInfo(iLinkInfo *d) {
+    if (targetValue_Anim(&d->opacity) > 0) {
+        setValue_Anim(&d->opacity, 0, 150);
+    }            
+    
+    //    if (d->buf) {
+//        delete_TextBuf(d->buf);
+//        d->buf = NULL;
+//    }
+}
+
+void draw_LinkInfo(const iLinkInfo *d, iInt2 topLeft) {
+    const float opacity = value_Anim(&d->opacity);
+    if (!d->buf || opacity <= 0.01f) {
+        return;
+    }
+    iPaint p;
+    init_Paint(&p);
+    iInt2 size = size_LinkInfo(d);
+    iRect rect = { topLeft, size };
+    SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_BLEND);
+    p.alpha = 255 * opacity;
+    fillRect_Paint(&p, rect, tmBackgroundAltText_ColorId);
+    drawRect_Paint(&p, rect, tmFrameAltText_ColorId);
+    SDL_SetTextureAlphaMod(d->buf->texture, p.alpha);
+    draw_TextBuf(d->buf, add_I2(topLeft, init_I2(hPad_LinkInfo_, vPad_LinkInfo_)),
+                 white_ColorId);
+    SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_NONE);
 }
diff --git a/src/ui/linkinfo.h b/src/ui/linkinfo.h
index dbedf359..a1669f95 100644
--- a/src/ui/linkinfo.h
+++ b/src/ui/linkinfo.h
@@ -23,10 +23,23 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
 #pragma once
 
 #include "text.h"
+#include "util.h"
+#include "../gmdocument.h"
 
 iDeclareType(LinkInfo)
 iDeclareTypeConstruction(LinkInfo)
     
 struct Impl_LinkInfo {
+    iGmLinkId linkId;
+    int       maxWidth;
     iTextBuf *buf;
+    iAnim     opacity;
+    iBool     isAltPos;
 };
+
+iBool   update_LinkInfo     (iLinkInfo *, const iGmDocument *doc, iGmLinkId linkId,
+                             int maxWidth); /* returns true if changed */
+void    invalidate_LinkInfo (iLinkInfo *);
+
+iInt2   size_LinkInfo       (const iLinkInfo *);
+void    draw_LinkInfo       (const iLinkInfo *, iInt2 topLeft);
Proxy Information
Original URL
gemini://git.skyjake.fi/lagrange/work%2Fv1.10/cdiff/5f7709f0b84e74fde847cdcf02c41990ef037daa
Status Code
Success (20)
Meta
text/gemini; charset=utf-8
Capsule Response Time
86.373695 milliseconds
Gemini-to-HTML Time
0.57013 milliseconds

This content has been proxied by September (3851b).