Lagrange [dev]

Improved keyboard UI focus navigation

=> a6f283838088921daa7fa29677e832400469c6dc

diff --git a/src/app.c b/src/app.c
index 8245e5c3..f8fbbabd 100644
--- a/src/app.c
+++ b/src/app.c
@@ -42,6 +42,8 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
 #include "ui/inputwidget.h"
 #include "ui/keys.h"
 #include "ui/labelwidget.h"
+#include "ui/listwidget.h"
+#include "ui/lookupwidget.h"
 #include "ui/root.h"
 #include "ui/sidebarwidget.h"
 #include "ui/text.h"
@@ -2142,45 +2144,24 @@ void processEvents_App(enum iAppEventMode eventMode) {
                 }
 #endif /* LAGRANGE_ENABLE_MOUSE_TOUCH_EMULATION */
                 iBool wasUsed = iFalse;
-                /* Focus navigation events take prioritity. */
+                /* Focus navigation events take priority. */
                 if (!wasUsed) {
                     /* Keyboard focus navigation with arrow keys. */
-                    iWidget *menubar = NULL;
-                    if (ev.type == SDL_KEYDOWN && ev.key.keysym.mod == 0 && focus_Widget() &&
-                        parentMenu_Widget(focus_Widget())) {
-                        setCurrent_Window(window_Widget(focus_Widget()));
-                        const int key = ev.key.keysym.sym;
-                        if (key == SDLK_DOWN || key == SDLK_UP) {
-                            iWidget *nextFocus = findFocusable_Widget(focus_Widget(),
-                                                                      key == SDLK_UP
-                                                                          ? backward_WidgetFocusDir
-                                                                          : forward_WidgetFocusDir);
-                            if (nextFocus && parent_Widget(nextFocus) == parent_Widget(focus_Widget())) {
-                                setFocus_Widget(nextFocus);
-                            }
+                    if (ev.type == SDL_KEYDOWN && keyMods_Sym(ev.key.keysym.mod) == 0 && focus_Widget()) {
+                        if (moveFocusInsideMenu_App(&ev)) {
                             wasUsed = iTrue;
                         }
-                        else if ((key == SDLK_LEFT || key == SDLK_RIGHT)) {
-                            /* Arrow keys in the menubar will switch between top-level menus. */
-                            if ((menubar = findParent_Widget(focus_Widget(), "menubar")) != NULL) {
-                                iWidget *button = parent_Widget(parent_Widget(focus_Widget()));
-                                size_t index = indexOfChild_Widget(menubar, button);
-                                const size_t curIndex = index;
-                                if (key == SDLK_LEFT && index > 0) {
-                                    index--;
-                                }
-                                else if (key == SDLK_RIGHT && index < childCount_Widget(menubar) - 1) {
-                                    index++;
-                                }
-                                if (curIndex != index) {
-                                    setFocus_Widget(child_Widget(menubar, index));
-                                    postCommand_Widget(child_Widget(menubar, index), "trigger");
-                                }
-                            }
-                            else {
-                                postCommand_Widget(focus_Widget(), "cancel");
+                        else {
+                            const int key = ev.key.keysym.sym;
+                            if ((key == SDLK_DOWN || key == SDLK_UP || key == SDLK_LEFT ||
+                                 key == SDLK_RIGHT) &&
+                                /* some widgets handle arrow keys themselves: */
+                                !isInstance_Object(focus_Widget(), &Class_DocumentWidget) &&
+                                !isInstance_Object(focus_Widget(), &Class_ListWidget) &&
+                                !isInstance_Object(focus_Widget(), &Class_InputWidget) &&
+                                !isInstance_Object(focus_Widget(), &Class_LookupWidget)) {
+                                wasUsed = moveFocusWithArrows_App(&ev);
                             }
-                            wasUsed = iTrue;
                         }
                     }
                 }
@@ -2720,6 +2701,116 @@ iAny *findWidget_App(const char *id) {
     return NULL;
 }
 
+iBool moveFocusInsideMenu_App(const void *sdlEvent) {
+    if (!focus_Widget()) {
+        return iFalse;
+    }
+    const SDL_Event *event = sdlEvent;
+    if (event->type != SDL_KEYDOWN) {
+        return iFalse;
+    }
+    const int key = event->key.keysym.sym;
+    /* The menubar has special behavior for focus changing to navigate between sibling menus. */
+    iWidget *menu = parentMenu_Widget(focus_Widget());
+    if (menu) {
+        const size_t focusIndex = indexOfChild_Widget(menu, focus_Widget());
+        if (key >= 'a' && key <= 'z') {
+            /* See if any menu item starts with a matching letter. */
+            iWidget *firstMatch = NULL;
+            size_t index = 0;
+            iForEach(ObjectList, i, children_Widget(menu)) {
+                if (isInstance_Object(i.object, &Class_LabelWidget)) {
+                    iLabelWidget *item = i.object;
+                    char prefix[2] = { key, 0 };
+                    if (startsWithCase_String(text_LabelWidget(item), prefix)) {
+                        if (!firstMatch) {
+                            firstMatch = i.object;
+                        }
+                        if (focus_Widget() != i.object && index > focusIndex) {
+                            setCurrent_Window(window_Widget(focus_Widget()));
+                            setFocus_Widget(i.object);
+                            return iTrue;
+                        }
+                    }
+                }
+                index++;
+            }
+            if (firstMatch) {
+                /* Loop back around. */
+                setCurrent_Window(window_Widget(focus_Widget()));
+                setFocus_Widget(firstMatch);
+                return iTrue;
+            }
+        }
+        else if (key == SDLK_PAGEUP || key == SDLK_PAGEDOWN || key == SDLK_HOME || key == SDLK_END) {
+            /* Move to top/bottom of menu. */
+            enum iDirection dir =
+                (key == SDLK_PAGEUP || key == SDLK_HOME ? up_Direction : down_Direction);
+            iWidget *next = focus_Widget();
+            for (;;) {
+                iWidget *adjacent = findAdjacentFocusable_Widget(next, dir);
+                if (!adjacent || adjacent == next) break;
+                next = adjacent;
+            }
+            setCurrent_Window(window_Widget(focus_Widget()));
+            setFocus_Widget(next);
+            return iTrue;
+        }
+    }
+    if (key == SDLK_LEFT || key == SDLK_RIGHT) {
+        /* Arrow keys in the menubar will switch between top-level menus. */
+        iWidget *menubar = findParent_Widget(focus_Widget(), "menubar");
+        if (menubar) {
+            iWidget *button = parent_Widget(parent_Widget(focus_Widget()));
+            size_t index = indexOfChild_Widget(menubar, button);
+            if (index == iInvalidPos) {
+                return iFalse;
+            }
+            const size_t curIndex = index;
+            if (key == SDLK_LEFT && index > 0) {
+                index--;
+            }
+            else if (key == SDLK_RIGHT && index < childCount_Widget(menubar) - 1) {
+                index++;
+            }
+            if (curIndex != index) {
+                setCurrent_Window(window_Widget(focus_Widget()));
+                setFocus_Widget(child_Widget(menubar, index));
+                postCommand_Widget(child_Widget(menubar, index), "trigger");
+            }
+            return iTrue;
+        }
+        else {
+            setCurrent_Window(window_Widget(focus_Widget()));
+            postCommand_Widget(focus_Widget(), "cancel");
+        }
+    }
+    return iFalse;
+}
+
+iBool moveFocusWithArrows_App(const void *sdlEvent) {
+    if (!focus_Widget()) {
+        return iFalse;
+    }
+    const SDL_Event *event = sdlEvent;
+    if (event->type != SDL_KEYDOWN) {
+        return iFalse;
+    }
+    const int key = event->key.keysym.sym;
+    iWidget *nextFocus = findAdjacentFocusable_Widget(focus_Widget(),
+                                                        key == SDLK_UP    ? up_Direction
+                                                      : key == SDLK_DOWN  ? down_Direction
+                                                      : key == SDLK_LEFT  ? left_Direction
+                                                      : key == SDLK_RIGHT ? right_Direction
+                                                                          : none_Direction);
+    if (nextFocus) {
+        setCurrent_Window(window_Widget(focus_Widget()));
+        setFocus_Widget(nextFocus);
+        return iTrue;
+    }
+    return iFalse;
+}
+
 void addTicker_App(iTickerFunc ticker, iAny *context) {
     iApp *d = &app_;
     insert_SortedArray(&d->tickers, &(iTicker){ context, get_Root(), ticker });
@@ -3035,6 +3126,10 @@ static iBool handlePrefsCommands_(iWidget *d, const char *cmd) {
         return iTrue;
     }
     else if (equal_Command(cmd, "tabs.changed")) {
+        if (isTerminal_Platform()) {
+            iWidget *tabs = findChild_Widget(d, "prefs.tabs");
+            setFocus_Widget((iWidget *) tabPageButton_Widget(tabs, currentTabPage_Widget(tabs)));
+        }
         refresh_Widget(d);
         return iFalse;
     }
diff --git a/src/app.h b/src/app.h
index 16708127..a9acc149 100644
--- a/src/app.h
+++ b/src/app.h
@@ -186,6 +186,8 @@ iLocalDef void postCommand_App(const char *command) {
 iDocumentWidget *document_Command       (const char *cmd);
 
 iAny *      findWidget_App              (const char *id);
+iBool       moveFocusWithArrows_App     (const void *sdlEvent);
+iBool       moveFocusInsideMenu_App     (const void *sdlEvent);
 void        commitFile_App              (const char *path, const char *tempPathWithNewContents); /* latter will be removed */
 void        openInDefaultBrowser_App    (const iString *url, const iString *mime);
 void        revealPath_App              (const iString *path);
diff --git a/src/defs.h b/src/defs.h
index 20898327..18c3aefa 100644
--- a/src/defs.h
+++ b/src/defs.h
@@ -142,6 +142,14 @@ enum iScrollType {
     max_ScrollType
 };
 
+enum iDirection {
+    none_Direction,
+    up_Direction,
+    right_Direction,
+    down_Direction,
+    left_Direction,
+};
+
 enum iToolbarAction {
     back_ToolbarAction        = 0,
     forward_ToolbarAction     = 1,
diff --git a/src/ui/inputwidget.c b/src/ui/inputwidget.c
index 5e14bac2..66201119 100644
--- a/src/ui/inputwidget.c
+++ b/src/ui/inputwidget.c
@@ -2654,7 +2654,14 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) {
                     (checkAcceptMods_InputWidget_(d, mods) ||
                      (~d->inFlags & lineBreaksEnabled_InputWidgetFlag))) {
                     d->inFlags |= enterPressed_InputWidgetFlag;
-                    setFocus_Widget(NULL);
+                    if (isTerminal_Platform() && cmp_String(id_Widget(w), "url")) {
+                        /* In dialogs, Return moves to the next focusable field rather than
+                           loosing focus entirely. */
+                        setFocus_Widget(findFocusable_Widget(w, forward_WidgetFocusDir));
+                    }
+                    else {
+                        setFocus_Widget(NULL);
+                    }
                     return iTrue;
                 }
                 return iFalse;
@@ -2810,11 +2817,14 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) {
                     refresh_Widget(d);
                     return iTrue;
                 }
-                if (isArrowUpDownConsumed_InputWidget_(d)) {
+                // if (isArrowUpDownConsumed_InputWidget_(d)) {
+                //     return iTrue;
+                // }
+                /* For moving to lookup from url entry. */
+                if (processEvent_Widget(as_Widget(d), ev)) {
                     return iTrue;
                 }
-                /* For moving to lookup from url entry. */
-                return processEvent_Widget(as_Widget(d), ev);
+                return moveFocusWithArrows_App(ev);
             case SDLK_PAGEUP:
             case SDLK_PAGEDOWN:
                 for (int count = 0; count < 5; count++) {
diff --git a/src/ui/listwidget.c b/src/ui/listwidget.c
index 51c89990..a1e4b72e 100644
--- a/src/ui/listwidget.c
+++ b/src/ui/listwidget.c
@@ -315,7 +315,7 @@ void setHoverItem_ListWidget(iListWidget *d, size_t index) {
     }
 }
 
-static void moveCursor_ListWidget_(iListWidget *d, int dir, uint32_t animSpan) {
+static iBool moveCursor_ListWidget_(iListWidget *d, int dir, uint32_t animSpan) {
     const size_t oldCursor = d->cursorItem;
     if (isEmpty_ListWidget(d)) {
         d->cursorItem = iInvalidPos;
@@ -338,6 +338,7 @@ static void moveCursor_ListWidget_(iListWidget *d, int dir, uint32_t animSpan) {
     if (d->cursorItem != iInvalidPos) {
         scrollToItem_ListWidget(d, d->cursorItem, prefs_App()->uiAnimations ? animSpan : 0);
     }
+    return d->cursorItem != oldCursor;
 }
 
 void setCursorItem_ListWidget(iListWidget *d, size_t index) {
@@ -531,11 +532,17 @@ static iBool processEvent_ListWidget_(iListWidget *d, const SDL_Event *ev) {
                 case SDLK_END: {
                     if (d->scrollMode == normal_ScrollMode) {
                         const int step = cursorKeyStep_ListWidget_(d, key);
-                        moveCursor_ListWidget_(d, step, iAbs(step) == 1 ? 0 : 150);
+                        const iBool wasChanged = moveCursor_ListWidget_(d, step, iAbs(step) == 1 ? 0 : 150);
+                        if (!wasChanged && (key == SDLK_UP || key == SDLK_DOWN)) {
+                            moveFocusWithArrows_App(ev);
+                        }
                         return iTrue;
                     }
                     return iFalse;
                 }
+                case SDLK_LEFT:
+                case SDLK_RIGHT:
+                    return moveFocusWithArrows_App(ev);
                 case SDLK_RETURN:
                 case SDLK_KP_ENTER:
                 case SDLK_SPACE:
diff --git a/src/ui/util.c b/src/ui/util.c
index 5adf4151..bae92205 100644
--- a/src/ui/util.c
+++ b/src/ui/util.c
@@ -1466,13 +1466,15 @@ void openMenuAnchorFlags_Widget(iWidget *d, iRect windowAnchorRect, int menuOpen
     arrange_Widget(d); /* need to know the height */
     iBool allowOverflow = (get_Window()->type == extra_WindowType);
     /* A vertical offset determined by a possible selected label in the menu. */
+    iWidget *focusedItem = NULL;
     if (deviceType_App() == desktop_AppDeviceType &&
         windowCoord.y < rootSize.y - lineHeight_Text(uiNormal_FontSize) * 3) {
-        iConstForEach(ObjectList, child, children_Widget(d)) {
-            const iWidget *item = constAs_Widget(child.object);
+        iForEach(ObjectList, child, children_Widget(d)) {
+            iWidget *item = as_Widget(child.object);
             if (flags_Widget(item) & selected_WidgetFlag) {
                 windowCoord.y -= item->rect.pos.y;
                 allowOverflow = iTrue;
+                focusedItem = item;
             }
         }
     }
@@ -1641,10 +1643,15 @@ void openMenuAnchorFlags_Widget(iWidget *d, iRect windowAnchorRect, int menuOpen
     }
     setupMenuTransition_Mobile(d, iTrue);
     if (isMenuFocused) {
-        iForEach(ObjectList, i, children_Widget(d)) {
-            if (flags_Widget(i.object) & focusable_WidgetFlag) {
-                setFocus_Widget(i.object);
-                break;
+        if (focusedItem) {
+            setFocus_Widget(focusedItem);
+        }
+        else {
+            iForEach(ObjectList, i, children_Widget(d)) {
+                if (flags_Widget(i.object) & focusable_WidgetFlag) {
+                    setFocus_Widget(i.object);
+                    break;
+                }
             }
         }
     }
@@ -1684,7 +1691,7 @@ void closeMenu_Widget(iWidget *d) {
         postCommand_Widget(d, "menu.closed");
         setupMenuTransition_Mobile(d, iFalse);
         if (focus_Widget() && hasParent_Widget(focus_Widget(), d)) {
-            setFocus_Widget(NULL);
+            setFocus_Widget(as_Widget(button));
         }
     }
 }
diff --git a/src/ui/widget.c b/src/ui/widget.c
index 3d839bea..0376661a 100644
--- a/src/ui/widget.c
+++ b/src/ui/widget.c
@@ -2470,28 +2470,28 @@ iWidget *hover_Widget(void) {
 }
 
 static const iWidget *findFocusable_Widget_(const iWidget *d, const iWidget *startFrom,
-                                            iBool *getNext, enum iWidgetFocusDir focusDir) {
+                                            iBool *getNext, enum iWidgetFocusDir cycleDir) {
     if (startFrom == d) {
         *getNext = iTrue;
         return NULL;
     }
     if ((d->flags & focusable_WidgetFlag) && isVisible_Widget(d) && !isDisabled_Widget(d) &&
         ~d->flags & destroyPending_WidgetFlag && *getNext) {
-        if ((~focusDir & notInput_WidgetFocusFlag) || !isInstance_Object(d, &Class_InputWidget)) {
+        if ((~cycleDir & notInput_WidgetFocusFlag) || !isInstance_Object(d, &Class_InputWidget)) {
             return d;
         }
     }
-    if ((focusDir & dirMask_WidgetFocusFlag) == forward_WidgetFocusDir) {
+    if ((cycleDir & dirMask_WidgetFocusFlag) == forward_WidgetFocusDir) {
         iConstForEach(ObjectList, i, d->children) {
             const iWidget *found =
-                findFocusable_Widget_(constAs_Widget(i.object), startFrom, getNext, focusDir);
+                findFocusable_Widget_(constAs_Widget(i.object), startFrom, getNext, cycleDir);
             if (found) return found;
         }
     }
     else {
         iReverseConstForEach(ObjectList, i, d->children) {
             const iWidget *found =
-                findFocusable_Widget_(constAs_Widget(i.object), startFrom, getNext, focusDir);
+                findFocusable_Widget_(constAs_Widget(i.object), startFrom, getNext, cycleDir);
             if (found) return found;
         }
     }
@@ -2536,14 +2536,14 @@ const iWidget *focusRoot_Widget(const iWidget *d) {
     return root_Widget(d);
 }
 
-iAny *findFocusable_Widget(const iWidget *startFrom, enum iWidgetFocusDir focusDir) {
+iAny *findFocusable_Widget(const iWidget *startFrom, enum iWidgetFocusDir cycleDir) {
     if (!get_Window()) {
         return NULL;
     }
     const iWidget *focusRoot = focusRoot_Widget(startFrom);
     iAssert(focusRoot != NULL);
     iBool getNext = (startFrom ? iFalse : iTrue);
-    const iWidget *found = findFocusable_Widget_(focusRoot, startFrom, &getNext, focusDir);
+    const iWidget *found = findFocusable_Widget_(focusRoot, startFrom, &getNext, cycleDir);
     if (!found && startFrom) {
         getNext = iTrue;
         /* Switch to the next root, if available. */
@@ -2551,9 +2551,9 @@ iAny *findFocusable_Widget(const iWidget *startFrom, enum iWidgetFocusDir focusD
             findTopmostFocusRoot_Widget_(otherRoot_Window(get_Window(), focusRoot->root)->widget),
             NULL,
             &getNext,
-            focusDir);
+            cycleDir);
     }
-    return iConstCast(iWidget *, found);
+    return as_Widget(iConstCast(iWidget *, found));
 }
 
 void setMouseGrab_Widget(iWidget *d) {
@@ -2651,11 +2651,145 @@ iBool hasVisibleChildOnTop_Widget(const iWidget *parent) {
     return iFalse;
 }
 
+void addRecentlyDeleted_Widget(iAnyObject *obj) {
+    /* We sometimes include pointers to widgets in command events. Before an event is processed,
+       it is possible that the referened widget has been destroyed. Keeping track of recently
+       deleted widgets allows ignoring these events. */
+    maybeInit_RecentlyDeleted_(&recentlyDeleted_);
+    iGuardMutex(&recentlyDeleted_.mtx, insert_PtrSet(recentlyDeleted_.objs, obj));
+}
+
+void clearRecentlyDeleted_Widget(void) {
+    if (recentlyDeleted_.objs) {
+        iGuardMutex(&recentlyDeleted_.mtx, clear_PtrSet(recentlyDeleted_.objs));
+    }
+}
+
+iBool isRecentlyDeleted_Widget(const iAnyObject *obj) {
+    return contains_RecentlyDeleted_(&recentlyDeleted_, obj);
+}
+
 iBeginDefineClass(Widget)
     .processEvent = processEvent_Widget,
     .draw         = draw_Widget,
 iEndDefineClass(Widget)
 
+/*----------------------------------------------------------------------------------------------*/
+
+iDeclareType(AdjacentFocusFinder);
+
+struct Impl_AdjacentFocusFinder {
+    iRect           bounds;
+    enum iDirection direction;
+    float           nearestDist;
+    const iWidget  *nearest_out;
+};
+
+static iBool isContained_Rangei(iRangei large, iRangei small) {
+    return contains_Rangei(large, small.start) &&
+           (contains_Rangei(large, small.end) || large.end == small.end);
+}
+
+static int mid_Rangei(const iRangei d) {
+    return (d.start + d.end) / 2;
+}
+
+static float offAxisRangeDistance_(iRangei src, iRangei dst) {
+    if (equal_Rangei(src, dst)) {
+        return 0; /* Ideal match. */
+    }
+    if (isOverlapping_Rangei(src, dst)) {
+        return iAbs(src.start - dst.start); /* Prefer matching left/top edge. */
+    }
+    /* Source and destination are not overlapping. Off-axis non-overlapping differences are
+       unfavorable because this is used for navigating to a particular linear direction. */
+    const int dist = iAbs(mid_Rangei(src) - mid_Rangei(dst));
+    return dist * dist;
+}
+
+static float adjacentDistance_Rect(const iRect *d, const iRect *other, enum iDirection dir) {
+    float dist = 0;
+    switch (dir) {
+        case left_Direction:
+            dist = -right_Rect(*other) + left_Rect(*d);
+            if (dist < 0) {
+                return -1.0f;
+            }
+            dist += offAxisRangeDistance_(ySpan_Rect(*d), ySpan_Rect(*other));
+            break;
+        case right_Direction:
+            dist = left_Rect(*other) - right_Rect(*d);
+            if (dist < 0) {
+                return -1.0f;
+            }
+            dist += offAxisRangeDistance_(ySpan_Rect(*d), ySpan_Rect(*other));
+            break;
+        case up_Direction:
+            dist = -bottom_Rect(*other) + top_Rect(*d);
+            if (dist < 0) {
+                return -1.0f;
+            }
+            dist += offAxisRangeDistance_(xSpan_Rect(*d), xSpan_Rect(*other));
+            dist /= aspect_UI;
+            break;
+        case down_Direction:
+            dist = top_Rect(*other) - bottom_Rect(*d);
+            if (dist < 0) {
+                return -1.0f;
+            }
+            dist += offAxisRangeDistance_(xSpan_Rect(*d), xSpan_Rect(*other));
+            dist /= aspect_UI;
+            break;
+        default:
+            return -1.0f;
+    }
+    return dist;
+}
+
+static void walkTree_AdjacentFocusFinder_(iAdjacentFocusFinder *d, const iWidget *parent) {
+    const iBool isMenu = findParent_Widget(parent, "menu") != NULL;
+    iConstForEach(ObjectList, i, parent->children) {
+        const iWidget *w = i.object;
+        if (isVisible_Widget(w) && !isDisabled_Widget(w) && ~w->flags & destroyPending_WidgetFlag) {
+            if (w->flags & focusable_WidgetFlag) {
+                const iRect bounds = bounds_Widget(w);
+                if (isOverlapping_Rect(d->bounds, bounds)) {
+                    /* Overlapping means it isn't adjacent. */
+                    continue;
+                }
+                const float dist = adjacentDistance_Rect(&d->bounds, &bounds, d->direction);
+                if (dist < 0) {
+                    continue; /* wrong direction */
+                }
+                if (!d->nearest_out || dist < d->nearestDist) {
+                    d->nearest_out = w;
+                    d->nearestDist = dist;
+                }
+            }
+            walkTree_AdjacentFocusFinder_(d, w);
+        }
+    }
+}
+
+iAny *findAdjacentFocusable_Widget(const iWidget *d, enum iDirection direction) {
+    if (!get_Window() || direction == none_Direction) {
+        return NULL;
+    }
+    const iWidget *focusRoot = focusRoot_Widget(d);
+    const iWidget *menu = parentMenu_Widget(d);
+    if (menu) {
+        /* Inside menus, don't allow exiting the current menu. */
+        // const iWidget *topMenu = findParent_Widget(menu, "menu");
+        focusRoot = menu;
+    }
+    iAdjacentFocusFinder args = { .bounds = bounds_Widget(d), .direction = direction };
+    walkTree_AdjacentFocusFinder_(&args, focusRoot);
+    if (!args.nearest_out && menu) {
+        args.nearest_out = d; /* Keep the focus unchanged in a menu. */
+    }
+    return as_Widget(iConstCast(iWidget *, args.nearest_out));
+}
+
 /*----------------------------------------------------------------------------------------------
    Debug utilities for inspecting widget trees.
 */
@@ -2723,58 +2857,3 @@ void identify_Widget(const iWidget *d) {
     printf("Root %d: %p\n", 1 + (d->root == get_Window()->roots[1]), d->root);
     fflush(stdout);
 }
-
-void addRecentlyDeleted_Widget(iAnyObject *obj) {
-    /* We sometimes include pointers to widgets in command events. Before an event is processed,
-       it is possible that the referened widget has been destroyed. Keeping track of recently
-       deleted widgets allows ignoring these events. */
-    maybeInit_RecentlyDeleted_(&recentlyDeleted_);
-    iGuardMutex(&recentlyDeleted_.mtx, insert_PtrSet(recentlyDeleted_.objs, obj));
-}
-
-void clearRecentlyDeleted_Widget(void) {
-    if (recentlyDeleted_.objs) {
-        iGuardMutex(&recentlyDeleted_.mtx, clear_PtrSet(recentlyDeleted_.objs));
-    }
-}
-
-iBool isRecentlyDeleted_Widget(const iAnyObject *obj) {
-    return contains_RecentlyDeleted_(&recentlyDeleted_, obj);
-}
-
-#if 0
-static uint32_t callDelayed_Widget_(uint32_t interval, void *ctx) {
-    iWidget *d = ctx;
-    lock_Mutex(d->delayedMutex);
-    d->delayedTimer = 0;
-    if (d->delayedCallback) {
-        d->delayedCallback(d);
-    }
-    unlock_Mutex(d->delayedMutex);
-    return 0; /* single-shot */
-}
-
-void setDelayedCallback_Widget(iWidget *d, int delay, void (*callback)(iWidget *)) {
-    if (!callback) {
-        if (d->delayedMutex) {
-            iGuardMutex(d->delayedMutex, {
-                SDL_RemoveTimer(d->delayedTimer);
-                d->delayedTimer    = 0;
-                d->delayedCallback = NULL;
-            });
-            delete_Mutex(d->delayedMutex);
-            d->delayedMutex = NULL;
-        }
-        return;
-    }
-    if (!d->delayedMutex) {
-        d->delayedMutex = new_Mutex();
-    }
-    lock_Mutex(d->delayedMutex);
-    if (d->delayedTimer) {
-        SDL_RemoveTimer(d->delayedTimer);
-    }
-    d->delayedTimer = SDL_AddTimer(delay, callDelayed_Widget_, d);
-    unlock_Mutex(d->delayedMutex);
-}
-#endif
diff --git a/src/ui/widget.h b/src/ui/widget.h
index 69f46b92..28ebe856 100644
--- a/src/ui/widget.h
+++ b/src/ui/widget.h
@@ -232,6 +232,7 @@ const iPtrArray *findChildren_Widget    (const iWidget *, const char *id);
 iAny *  findParent_Widget               (const iWidget *, const char *id);
 iAny *  findParentClass_Widget          (const iWidget *, const iAnyClass *class);
 iAny *  findFocusable_Widget            (const iWidget *startFrom, enum iWidgetFocusDir focusDir);
+iAny *  findAdjacentFocusable_Widget    (const iWidget *, enum iDirection direction);
 iAny *  findOverflowScrollable_Widget   (iWidget *);
 size_t  childCount_Widget               (const iWidget *);
 void    draw_Widget                     (const iWidget *);
diff --git a/src/ui/window.c b/src/ui/window.c
index 2c7b2ada..56c0d403 100644
--- a/src/ui/window.c
+++ b/src/ui/window.c
@@ -1309,32 +1309,6 @@ iBool processEvent_Window(iWindow *d, const SDL_Event *ev) {
                 postCommand_App("media.player.update"); /* in case a player needs updating */
                 return iFalse; /* unfreeze all frozen windows */
             }
-#if 0
-            if (event.type == SDL_USEREVENT && isCommand_UserEvent(ev, "window.sysframe") && mw) {
-                /* This command is sent on Android to update the keyboard height. */
-                const char *cmd = command_UserEvent(ev);
-                /*
-                    0
-                    |
-                 top
-                 |  |
-                 | bottom (top of keyboard)   :
-                 |  |                         : keyboardHeight
-                 maxDrawableHeight            :
-                    |
-                   fullheight
-                 */
-                const int top    = argLabel_Command(cmd, "top");
-                const int bottom = argLabel_Command(cmd, "bottom");
-                const int full   = argLabel_Command(cmd, "fullheight");
-                //if (!SDL_IsScreenKeyboardShown(mw->base.win)) {
-                if (bottom == full) {
-                    mw->maxDrawableHeight = bottom - top;
-                }
-                setKeyboardHeight_MainWindow(mw, top + mw->maxDrawableHeight - bottom);
-                return iTrue;
-            }
-#endif
             if (processEvent_Touch(&event)) {
                 return iTrue;
             }
Proxy Information
Original URL
gemini://git.skyjake.fi/lagrange/dev/cdiff/a6f283838088921daa7fa29677e832400469c6dc
Status Code
Success (20)
Meta
text/gemini; charset=utf-8
Capsule Response Time
34.170189 milliseconds
Gemini-to-HTML Time
1.58068 milliseconds

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