Lagrange [work/v1.7]

Mobile: Draw optimizations; focus handling

=> 33620846cca5678fbd662ea1a48fad302727dae7

diff --git a/src/ui/inputwidget.c b/src/ui/inputwidget.c
index 6e9ef6c2..802a2d6c 100644
--- a/src/ui/inputwidget.c
+++ b/src/ui/inputwidget.c
@@ -1565,6 +1565,11 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) {
     }
     switch (processEvent_Click(&d->click, ev)) {
         case none_ClickResult:
+            if (ev->type == SDL_MOUSEBUTTONUP &&
+                deviceType_App() != desktop_AppDeviceType && isFocused_Widget(d)) {
+                setFocus_Widget(NULL);
+                return iTrue;
+            }
             break;
         case started_ClickResult: {
             setFocus_Widget(w);
diff --git a/src/ui/mobile.c b/src/ui/mobile.c
index 6ea672e6..9e2dc4f7 100644
--- a/src/ui/mobile.c
+++ b/src/ui/mobile.c
@@ -357,6 +357,7 @@ static iWidget *addChildPanel_(iWidget *parent, iLabelWidget *panelButton,
     setId_Widget(panel, "panel");
     setUserData_Object(panelButton, panel);
     setBackgroundColor_Widget(panel, uiBackground_ColorId);
+    setDrawBufferEnabled_Widget(panel, iTrue);
     setId_Widget(addChild_Widget(panel, iClob(makePadding_Widget(0))), "panel.toppad");
     if (titleText) {
         iLabelWidget *title =
@@ -601,6 +602,7 @@ void initPanels_Mobile(iWidget *panels, iWidget *parentWidget,
     /* The panel roots. */
     iWidget *topPanel = new_Widget(); {
         setId_Widget(topPanel, "panel.top");
+        setDrawBufferEnabled_Widget(topPanel, iTrue);
         setCommandHandler_Widget(topPanel, topPanelHandler_);
         setFlags_Widget(topPanel,
                         arrangeVertical_WidgetFlag | resizeWidthOfChildren_WidgetFlag |
@@ -730,7 +732,7 @@ void initPanels_Mobile(iWidget *panels, iWidget *parentWidget,
     updatePanelSheetMetrics_(panels);
     arrange_Widget(panels);
     postCommand_App("widget.overflow"); /* with the correct dimensions */    
-    printTree_Widget(panels);
+//    printTree_Widget(panels);
 }
 
 #if 0
diff --git a/src/ui/paint.c b/src/ui/paint.c
index 79adb7d1..af62f908 100644
--- a/src/ui/paint.c
+++ b/src/ui/paint.c
@@ -24,6 +24,8 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
 
 #include 
 
+iInt2 origin_Paint;
+
 iLocalDef SDL_Renderer *renderer_Paint_(const iPaint *d) {
     iAssert(d->dst);
     return d->dst->render;
@@ -62,10 +64,11 @@ void endTarget_Paint(iPaint *d) {
 }
 
 void setClip_Paint(iPaint *d, iRect rect) {
-    rect = intersect_Rect(rect, rect_Root(get_Root()));
+    //rect = intersect_Rect(rect, rect_Root(get_Root()));
     if (isEmpty_Rect(rect)) {
         rect = init_Rect(0, 0, 1, 1);
     }
+    addv_I2(&rect.pos, origin_Paint);
     SDL_RenderSetClipRect(renderer_Paint_(d), (const SDL_Rect *) &rect);
 }
 
@@ -85,6 +88,7 @@ void unsetClip_Paint(iPaint *d) {
 }
 
 void drawRect_Paint(const iPaint *d, iRect rect, int color) {
+    addv_I2(&rect.pos, origin_Paint);
     iInt2 br = bottomRight_Rect(rect);
     /* Keep the right/bottom edge visible in the window. */
     if (br.x == d->dst->size.x) br.x--;
@@ -115,11 +119,14 @@ void drawRectThickness_Paint(const iPaint *d, iRect rect, int thickness, int col
 }
 
 void fillRect_Paint(const iPaint *d, iRect rect, int color) {
+    addv_I2(&rect.pos, origin_Paint);
     setColor_Paint_(d, color);
+//    printf("fillRect_Paint: %d,%d %dx%d\n", rect.pos.x, rect.pos.y, rect.size.x, rect.size.y);
     SDL_RenderFillRect(renderer_Paint_(d), (SDL_Rect *) &rect);
 }
 
 void drawSoftShadow_Paint(const iPaint *d, iRect inner, int thickness, int color, int alpha) {
+    addv_I2(&inner.pos, origin_Paint);
     SDL_Renderer *render = renderer_Paint_(d);
     SDL_Texture *shadow = get_Window()->borderShadow;
     const iInt2 size = size_SDLTexture(shadow);
@@ -146,9 +153,14 @@ void drawSoftShadow_Paint(const iPaint *d, iRect inner, int thickness, int color
                    &(SDL_Rect){ outer.pos.x, inner.pos.y, thickness, inner.size.y });
 }
 
-void drawLines_Paint(const iPaint *d, const iInt2 *points, size_t count, int color) {
+void drawLines_Paint(const iPaint *d, const iInt2 *points, size_t n, int color) {
     setColor_Paint_(d, color);
-    SDL_RenderDrawLines(renderer_Paint_(d), (const SDL_Point *) points, count);
+    iInt2 *offsetPoints = malloc(sizeof(iInt2) * n);
+    for (size_t i = 0; i < n; i++) {
+        offsetPoints[i] = add_I2(points[i], origin_Paint);
+    }
+    SDL_RenderDrawLines(renderer_Paint_(d), (const SDL_Point *) offsetPoints, n);
+    free(offsetPoints);
 }
 
 iInt2 size_SDLTexture(SDL_Texture *d) {
diff --git a/src/ui/paint.h b/src/ui/paint.h
index 90cc2aef..e6701635 100644
--- a/src/ui/paint.h
+++ b/src/ui/paint.h
@@ -36,6 +36,8 @@ struct Impl_Paint {
     uint8_t      alpha;
 };
 
+extern iInt2 origin_Paint; /* add this to all drawn positions so buffered graphics are correctly offset */
+
 void    init_Paint          (iPaint *);
 
 void    beginTarget_Paint   (iPaint *, SDL_Texture *target);
diff --git a/src/ui/root.c b/src/ui/root.c
index a792e93d..7b2b5b15 100644
--- a/src/ui/root.c
+++ b/src/ui/root.c
@@ -685,6 +685,34 @@ static iBool handleNavBarCommands_(iWidget *navBar, const char *cmd) {
         }
         return iTrue;
     }
+    else if (deviceType_App() != desktop_AppDeviceType &&
+             (equal_Command(cmd, "focus.gained") || equal_Command(cmd, "focus.lost"))) {
+        iInputWidget *url = findChild_Widget(navBar, "url");
+        if (pointer_Command(cmd) == url) {
+            const iBool isFocused = equal_Command(cmd, "focus.gained");
+            setFlags_Widget(findChild_Widget(navBar, "navbar.clear"), hidden_WidgetFlag, !isFocused);
+            showCollapsed_Widget(findChild_Widget(navBar, "navbar.cancel"), isFocused);
+            showCollapsed_Widget(findChild_Widget(navBar, "pagemenubutton"), !isFocused);
+            showCollapsed_Widget(findChild_Widget(navBar, "reload"), !isFocused);
+        }
+        return iFalse;
+    }
+    else if (equal_Command(cmd, "navbar.clear")) {
+        iInputWidget *url = findChild_Widget(navBar, "url");
+        selectAll_InputWidget(url);
+        /* Emulate a Backspace keypress. */
+        class_InputWidget(url)->processEvent(
+            as_Widget(url),
+            (SDL_Event *) &(SDL_KeyboardEvent){ .type      = SDL_KEYDOWN,
+                                                .timestamp = SDL_GetTicks(),
+                                                .state     = SDL_PRESSED,
+                                                .keysym    = { .sym = SDLK_BACKSPACE } });
+        return iTrue;
+    }
+    else if (equal_Command(cmd, "navbar.cancel")) {
+        setFocus_Widget(NULL);
+        return iTrue;
+    }
     else if (equal_Command(cmd, "input.edited")) {
         iAnyObject *   url  = findChild_Widget(navBar, "url");
         const iString *text = text_InputWidget(url);
@@ -941,7 +969,7 @@ void updateMetrics_Root(iRoot *d) {
         setFixedSize_Widget(appIcon, init_I2(appIconSize_Root(), appMin->rect.size.y));
     }
     iWidget *navBar     = findChild_Widget(d->widget, "navbar");
-    iWidget *lock       = findChild_Widget(navBar, "navbar.lock");
+//    iWidget *lock       = findChild_Widget(navBar, "navbar.lock");
     iWidget *url        = findChild_Widget(d->widget, "url");
     iWidget *rightEmbed = findChild_Widget(navBar, "url.rightembed");
     iWidget *embedPad   = findChild_Widget(navBar, "url.embedpad");
@@ -1044,6 +1072,7 @@ void createUserInterface_Root(iRoot *d) {
     /* Navigation bar. */ {
         navBar = new_Widget();
         setId_Widget(navBar, "navbar");
+        setDrawBufferEnabled_Widget(navBar, iTrue);
         setFlags_Widget(navBar,
                         hittable_WidgetFlag | /* context menu */
                             arrangeHeight_WidgetFlag |
@@ -1095,6 +1124,16 @@ void createUserInterface_Root(iRoot *d) {
                 setFont_LabelWidget(lock, symbols_FontId + uiNormal_FontSize);
                 updateTextCStr_LabelWidget(lock, "\U0001f512");
             }
+            /* Button for clearing the URL bar contents. */ {
+                iLabelWidget *clear = addChildFlags_Widget(
+                    as_Widget(url),
+                    iClob(newIcon_LabelWidget(delete_Icon, 0, 0, "navbar.clear")),
+                    hidden_WidgetFlag | embedFlags | moveToParentLeftEdge_WidgetFlag);
+                setId_Widget(as_Widget(clear), "navbar.clear");
+                setFont_LabelWidget(clear, symbols2_FontId + uiNormal_FontSize);
+                setFlags_Widget(as_Widget(clear), noBackground_WidgetFlag, iFalse);
+                setBackgroundColor_Widget(as_Widget(clear), uiBackground_ColorId);
+            }
             iWidget *rightEmbed = new_Widget();
             setId_Widget(rightEmbed, "url.rightembed");
             addChildFlags_Widget(as_Widget(url),
@@ -1151,6 +1190,13 @@ void createUserInterface_Root(iRoot *d) {
             setFlags_Widget(urlButtons, embedFlags | arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag, iTrue);
             /* Mobile page menu. */
             if (deviceType_App() != desktop_AppDeviceType) {
+                iLabelWidget *navCancel = new_LabelWidget("${cancel}", "navbar.cancel");
+                addChildFlags_Widget(urlButtons, iClob(navCancel),
+                                     (embedFlags | tight_WidgetFlag | hidden_WidgetFlag |
+                                      collapse_WidgetFlag) & ~noBackground_WidgetFlag);
+                as_Widget(navCancel)->sizeRef = as_Widget(url);
+//                setFont_LabelWidget(navCancel, defaultBold_FontId);
+                setId_Widget(as_Widget(navCancel), "navbar.cancel");
                 iLabelWidget *pageMenuButton;
                 /* In a mobile layout, the reload button is replaced with the Page/Ellipsis menu. */
                 pageMenuButton = makeMenuButton_LabelWidget(pageMenuCStr_,
@@ -1172,13 +1218,14 @@ void createUserInterface_Root(iRoot *d) {
                 setId_Widget(as_Widget(pageMenuButton), "pagemenubutton");
                 setFont_LabelWidget(pageMenuButton, uiContentBold_FontId);
                 setAlignVisually_LabelWidget(pageMenuButton, iTrue);
-                addChildFlags_Widget(urlButtons, iClob(pageMenuButton), embedFlags | tight_WidgetFlag);
+                addChildFlags_Widget(urlButtons, iClob(pageMenuButton),
+                                     embedFlags | tight_WidgetFlag | collapse_WidgetFlag);
                 updateSize_LabelWidget(pageMenuButton);
             }
             /* Reload button. */ {
                 iLabelWidget *reload = newIcon_LabelWidget(reloadCStr_, 0, 0, "navigate.reload");
                 setId_Widget(as_Widget(reload), "reload");
-                addChildFlags_Widget(urlButtons, iClob(reload), embedFlags);
+                addChildFlags_Widget(urlButtons, iClob(reload), embedFlags | collapse_WidgetFlag);
                 updateSize_LabelWidget(reload);
             }
             addChildFlags_Widget(as_Widget(url), iClob(urlButtons), moveToParentRightEdge_WidgetFlag);
@@ -1287,6 +1334,7 @@ void createUserInterface_Root(iRoot *d) {
         iWidget *toolBar = new_Widget();
         addChild_Widget(root, iClob(toolBar));
         setId_Widget(toolBar, "toolbar");
+        setDrawBufferEnabled_Widget(toolBar, iTrue);
         setCommandHandler_Widget(toolBar, handleToolBarCommands_);
         setFlags_Widget(toolBar, moveToParentBottomEdge_WidgetFlag |
                                      parentCannotResizeHeight_WidgetFlag |
diff --git a/src/ui/sidebarwidget.c b/src/ui/sidebarwidget.c
index b816b572..eb129424 100644
--- a/src/ui/sidebarwidget.c
+++ b/src/ui/sidebarwidget.c
@@ -663,8 +663,9 @@ void init_SidebarWidget(iSidebarWidget *d, enum iSidebarSide side) {
     /* On a phone, the right sidebar is used exclusively for Identities. */
     const iBool isPhone = deviceType_App() == phone_AppDeviceType;
     if (!isPhone || d->side == left_SidebarSide) {
-        iWidget *buttons = new_Widget();
+        iWidget *buttons = new_Widget();        
         setId_Widget(buttons, "buttons");
+        setDrawBufferEnabled_Widget(buttons, iTrue);
         for (int i = 0; i < max_SidebarMode; i++) {
             if (deviceType_App() == phone_AppDeviceType && i == identities_SidebarMode) {
                 continue;
diff --git a/src/ui/text.c b/src/ui/text.c
index 231281eb..f7fff4bc 100644
--- a/src/ui/text.c
+++ b/src/ui/text.c
@@ -25,6 +25,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
 #include "metrics.h"
 #include "embedded.h"
 #include "window.h"
+#include "paint.h"
 #include "app.h"
 
 #define STB_TRUETYPE_IMPLEMENTATION
@@ -1712,6 +1713,8 @@ static iRect run_Font_(iFont *d, const iRunArgs *args) {
                     }
                     SDL_Rect src;
                     memcpy(&src, &glyph->rect[hoff], sizeof(SDL_Rect));
+                    dst.x += origin_Paint.x;
+                    dst.y += origin_Paint.y;
                     if (args->mode & fillBackground_RunMode) {
                         /* Alpha blending looks much better if the RGB components don't change in
                            the partially transparent pixels. */
@@ -2182,6 +2185,8 @@ void init_TextBuf(iTextBuf *d, iWrapText *wrapText, int font, int color) {
     }
     if (d->texture) {
         SDL_Texture *oldTarget = SDL_GetRenderTarget(render);
+        const iInt2 oldOrigin = origin_Paint;
+        origin_Paint = zero_I2();
         SDL_SetRenderTarget(render, d->texture);
         SDL_SetRenderDrawBlendMode(render, SDL_BLENDMODE_NONE);
         SDL_SetRenderDrawColor(render, 255, 255, 255, 0);
@@ -2190,6 +2195,7 @@ void init_TextBuf(iTextBuf *d, iWrapText *wrapText, int font, int color) {
         draw_WrapText(wrapText, font, zero_I2(), color | fillBackground_ColorId);
         SDL_SetTextureBlendMode(text_.cache, SDL_BLENDMODE_BLEND);
         SDL_SetRenderTarget(render, oldTarget);
+        origin_Paint = oldOrigin;
         SDL_SetTextureBlendMode(d->texture, SDL_BLENDMODE_BLEND);
     }
 }
@@ -2203,6 +2209,7 @@ iTextBuf *newRange_TextBuf(int font, int color, iRangecc text) {
 }
 
 void draw_TextBuf(const iTextBuf *d, iInt2 pos, int color) {
+    addv_I2(&pos, origin_Paint);
     const iColor clr = get_Color(color);
     SDL_SetTextureColorMod(d->texture, clr.r, clr.g, clr.b);
     SDL_RenderCopy(text_.render,
diff --git a/src/ui/text_simple.c b/src/ui/text_simple.c
index e88b09a8..bf33b4be 100644
--- a/src/ui/text_simple.c
+++ b/src/ui/text_simple.c
@@ -306,6 +306,8 @@ static iRect runSimple_Font_(iFont *d, const iRunArgs *args) {
                 src.y += over;
                 src.h -= over;
             }
+            dst.x += origin_Paint.x;
+            dst.y += origin_Paint.y;
             if (args->mode & fillBackground_RunMode) {
                 /* Alpha blending looks much better if the RGB components don't change in
                    the partially transparent pixels. */
diff --git a/src/ui/touch.c b/src/ui/touch.c
index dac1152e..f0456acb 100644
--- a/src/ui/touch.c
+++ b/src/ui/touch.c
@@ -614,7 +614,7 @@ iBool processEvent_Touch(const SDL_Event *ev) {
 //                   pixels.y, y_F3(amount), y_F3(touch->accum),
 //                   touch->edge);
             if (pixels.x || pixels.y) {
-                setFocus_Widget(NULL);
+                //setFocus_Widget(NULL);
                 dispatchMotion_Touch_(touch->pos[0], 0);
                 setCurrent_Root(touch->affinity->root);
                 dispatchEvent_Widget(touch->affinity, (SDL_Event *) &(SDL_MouseWheelEvent){
diff --git a/src/ui/translation.c b/src/ui/translation.c
index cef68dce..b86e6e52 100644
--- a/src/ui/translation.c
+++ b/src/ui/translation.c
@@ -136,7 +136,8 @@ static void draw_TranslationProgressWidget_(const iTranslationProgressWidget *d)
             get_Color(palette[palCur]), get_Color(palette[palNext]), palPos - (int) palPos);
         SDL_SetRenderDrawColor(renderer_Window(get_Window()), back.r, back.g, back.b, p.alpha);
         SDL_RenderFillRect(renderer_Window(get_Window()),
-                           &(SDL_Rect){ pos.x, pos.y, spr->size.x, spr->size.y });
+                           &(SDL_Rect){ pos.x + origin_Paint.x, pos.y + origin_Paint.y,
+                                        spr->size.x, spr->size.y });
         if (fg >= 0) {
             setOpacity_Text(opacity * 2);
             drawRange_Text(d->font, addX_I2(pos, spr->xoff), fg, range_String(&spr->text));
diff --git a/src/ui/uploadwidget.c b/src/ui/uploadwidget.c
index fb8aaf0a..78a1196a 100644
--- a/src/ui/uploadwidget.c
+++ b/src/ui/uploadwidget.c
@@ -376,11 +376,11 @@ static iBool processEvent_UploadWidget_(iUploadWidget *d, const SDL_Event *ev) {
     return processEvent_Widget(w, ev);
 }
 
-static void draw_UploadWidget_(const iUploadWidget *d) {
-    draw_Widget(constAs_Widget(d));
-}
+//static void draw_UploadWidget_(const iUploadWidget *d) {
+//    draw_Widget(constAs_Widget(d));
+//}
 
 iBeginDefineSubclass(UploadWidget, Widget)
     .processEvent = (iAny *) processEvent_UploadWidget_,
-    .draw         = (iAny *) draw_UploadWidget_,
+    .draw         = draw_Widget,
 iEndDefineSubclass(UploadWidget)
diff --git a/src/ui/util.c b/src/ui/util.c
index 6069e800..05d39c01 100644
--- a/src/ui/util.c
+++ b/src/ui/util.c
@@ -685,6 +685,7 @@ static iWidget *makeMenuSeparator_(void) {
 
 iWidget *makeMenu_Widget(iWidget *parent, const iMenuItem *items, size_t n) {
     iWidget *menu = new_Widget();
+    setDrawBufferEnabled_Widget(menu, iTrue);
     setBackgroundColor_Widget(menu, uiBackgroundMenu_ColorId);
     if (deviceType_App() != desktop_AppDeviceType) {
         setPadding1_Widget(menu, 2 * gap_UI);
diff --git a/src/ui/widget.c b/src/ui/widget.c
index 659a00cc..66cd0e7b 100644
--- a/src/ui/widget.c
+++ b/src/ui/widget.c
@@ -40,6 +40,67 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
 #   include "../ios.h"
 #endif
 
+struct Impl_WidgetDrawBuffer {
+    SDL_Texture *texture;
+    iInt2        size;
+    iBool        isValid;
+    SDL_Texture *oldTarget;
+    iInt2        oldOrigin;
+};
+
+static void init_WidgetDrawBuffer(iWidgetDrawBuffer *d) {
+    d->texture   = NULL;
+    d->size      = zero_I2();
+    d->isValid   = iFalse;
+    d->oldTarget = NULL;
+}
+
+static void deinit_WidgetDrawBuffer(iWidgetDrawBuffer *d) {
+    SDL_DestroyTexture(d->texture);
+}
+
+iDefineTypeConstruction(WidgetDrawBuffer)
+    
+static void realloc_WidgetDrawBuffer(iWidgetDrawBuffer *d, SDL_Renderer *render, iInt2 size) {
+    if (!isEqual_I2(d->size, size)) {
+        d->size = size;
+        if (d->texture) {
+            SDL_DestroyTexture(d->texture);
+        }
+        d->texture = SDL_CreateTexture(render,
+                                       SDL_PIXELFORMAT_RGBA8888,
+                                       SDL_TEXTUREACCESS_STATIC | SDL_TEXTUREACCESS_TARGET,
+                                       size.x,
+                                       size.y);
+        SDL_SetTextureBlendMode(d->texture, SDL_BLENDMODE_BLEND);
+        d->isValid = iFalse;
+    }
+}
+
+static void release_WidgetDrawBuffer(iWidgetDrawBuffer *d) {
+    if (d->texture) {
+        SDL_DestroyTexture(d->texture);
+        d->texture = NULL;
+    }
+    d->size = zero_I2();
+    d->isValid = iFalse;
+}
+
+static iRect boundsForDraw_Widget_(const iWidget *d) {
+    iRect bounds = bounds_Widget(d);
+    if (d->flags & drawBackgroundToBottom_WidgetFlag) {
+        bounds.size.y = iMaxi(bounds.size.y, size_Root(d->root).y - top_Rect(bounds));
+    }
+    return bounds;
+}
+
+static iBool checkDrawBuffer_Widget_(const iWidget *d) {
+    return d->drawBuf && d->drawBuf->isValid &&
+           isEqual_I2(d->drawBuf->size, boundsForDraw_Widget_(d).size);
+}
+
+/*----------------------------------------------------------------------------------------------*/
+
 static void printInfo_Widget_(const iWidget *);
 
 void releaseChildren_Widget(iWidget *d) {
@@ -66,6 +127,7 @@ void init_Widget(iWidget *d) {
     d->children       = NULL;
     d->parent         = NULL;
     d->commandHandler = NULL;
+    d->drawBuf        = NULL;
     iZap(d->padding);
 }
 
@@ -82,6 +144,7 @@ static void visualOffsetAnimation_Widget_(void *ptr) {
 
 void deinit_Widget(iWidget *d) {
     releaseChildren_Widget(d);
+    delete_WidgetDrawBuffer(d->drawBuf);
 #if 0 && !defined (NDEBUG)
     printf("widget %p (%s) deleted (on top:%d)\n", d, cstr_String(&d->id),
            d->flags & keepOnTop_WidgetFlag ? 1 : 0);
@@ -1036,7 +1099,8 @@ iBool scrollOverflow_Widget(iWidget *d, int delta) {
     const iInt2 newPos = windowToInner_Widget(d->parent, bounds.pos);
     if (!isEqual_I2(newPos, d->rect.pos)) {
         d->rect.pos = newPos;
-        refresh_Widget(d);
+//        refresh_Widget(d);
+        postRefresh_App();
     }
     return height_Rect(bounds) > height_Rect(winRect);
 }
@@ -1077,6 +1141,9 @@ iBool processEvent_Widget(iWidget *d, const SDL_Event *ev) {
             }
             if (ev->user.code == command_UserEventCode) {
                 const char *cmd = command_UserEvent(ev);
+                if (d->drawBuf && equal_Command(cmd, "theme.changed")) {
+                    d->drawBuf->isValid = iFalse;
+                }
                 if (d->flags & (leftEdgeDraggable_WidgetFlag | rightEdgeDraggable_WidgetFlag) &&
                     isVisible_Widget(d) && ~d->flags & disabled_WidgetFlag &&
                     equal_Command(cmd, "edgeswipe.moved")) {
@@ -1147,14 +1214,13 @@ int backgroundFadeColor_Widget(void) {
     }
 }
 
-void drawBackground_Widget(const iWidget *d) {
-    if (d->flags & noBackground_WidgetFlag) {
-        return;
-    }
-    if (d->flags & hidden_WidgetFlag && ~d->flags & visualOffset_WidgetFlag) {
-        return;
-    }
-    /* Popup menus have a shadowed border. */
+iLocalDef iBool isDrawn_Widget_(const iWidget *d) {
+    return ~d->flags & hidden_WidgetFlag || d->flags & visualOffset_WidgetFlag;
+}
+
+static void drawLayerEffects_Widget_(const iWidget *d) {
+    /* Layered effects are not buffered, so they are drawn here separately. */
+    iAssert(isDrawn_Widget_(d));
     iBool shadowBorder   = (d->flags & keepOnTop_WidgetFlag && ~d->flags & mouseModal_WidgetFlag) != 0;
     iBool fadeBackground = (d->bgColor >= 0 || d->frameColor >= 0) && d->flags & mouseModal_WidgetFlag;
     if (deviceType_App() == phone_AppDeviceType) {
@@ -1163,13 +1229,12 @@ void drawBackground_Widget(const iWidget *d) {
             shadowBorder = iFalse;
         }
     }
+    const iBool isFaded = fadeBackground && ~d->flags & noFadeBackground_WidgetFlag;
     if (shadowBorder && ~d->flags & noShadowBorder_WidgetFlag) {
         iPaint p;
         init_Paint(&p);
         drawSoftShadow_Paint(&p, bounds_Widget(d), 12 * gap_UI, black_ColorId, 30);
     }
-    const iBool isFaded = fadeBackground &&
-                          ~d->flags & noFadeBackground_WidgetFlag;
     if (isFaded) {
         iPaint p;
         init_Paint(&p);
@@ -1183,10 +1248,20 @@ void drawBackground_Widget(const iWidget *d) {
         fillRect_Paint(&p, rect_Root(d->root), backgroundFadeColor_Widget());
         SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_NONE);
     }
+}
+
+void drawBackground_Widget(const iWidget *d) {
+    if (d->flags & noBackground_WidgetFlag) {
+        return;
+    }
+    if (!isDrawn_Widget_(d)) {
+        return;
+    }
+    /* Popup menus have a shadowed border. */
     if (d->bgColor >= 0 || d->frameColor >= 0) {
         iRect rect = bounds_Widget(d);
         if (d->flags & drawBackgroundToBottom_WidgetFlag) {
-            rect.size.y = size_Root(d->root).y - top_Rect(rect);
+            rect.size.y = iMax(rect.size.y, size_Root(d->root).y - top_Rect(rect));
         }
         iPaint p;
         init_Paint(&p);
@@ -1242,8 +1317,54 @@ void drawBackground_Widget(const iWidget *d) {
     }
 }
 
-iLocalDef iBool isDrawn_Widget_(const iWidget *d) {
-    return ~d->flags & hidden_WidgetFlag || d->flags & visualOffset_WidgetFlag;
+int drawCount_;
+
+static iBool isRoot_Widget_(const iWidget *d) {
+    return d == d->root->widget;
+}
+
+iLocalDef iBool isFullyContainedByOther_Rect(const iRect d, const iRect other) {
+    if (isEmpty_Rect(other)) {
+        /* Nothing is contained by empty. */
+        return iFalse;
+    }
+    if (isEmpty_Rect(d)) {
+        /* Empty is fully contained by anything. */
+        return iTrue;
+    }
+    return equal_Rect(intersect_Rect(d, other), d);
+}
+
+static void addToPotentiallyVisible_Widget_(const iWidget *d, iPtrArray *pvs, iRect *fullyMasked) {
+    if (isDrawn_Widget_(d)) {
+        iRect bounds = bounds_Widget(d);
+        if (d->flags & drawBackgroundToBottom_WidgetFlag) {
+            bounds.size.y = size_Root(d->root).y - top_Rect(bounds);
+        }
+        if (isFullyContainedByOther_Rect(bounds, *fullyMasked)) {
+            return; /* can't be seen */
+        }                
+        pushBack_PtrArray(pvs, d);
+        if (d->bgColor >= 0 && ~d->flags & noBackground_WidgetFlag &&
+            isFullyContainedByOther_Rect(*fullyMasked, bounds)) {
+            *fullyMasked = bounds;
+        }
+    }    
+}
+
+static void findPotentiallyVisible_Widget_(const iWidget *d, iPtrArray *pvs) {
+    iRect fullyMasked = zero_Rect();
+    if (isRoot_Widget_(d)) {
+        iReverseConstForEach(PtrArray, i, onTop_Root(d->root)) {
+            addToPotentiallyVisible_Widget_(i.ptr, pvs, &fullyMasked);
+        }
+    }
+    iReverseConstForEach(ObjectList, i, d->children) {
+        const iWidget *child = i.object;
+        if (~child->flags & keepOnTop_WidgetFlag) {
+            addToPotentiallyVisible_Widget_(child, pvs, &fullyMasked);
+        }
+    }
 }
 
 void drawChildren_Widget(const iWidget *d) {
@@ -1253,21 +1374,85 @@ void drawChildren_Widget(const iWidget *d) {
     iConstForEach(ObjectList, i, d->children) {
         const iWidget *child = constAs_Widget(i.object);
         if (~child->flags & keepOnTop_WidgetFlag && isDrawn_Widget_(child)) {
+            drawCount_++;
             class_Widget(child)->draw(child);
         }
     }
+}
+
+void drawRoot_Widget(const iWidget *d) {
+    iAssert(d == d->root->widget);
     /* Root draws the on-top widgets on top of everything else. */
-    if (d == d->root->widget) {
-        iConstForEach(PtrArray, i, onTop_Root(d->root)) {
-            const iWidget *top = *i.value;
-            class_Widget(top)->draw(top);
-        }
-    }
+    iPtrArray pvs;
+    init_PtrArray(&pvs);
+    findPotentiallyVisible_Widget_(d, &pvs);
+    iReverseConstForEach(PtrArray, i, &pvs) {
+        drawCount_++;
+        class_Widget(i.ptr)->draw(i.ptr);
+    }
+    deinit_PtrArray(&pvs);
+}
+
+void setDrawBufferEnabled_Widget(iWidget *d, iBool enable) {
+    if (enable && !d->drawBuf) {
+        d->drawBuf = new_WidgetDrawBuffer();        
+    }
+    else if (!enable && d->drawBuf) {
+        delete_WidgetDrawBuffer(d->drawBuf);
+        d->drawBuf = NULL;
+    }
+}
+
+static void beginBufferDraw_Widget_(const iWidget *d) {
+    if (d->drawBuf) {
+//        printf("[%p] drawbuffer update %d\n", d, d->drawBuf->isValid);
+        const iRect bounds = bounds_Widget(d);
+        SDL_Renderer *render = renderer_Window(get_Window());
+        d->drawBuf->oldTarget = SDL_GetRenderTarget(render);
+        d->drawBuf->oldOrigin = origin_Paint;
+        realloc_WidgetDrawBuffer(d->drawBuf, render, boundsForDraw_Widget_(d).size);
+        SDL_SetRenderTarget(render, d->drawBuf->texture);
+        //SDL_SetRenderDrawColor(render, 255, 0, 0, 128);
+        SDL_SetRenderDrawColor(render, 0, 0, 0, 0);
+        SDL_RenderClear(render);
+        origin_Paint = neg_I2(bounds.pos); /* with current visual offset */
+//        printf("beginBufferDraw: origin %d,%d\n", origin_Paint.x, origin_Paint.y);
+//        fflush(stdout);
+    }    
+}
+
+static void endBufferDraw_Widget_(const iWidget *d) {
+    if (d->drawBuf) {
+        d->drawBuf->isValid = iTrue;
+        SDL_SetRenderTarget(renderer_Window(get_Window()), d->drawBuf->oldTarget);
+        origin_Paint = d->drawBuf->oldOrigin;
+//        printf("endBufferDraw: origin %d,%d\n", origin_Paint.x, origin_Paint.y);
+//        fflush(stdout);
+    }    
 }
 
 void draw_Widget(const iWidget *d) {
-    drawBackground_Widget(d);
-    drawChildren_Widget(d);
+    if (!isDrawn_Widget_(d)) {
+        if (d->drawBuf) {
+//            printf("[%p] drawBuffer released\n", d);
+            release_WidgetDrawBuffer(d->drawBuf);
+        }
+        return;
+    }
+    drawLayerEffects_Widget_(d);
+    if (!d->drawBuf || !checkDrawBuffer_Widget_(d)) {
+        beginBufferDraw_Widget_(d);
+        drawBackground_Widget(d);
+        drawChildren_Widget(d);
+        endBufferDraw_Widget_(d);
+    }
+    if (d->drawBuf) {
+        iAssert(d->drawBuf->isValid);
+        const iRect bounds = bounds_Widget(d);
+        SDL_RenderCopy(renderer_Window(get_Window()), d->drawBuf->texture, NULL,
+                       &(SDL_Rect){ bounds.pos.x, bounds.pos.y,
+                                    d->drawBuf->size.x, d->drawBuf->size.y });
+    }
 }
 
 iAny *addChild_Widget(iWidget *d, iAnyObject *child) {
@@ -1659,12 +1844,20 @@ void postCommand_Widget(const iAnyObject *d, const char *cmd, ...) {
     deinit_String(&str);
 }
 
-void refresh_Widget(const iAnyObject *d) {
+void refresh_Widget(const iAnyObject *d) {    
     /* TODO: Could be widget specific, if parts of the tree are cached. */
     /* TODO: The visbuffer in DocumentWidget and ListWidget could be moved to be a general
        purpose feature of Widget. */
     iAssert(isInstance_Object(d, &Class_Widget));
-    iUnused(d);
+    /* Mark draw buffers invalid. */
+    for (const iWidget *w = d; w; w = w->parent) {
+        if (w->drawBuf) {
+//            if (w->drawBuf->isValid) {
+//                printf("[%p] drawbuffer invalidated by %p\n", w, d); fflush(stdout);
+//            }
+            w->drawBuf->isValid = iFalse;
+        }
+    }
     postRefresh_App();
 }
 
diff --git a/src/ui/widget.h b/src/ui/widget.h
index 1a944c0a..fd4d8898 100644
--- a/src/ui/widget.h
+++ b/src/ui/widget.h
@@ -131,6 +131,8 @@ enum iWidgetFocusDir {
     backward_WidgetFocusDir,
 };
 
+iDeclareType(WidgetDrawBuffer)
+
 struct Impl_Widget {
     iObject      object;
     iString      id;
@@ -148,6 +150,7 @@ struct Impl_Widget {
     iWidget *    parent;
     iBool      (*commandHandler)(iWidget *, const char *);
     iRoot *      root;
+    iWidgetDrawBuffer *drawBuf;
 };
 
 iDeclareObjectConstruction(Widget)
@@ -203,6 +206,12 @@ size_t  childCount_Widget               (const iWidget *);
 void    draw_Widget                     (const iWidget *);
 void    drawBackground_Widget           (const iWidget *);
 void    drawChildren_Widget             (const iWidget *);
+void    drawRoot_Widget                 (const iWidget *); /* root only */
+void    setDrawBufferEnabled_Widget     (iWidget *, iBool enable);
+
+iLocalDef iBool isDrawBufferEnabled_Widget(const iWidget *d) {
+    return d && d->drawBuf;
+}
 
 iLocalDef int width_Widget(const iAnyObject *d) {
     if (d) {
diff --git a/src/ui/window.c b/src/ui/window.c
index 096853cc..3385f436 100644
--- a/src/ui/window.c
+++ b/src/ui/window.c
@@ -1060,12 +1060,13 @@ void draw_Window(iWindow *d) {
     d->frameTime = SDL_GetTicks();
     if (isExposed_Window(d)) {
         d->isInvalidated = iFalse;
+        extern int drawCount_;
         iForIndices(i, d->roots) {
             iRoot *root = d->roots[i];
             if (root) {
                 setCurrent_Root(root);
                 unsetClip_Paint(&p); /* update clip to current root */
-                draw_Widget(root->widget);
+                drawRoot_Widget(root->widget);
 #if defined (LAGRANGE_ENABLE_CUSTOM_FRAME)
                 /* App icon. */
                 const iWidget *appIcon = findChild_Widget(root->widget, "winbar.icon");
@@ -1105,6 +1106,10 @@ void draw_Window(iWindow *d) {
             }
         }
         setCurrent_Root(NULL);
+#if !defined (NDEBUG)
+        draw_Text(defaultBold_FontId, zero_I2(), red_ColorId, "%d", drawCount_);
+        drawCount_ = 0;
+#endif
     }
 #if 0
     /* Text cache debugging. */ {
Proxy Information
Original URL
gemini://git.skyjake.fi/lagrange/work%2Fv1.7/cdiff/33620846cca5678fbd662ea1a48fad302727dae7
Status Code
Success (20)
Meta
text/gemini; charset=utf-8
Capsule Response Time
89.396272 milliseconds
Gemini-to-HTML Time
1.591339 milliseconds

This content has been proxied by September (ba2dc).