Lagrange [work/v1.15]

Promoting a dialog to a separate window

=> 2d6ffdd49f83a017b499d515b2973cecf5d656dd

diff --git a/src/app.c b/src/app.c
index fd000dbd..8638e038 100644
--- a/src/app.c
+++ b/src/app.c
@@ -140,6 +140,8 @@ static const char *defaultDownloadDir_App_ = "~/Downloads";
 
 static const int idleThreshold_App_ = 1000; /* ms */
 
+typedef iWindow iMainOrExtraWindow;
+
 struct Impl_App {
     iCommandLine args;
     iString *    overrideDataPath;
@@ -151,8 +153,9 @@ struct Impl_App {
     iGmCerts *   certs;
     iVisited *   visited;
     iBookmarks * bookmarks;
-    iMainWindow *window; /* currently active MainWindow */
+    iMainOrExtraWindow *window; /* currently active MainWindow or extra Window */
     iPtrArray    mainWindows;
+    iPtrArray    extraWindows;
     iPtrArray    popupWindows;
     iSortedArray tickers; /* per-frame callbacks, used for animations */
     uint32_t     lastTickerTime;
@@ -711,7 +714,7 @@ static iBool loadState_App_(iApp *d) {
         };
         int              numWins       = 0;
         iMainWindow *    win           = NULL;
-        iMainWindow *    currentWin    = d->window;
+        iMainWindow *    currentWin    = as_MainWindow(d->window);
         iArray *         currentTabs; /* two per window (per root per window) */
         iBool            isFirstTab[2];
         currentTabs = collectNew_Array(sizeof(iCurrentTabs));
@@ -729,7 +732,7 @@ static iBool loadState_App_(iApp *d) {
                 win = d->window;
 #else
                 if (numWins == 1) {
-                    win = d->window;
+                    win = as_MainWindow(d->window);
                 }
                 else {
                     win = new_MainWindow(initialWindowRect_App_(d, numWins - 1));
@@ -870,7 +873,7 @@ static void saveState_App_(const iApp *d) {
                 writeData_File(f, magicWindow_App_, 4);
                 writeU32_File(f, win->splitMode);
                 writeU32_File(f, (win->base.keyRoot == win->base.roots[0] ? 0 : 1) |
-                                 (win == d->window ? current_WindowStateFlag : 0));
+                                 (constAs_Window(win) == d->window ? current_WindowStateFlag : 0));
             }
             /* State of UI elements. */ {
                 iForIndices(i, win->base.roots) {
@@ -1305,9 +1308,10 @@ static void init_App_(iApp *d, int argc, char **argv) {
         }
     }
     init_PtrArray(&d->mainWindows);
+    init_PtrArray(&d->extraWindows);
     init_PtrArray(&d->popupWindows);
-    d->window = new_MainWindow(*winRect0); /* first window is always created */
-    addWindow_App(d->window);
+    d->window = (iWindow *) new_MainWindow(*winRect0); /* first window is always created */
+    addWindow_App(as_MainWindow(d->window));
     load_Visited(d->visited, dataDir_App_());
     load_Bookmarks(d->bookmarks, dataDir_App_());
     load_MimeHooks(d->mimehooks, dataDir_App_());
@@ -1378,9 +1382,9 @@ static void init_App_(iApp *d, int argc, char **argv) {
         iRelease(openCmds);
     }
     fetchRemote_Bookmarks(d->bookmarks);
-    if (deviceType_App() != desktop_AppDeviceType) {
+    if (deviceType_App() != desktop_AppDeviceType && d->window == main_WindowType) {
         /* HACK: Force a resize so widgets update their state. */
-        resize_MainWindow(d->window, -1, -1);
+        resize_MainWindow(as_MainWindow(d->window), -1, -1);
     }
 #if defined (iPlatformAndroid)
     /* See if there is something to import from backup. */
@@ -1394,6 +1398,11 @@ static void deinit_App(iApp *d) {
     }
     iAssert(isEmpty_PtrArray(&d->popupWindows));
     deinit_PtrArray(&d->popupWindows);
+    iReverseForEach(PtrArray, k, &d->extraWindows) {
+        delete_Window(k.ptr);
+    }
+    iAssert(isEmpty_PtrArray(&d->extraWindows));
+    deinit_PtrArray(&d->extraWindows);
 #if defined (LAGRANGE_ENABLE_IDLE_SLEEP)
     SDL_RemoveTimer(d->sleepTimer);
 #endif
@@ -1681,7 +1690,8 @@ static iBool nextEvent_App_(iApp *d, enum iAppEventMode eventMode, SDL_Event *ev
     if (eventMode == waitForNewEvents_AppEventMode && isWaitingAllowed_App_(d)) {
         /* We may be allowed to block here until an event comes in. */
         if (isWaitingAllowed_App_(d)) {
-            if (isAppleDesktop_Platform() && d->window && d->window->enableBackBuf) {
+            if (isAppleDesktop_Platform() && d->window && d->window->type == main_WindowType &&
+                as_MainWindow(d->window)->enableBackBuf) {
                 /* SDL Metal workaround: if we block here for too long, there will be a longer
                    ~100ms stutter later on after refresh resumes, when the render pipeline does
                    some kind of an update (?). */
@@ -1703,15 +1713,27 @@ static iBool nextEvent_App_(iApp *d, enum iAppEventMode eventMode, SDL_Event *ev
 
 static iPtrArray *listWindows_App_(const iApp *d, iPtrArray *windows) {
     clear_PtrArray(windows);
-    iReverseConstForEach(PtrArray, i, &d->popupWindows) {
-        pushBack_PtrArray(windows, i.ptr);
+    /* Popups. */ {
+        iReverseConstForEach(PtrArray, i, &d->popupWindows) {
+            pushBack_PtrArray(windows, i.ptr);
+        }
     }
+    /* Current active main window. */
     if (d->window) {
         pushBack_PtrArray(windows, d->window);
     }
-    iConstForEach(PtrArray, j, &d->mainWindows) {
-        if (j.ptr != d->window) {
-            pushBack_PtrArray(windows, j.ptr);
+    /* Other extra windows. */ {
+        iReverseConstForEach(PtrArray, i, &d->extraWindows) {
+            if (i.ptr != d->window) {
+                pushBack_PtrArray(windows, i.ptr);
+            }
+        }
+    }
+    /* Other main windows. */ {
+        iConstForEach(PtrArray, i, &d->mainWindows) {
+            if (i.ptr != d->window) {
+                pushBack_PtrArray(windows, i.ptr);
+            }
         }
     }
     return windows;
@@ -1762,7 +1784,7 @@ void processEvents_App(enum iAppEventMode eventMode) {
 #endif
                 postRefresh_App();
                 break;
-            case SDL_APP_WILLENTERBACKGROUND:
+            case SDL_APP_WILLENTERBACKGROUND: {
 #if defined (iPlatformAppleMobile)
                 updateNowPlayingInfo_iOS();
 #endif
@@ -1771,18 +1793,22 @@ void processEvents_App(enum iAppEventMode eventMode) {
                 saveIdentities_GmCerts(certs_App());
                 save_Bookmarks(d->bookmarks, dataDir_App_());
 #endif
-                setFreezeDraw_MainWindow(d->window, iTrue);
+                iForEach(PtrArray, i, &d->mainWindows) {
+                    setFreezeDraw_MainWindow(*i.value, iTrue);
+                }
                 savePrefs_App_(d);
                 saveState_App_(d);
                 d->isSuspended = iTrue;
                 break;
-            case SDL_APP_TERMINATING:
-                if (d->window) {
-                    setFreezeDraw_MainWindow(d->window, iTrue);
+            }
+            case SDL_APP_TERMINATING: {
+                iForEach(PtrArray, i, &d->mainWindows) {
+                    setFreezeDraw_MainWindow(*i.value, iTrue);
                 }
                 savePrefs_App_(d);
                 saveState_App_(d);
                 break;
+            }
             case SDL_DROPFILE: {
                 if (!d->window) {
                     /* Need to open an empty window now. */
@@ -1952,7 +1978,8 @@ void processEvents_App(enum iAppEventMode eventMode) {
                     }
                 }
                 /* Per-window processing. */
-                if (!wasUsed && !isEmpty_PtrArray(&d->mainWindows)) {
+                if (!wasUsed && (!isEmpty_PtrArray(&d->mainWindows) ||
+                                 !isEmpty_PtrArray(&d->extraWindows))) {
                     listWindows_App_(d, &windows);
                     iConstForEach(PtrArray, iter, &windows) {
                         iWindow *window = iter.ptr;
@@ -2079,11 +2106,18 @@ static void runTickers_App_(iApp *d) {
         d->lastTickerTime = 0;
         return;
     }
-    iForIndices(i, d->window->base.roots) {
-        iRoot *root = d->window->base.roots[i];
-        if (root) {
-            root->didAnimateVisualOffsets = iFalse;
+    /* Update window state. */ {
+        iPtrArray *winList = listWindows_App();
+        iForEach(PtrArray, i, winList) {
+            iWindow *win = *i.value;
+            iForIndices(i, win->roots) {
+                iRoot *root = win->roots[i];
+                if (root) {
+                    root->didAnimateVisualOffsets = iFalse;
+                }
+            }
         }
+        delete_PtrArray(winList);
     }
     /* Tickers may add themselves again, so we'll run off a copy. */
     iSortedArray *pending = copy_SortedArray(&d->tickers);
@@ -2119,10 +2153,10 @@ static int resizeWatcher_(void *user, SDL_Event *event) {
         }
 #endif
         /* Find the window that is being resized and redraw it immediately. */
-        iConstForEach(PtrArray, i, &d->mainWindows) {
-            const iMainWindow *win = i.ptr;
+        iForEach(PtrArray, i, &d->mainWindows) {
+            iMainWindow *win = i.ptr;
             if (SDL_GetWindowID(win->base.win) == winev->windowID) {
-                drawWhileResizing_MainWindow(d->window, winev->data1, winev->data2);
+                drawWhileResizing_MainWindow(win, winev->data1, winev->data2);
                 break;
             }
         }
@@ -2132,9 +2166,9 @@ static int resizeWatcher_(void *user, SDL_Event *event) {
 
 static int run_App_(iApp *d) {
     /* Initial arrangement. */
-    iForIndices(i, d->window->base.roots) {
-        if (d->window->base.roots[i]) {
-            arrange_Widget(d->window->base.roots[i]->widget);
+    iForIndices(i, d->window->roots) {
+        if (d->window->roots[i]) {
+            arrange_Widget(d->window->roots[i]->widget);
         }
     }
     d->isRunning = iTrue;
@@ -2147,7 +2181,9 @@ static int run_App_(iApp *d) {
         runTickers_App_(d);
         refresh_App();
         /* Change the widget tree while we are not iterating through it. */
-        checkPendingSplit_MainWindow(d->window);
+        if (d->window && d->window->type == main_WindowType) {
+            checkPendingSplit_MainWindow(as_MainWindow(d->window));
+        }
         recycle_Garbage();
     }
     SDL_DelEventWatch(resizeWatcher_, d);
@@ -2175,6 +2211,7 @@ void refresh_App(void) {
                 }
             }
         }
+        listWindows_App_(d, &windows); /* maybe some windows were deleted, too */
     }
     /* TODO: `pendingRefresh` should be window-specific. */
     if (d->warmupFrames || exchange_Atomic(&d->pendingRefresh, iFalse)) {
@@ -2473,9 +2510,16 @@ const iPtrArray *mainWindows_App(void) {
     return &app_.mainWindows;
 }
 
-void setActiveWindow_App(iMainWindow *win) {
+void setActiveWindow_App(iAnyWindow *mainOrExtraWin) {
     iApp *d = &app_;
-    d->window = win;
+    d->window = mainOrExtraWin;
+    if (d->window) {
+        iAssert(d->window->type == main_WindowType || d->window->type == extra_WindowType);
+    }
+}
+
+iWindow *activeWindow_App(void) {
+    return app_.window;
 }
 
 void addPopup_App(iWindow *popup) {
@@ -2488,6 +2532,16 @@ void removePopup_App(iWindow *popup) {
     removeOne_PtrArray(&d->popupWindows, popup);
 }
 
+void addExtraWindow_App(iWindow *extra) {
+    iApp *d = &app_;
+    pushBack_PtrArray(&d->extraWindows, extra);
+}
+
+void removeExtraWindow_App(iWindow *extra) {
+    iApp *d = &app_;
+    removeOne_PtrArray(&d->extraWindows, extra);
+}
+
 iMimeHooks *mimeHooks_App(void) {
     return app_.mimehooks;
 }
@@ -2638,7 +2692,7 @@ static iBool handlePrefsCommands_(iWidget *d, const char *cmd) {
             postCommandf_App("prefs.dialogtab arg:%u",
                              tabPageIndex_Widget(tabs, currentTabPage_Widget(tabs)));
         }
-        destroy_Widget(d);
+        destroyDialog_Widget(d);
         postCommand_App("prefs.changed");
         return iTrue;
     }
@@ -2728,7 +2782,23 @@ iDocumentWidget *document_Root(iRoot *d) {
 }
 
 iDocumentWidget *document_App(void) {
-    return document_Root(get_Root());
+    iDocumentWidget *doc = document_Root(get_Root());
+    if (doc) {
+        return doc;
+    }
+    /* Try another window. */
+    iConstForEach(PtrArray, i, mainWindows_App()) {
+        iWindow *win = *i.value;
+        iForIndices(j, win->roots) {
+            if (win->roots[j]) {
+                doc = document_Root(win->roots[j]);
+                if (doc) {
+                    return doc;
+                }
+            }
+        }
+    }
+    return NULL;
 }
 
 iDocumentWidget *document_Command(const char *cmd) {
@@ -2772,32 +2842,44 @@ iDocumentWidget *newTab_App(const iDocumentWidget *duplicateOf, iBool switchToNe
     return doc;
 }
 
-void closeWindow_App(iMainWindow *win) {
+void closeWindow_App(iWindow *win) {
     iApp *d = &app_;
-    iForIndices(r, win->base.roots) {
-        if (win->base.roots[r]) {
-            setTreeFlags_Widget(win->base.roots[r]->widget, destroyPending_WidgetFlag, iTrue);
+    iAssert(win->type == main_WindowType || win->type == extra_WindowType);
+    const iBool isMain = (win->type == main_WindowType);
+    iWindow *activeWindow = d->window;
+    iForIndices(r, win->roots) {
+        if (win->roots[r]) {
+            setTreeFlags_Widget(win->roots[r]->widget, destroyPending_WidgetFlag, iTrue);
         }
     }
-    collect_Garbage(win, (iDeleteFunc) delete_MainWindow);
+    collect_Garbage(win, isMain ? (iDeleteFunc) delete_MainWindow
+                                : (iDeleteFunc) delete_Window);
     postRefresh_App();
-    if (isAppleDesktop_Platform() && size_PtrArray(&d->mainWindows) == 1) {
+    if (isAppleDesktop_Platform() && isMain && size_PtrArray(&d->mainWindows) == 1) {
         /* The one and only window is being closed. On macOS, the app will keep running, which
            means we must save the state of the window now or otherwise it will be lost. A newly
            opened window will use this saved state if it's the only window of the app. */
         saveState_App_(d);        
     }
-    if (d->window == win) {
+    if (activeWindow == win) {
+        d->window = NULL;
         /* Activate another window. */
         iForEach(PtrArray, i, &d->mainWindows) {
-            if (i.ptr != d->window) {
+            if (i.ptr != activeWindow) {
                 SDL_RaiseWindow(i.ptr);
                 setActiveWindow_App(i.ptr);
                 setCurrent_Window(i.ptr);
-                break;
             }
         }
     }
+    if (!d->window) {
+        iForEach(PtrArray, j, &d->extraWindows) {
+            iWindow *win = j.ptr;
+            SDL_RaiseWindow(win->win);
+            setActiveWindow_App(win);
+            setCurrent_Window(win);
+        }
+    }
 }
 
 static iBool handleIdentityCreationCommands_(iWidget *dlg, const char *cmd) {
@@ -2982,7 +3064,8 @@ static const iString *popClosedTabUrl_App_(iApp *d) {
 }
 
 static iBool handleNonWindowRelatedCommand_App_(iApp *d, const char *cmd) {
-    const iBool isFrozen = !d->window || d->window->isDrawFrozen;
+    const iBool isFrozen = !d->window ||
+        (d->window->type == main_WindowType && as_MainWindow(d->window)->isDrawFrozen);
     /* Commands related to preferences. */
     if (equal_Command(cmd, "prefs.changed")) {
         savePrefs_App_(d);
@@ -3587,8 +3670,8 @@ static iBool handleNonWindowRelatedCommand_App_(iApp *d, const char *cmd) {
     }
     else if (equal_Command(cmd, "ipc.signal")) {
         if (argLabel_Command(cmd, "raise")) {
-            if (d->window && d->window->base.win) {
-                SDL_RaiseWindow(d->window->base.win);
+            if (d->window && d->window->win) {
+                SDL_RaiseWindow(d->window->win);
             }
         }
         signal_Ipc(arg_Command(cmd));
@@ -3787,8 +3870,9 @@ static iBool handleOpenCommand_App_(iApp *d, const char *cmd) {
 
 iBool handleCommand_App(const char *cmd) {
     iApp *d = &app_;
-    const iBool isFrozen   = !d->window || d->window->isDrawFrozen;
+    const iBool isFrozen   = isDrawFrozen_Window(d->window);
     const iBool isHeadless = numWindows_App() == 0;
+    const iBool isMainWin  = d->window && d->window->type == main_WindowType;
     if (handleNonWindowRelatedCommand_App_(d, cmd)) {
         return iTrue;
     }
@@ -3804,25 +3888,26 @@ iBool handleCommand_App(const char *cmd) {
                                              suffixPtr_Command(cmd, "where")));
         return iTrue;
     }
-    else if (equal_Command(cmd, "ui.split")) {
+    else if (equal_Command(cmd, "ui.split") && isMainWin) {
         if (argLabel_Command(cmd, "swap")) {
-            swapRoots_MainWindow(d->window);
+            swapRoots_MainWindow(as_MainWindow(d->window));
             return iTrue;
         }
         if (argLabel_Command(cmd, "focusother")) {
-            iWindow *baseWin = &d->window->base;
+            iWindow *baseWin = d->window;
             if (baseWin->roots[1]) {
                 baseWin->keyRoot =
                     (baseWin->keyRoot == baseWin->roots[1] ? baseWin->roots[0] : baseWin->roots[1]);
             }
         }
-        d->window->pendingSplitMode =
+        iMainWindow *mw = as_MainWindow(d->window);
+        mw->pendingSplitMode =
             (argLabel_Command(cmd, "axis") ? vertical_WindowSplit : 0) | (arg_Command(cmd) << 1);
         const char *url = suffixPtr_Command(cmd, "url");
-        setCStr_String(d->window->pendingSplitUrl, url ? url : "");
-        setRange_String(d->window->pendingSplitSetIdent, range_Command(cmd, "setident"));
+        setCStr_String(mw->pendingSplitUrl, url ? url : "");
+        setRange_String(mw->pendingSplitSetIdent, range_Command(cmd, "setident"));
         if (hasLabel_Command(cmd, "origin")) {
-            set_String(d->window->pendingSplitOrigin, string_Command(cmd, "origin"));
+            set_String(mw->pendingSplitOrigin, string_Command(cmd, "origin"));
         }
         postRefresh_App();
         return iTrue;
@@ -3841,9 +3926,9 @@ iBool handleCommand_App(const char *cmd) {
         }
         return iTrue;
     }
-    else if (equal_Command(cmd, "window.fullscreen")) {
-        const iBool wasFull = snap_MainWindow(d->window) == fullscreen_WindowSnap;
-        setSnap_MainWindow(d->window, wasFull ? 0 : fullscreen_WindowSnap);
+    else if (equal_Command(cmd, "window.fullscreen") && isMainWin) {
+        const iBool wasFull = snap_MainWindow(as_MainWindow(d->window)) == fullscreen_WindowSnap;
+        setSnap_MainWindow(as_MainWindow(d->window), wasFull ? 0 : fullscreen_WindowSnap);
         postCommandf_App("window.fullscreen.changed arg:%d", !wasFull);
         return iTrue;
     }
@@ -4094,7 +4179,7 @@ iBool handleCommand_App(const char *cmd) {
     }
     else if (equal_Command(cmd, "keyroot.next")) {
         if (setKeyRoot_Window(as_Window(d->window),
-                              otherRoot_Window(as_Window(d->window), d->window->base.keyRoot))) {
+                              otherRoot_Window(as_Window(d->window), d->window->keyRoot))) {
             setFocus_Widget(NULL);
         }
         return iTrue;
@@ -4216,6 +4301,10 @@ iBool handleCommand_App(const char *cmd) {
             iWidget *button  = findUserData_Widget(findChild_Widget(dlg, "panel.top"), idPanel);
             postCommand_Widget(button, "panel.open");
         }
+        if (deviceType_App() == desktop_AppDeviceType) {
+            /* Detach into a window if it doesn't fit otherwise. */
+            promoteDialogToWindow_Widget(dlg);
+        }
     }
     else if (equal_Command(cmd, "navigate.home")) {
         /* Look for bookmarks tagged "homepage". */
@@ -4309,6 +4398,9 @@ iBool handleCommand_App(const char *cmd) {
     else if (startsWith_CStr(cmd, "feeds.update.")) {
         const iWidget *navBar = findChild_Widget(get_Window()->roots[0]->widget, "navbar");
         iAnyObject *prog = findChild_Widget(navBar, "feeds.progress");
+        if (!navBar || !prog) {
+            return iFalse;
+        }
         if (equal_Command(cmd, "feeds.update.started") ||
             equal_Command(cmd, "feeds.update.progress")) {
             const int num   = arg_Command(cmd);
@@ -4334,7 +4426,7 @@ iBool handleCommand_App(const char *cmd) {
         /* Set of open tabs has changed. */
         postCommand_App("document.openurls.changed");
         if (deviceType_App() == phone_AppDeviceType) {
-            showToolbar_Root(d->window->base.roots[0], iTrue);
+            showToolbar_Root(d->window->roots[0], iTrue);
         }
         return iFalse;
     }
@@ -4578,20 +4670,21 @@ iStringSet *listOpenURLs_App(void) {
 }
 
 iMainWindow *mainWindow_App(void) {
-    return app_.window;
+    iApp *d = &app_;
+    if (d->window && d->window->type == main_WindowType) {
+        return as_MainWindow(d->window);
+    }
+    return NULL;
 }
 
 void closePopups_App(iBool doForce) {
+    iUnused(doForce); /* not needed any more? */
     iApp *d = &app_;
     const uint32_t now = SDL_GetTicks();
     iForEach(PtrArray, i, &d->popupWindows) {
         iWindow *win = i.ptr;
-//        if (doForce) {
-//            collect_Garbage(win, (iDeleteFunc) delete_Window);
-//        }
-//        else
-            if (now - win->focusGainedAt > 200) {
-            postCommand_Root(((const iWindow *) i.ptr)->roots[0], "cancel");
+        if (now - win->focusGainedAt > 200) {
+            postCommand_Root(win->roots[0], "cancel");
         }
     }
 }
diff --git a/src/app.h b/src/app.h
index ce59bb87..0a35a72b 100644
--- a/src/app.h
+++ b/src/app.h
@@ -41,6 +41,8 @@ iDeclareType(Root)
 iDeclareType(Visited)
 iDeclareType(Window)
 
+typedef void iAnyWindow;
+
 /* Command line options strings. */
 #define dump_CommandLineOption              "dump;d"
 #define dumpIdentity_CommandLineOption      "dump-identity;I"
@@ -134,13 +136,17 @@ void        removeTicker_App    (iTickerFunc ticker, iAny *context);
 
 void        addWindow_App       (iMainWindow *win);
 void        removeWindow_App    (iMainWindow *win);
-void        setActiveWindow_App (iMainWindow *win);
-void        closeWindow_App     (iMainWindow *win);
+void        setActiveWindow_App (iAnyWindow *mainOrExtraWin);
+iWindow *   activeWindow_App    (void);
+void        closeWindow_App     (iWindow *win); /* main or extra window */
 size_t      numWindows_App      (void);
 size_t      windowIndex_App     (const iMainWindow *win);
 iMainWindow *newMainWindow_App  (void);
 const iPtrArray *mainWindows_App(void);
 iMainWindow *    mainWindow_App (void); /* currently active main window */
+
+void        addExtraWindow_App  (iWindow *extra);
+void        removeExtraWindow_App(iWindow *extra);
 void        addPopup_App        (iWindow *popup);
 void        removePopup_App     (iWindow *popup);
 void        closePopups_App     (iBool doForce);
diff --git a/src/macos.m b/src/macos.m
index 14b4cfc0..b3676444 100644
--- a/src/macos.m
+++ b/src/macos.m
@@ -930,7 +930,7 @@ void log_MacOS(const char *msg) {
 void showPopupMenu_MacOS(iWidget *source, iInt2 windowCoord, const iMenuItem *items, size_t n) {
     NSMenu *      menu         = [[NSMenu alloc] init];
     MenuCommands *menuCommands = [[MenuCommands alloc] init];
-    iWindow *     window       = as_Window(mainWindow_App());
+    iWindow *     window       = activeWindow_App();
     NSWindow *    nsWindow     = nsWindow_(window->win);
     /* View coordinates are flipped. */
     iBool isCentered = iFalse;
diff --git a/src/ui/color.c b/src/ui/color.c
index 3f392987..8571c7d7 100644
--- a/src/ui/color.c
+++ b/src/ui/color.c
@@ -358,6 +358,13 @@ iColor get_Color(int color) {
     return *rgba;
 }
 
+iColor default_Color(int color) {
+    if (color >= 0 && color < iElemCount(darkPalette_)) {
+        return (isDark_ColorTheme(prefs_App()->theme) ? darkPalette_ : lightPalette_)[color];
+    }
+    return (iColor){ 0, 0, 0, 0 };
+}
+
 iColor getMixed_Color(int color1, int color2, float t) {
     return mix_Color(get_Color(color1), get_Color(color2), t);
 }
diff --git a/src/ui/color.h b/src/ui/color.h
index ea285ca6..14b2d8bb 100644
--- a/src/ui/color.h
+++ b/src/ui/color.h
@@ -254,6 +254,7 @@ void            set_Color       (int color, iColor rgba);
 iColor          mix_Color       (iColor c1, iColor c2, float t);
 iColor          getMixed_Color  (int color1, int color2, float t);
 int             delta_Color     (iColor c1, iColor c2);
+iColor          default_Color   (int color); /* affected by dark/light mode */
 
 iLocalDef iHSLColor get_HSLColor(int color) {
     return hsl_Color(get_Color(color));
diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c
index 6da9a4d5..c6f99bb2 100644
--- a/src/ui/documentwidget.c
+++ b/src/ui/documentwidget.c
@@ -2207,7 +2207,7 @@ static void updateWindowTitle_DocumentWidget_(const iDocumentWidget *d) {
         iString *text = collect_String(joinCStr_StringArray(title, " \u2014 "));
         if (setWindow) {
             /* Longest version for the window title, and omit the icon. */
-            setTitle_MainWindow(get_MainWindow(), text);
+            setTitle_Window(as_Window(get_MainWindow()), text);
             setWindow = iFalse;
         }
         const iChar siteIcon = siteIcon_GmDocument(d->view.doc);
diff --git a/src/ui/mobile.c b/src/ui/mobile.c
index bfcdc8f6..d8b6d7b6 100644
--- a/src/ui/mobile.c
+++ b/src/ui/mobile.c
@@ -1090,6 +1090,10 @@ void setupMenuTransition_Mobile(iWidget *sheet, iBool isIncoming) {
 }
 
 void setupSheetTransition_Mobile(iWidget *sheet, int flags) {
+    if (isPromoted_Widget(sheet)) {
+        /* This has been promoted to a window, shouldn't animate it. */
+        return;
+    }
     const iBool isIncoming = (flags & incoming_TransitionFlag) != 0;
     const int   dir        = flags & dirMask_TransitionFlag;
     if (!isUsingPanelLayout_Mobile()) {
diff --git a/src/ui/root.c b/src/ui/root.c
index 1dbced78..e93adc57 100644
--- a/src/ui/root.c
+++ b/src/ui/root.c
@@ -318,7 +318,7 @@ iPtrArray *onTop_Root(iRoot *d) {
     return d->onTop;
 }
 
-static iBool handleRootCommands_(iWidget *root, const char *cmd) {
+iBool handleRootCommands_Widget(iWidget *root, const char *cmd) {
     iUnused(root);
     if (equal_Command(cmd, "menu.open")) {
         iWidget *button = pointer_Command(cmd);
@@ -1386,7 +1386,7 @@ void createUserInterface_Root(iRoot *d) {
     /* Children of root cover the entire window. */
     setFlags_Widget(
         root, resizeChildren_WidgetFlag | fixedSize_WidgetFlag | focusRoot_WidgetFlag, iTrue);
-    setCommandHandler_Widget(root, handleRootCommands_);
+    setCommandHandler_Widget(root, handleRootCommands_Widget);
     iWidget *div = makeVDiv_Widget();
     setId_Widget(div, "navdiv");
     addChild_Widget(root, iClob(div));
@@ -1937,7 +1937,9 @@ static void setupMovableElements_Root_(iRoot *d) {
     iWidget *navMenu   = findChild_Widget(d->widget, "navbar.menu");
     setFlags_Widget(menuBar, hidden_WidgetFlag, !prefs->menuBar);
     setFlags_Widget(navMenu, hidden_WidgetFlag, prefs->menuBar);
-    iChangeFlags(navBar->flags2, permanentVisualOffset_WidgetFlag2, iFalse);
+    if (navBar) {
+        iChangeFlags(navBar->flags2, permanentVisualOffset_WidgetFlag2, iFalse);
+    }
     if (prefs->bottomNavBar) {
         if (deviceType_App() == phone_AppDeviceType) {
             /* When at the bottom, the navbar is at the top of the bottombar, and gets fully hidden
@@ -1948,7 +1950,7 @@ static void setupMovableElements_Root_(iRoot *d) {
                 iRelease(navBar);
             }
         }
-        else {
+        else if (navBar) {
             /* On desktop/tablet, a bottom navbar is at the bottom of the main layout. */
             removeChild_Widget(navBar->parent, navBar);
             addChildPos_Widget(div, navBar, back_WidgetAddPos);
@@ -1958,7 +1960,7 @@ static void setupMovableElements_Root_(iRoot *d) {
                          deviceType_App() == tablet_AppDeviceType);
         }
     }
-    else {
+    else if (navBar) {
         /* In the top navbar layout, the navbar is always the first (or second) child. */
         removeChild_Widget(navBar->parent, navBar);
         if (winBar) {
@@ -1974,20 +1976,22 @@ static void setupMovableElements_Root_(iRoot *d) {
         }
         iRelease(navBar);
     }
-    iChangeFlags(tabBar->flags2, permanentVisualOffset_WidgetFlag2, prefs->bottomTabBar);
-    /* Tab button frames. */
-    iForEach(ObjectList, i, children_Widget(tabBar)) {
-        if (isInstance_Object(i.object, &Class_LabelWidget)) {
-            setNoTopFrame_LabelWidget(i.object, !prefs->bottomTabBar);
-            setNoBottomFrame_LabelWidget(i.object, prefs->bottomTabBar);
+    if (tabBar) {
+        iChangeFlags(tabBar->flags2, permanentVisualOffset_WidgetFlag2, prefs->bottomTabBar);
+        /* Tab button frames. */
+        iForEach(ObjectList, i, children_Widget(tabBar)) {
+            if (isInstance_Object(i.object, &Class_LabelWidget)) {
+                setNoTopFrame_LabelWidget(i.object, !prefs->bottomTabBar);
+                setNoBottomFrame_LabelWidget(i.object, prefs->bottomTabBar);
+            }
+        }
+        /* Adjust safe area paddings. */
+        if (deviceType_App() == tablet_AppDeviceType && prefs->bottomTabBar && !prefs->bottomNavBar) {
+            tabBar->padding[3] = bottomSafeInset_Mobile();
+        }
+        else {
+            tabBar->padding[3] = 0;
         }
-    }
-    /* Adjust safe area paddings. */
-    if (deviceType_App() == tablet_AppDeviceType && prefs->bottomTabBar && !prefs->bottomNavBar) {
-        tabBar->padding[3] = bottomSafeInset_Mobile();
-    }
-    else {
-        tabBar->padding[3] = 0;
     }
     setTabBarPosition_Widget(docTabs, prefs->bottomTabBar);
     arrange_Widget(d->widget);
@@ -2205,9 +2209,11 @@ iRect visibleRect_Root(const iRoot *d) {
         visRect = intersect_Rect(visRect, init_Rect(usable.x, usable.y, usable.w, usable.h));        
     }
 #endif
-    const int keyboardHeight = get_MainWindow()->keyboardHeight;
-    if (keyboardHeight > bottom) {
-        adjustEdges_Rect(&visRect, 0, 0, -keyboardHeight + bottom, 0);
+    if (get_MainWindow()) {
+        const int keyboardHeight = get_MainWindow()->keyboardHeight;
+        if (keyboardHeight > bottom) {
+            adjustEdges_Rect(&visRect, 0, 0, -keyboardHeight + bottom, 0);
+        }
     }
     return visRect;
 }
diff --git a/src/ui/root.h b/src/ui/root.h
index 1ec18bf2..4d3797a0 100644
--- a/src/ui/root.h
+++ b/src/ui/root.h
@@ -56,3 +56,5 @@ iRect       safeRect_Root                       (const iRoot *);
 iRect       visibleRect_Root                    (const iRoot *); /* may be obstructed by software keyboard */
 iBool       isNarrow_Root                       (const iRoot *);
 int         appIconSize_Root                    (void);
+
+iBool       handleRootCommands_Widget           (iWidget *, const char *cmd);
diff --git a/src/ui/util.c b/src/ui/util.c
index 87a8ec0b..1b637aca 100644
--- a/src/ui/util.c
+++ b/src/ui/util.c
@@ -1383,6 +1383,46 @@ void closeMenu_Widget(iWidget *d) {
     setupMenuTransition_Mobile(d, iFalse);
 }
 
+iWindow *promoteDialogToWindow_Widget(iWidget *dlg) {
+    arrange_Widget(dlg);
+    removeChild_Widget(parent_Widget(dlg), dlg);
+    setVisualOffset_Widget(dlg, 0, 0, 0);
+    setFlags_Widget(dlg, horizontalOffset_WidgetFlag | visualOffset_WidgetFlag |
+                    centerHorizontal_WidgetFlag | overflowScrollable_WidgetFlag, iFalse);
+    /* Check for a dialog heading. */
+    printTree_Widget(dlg);
+    iWidget *child = child_Widget(dlg, 0);
+    iWindow *x = newExtra_Window(dlg);
+    if (isInstance_Object(child, &Class_LabelWidget)) {
+        iLabelWidget *heading = (iLabelWidget *) child;
+        iString *title = copy_String(sourceText_LabelWidget(heading));
+        translate_Lang(title);
+        setTitle_Window(x, title);
+        delete_String(title);
+        iRelease(removeChild_Widget(dlg, heading));
+        arrange_Widget(dlg);
+    }
+    addExtraWindow_App(x);
+    SDL_ShowWindow(x->win);
+    SDL_RaiseWindow(x->win);
+    setActiveWindow_App(x);
+    return x;
+}
+
+iBool isPromoted_Widget(iWidget *dlg) {
+    return type_Window(window_Widget(dlg)) == extra_WindowType;
+}
+
+void destroyDialog_Widget(iWidget *dlg) {
+    if (isPromoted_Widget(dlg)) {
+        /* Promoted dialog. */
+        closeWindow_App(window_Widget(dlg));
+    }
+    else {
+        destroy_Widget(dlg);
+    }
+}
+
 iLabelWidget *findMenuItem_Widget(iWidget *menu, const char *command) {
     iForEach(ObjectList, i, children_Widget(menu)) {
         if (isInstance_Object(i.object, &Class_LabelWidget)) {
@@ -1653,9 +1693,11 @@ static iBool tabSwitcher_(iWidget *tabs, const char *cmd) {
             iWidget *nextTabs = findChild_Widget(otherRoot_Window(get_Window(), tabs->root)->widget,
                                                  "doctabs");
             iWidget *nextPages = findChild_Widget(nextTabs, "tabs.pages");
-            tabIndex = (int) (dir < 0 ? childCount_Widget(nextPages) - 1 : 0);
-            showTabPage_Widget(nextTabs, child_Widget(nextPages, tabIndex));
-            postCommand_App("keyroot.next");
+            if (nextPages) {
+                tabIndex = (int) (dir < 0 ? childCount_Widget(nextPages) - 1 : 0);
+                showTabPage_Widget(nextTabs, child_Widget(nextPages, tabIndex));
+                postCommand_App("keyroot.next");
+            }
         }
         else {
             showTabPage_Widget(tabs, child_Widget(pages, tabIndex + dir));
@@ -1689,10 +1731,12 @@ iWidget *makeTabs_Widget(iWidget *parent) {
 }
 
 void setTabBarPosition_Widget(iWidget *tabs, iBool atBottom) {
-    iWidget *buttons = findChild_Widget(tabs, "tabs.buttons");
-    removeChild_Widget(tabs, buttons);
-    addChildPos_Widget(tabs, buttons, atBottom ? back_WidgetAddPos : front_WidgetAddPos);
-    iRelease(buttons);
+    if (tabs) {
+        iWidget *buttons = findChild_Widget(tabs, "tabs.buttons");
+        removeChild_Widget(tabs, buttons);
+        addChildPos_Widget(tabs, buttons, atBottom ? back_WidgetAddPos : front_WidgetAddPos);
+        iRelease(buttons);
+    }
 }
 
 void setVerticalTabBar_Widget(iWidget *tabs) {
@@ -1891,10 +1935,12 @@ size_t tabPageIndex_Widget(const iWidget *tabs, const iAnyObject *page) {
 }
 
 const iWidget *currentTabPage_Widget(const iWidget *tabs) {
-    iWidget *pages = findChild_Widget(tabs, "tabs.pages");
-    iConstForEach(ObjectList, i, pages->children) {
-        if (isVisible_Widget(i.object)) {
-            return constAs_Widget(i.object);
+    if (tabs) {
+        iWidget *pages = findChild_Widget(tabs, "tabs.pages");
+        iConstForEach(ObjectList, i, pages->children) {
+            if (isVisible_Widget(i.object)) {
+                return constAs_Widget(i.object);
+            }
         }
     }
     return NULL;
diff --git a/src/ui/util.h b/src/ui/util.h
index bb661944..12c91ee0 100644
--- a/src/ui/util.h
+++ b/src/ui/util.h
@@ -35,7 +35,8 @@ iDeclareType(Click)
 iDeclareType(Widget)
 iDeclareType(LabelWidget)
 iDeclareType(InputWidget)
-
+iDeclareType(Window)
+    
 iBool           isCommand_SDLEvent  (const SDL_Event *d);
 iBool           isCommand_UserEvent (const SDL_Event *, const char *cmd);
 const char *    command_UserEvent   (const SDL_Event *);
@@ -395,6 +396,10 @@ iWidget *   makeUserDataImporter_Dialog     (const iString *archivePath);
 const char *    languageId_String   (const iString *menuItemLabel);
 int             languageIndex_CStr  (const char *langId);
 
+iWindow *   promoteDialogToWindow_Widget    (iWidget *);
+iBool       isPromoted_Widget               (iWidget *);
+void        destroyDialog_Widget            (iWidget *);
+
 /*-----------------------------------------------------------------------------------------------*/
 
 iDeclareType(PerfTimer)
diff --git a/src/ui/widget.c b/src/ui/widget.c
index 730832d8..7e3b6099 100644
--- a/src/ui/widget.c
+++ b/src/ui/widget.c
@@ -231,8 +231,13 @@ static void aboutToBeDestroyed_Widget_(iWidget *d) {
     }
 }
 
+iLocalDef iBool isRoot_Widget_(const iWidget *d) {
+    return d && d->root && d->root->widget == d;
+}
+
 void destroy_Widget(iWidget *d) {
     if (d) {
+        iAssert(!isRoot_Widget_(d));
         if (isVisible_Widget(d)) {
             postRefresh_App();
         }
@@ -263,7 +268,7 @@ void setFlags_Widget(iWidget *d, int64_t flags, iBool set) {
             flags &= ~drawKey_WidgetFlag;
         }
         iChangeFlags(d->flags, flags, set);
-        if (flags & keepOnTop_WidgetFlag) {
+        if (flags & keepOnTop_WidgetFlag && !isRoot_Widget_(d)) {
             iPtrArray *onTop = onTop_Root(d->root);
             if (set) {
                 iAssert(indexOf_PtrArray(onTop, d) == iInvalidPos);
@@ -274,11 +279,13 @@ void setFlags_Widget(iWidget *d, int64_t flags, iBool set) {
                 iAssert(indexOf_PtrArray(onTop, d) == iInvalidPos);
             }
         }
+#if !defined (NDEBUG)
         if (d->flags & arrangeWidth_WidgetFlag &&
             d->flags & resizeToParentWidth_WidgetFlag) {
             printf("[Widget] Conflicting flags for ");
             identify_Widget(d);
         }
+#endif
     }
 }
 
@@ -334,6 +341,7 @@ iWindow *window_Widget(const iAnyObject *d) {
 }
 
 void showCollapsed_Widget(iWidget *d, iBool show) {
+    if (!d) return;
     const iBool isVisible = !(d->flags & hidden_WidgetFlag);
     if ((isVisible && !show) || (!isVisible && show)) {
         setFlags_Widget(d, hidden_WidgetFlag, !show);
@@ -377,8 +385,10 @@ void setRoot_Widget(iWidget *d, iRoot *root) {
         iAssert(indexOf_PtrArray(onTop_Root(root), d) == iInvalidPos);
         /* Move it over the new root's onTop list. */
         removeOne_PtrArray(onTop_Root(d->root), d);
-        iAssert(indexOf_PtrArray(onTop_Root(d->root), d) == iInvalidPos);
-        pushBack_PtrArray(onTop_Root(root), d);
+        if (d != root->widget) {
+            iAssert(indexOf_PtrArray(onTop_Root(d->root), d) == iInvalidPos);
+            pushBack_PtrArray(onTop_Root(root), d);
+        }
     }
     d->root = root;
     iForEach(ObjectList, i, d->children) {
@@ -997,6 +1007,15 @@ void arrange_Widget(iWidget *d) {
         clampCenteredInRoot_Widget_(d);
         notifySizeChanged_Widget_(d);
         d->root->didChangeArrangement = iTrue;
+        if (type_Window(window_Widget(d)) == extra_WindowType &&
+            (d == root_Widget(d) || d->parent == root_Widget(d))) {
+            /* Size of extra windows will change depending on the contents. */
+            iWindow *win = window_Widget(d);
+            SDL_SetWindowSize(win->win,
+                              width_Widget(d) / win->pixelRatio,
+                              height_Widget(d) / win->pixelRatio);
+            win->size = d->rect.size;
+        }
     }
 }
 
@@ -1157,19 +1176,26 @@ void unhover_Widget(void) {
     *hover = NULL;
 }
 
+iLocalDef iBool redispatchEvent_Widget_(iWidget *d, iWidget *dst, const SDL_Event *ev) {
+    if (d != dst) {
+        return dispatchEvent_Widget(dst, ev);
+    }
+    return iFalse;
+}
+
 iBool dispatchEvent_Widget(iWidget *d, const SDL_Event *ev) {
     if (!d->parent) {
         if (window_Widget(d)->focus && window_Widget(d)->focus->root == d->root &&
             (isKeyboardEvent_(ev) || ev->type == SDL_USEREVENT)) {
             /* Root dispatches keyboard events directly to the focused widget. */
-            if (dispatchEvent_Widget(window_Widget(d)->focus, ev)) {
+            if (redispatchEvent_Widget_(d, window_Widget(d)->focus, ev)) {
                 return iTrue;
             }
         }
         /* Root offers events first to widgets on top. */
         iReverseForEach(PtrArray, i, d->root->onTop) {
             iWidget *widget = *i.value;
-            if (isVisible_Widget(widget) && dispatchEvent_Widget(widget, ev)) {
+            if (isVisible_Widget(widget) && redispatchEvent_Widget_(d, widget, ev)) {
 #if 0
                 if (ev->type == SDL_KEYDOWN) {
                     printf("[%p] %s:'%s' (on top) ate the key\n",
@@ -1218,6 +1244,7 @@ iBool dispatchEvent_Widget(iWidget *d, const SDL_Event *ev) {
            handle the events first. */
         iReverseForEach(ObjectList, i, d->children) {
             iWidget *child = as_Widget(i.object);
+            iAssert(child != d); /* cannot be child of self */
             iAssert(child->root == d->root);
             if (child == window_Widget(d)->focus &&
                 (isKeyboardEvent_(ev) || ev->type == SDL_USEREVENT)) {
@@ -1697,10 +1724,6 @@ void drawBackground_Widget(const iWidget *d) {
 
 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. */
@@ -2350,7 +2373,7 @@ void refresh_Widget(const iAnyObject *d) {
 
 void raise_Widget(iWidget *d) {
     iPtrArray *onTop = onTop_Root(d->root);
-    if (d->flags & keepOnTop_WidgetFlag) {
+    if (d->flags & keepOnTop_WidgetFlag && !isRoot_Widget_(d)) {
         iAssert(indexOf_PtrArray(onTop, d) != iInvalidPos);
         removeOne_PtrArray(onTop, d);
         pushBack_PtrArray(onTop, d);
diff --git a/src/ui/window.c b/src/ui/window.c
index 108c90b7..a1c33923 100644
--- a/src/ui/window.c
+++ b/src/ui/window.c
@@ -405,8 +405,7 @@ static float displayScale_Window_(const iWindow *d) {
 }
 
 static void drawBlank_Window_(iWindow *d) {
-//    const iColor bg = get_Color(uiBackground_ColorId);
-    const iColor bg = { 128, 128, 128, 255 }; /* TODO: Have no root yet. */
+    const iColor bg = default_Color(uiBackground_ColorId);
     SDL_SetRenderDrawColor(d->render, bg.r, bg.g, bg.b, 255);
     SDL_RenderClear(d->render);
     SDL_RenderPresent(d->render);
@@ -616,6 +615,9 @@ void deinit_Window(iWindow *d) {
     if (d->type == popup_WindowType) {
         removePopup_App(d);
     }
+    else if (d->type == extra_WindowType) {
+        removeExtraWindow_App(d);
+    }
     deinitRoots_Window_(d);
     delete_Text(d->text);
     SDL_DestroyRenderer(d->render);
@@ -785,25 +787,28 @@ iRoot *otherRoot_Window(const iWindow *d, iRoot *root) {
     return root == d->roots[0] && d->roots[1] ? d->roots[1] : d->roots[0];
 }
 
-static void invalidate_MainWindow_(iMainWindow *d, iBool forced) {
-    if (d && (!d->base.isInvalidated || forced)) {
-        d->base.isInvalidated = iTrue;
-        if (d->enableBackBuf && d->backBuf) {
-            SDL_DestroyTexture(d->backBuf);
-            d->backBuf = NULL;
+static void invalidate_Window_(iAnyWindow *d, iBool forced) {
+    iWindow *w = as_Window(d);
+    if (w && (!w->isInvalidated || forced)) {
+        w->isInvalidated = iTrue;
+        if (w->type == main_WindowType) {
+            iMainWindow *mw = as_MainWindow(w);
+            if (mw->enableBackBuf && mw->backBuf) {
+                SDL_DestroyTexture(mw->backBuf);
+                mw->backBuf = NULL;
+            }
         }
-        resetFontCache_Text(text_Window(d));
+        resetFontCache_Text(text_Window(w));
         postCommand_App("theme.changed auto:1"); /* forces UI invalidation */
     }
 }
 
 void invalidate_Window(iAnyWindow *d) {
-    if (type_Window(d) == main_WindowType) {
-        invalidate_MainWindow_(as_MainWindow(d), iFalse);
-    }
-    else {
-        iAssert(type_Window(d) == main_WindowType);
-    }
+    invalidate_Window_(d, iFalse);
+}
+
+static void invalidate_MainWindow_(iMainWindow *d, iBool forced) {
+    invalidate_Window_(as_Window(d), forced);
 }
 
 static iBool isNormalPlacement_MainWindow_(const iMainWindow *d) {
@@ -890,26 +895,72 @@ static iBool handleWindowEvent_Window_(iWindow *d, const SDL_WindowEvent *ev) {
             checkPixelRatioChange_Window_(as_Window(d));
             return iTrue;
 #endif
+        case SDL_WINDOWEVENT_CLOSE:
+            if (d->type == extra_WindowType) {
+                closeWindow_App(d);
+                return iTrue;
+            }
+            return iFalse;
         case SDL_WINDOWEVENT_EXPOSED:
             d->isExposed = iTrue;
-//            checkPixelRatioChange_Window_(as_Window(d));
+            if (d->type == extra_WindowType) {
+                checkPixelRatioChange_Window_(d);
+            }
             postRefresh_App();
             return iTrue;
         case SDL_WINDOWEVENT_RESTORED:
         case SDL_WINDOWEVENT_SHOWN:
             postRefresh_App();
             return iTrue;
+        case SDL_WINDOWEVENT_MOVED:
+            if (d->type == extra_WindowType) {
+                checkPixelRatioChange_Window_(d);
+            }
+            return iFalse;
+        case SDL_WINDOWEVENT_FOCUS_GAINED:
+            if (d->type == extra_WindowType) {
+                d->focusGainedAt = SDL_GetTicks();
+                setCapsLockDown_Keys(iFalse);
+                postCommand_App("window.focus.gained");
+                d->isExposed = iTrue;
+                setActiveWindow_App(d);
+#if !defined (iPlatformDesktop)
+                /* Returned to foreground, may have lost buffered content. */
+                invalidate_Window(d);
+                postCommand_App("window.unfreeze");
+#endif
+            }
+            return iFalse;
+        case SDL_WINDOWEVENT_TAKE_FOCUS:
+            if (d->type == extra_WindowType) {
+                SDL_SetWindowInputFocus(d->win);
+                postRefresh_App();
+                return iTrue;
+            }
+            return iFalse;
         case SDL_WINDOWEVENT_FOCUS_LOST:
-            /* Popup windows are currently only used for menus. */
-            closeMenu_Widget(d->roots[0]->widget);
+            if (d->type == popup_WindowType) {
+                /* Popup windows are currently only used for menus. */
+                closeMenu_Widget(d->roots[0]->widget);
+            }
+            else {
+                postCommand_App("window.focus.lost");
+                closePopups_App(iTrue);
+            }
             return iTrue;
         case SDL_WINDOWEVENT_LEAVE:
             unhover_Widget();
             d->isMouseInside = iFalse;
+            if (d->type == extra_WindowType) {
+                postCommand_App("window.mouse.exited");
+            }
             postRefresh_App();
             return iTrue;
         case SDL_WINDOWEVENT_ENTER:
             d->isMouseInside = iTrue;
+            if (d->type == extra_WindowType) {
+                postCommand_App("window.mouse.entered");
+            }
             return iTrue;
     }
     return iFalse;
@@ -1078,7 +1129,7 @@ static iBool handleWindowEvent_MainWindow_(iMainWindow *d, const SDL_WindowEvent
             return iTrue;
         case SDL_WINDOWEVENT_CLOSE:
 #if defined (iPlatformAppleDesktop)
-            closeWindow_App(d);
+            closeWindow_App(as_Window(d));
 #else
             if (numWindows_App() == 1) {
                 postCommand_App("quit");
@@ -1106,7 +1157,8 @@ void updateHover_Window(iWindow *d) {
 }
 
 iBool processEvent_Window(iWindow *d, const SDL_Event *ev) {
-    iMainWindow *mw = (type_Window(d) == main_WindowType ? as_MainWindow(d) : NULL);
+    iMainWindow *mw     = (type_Window(d) == main_WindowType ? as_MainWindow(d) : NULL);
+    iWindow *    extraw = (type_Window(d) == extra_WindowType ? d : NULL);
     switch (ev->type) {
 #if defined (LAGRANGE_ENABLE_CUSTOM_FRAME)
         case SDL_SYSWMEVENT: {
@@ -1128,8 +1180,8 @@ iBool processEvent_Window(iWindow *d, const SDL_Event *ev) {
         }
         case SDL_RENDER_TARGETS_RESET:
         case SDL_RENDER_DEVICE_RESET: {            
-            if (mw) {
-                invalidate_MainWindow_(mw, iTrue /* force full reset */);
+            if (mw || extraw) {
+                invalidate_Window_(d, iTrue /* force full reset */);
             }
             break;
         }
@@ -1252,7 +1304,7 @@ iBool processEvent_Window(iWindow *d, const SDL_Event *ev) {
                     updateMetrics_Root(d->roots[i]);
                 }
             }
-            if (isCommand_UserEvent(&event, "lang.changed") && mw) {
+            if (isCommand_UserEvent(&event, "lang.changed") && (mw || extraw)) {
 #if defined (LAGRANGE_MAC_MENUBAR)
                 /* Retranslate the menus. */
                 removeMacMenus_();
@@ -1423,8 +1475,10 @@ void draw_Window(iWindow *d) {
         drawCount_ = 0;
 #endif
     }
-    drawRectThickness_Paint(&p, (iRect){ zero_I2(), sub_I2(d->size, one_I2()) }, gap_UI / 4,
-                            root->widget->frameColor);
+    if (type_Window(d) == popup_WindowType) {
+        drawRectThickness_Paint(&p, (iRect){ zero_I2(), sub_I2(d->size, one_I2()) }, gap_UI / 4,
+                                root->widget->frameColor);
+    }
     setCurrent_Root(NULL);
     SDL_RenderPresent(d->render);
     isDrawing_ = iFalse;
@@ -1518,6 +1572,10 @@ void draw_MainWindow(iMainWindow *d) {
         iRoot *root = d->base.roots[i];
         if (root) {
             /* Some widgets may need a just-in-time visual update. */
+            if (root->didChangeArrangement && root->window->type == extra_WindowType) {
+                iWindow *x = root->window;
+                SDL_SetWindowSize(x->win, x->size.x / x->pixelRatio, x->size.y / x->pixelRatio);
+            }
             notifyVisualOffsetChange_Root(root);
             root->didChangeArrangement = iFalse;
         }
@@ -1605,8 +1663,8 @@ void resize_MainWindow(iMainWindow *d, int w, int h) {
     }
 }
 
-void setTitle_MainWindow(iMainWindow *d, const iString *title) {
-    SDL_SetWindowTitle(d->base.win, cstr_String(title));
+void setTitle_Window(iWindow *d, const iString *title) {
+    SDL_SetWindowTitle(d->win, cstr_String(title));
     iLabelWidget *bar = findChild_Widget(get_Root()->widget, "winbar.title");
     if (bar) {
         updateText_LabelWidget(bar, title);
@@ -2034,22 +2092,46 @@ iWindow *newPopup_Window(iInt2 screenPos, iWidget *rootWidget) {
 #if defined (iPlatformAppleDesktop)
     hideTitleBar_MacOS(win); /* make it a borderless window, but retain shadow */
 #endif
-    /* At least on macOS, with an external display on the left (negative coordinates), the 
+    /* At least on macOS, with an external display on the left (negative coordinates), the
        window will not be correct placed during creation. Ensure it ends up on the right display. */
     SDL_SetWindowPosition(win->win, winRect.pos.x, winRect.pos.y);
     SDL_SetWindowSize(win->win, winRect.size.x, winRect.size.y);
-    win->pixelRatio = pixelRatio;
-    iRoot *root   = new_Root();
-    win->roots[0] = root;
-    win->keyRoot  = root;
-    root->widget  = rootWidget;
-    root->window  = win;
+    win->pixelRatio      = pixelRatio;
+    iRoot *root          = new_Root();
+    win->roots[0]        = root;
+    win->keyRoot         = root;
+    root->widget         = rootWidget;
+    root->window         = win;
     rootWidget->rect.pos = zero_I2();
     setRoot_Widget(rootWidget, root);
     setDrawBufferEnabled_Widget(rootWidget, iFalse);
     setForceSoftwareRender_App(oldSw);
-#if !defined (NDEBUG)
+#if !defined(NDEBUG)
     stop_PerfTimer(newPopup_Window);
 #endif
     return win;
 }
+
+iWindow *newExtra_Window(iWidget *rootWidget) {
+    const float pixelRatio = get_Window()->pixelRatio;
+    iRect       winRect    = (iRect){ init1_I2(-1), divf_I2(rootWidget->rect.size, pixelRatio) };
+    iWindow    *win        = new_Window(extra_WindowType, winRect, 0);
+    win->pixelRatio        = pixelRatio;
+    iRoot *root            = new_Root();
+    win->roots[0]          = root;
+    win->keyRoot           = root;
+    /* Make a simple root widget that sizes itself according to the actual root. */
+    iWidget *frameRoot = new_Widget();
+    setFlags_Widget(frameRoot, arrangeSize_WidgetFlag | focusRoot_WidgetFlag, iTrue);
+    setCommandHandler_Widget(frameRoot, handleRootCommands_Widget);
+    addChild_Widget(frameRoot, rootWidget);
+    iRelease(rootWidget);
+    arrange_Widget(frameRoot);
+    root->widget         = frameRoot;
+    root->window         = win;
+    rootWidget->rect.pos = zero_I2();
+    setRoot_Widget(frameRoot, root);
+    setDrawBufferEnabled_Widget(frameRoot, iFalse);
+    setDrawBufferEnabled_Widget(rootWidget, iFalse);
+    return win;     
+}
diff --git a/src/ui/window.h b/src/ui/window.h
index d257cfd6..2c93e45b 100644
--- a/src/ui/window.h
+++ b/src/ui/window.h
@@ -34,6 +34,7 @@ extern const iMenuItem topLevelMenus_Window[6];
 
 enum iWindowType {
     main_WindowType,
+    extra_WindowType,
     popup_WindowType,
 };
 
@@ -147,6 +148,7 @@ int             numRoots_Window         (const iWindow *);
 //iRoot *         findRoot_Window         (const iWindow *, const iWidget *widget);
 iRoot *         otherRoot_Window        (const iWindow *, iRoot *root);
 
+void        setTitle_Window         (iWindow *, const iString *title);
 iBool       processEvent_Window     (iWindow *, const SDL_Event *);
 iBool       dispatchEvent_Window    (iWindow *, const SDL_Event *);
 void        invalidate_Window       (iAnyWindow *); /* discard all cached graphics */
@@ -167,13 +169,22 @@ iLocalDef iBool isExposed_Window(const iWindow *d) {
     return d->isExposed;
 }
 
+iLocalDef iBool isDrawFrozen_Window(const iWindow *d) {
+    if (d && d->type == main_WindowType) {
+        return ((const iMainWindow *) d)->isDrawFrozen;
+    }
+    return iFalse;
+}
+
 iLocalDef iWindow *as_Window(iAnyWindow *d) {
-    iAssert(type_Window(d) == main_WindowType || type_Window(d) == popup_WindowType);
+    iAssert(type_Window(d) == main_WindowType || type_Window(d) == extra_WindowType ||
+            type_Window(d) == popup_WindowType);
     return (iWindow *) d;
 }
 
 iLocalDef const iWindow *constAs_Window(const iAnyWindow *d) {
-    iAssert(type_Window(d) == main_WindowType || type_Window(d) == popup_WindowType);
+    iAssert(type_Window(d) == main_WindowType || type_Window(d) == extra_WindowType ||
+            type_Window(d) == popup_WindowType);
     return (const iWindow *) d;
 }
 
@@ -188,7 +199,6 @@ iLocalDef iWindow *asWindow_MainWindow(iMainWindow *d) {
     return &d->base;
 }
 
-void        setTitle_MainWindow             (iMainWindow *, const iString *title);
 void        setSnap_MainWindow              (iMainWindow *, int snapMode);
 void        setFreezeDraw_MainWindow        (iMainWindow *, iBool freezeDraw);
 void        setKeyboardHeight_MainWindow    (iMainWindow *, int height);
@@ -224,4 +234,5 @@ iLocalDef const iMainWindow *constAs_MainWindow(const iAnyWindow *d) {
 
 /*----------------------------------------------------------------------------------------------*/
 
-iWindow *   newPopup_Window    (iInt2 screenPos, iWidget *rootWidget);
+iWindow *   newPopup_Window     (iInt2 screenPos, iWidget *rootWidget);
+iWindow *   newExtra_Window     (iWidget *rootWidget);
Proxy Information
Original URL
gemini://git.skyjake.fi/lagrange/work%2Fv1.15/cdiff/2d6ffdd49f83a017b499d515b2973cecf5d656dd
Status Code
Success (20)
Meta
text/gemini; charset=utf-8
Capsule Response Time
91.48659 milliseconds
Gemini-to-HTML Time
1.987082 milliseconds

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