Lagrange [work/v1.7]

Experimenting with independent popup windows

=> 2d81addf78d6a8b0fb2f2959b04a385c4adffdf2

diff --git a/src/app.c b/src/app.c
index 91b3a06d..c1c1da27 100644
--- a/src/app.c
+++ b/src/app.c
@@ -118,6 +118,7 @@ struct Impl_App {
     iVisited *   visited;
     iBookmarks * bookmarks;
     iMainWindow *window;
+    iPtrArray    popupWindows;
     iSortedArray tickers; /* per-frame callbacks, used for animations */
     uint32_t     lastTickerTime;
     uint32_t     elapsedSinceLastTicker;
@@ -801,6 +802,7 @@ static void init_App_(iApp *d, int argc, char **argv) {
             d->initialWindowRect.size.y = toInt_String(value_CommandLineArg(arg, 0));
         }
     }
+    init_PtrArray(&d->popupWindows);
     d->window = new_MainWindow(d->initialWindowRect);
     load_Visited(d->visited, dataDir_App_());
     load_Bookmarks(d->bookmarks, dataDir_App_());
@@ -853,6 +855,11 @@ static void init_App_(iApp *d, int argc, char **argv) {
 }
 
 static void deinit_App(iApp *d) {
+    iReverseForEach(PtrArray, i, &d->popupWindows) {
+        delete_Window(i.ptr);
+    }
+    iAssert(isEmpty_PtrArray(&d->popupWindows));
+    deinit_PtrArray(&d->popupWindows);
 #if defined (LAGRANGE_ENABLE_IDLE_SLEEP)
     SDL_RemoveTimer(d->sleepTimer);
 #endif
@@ -1086,6 +1093,15 @@ static iBool nextEvent_App_(iApp *d, enum iAppEventMode eventMode, SDL_Event *ev
     return SDL_PollEvent(event);
 }
 
+static const iPtrArray *listWindows_App_(const iApp *d) {
+    iPtrArray *list = collectNew_PtrArray();
+    iReverseConstForEach(PtrArray, i, &d->popupWindows) {
+        pushBack_PtrArray(list, i.ptr);
+    }
+    pushBack_PtrArray(list, d->window);
+    return list;
+}
+
 void processEvents_App(enum iAppEventMode eventMode) {
     iApp *d = &app_;
     iRoot *oldCurrentRoot = current_Root(); /* restored afterwards */
@@ -1125,17 +1141,17 @@ void processEvents_App(enum iAppEventMode eventMode) {
 #if defined (iPlatformAppleMobile)
                 updateNowPlayingInfo_iOS();
 #endif
-                setFreezeDraw_Window(as_Window(d), iTrue);
+                setFreezeDraw_MainWindow(d->window, iTrue);
                 savePrefs_App_(d);
                 saveState_App_(d);
                 break;
             case SDL_APP_TERMINATING:
-                setFreezeDraw_Window(as_Window(d), iTrue);
+                setFreezeDraw_MainWindow(d->window, iTrue);
                 savePrefs_App_(d);
                 saveState_App_(d);
                 break;
             case SDL_DROPFILE: {
-                iBool wasUsed = processEvent_MainWindow(d->window, &ev);
+                iBool wasUsed = processEvent_Window(as_Window(d->window), &ev);
                 if (!wasUsed) {
                     iBool newTab = iFalse;
                     if (elapsedSeconds_Time(&d->lastDropTime) < 0.1) {
@@ -1175,23 +1191,6 @@ void processEvents_App(enum iAppEventMode eventMode) {
                 }
                 d->isIdling = iFalse;
 #endif
-                if (ev.type == SDL_USEREVENT && ev.user.code == arrange_UserEventCode) {
-                    printf("[App] rearrange\n");
-                    resize_MainWindow(d->window, -1, -1);
-                    iForIndices(i, d->window->base.roots) {
-                        if (d->window->base.roots[i]) {
-                            d->window->base.roots[i]->pendingArrange = iFalse;
-                        }
-                    }
-//                    if (ev.user.data2 == d->window->roots[0]) {
-//                        arrange_Widget(d->window->roots[0]->widget);
-//                    }
-//                    else if (d->window->roots[1]) {
-//                        arrange_Widget(d->window->roots[1]->widget);
-//                    }
-//                    postRefresh_App();
-                    continue;
-                }
                 gotEvents = iTrue;
                 /* Keyboard modifier mapping. */
                 if (ev.type == SDL_KEYDOWN || ev.type == SDL_KEYUP) {
@@ -1268,10 +1267,22 @@ void processEvents_App(enum iAppEventMode eventMode) {
                     }
                 }
 #endif
-                d->window->base.lastHover = d->window->base.hover;
-                iBool wasUsed = processEvent_MainWindow(d->window, &ev);
+                /* Per-window processing. */
+                iBool wasUsed = iFalse;
+                const iPtrArray *windows = listWindows_App_(d);
+                iConstForEach(PtrArray, iter, windows) {
+                    iWindow *window = iter.ptr;
+                    setCurrent_Window(window);
+                    window->lastHover = window->hover;
+                    wasUsed = processEvent_Window(window, &ev);
+                    if (ev.type == SDL_MOUSEMOTION) {
+                        break;
+                    }
+                    if (wasUsed) break;
+                }
+                setCurrent_Window(d->window);
                 if (!wasUsed) {
-                    /* There may be a key bindings for this. */
+                    /* There may be a key binding for this. */
                     wasUsed = processEvent_Keys(&ev);
                 }
                 if (!wasUsed) {
@@ -1289,24 +1300,32 @@ void processEvents_App(enum iAppEventMode eventMode) {
                     handleCommand_MacOS(command_UserEvent(&ev));
 #endif
                     if (isMetricsChange_UserEvent(&ev)) {
-                        iForIndices(i, d->window->base.roots) {
-                            iRoot *root = d->window->base.roots[i];
-                            if (root) {
-                                arrange_Widget(root->widget);
-                            }
+                        iConstForEach(PtrArray, iter, windows) {
+                            iWindow *window = iter.ptr;
+                            iForIndices(i, window->roots) {
+                                iRoot *root = window->roots[i];
+                                if (root) {
+                                    arrange_Widget(root->widget);
+                                }
+                            }                            
                         }
                     }
                     if (!wasUsed) {
                         /* No widget handled the command, so we'll do it. */
+                        setCurrent_Window(d->window);
                         handleCommand_App(ev.user.data1);
                     }
                     /* Allocated by postCommand_Apps(). */
                     free(ev.user.data1);
                 }
-                /* Update when hover has changed. */
-                if (d->window->base.lastHover != d->window->base.hover) {
-                    refresh_Widget(d->window->base.lastHover);
-                    refresh_Widget(d->window->base.hover);
+                /* Refresh after hover changes. */ {
+                    iConstForEach(PtrArray, iter, windows) {
+                        iWindow *window = iter.ptr;
+                        if (window->lastHover != window->hover) {
+                            refresh_Widget(window->lastHover);
+                            refresh_Widget(window->hover);
+                        }
+                    }
                 }
                 break;
             }
@@ -1394,25 +1413,46 @@ static int run_App_(iApp *d) {
 
 void refresh_App(void) {
     iApp *d = &app_;
-    iForIndices(i, d->window->base.roots) {
-        iRoot *root = d->window->base.roots[i];
-        if (root) {
-            destroyPending_Root(root);
-        }
-    }
 #if defined (LAGRANGE_ENABLE_IDLE_SLEEP)
     if (d->warmupFrames == 0 && d->isIdling) {
         return;
     }
 #endif
+    const iPtrArray *windows = listWindows_App_(d);
+    /* Destroy pending widgets. */ {
+        iConstForEach(PtrArray, j, windows) { 
+            iWindow *win = j.ptr;
+            setCurrent_Window(win);
+            iForIndices(i, win->roots) {
+                iRoot *root = win->roots[i];
+                if (root) {
+                    destroyPending_Root(root);
+                }
+            }
+        }
+    }
+    /* TODO: Pending refresh is window-specific. */
     if (!exchange_Atomic(&d->pendingRefresh, iFalse)) {
         return;
     }
-//    iTime draw;
-//    initCurrent_Time(&draw);
-    draw_MainWindow(d->window);
-//    printf("draw: %lld \u03bcs\n", (long long) (elapsedSeconds_Time(&draw) * 1000000));
-//    fflush(stdout);
+    /* Draw each window. */ {
+        iConstForEach(PtrArray, j, windows) {
+            iWindow *win = j.ptr;
+            setCurrent_Window(win);
+            switch (win->type) {
+                case main_WindowType:
+    //                iTime draw;
+    //                initCurrent_Time(&draw);
+                    draw_MainWindow(as_MainWindow(win));
+    //                printf("draw: %lld \u03bcs\n", (long long) (elapsedSeconds_Time(&draw) * 1000000));
+    //                fflush(stdout);
+                    break;
+                default:
+                    draw_Window(win);
+                    break;        
+            }
+        }
+    }
     if (d->warmupFrames > 0) {
         d->warmupFrames--;
     }
@@ -1485,12 +1525,6 @@ void postRefresh_App(void) {
     }
 }
 
-void postImmediateRefresh_App(void) {
-    SDL_Event ev = { .type = SDL_USEREVENT };
-    ev.user.code = immediateRefresh_UserEventCode;
-    SDL_PushEvent(&ev);
-}
-
 void postCommand_Root(iRoot *d, const char *command) {
     iAssert(command);
     if (strlen(command) == 0) {
@@ -1546,7 +1580,7 @@ void postCommandf_App(const char *command, ...) {
 }
 
 void rootOrder_App(iRoot *roots[2]) {
-    const iWindow *win = as_Window(app_.window);
+    const iWindow *win = get_Window();
     roots[0] = win->keyRoot;
     roots[1] = (roots[0] == win->roots[0] ? win->roots[1] : win->roots[0]);
 }
@@ -1583,6 +1617,16 @@ void removeTicker_App(iTickerFunc ticker, iAny *context) {
     remove_SortedArray(&d->tickers, &(iTicker){ context, NULL, ticker });
 }
 
+void addPopup_App(iWindow *popup) {
+    iApp *d = &app_;
+    pushBack_PtrArray(&d->popupWindows, popup);
+}
+
+void removePopup_App(iWindow *popup) {
+    iApp *d = &app_;
+    removeOne_PtrArray(&d->popupWindows, popup);
+}
+
 iMimeHooks *mimeHooks_App(void) {
     return app_.mimehooks;
 }
@@ -1836,8 +1880,10 @@ iDocumentWidget *newTab_App(const iDocumentWidget *duplicateOf, iBool switchToNe
 static iBool handleIdentityCreationCommands_(iWidget *dlg, const char *cmd) {
     iApp *d = &app_;
     if (equal_Command(cmd, "ident.showmore")) {
-        iForEach(ObjectList, i,
-                 children_Widget(findChild_Widget(dlg,                                                                 isUsingPanelLayout_Mobile() ? "panel.top" : "headings"))) {
+        iForEach(ObjectList,
+                 i,
+                 children_Widget(findChild_Widget(
+                     dlg, isUsingPanelLayout_Mobile() ? "panel.top" : "headings"))) {
             if (flags_Widget(i.object) & collapse_WidgetFlag) {
                 setFlags_Widget(i.object, hidden_WidgetFlag, iFalse);
             }
@@ -1978,9 +2024,15 @@ const iString *searchQueryUrl_App(const iString *queryStringUnescaped) {
     return collectNewFormat_String("%s?%s", cstr_String(&d->prefs.searchUrl), cstr_String(escaped));
 }
 
+static void resetFonts_App_(iApp *d) {
+    iConstForEach(PtrArray, win, listWindows_App_(d)) {
+        resetFonts_Text(text_Window(win.ptr));
+    }    
+}
+
 iBool handleCommand_App(const char *cmd) {
     iApp *d = &app_;
-    const iBool isFrozen = !d->window || d->window->base.isDrawFrozen;
+    const iBool isFrozen = !d->window || d->window->isDrawFrozen;
     if (equal_Command(cmd, "config.error")) {
         makeSimpleMessage_Widget(uiTextCaution_ColorEscape "CONFIG ERROR",
                                  format_CStr("Error in config file: %s\n"
@@ -2047,18 +2099,18 @@ iBool handleCommand_App(const char *cmd) {
         return iTrue;
     }
     else if (equal_Command(cmd, "font.reset")) {
-        resetFonts_Text();
+        resetFonts_App_(d);
         return iTrue;
     }
     else if (equal_Command(cmd, "font.user")) {
         const char *path = suffixPtr_Command(cmd, "path");
         if (cmp_String(&d->prefs.symbolFontPath, path)) {
             if (!isFrozen) {
-                setFreezeDraw_Window(get_Window(), iTrue);
+                setFreezeDraw_MainWindow(get_MainWindow(), iTrue);
             }
             setCStr_String(&d->prefs.symbolFontPath, path);
             loadUserFonts_Text();
-            resetFonts_Text();
+            resetFonts_App_(d);
             if (!isFrozen) {
                 postCommand_App("font.changed");
                 postCommand_App("window.unfreeze");
@@ -2068,10 +2120,10 @@ iBool handleCommand_App(const char *cmd) {
     }
     else if (equal_Command(cmd, "font.set")) {
         if (!isFrozen) {
-            setFreezeDraw_Window(get_Window(), iTrue);
+            setFreezeDraw_MainWindow(get_MainWindow(), iTrue);
         }
         d->prefs.font = arg_Command(cmd);
-        setContentFont_Text(d->prefs.font);
+        setContentFont_Text(text_Window(d->window), d->prefs.font);
         if (!isFrozen) {
             postCommand_App("font.changed");
             postCommand_App("window.unfreeze");
@@ -2080,10 +2132,10 @@ iBool handleCommand_App(const char *cmd) {
     }
     else if (equal_Command(cmd, "headingfont.set")) {
         if (!isFrozen) {
-            setFreezeDraw_Window(get_Window(), iTrue);
+            setFreezeDraw_MainWindow(get_MainWindow(), iTrue);
         }
         d->prefs.headingFont = arg_Command(cmd);
-        setHeadingFont_Text(d->prefs.headingFont);
+        setHeadingFont_Text(text_Window(d->window), d->prefs.headingFont);
         if (!isFrozen) {
             postCommand_App("font.changed");
             postCommand_App("window.unfreeze");
@@ -2092,10 +2144,10 @@ iBool handleCommand_App(const char *cmd) {
     }
     else if (equal_Command(cmd, "zoom.set")) {
         if (!isFrozen) {
-            setFreezeDraw_Window(get_Window(), iTrue); /* no intermediate draws before docs updated */
+            setFreezeDraw_MainWindow(get_MainWindow(), iTrue); /* no intermediate draws before docs updated */
         }
         d->prefs.zoomPercent = arg_Command(cmd);
-        setContentFontSize_Text((float) d->prefs.zoomPercent / 100.0f);
+        setContentFontSize_Text(text_Window(d->window), (float) d->prefs.zoomPercent / 100.0f);
         if (!isFrozen) {
             postCommand_App("font.changed");
             postCommand_App("window.unfreeze");
@@ -2104,14 +2156,14 @@ iBool handleCommand_App(const char *cmd) {
     }
     else if (equal_Command(cmd, "zoom.delta")) {
         if (!isFrozen) {
-            setFreezeDraw_Window(get_Window(), iTrue); /* no intermediate draws before docs updated */
+            setFreezeDraw_MainWindow(get_MainWindow(), iTrue); /* no intermediate draws before docs updated */
         }
         int delta = arg_Command(cmd);
         if (d->prefs.zoomPercent < 100 || (delta < 0 && d->prefs.zoomPercent == 100)) {
             delta /= 2;
         }
         d->prefs.zoomPercent = iClamp(d->prefs.zoomPercent + delta, 50, 200);
-        setContentFontSize_Text((float) d->prefs.zoomPercent / 100.0f);
+        setContentFontSize_Text(text_Window(d->window), (float) d->prefs.zoomPercent / 100.0f);
         if (!isFrozen) {
             postCommand_App("font.changed");
             postCommand_App("window.unfreeze");
@@ -2211,7 +2263,7 @@ iBool handleCommand_App(const char *cmd) {
              equal_Command(cmd, "prefs.mono.gopher.changed")) {
         const iBool isSet = (arg_Command(cmd) != 0);
         if (!isFrozen) {
-            setFreezeDraw_Window(as_Window(d->window), iTrue);
+            setFreezeDraw_MainWindow(get_MainWindow(), iTrue);
         }
         if (startsWith_CStr(cmd, "prefs.mono.gemini")) {
             d->prefs.monospaceGemini = isSet;
@@ -2936,3 +2988,7 @@ iStringSet *listOpenURLs_App(void) {
     iRelease(docs);
     return set;
 }
+
+iMainWindow *mainWindow_App(void) {
+    return app_.window;
+}
diff --git a/src/app.h b/src/app.h
index 08589000..8966e8c7 100644
--- a/src/app.h
+++ b/src/app.h
@@ -22,8 +22,6 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
 
 #pragma once
 
-/* Application core: event loop, base event processing, audio synth. */
-
 #include 
 #include 
 #include 
@@ -35,6 +33,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
 iDeclareType(Bookmarks)
 iDeclareType(DocumentWidget)
 iDeclareType(GmCerts)
+iDeclareType(MainWindow)
 iDeclareType(MimeHooks)
 iDeclareType(Periodic)
 iDeclareType(Root)
@@ -61,14 +60,12 @@ enum iAppEventMode {
 enum iUserEventCode {
     command_UserEventCode = 1,
     refresh_UserEventCode,
-    arrange_UserEventCode,
     asleep_UserEventCode,
     /* The start of a potential touch tap event is notified via a custom event because
        sending SDL_MOUSEBUTTONDOWN would be premature: we don't know how long the tap will
        take, it could turn into a tap-and-hold for example. */
     widgetTapBegins_UserEventCode,
     widgetTouchEnds_UserEventCode, /* finger lifted, but momentum may continue */
-    immediateRefresh_UserEventCode, /* refresh even though more events are pending */
 };
 
 const iString *execPath_App     (void);
@@ -119,8 +116,9 @@ iAny *      findWidget_App      (const char *id);
 void        addTicker_App       (iTickerFunc ticker, iAny *context);
 void        addTickerRoot_App   (iTickerFunc ticker, iRoot *root, iAny *context);
 void        removeTicker_App    (iTickerFunc ticker, iAny *context);
+void        addPopup_App        (iWindow *popup);
+void        removePopup_App     (iWindow *popup);
 void        postRefresh_App     (void);
-void        postImmediateRefresh_App(void);
 void        postCommand_Root    (iRoot *, const char *command);
 void        postCommandf_Root   (iRoot *, const char *command, ...);
 void        postCommandf_App    (const char *command, ...);
@@ -138,3 +136,5 @@ iDocumentWidget *   document_Command    (const char *cmd);
 
 void        openInDefaultBrowser_App    (const iString *url);
 void        revealPath_App              (const iString *path);
+
+iMainWindow *mainWindow_App(void);
diff --git a/src/ios.m b/src/ios.m
index 3fb0af48..b46fb8dc 100644
--- a/src/ios.m
+++ b/src/ios.m
@@ -247,14 +247,14 @@ didPickDocumentsAtURLs:(NSArray *)urls {
     UIView *view         = [viewController_(get_Window()) view];
     CGRect keyboardFrame = [view convertRect:rawFrame fromView:nil];
 //    NSLog(@"keyboardFrame: %@", NSStringFromCGRect(keyboardFrame));
-    iWindow *window = get_Window();
-    const iInt2 rootSize = size_Root(window->roots[0]);
-    const int keyTop = keyboardFrame.origin.y * window->pixelRatio;
-    setKeyboardHeight_Window(window, rootSize.y - keyTop);
+    iMainWindow *window = get_MainWindow();
+    const iInt2 rootSize = size_Root(window->base.roots[0]);
+    const int keyTop = keyboardFrame.origin.y * window->base.pixelRatio;
+    setKeyboardHeight_MainWindow(window, rootSize.y - keyTop);
 }
 
 -(void)keyboardOffScreen:(NSNotification *)notification {
-    setKeyboardHeight_Window(get_Window(), 0);
+    setKeyboardHeight_MainWindow(get_MainWindow(), 0);
 }
 @end
 
diff --git a/src/macos.h b/src/macos.h
index 0d3f097a..20b95943 100644
--- a/src/macos.h
+++ b/src/macos.h
@@ -24,6 +24,8 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
 
 #include "ui/util.h"
 
+iDeclareType(Window)
+
 /* Platform-specific functionality for macOS */
 
 iBool   shouldDefaultToMetalRenderer_MacOS  (void);
@@ -31,6 +33,7 @@ iBool   shouldDefaultToMetalRenderer_MacOS  (void);
 void    enableMomentumScroll_MacOS  (void);
 void    registerURLHandler_MacOS    (void);
 void    setupApplication_MacOS      (void);
+void    hideTitleBar_MacOS          (iWindow *window);
 void    insertMenuItems_MacOS       (const char *menuLabel, int atIndex, const iMenuItem *items, size_t count);
 void    removeMenu_MacOS            (int atIndex);
 void    enableMenu_MacOS            (const char *menuLabel, iBool enable);
diff --git a/src/macos.m b/src/macos.m
index d588fa4a..298db0f8 100644
--- a/src/macos.m
+++ b/src/macos.m
@@ -30,6 +30,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
 #include "ui/window.h"
 
 #include 
+#include 
 
 #import 
 
@@ -51,6 +52,16 @@ static iInt2 macVer_(void) {
     return init_I2(10, 10);
 }
 
+static NSWindow *nsWindow_(SDL_Window *window) {
+    SDL_SysWMinfo wm;
+    SDL_VERSION(&wm.version);
+    if (SDL_GetWindowWMInfo(window, &wm)) {
+        return wm.info.cocoa.window;
+    }
+    iAssert(false);
+    return nil;
+}
+
 static NSString *currentSystemAppearance_(void) {
     /* This API does not exist on 10.13. */
     if ([NSApp respondsToSelector:@selector(effectiveAppearance)]) {
@@ -370,6 +381,11 @@ void setupApplication_MacOS(void) {
     windowCloseItem.action = @selector(closeTab);
 }
 
+void hideTitleBar_MacOS(iWindow *window) {
+    NSWindow *w = nsWindow_(window->win);
+    w.styleMask = 0; /* borderless */
+}
+
 void enableMenu_MacOS(const char *menuLabel, iBool enable) {
     menuLabel = translateCStr_Lang(menuLabel);
     NSApplication *app = [NSApplication sharedApplication];
@@ -377,7 +393,6 @@ void enableMenu_MacOS(const char *menuLabel, iBool enable) {
     NSString *label = [NSString stringWithUTF8String:menuLabel];
     NSMenuItem *menuItem = [appMenu itemAtIndex:[appMenu indexOfItemWithTitle:label]];
     [menuItem setEnabled:enable];
-    [label release];
 }
 
 void enableMenuItem_MacOS(const char *menuItemCommand, iBool enable) {
diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c
index ed9e41d6..6f9824de 100644
--- a/src/ui/documentwidget.c
+++ b/src/ui/documentwidget.c
@@ -750,7 +750,7 @@ static uint32_t mediaUpdateInterval_DocumentWidget_(const iDocumentWidget *d) {
     if (document_App() != d) {
         return 0;
     }
-    if (get_Window()->isDrawFrozen) {
+    if (as_MainWindow(window_Widget(d))->isDrawFrozen) {
         return 0;
     }
     static const uint32_t invalidInterval_ = ~0u;
diff --git a/src/ui/inputwidget.c b/src/ui/inputwidget.c
index a561d5bd..f02bf408 100644
--- a/src/ui/inputwidget.c
+++ b/src/ui/inputwidget.c
@@ -2352,7 +2352,7 @@ static void draw_InputWidget_(const iInputWidget *d) {
     }
     /* Draw the insertion point. */
     if (isFocused && d->cursorVis && contains_Range(&visLines, d->cursor.y) &&
-        isEmpty_Range(&d->mark)) {
+        (deviceType_App() == desktop_AppDeviceType || isEmpty_Range(&d->mark))) {
         iInt2    curSize;
         iRangecc cursorChar    = iNullRange;
         int      visWrapsAbove = 0;
diff --git a/src/ui/root.c b/src/ui/root.c
index 52a08eca..9e290b05 100644
--- a/src/ui/root.c
+++ b/src/ui/root.c
@@ -298,16 +298,6 @@ void destroyPending_Root(iRoot *d) {
     setCurrent_Root(oldRoot);
 }
 
-void postArrange_Root(iRoot *d) {
-    if (!d->pendingArrange) {
-        d->pendingArrange = iTrue;
-        SDL_Event ev = { .type = SDL_USEREVENT };
-        ev.user.code = arrange_UserEventCode;
-        ev.user.data2 = d;
-        SDL_PushEvent(&ev);
-    }
-}
-
 iPtrArray *onTop_Root(iRoot *d) {
     if (!d->onTop) {
         d->onTop = new_PtrArray();
diff --git a/src/ui/root.h b/src/ui/root.h
index 740e97c9..04dd5e16 100644
--- a/src/ui/root.h
+++ b/src/ui/root.h
@@ -9,6 +9,7 @@ iDeclareType(Root)
 
 struct Impl_Root {
     iWidget *  widget;
+    iWindow *  window;
     iPtrArray *onTop; /* order is important; last one is topmost */
     iPtrSet *  pendingDestruction;
     iBool      pendingArrange;
@@ -29,7 +30,6 @@ iAnyObject *findWidget_Root                     (const char *id); /* under curre
 
 iPtrArray * onTop_Root                          (iRoot *);
 void        destroyPending_Root                 (iRoot *);
-void        postArrange_Root                    (iRoot *);
 
 void        updateMetrics_Root                  (iRoot *);
 void        updatePadding_Root                  (iRoot *); /* TODO: is part of metrics? */
diff --git a/src/ui/text.c b/src/ui/text.c
index f7fff4bc..bf71b0e9 100644
--- a/src/ui/text.c
+++ b/src/ui/text.c
@@ -290,7 +290,9 @@ struct Impl_Text {
     iRegExp *      ansiEscape;
 };
 
-static iText   text_;
+iDefineTypeConstructionArgs(Text, (SDL_Renderer *render), render)
+
+static iText  *activeText_;
 static iBlock *userFont_;
 
 static void initFonts_Text_(iText *d) {
@@ -501,8 +503,7 @@ void loadUserFonts_Text(void) {
     }
 }
 
-void init_Text(SDL_Renderer *render) {
-    iText *d = &text_;
+void init_Text(iText *d, SDL_Renderer *render) {
     loadUserFonts_Text();
     d->contentFont     = nunito_TextFont;
     d->headingFont     = nunito_TextFont;
@@ -521,8 +522,7 @@ void init_Text(SDL_Renderer *render) {
     initFonts_Text_(d);
 }
 
-void deinit_Text(void) {
-    iText *d = &text_;
+void deinit_Text(iText *d) {
     SDL_FreePalette(d->grayscale);
     deinitFonts_Text_(d);
     deinitCache_Text_(d);
@@ -530,30 +530,34 @@ void deinit_Text(void) {
     iRelease(d->ansiEscape);
 }
 
+void setCurrent_Text(iText *d) {
+    activeText_ = d;
+}
+
 void setOpacity_Text(float opacity) {
-    SDL_SetTextureAlphaMod(text_.cache, iClamp(opacity, 0.0f, 1.0f) * 255 + 0.5f);
+    SDL_SetTextureAlphaMod(activeText_->cache, iClamp(opacity, 0.0f, 1.0f) * 255 + 0.5f);
 }
 
-void setContentFont_Text(enum iTextFont font) {
-    if (text_.contentFont != font) {
-        text_.contentFont = font;
-        resetFonts_Text();
+void setContentFont_Text(iText *d, enum iTextFont font) {
+    if (d->contentFont != font) {
+        d->contentFont = font;
+        resetFonts_Text(d);
     }
 }
 
-void setHeadingFont_Text(enum iTextFont font) {
-    if (text_.headingFont != font) {
-        text_.headingFont = font;
-        resetFonts_Text();
+void setHeadingFont_Text(iText *d, enum iTextFont font) {
+    if (d->headingFont != font) {
+        d->headingFont = font;
+        resetFonts_Text(d);
     }
 }
 
-void setContentFontSize_Text(float fontSizeFactor) {
+void setContentFontSize_Text(iText *d, float fontSizeFactor) {
     fontSizeFactor *= contentScale_Text_;
     iAssert(fontSizeFactor > 0);
-    if (iAbs(text_.contentFontSize - fontSizeFactor) > 0.001f) {
-        text_.contentFontSize = fontSizeFactor;
-        resetFonts_Text();
+    if (iAbs(d->contentFontSize - fontSizeFactor) > 0.001f) {
+        d->contentFontSize = fontSizeFactor;
+        resetFonts_Text(d);
     }
 }
 
@@ -565,8 +569,7 @@ static void resetCache_Text_(iText *d) {
     initCache_Text_(d);
 }
 
-void resetFonts_Text(void) {
-    iText *d = &text_;
+void resetFonts_Text(iText *d) {
     deinitFonts_Text_(d);
     deinitCache_Text_(d);
     initCache_Text_(d);
@@ -574,7 +577,7 @@ void resetFonts_Text(void) {
 }
 
 iLocalDef iFont *font_Text_(enum iFontId id) {
-    return &text_.fonts[id & mask_FontId];
+    return &activeText_->fonts[id & mask_FontId];
 }
 
 static SDL_Surface *rasterizeGlyph_Font_(const iFont *d, uint32_t glyphIndex, float xShift) {
@@ -584,7 +587,7 @@ static SDL_Surface *rasterizeGlyph_Font_(const iFont *d, uint32_t glyphIndex, fl
     SDL_Surface *surface8 =
         SDL_CreateRGBSurfaceWithFormatFrom(bmp, w, h, 8, w, SDL_PIXELFORMAT_INDEX8);
     SDL_SetSurfaceBlendMode(surface8, SDL_BLENDMODE_NONE);
-    SDL_SetSurfacePalette(surface8, text_.grayscale);
+    SDL_SetSurfacePalette(surface8, activeText_->grayscale);
 #if LAGRANGE_RASTER_DEPTH != 8
     /* Convert to the cache format. */
     SDL_Surface *surf = SDL_ConvertSurfaceFormat(surface8, LAGRANGE_RASTER_FORMAT, 0);
@@ -631,7 +634,7 @@ static void allocate_Font_(iFont *d, iGlyph *glyph, int hoff) {
         &d->font, index_Glyph_(glyph), d->xScale, d->yScale, hoff * 0.5f, 0.0f, &x0, &y0, &x1, &y1);
     glRect->size = init_I2(x1 - x0, y1 - y0);
     /* Determine placement in the glyph cache texture, advancing in rows. */
-    glRect->pos    = assignCachePos_Text_(&text_, glRect->size);
+    glRect->pos    = assignCachePos_Text_(activeText_, glRect->size);
     glyph->d[hoff] = init_I2(x0, y0);
     glyph->d[hoff].y += d->vertOffset;
     if (hoff == 0) { /* hoff==1 uses same metrics as `glyph` */
@@ -737,11 +740,11 @@ static iGlyph *glyphByIndex_Font_(iFont *d, uint32_t glyphIndex) {
     }
     else {
         /* If the cache is running out of space, clear it and we'll recache what's needed currently. */
-        if (text_.cacheBottom > text_.cacheSize.y - maxGlyphHeight_Text_(&text_)) {
+        if (activeText_->cacheBottom > activeText_->cacheSize.y - maxGlyphHeight_Text_(activeText_)) {
 #if !defined (NDEBUG)
             printf("[Text] glyph cache is full, clearing!\n"); fflush(stdout);
 #endif
-            resetCache_Text_(&text_);
+            resetCache_Text_(activeText_);
         }
         glyph       = new_Glyph(glyphIndex);
         glyph->font = d;
@@ -858,7 +861,7 @@ static void finishRun_AttributedText_(iAttributedText *d, iAttributedRun *run, i
 }
 
 static enum iFontId fontId_Text_(const iFont *font) {
-    return (enum iFontId) (font - text_.fonts);
+    return (enum iFontId) (font - activeText_->fonts);
 }
 
 static void prepare_AttributedText_(iAttributedText *d, int overrideBaseDir, iChar overrideChar) {
@@ -949,7 +952,7 @@ static void prepare_AttributedText_(iAttributedText *d, int overrideBaseDir, iCh
             /* Do a regexp match in the source text. */
             iRegExpMatch m;
             init_RegExpMatch(&m);
-            if (match_RegExp(text_.ansiEscape, srcPos, d->source.end - srcPos, &m)) {
+            if (match_RegExp(activeText_->ansiEscape, srcPos, d->source.end - srcPos, &m)) {
                 finishRun_AttributedText_(d, &run, pos - 1);
                 run.fgColor = ansiForeground_Color(capturedRange_RegExpMatch(&m, 1),
                                                    tmParagraph_ColorId);
@@ -1082,9 +1085,9 @@ static void cacheGlyphs_Font_(iFont *d, const iArray *glyphIndices) {
     while (index < size_Array(glyphIndices)) {
         for (; index < size_Array(glyphIndices); index++) {
             const uint32_t glyphIndex = constValue_Array(glyphIndices, index, uint32_t);
-            const int lastCacheBottom = text_.cacheBottom;
+            const int lastCacheBottom = activeText_->cacheBottom;
             iGlyph *glyph = glyphByIndex_Font_(d, glyphIndex);
-            if (text_.cacheBottom < lastCacheBottom) {
+            if (activeText_->cacheBottom < lastCacheBottom) {
                 /* The cache was reset due to running out of space. We need to restart from
                    the beginning! */
                 bufX = 0;
@@ -1103,7 +1106,7 @@ static void cacheGlyphs_Font_(iFont *d, const iArray *glyphIndices) {
                                 LAGRANGE_RASTER_DEPTH,
                                 LAGRANGE_RASTER_FORMAT);
                     SDL_SetSurfaceBlendMode(buf, SDL_BLENDMODE_NONE);
-                    SDL_SetSurfacePalette(buf, text_.grayscale);
+                    SDL_SetSurfacePalette(buf, activeText_->grayscale);
                 }
                 SDL_Surface *surfaces[2] = {
                     !isRasterized_Glyph_(glyph, 0) ?
@@ -1147,19 +1150,19 @@ static void cacheGlyphs_Font_(iFont *d, const iArray *glyphIndices) {
         }
         /* Finished or the buffer is full, copy the glyphs to the cache texture. */
         if (!isEmpty_Array(rasters)) {
-            SDL_Texture *bufTex = SDL_CreateTextureFromSurface(text_.render, buf);
+            SDL_Texture *bufTex = SDL_CreateTextureFromSurface(activeText_->render, buf);
             SDL_SetTextureBlendMode(bufTex, SDL_BLENDMODE_NONE);
             if (!isTargetChanged) {
                 isTargetChanged = iTrue;
-                oldTarget = SDL_GetRenderTarget(text_.render);
-                SDL_SetRenderTarget(text_.render, text_.cache);
+                oldTarget = SDL_GetRenderTarget(activeText_->render);
+                SDL_SetRenderTarget(activeText_->render, activeText_->cache);
             }
 //            printf("copying %zu rasters from %p\n", size_Array(rasters), bufTex); fflush(stdout);
             iConstForEach(Array, i, rasters) {
                 const iRasterGlyph *rg = i.value;
 //                iAssert(isEqual_I2(rg->rect.size, rg->glyph->rect[rg->hoff].size));
                 const iRect *glRect = &rg->glyph->rect[rg->hoff];
-                SDL_RenderCopy(text_.render,
+                SDL_RenderCopy(activeText_->render,
                                bufTex,
                                (const SDL_Rect *) &rg->rect,
                                (const SDL_Rect *) glRect);
@@ -1179,7 +1182,7 @@ static void cacheGlyphs_Font_(iFont *d, const iArray *glyphIndices) {
         SDL_FreeSurface(buf);
     }
     if (isTargetChanged) {
-        SDL_SetRenderTarget(text_.render, oldTarget);
+        SDL_SetRenderTarget(activeText_->render, oldTarget);
     }
 }
 
@@ -1706,9 +1709,9 @@ static iRect run_Font_(iFont *d, const iRunArgs *args) {
                     }
                     if (~mode & permanentColorFlag_RunMode) {
                         const iColor clr = run->fgColor;
-                        SDL_SetTextureColorMod(text_.cache, clr.r, clr.g, clr.b);
+                        SDL_SetTextureColorMod(activeText_->cache, clr.r, clr.g, clr.b);
                         if (args->mode & fillBackground_RunMode) {
-                            SDL_SetRenderDrawColor(text_.render, clr.r, clr.g, clr.b, 0);
+                            SDL_SetRenderDrawColor(activeText_->render, clr.r, clr.g, clr.b, 0);
                         }
                     }
                     SDL_Rect src;
@@ -1719,9 +1722,9 @@ static iRect run_Font_(iFont *d, const iRunArgs *args) {
                         /* Alpha blending looks much better if the RGB components don't change in
                            the partially transparent pixels. */
                         /* TODO: Backgrounds of all glyphs should be cleared before drawing anything else. */
-                        SDL_RenderFillRect(text_.render, &dst);
+                        SDL_RenderFillRect(activeText_->render, &dst);
                     }
-                    SDL_RenderCopy(text_.render, text_.cache, &src, &dst);
+                    SDL_RenderCopy(activeText_->render, activeText_->cache, &src, &dst);
 #if 0
                     /* Show spaces and direction. */
                     if (logicalText[logPos] == 0x20) {
@@ -1863,7 +1866,7 @@ iTextMetrics measureN_Text(int fontId, const char *text, size_t n) {
 }
 
 static void drawBoundedN_Text_(int fontId, iInt2 pos, int xposBound, int color, iRangecc text, size_t maxLen) {
-    iText *      d    = &text_;
+    iText *      d    = activeText_;
     iFont *      font = font_Text_(fontId);
     const iColor clr  = get_Color(color & mask_ColorId);
     SDL_SetTextureColorMod(d->cache, clr.r, clr.g, clr.b);
@@ -2057,7 +2060,7 @@ iTextMetrics draw_WrapText(iWrapText *d, int fontId, iInt2 pos, int color) {
 }
 
 SDL_Texture *glyphCache_Text(void) {
-    return text_.cache;
+    return activeText_->cache;
 }
 
 static void freeBitmap_(void *ptr) {
@@ -2170,7 +2173,7 @@ iString *renderBlockChars_Text(const iBlock *fontData, int height, enum iTextBlo
 iDefineTypeConstructionArgs(TextBuf, (iWrapText *wrapText, int font, int color), wrapText, font, color)
 
 void init_TextBuf(iTextBuf *d, iWrapText *wrapText, int font, int color) {
-    SDL_Renderer *render = text_.render;
+    SDL_Renderer *render = activeText_->render;
     d->size = measure_WrapText(wrapText, font).bounds.size;
     SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "0");
     if (d->size.x * d->size.y) {
@@ -2191,9 +2194,9 @@ void init_TextBuf(iTextBuf *d, iWrapText *wrapText, int font, int color) {
         SDL_SetRenderDrawBlendMode(render, SDL_BLENDMODE_NONE);
         SDL_SetRenderDrawColor(render, 255, 255, 255, 0);
         SDL_RenderClear(render);
-        SDL_SetTextureBlendMode(text_.cache, SDL_BLENDMODE_NONE); /* blended when TextBuf is drawn */
+        SDL_SetTextureBlendMode(activeText_->cache, SDL_BLENDMODE_NONE); /* blended when TextBuf is drawn */
         draw_WrapText(wrapText, font, zero_I2(), color | fillBackground_ColorId);
-        SDL_SetTextureBlendMode(text_.cache, SDL_BLENDMODE_BLEND);
+        SDL_SetTextureBlendMode(activeText_->cache, SDL_BLENDMODE_BLEND);
         SDL_SetRenderTarget(render, oldTarget);
         origin_Paint = oldOrigin;
         SDL_SetTextureBlendMode(d->texture, SDL_BLENDMODE_BLEND);
@@ -2212,7 +2215,7 @@ 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,
+    SDL_RenderCopy(activeText_->render,
                    d->texture,
                    &(SDL_Rect){ 0, 0, d->size.x, d->size.y },
                    &(SDL_Rect){ pos.x, pos.y, d->size.x, d->size.y });
diff --git a/src/ui/text.h b/src/ui/text.h
index ac6cc1c1..1da43818 100644
--- a/src/ui/text.h
+++ b/src/ui/text.h
@@ -139,15 +139,20 @@ enum iTextFont {
 
 extern int gap_Text; /* affected by content font size */
 
-void    init_Text               (SDL_Renderer *);
-void    deinit_Text             (void);
+iDeclareType(Text)
+iDeclareTypeConstructionArgs(Text, SDL_Renderer *)
+
+void    init_Text               (iText *, SDL_Renderer *);
+void    deinit_Text             (iText *);
+
+void    setCurrent_Text         (iText *);
 
 void    loadUserFonts_Text      (void); /* based on Prefs */
 
-void    setContentFont_Text     (enum iTextFont font);
-void    setHeadingFont_Text     (enum iTextFont font);
-void    setContentFontSize_Text (float fontSizeFactor); /* affects all except `default*` fonts */
-void    resetFonts_Text         (void);
+void    setContentFont_Text     (iText *, enum iTextFont font);
+void    setHeadingFont_Text     (iText *, enum iTextFont font);
+void    setContentFontSize_Text (iText *, float fontSizeFactor); /* affects all except `default*` fonts */
+void    resetFonts_Text         (iText *);
 
 int     lineHeight_Text         (int fontId);
 iRect   visualBounds_Text       (int fontId, iRangecc text);
diff --git a/src/ui/text_simple.c b/src/ui/text_simple.c
index bf33b4be..8b1de64a 100644
--- a/src/ui/text_simple.c
+++ b/src/ui/text_simple.c
@@ -92,7 +92,7 @@ static iRect runSimple_Font_(iFont *d, const iRunArgs *args) {
     }
     if (args->mode & fillBackground_RunMode) {
         const iColor initial = get_Color(args->color);
-        SDL_SetRenderDrawColor(text_.render, initial.r, initial.g, initial.b, 0);
+        SDL_SetRenderDrawColor(activeText_->render, initial.r, initial.g, initial.b, 0);
     }
     /* Text rendering is not very straightforward! Let's dive in... */
     iChar       prevCh = 0;
@@ -114,14 +114,14 @@ static iRect runSimple_Font_(iFont *d, const iRunArgs *args) {
             chPos++;
             iRegExpMatch m;
             init_RegExpMatch(&m);
-            if (match_RegExp(text_.ansiEscape, chPos, args->text.end - chPos, &m)) {
+            if (match_RegExp(activeText_->ansiEscape, chPos, args->text.end - chPos, &m)) {
                 if (mode & draw_RunMode && ~mode & permanentColorFlag_RunMode) {
                     /* Change the color. */
                     const iColor clr =
                         ansiForeground_Color(capturedRange_RegExpMatch(&m, 1), tmParagraph_ColorId);
-                    SDL_SetTextureColorMod(text_.cache, clr.r, clr.g, clr.b);
+                    SDL_SetTextureColorMod(activeText_->cache, clr.r, clr.g, clr.b);
                     if (args->mode & fillBackground_RunMode) {
-                        SDL_SetRenderDrawColor(text_.render, clr.r, clr.g, clr.b, 0);
+                        SDL_SetRenderDrawColor(activeText_->render, clr.r, clr.g, clr.b, 0);
                     }
                 }
                 chPos = end_RegExpMatch(&m);
@@ -205,9 +205,9 @@ static iRect runSimple_Font_(iFont *d, const iRunArgs *args) {
                 }
                 if (mode & draw_RunMode && ~mode & permanentColorFlag_RunMode) {
                     const iColor clr = get_Color(colorNum);
-                    SDL_SetTextureColorMod(text_.cache, clr.r, clr.g, clr.b);
+                    SDL_SetTextureColorMod(activeText_->cache, clr.r, clr.g, clr.b);
                     if (args->mode & fillBackground_RunMode) {
-                        SDL_SetRenderDrawColor(text_.render, clr.r, clr.g, clr.b, 0);
+                        SDL_SetRenderDrawColor(activeText_->render, clr.r, clr.g, clr.b, 0);
                     }
                 }
                 prevCh = 0;
@@ -311,9 +311,9 @@ static iRect runSimple_Font_(iFont *d, const iRunArgs *args) {
             if (args->mode & fillBackground_RunMode) {
                 /* Alpha blending looks much better if the RGB components don't change in
                    the partially transparent pixels. */
-                SDL_RenderFillRect(text_.render, &dst);
+                SDL_RenderFillRect(activeText_->render, &dst);
             }
-            SDL_RenderCopy(text_.render, text_.cache, &src, &dst);
+            SDL_RenderCopy(activeText_->render, activeText_->cache, &src, &dst);
         }
         xpos += advance;
         if (!isSpace_Char(ch)) {
diff --git a/src/ui/util.c b/src/ui/util.c
index 721aed2d..38977b96 100644
--- a/src/ui/util.c
+++ b/src/ui/util.c
@@ -613,6 +613,8 @@ iBool isAction_Widget(const iWidget *d) {
 /*-----------------------------------------------------------------------------------------------*/
 
 static iBool isCommandIgnoredByMenus_(const char *cmd) {
+    if (equal_Command(cmd, "window.focus.lost") ||
+        equal_Command(cmd, "window.focus.gained")) return iTrue;
     /* TODO: Perhaps a common way of indicating which commands are notifications and should not
        be reacted to by menus? */
     return equal_Command(cmd, "media.updated") ||
@@ -810,6 +812,10 @@ static void updateMenuItemFonts_Widget_(iWidget *d) {
     }
 }
 
+iLocalDef iBool isUsingMenuPopupWindows_(void) {
+    return deviceType_App() == desktop_AppDeviceType;
+}
+
 void openMenuFlags_Widget(iWidget *d, iInt2 windowCoord, iBool postCommands) {
     const iRect rootRect        = rect_Root(d->root);
     const iInt2 rootSize        = rootRect.size;
@@ -822,6 +828,26 @@ void openMenuFlags_Widget(iWidget *d, iInt2 windowCoord, iBool postCommands) {
     processEvents_App(postedEventsOnly_AppEventMode);
     setFlags_Widget(d, hidden_WidgetFlag, iFalse);
     setFlags_Widget(d, commandOnMouseMiss_WidgetFlag, iTrue);
+    if (isUsingMenuPopupWindows_()) {
+        if (postCommands) {
+            postCommand_Widget(d, "menu.opened");
+        }
+        updateMenuItemFonts_Widget_(d);
+        iRoot *oldRoot = current_Root();
+        setFlags_Widget(d, keepOnTop_WidgetFlag, iFalse);
+        setUserData_Object(d, parent_Widget(d));
+        removeChild_Widget(parent_Widget(d), d); /* we'll borrow the widget for a while */
+        iInt2 mousePos;
+        SDL_GetGlobalMouseState(&mousePos.x, &mousePos.y);
+        iWindow *win = newPopup_Window(sub_I2(mousePos, divi_I2(gap2_UI, 2)), d);
+        SDL_SetWindowTitle(win->win, "Menu");
+        addPopup_App(win); /* window takes the widget */
+        SDL_ShowWindow(win->win);
+        draw_Window(win);
+        setCurrent_Window(mainWindow_App());
+        setCurrent_Root(oldRoot);
+        return;
+    }
     raise_Widget(d);
     setFlags_Widget(findChild_Widget(d, "menu.cancel"), disabled_WidgetFlag, iFalse);
     if (isPortraitPhone) {
@@ -836,7 +862,7 @@ void openMenuFlags_Widget(iWidget *d, iInt2 windowCoord, iBool postCommands) {
     arrange_Widget(d);
     if (isPortraitPhone) {
         if (isSlidePanel) {
-            d->rect.pos = zero_I2(); //neg_I2(bounds_Widget(parent_Widget(d)).pos);
+            d->rect.pos = zero_I2();
         }
         else {
             d->rect.pos = init_I2(0, rootSize.y);
@@ -856,7 +882,7 @@ void openMenuFlags_Widget(iWidget *d, iInt2 windowCoord, iBool postCommands) {
         float l, t, r, b;
         safeAreaInsets_iOS(&l, &t, &r, &b);
         topExcess    += t;
-        bottomExcess += iMax(b, get_Window()->keyboardHeight);
+        bottomExcess += iMax(b, get_MainWindow()->keyboardHeight);
         leftExcess   += l;
         rightExcess  += r;
     }
@@ -884,6 +910,18 @@ void closeMenu_Widget(iWidget *d) {
     if (d == NULL || flags_Widget(d) & hidden_WidgetFlag) {
         return; /* Already closed. */
     }
+    if (isUsingMenuPopupWindows_()) {
+        iWindow *win = window_Widget(d);
+        iAssert(type_Window(win) == popup_WindowType);
+        iWidget *originalParent = userData_Object(d);
+        setUserData_Object(d, NULL);
+        win->roots[0]->widget = NULL;
+        setRoot_Widget(d, originalParent->root);
+        addChild_Widget(originalParent, d);
+        setFlags_Widget(d, keepOnTop_WidgetFlag, iTrue);
+        SDL_HideWindow(win->win);
+        collect_Garbage(win, (iDeleteFunc) delete_Window); /* get rid of it after event processing */
+    }
     setFlags_Widget(d, hidden_WidgetFlag, iTrue);
     setFlags_Widget(findChild_Widget(d, "menu.cancel"), disabled_WidgetFlag, iTrue);
     postRefresh_App();
diff --git a/src/ui/widget.c b/src/ui/widget.c
index 23c19315..7b33a752 100644
--- a/src/ui/widget.c
+++ b/src/ui/widget.c
@@ -271,6 +271,10 @@ iWidget *root_Widget(const iWidget *d) {
     return d ? d->root->widget : NULL;
 }
 
+iWindow *window_Widget(const iAnyObject *d) {
+    return constAs_Widget(d)->root->window;
+}
+
 void showCollapsed_Widget(iWidget *d, iBool show) {
     const iBool isVisible = !(d->flags & hidden_WidgetFlag);
     if ((isVisible && !show) || (!isVisible && show)) {
@@ -979,11 +983,10 @@ void unhover_Widget(void) {
 }
 
 iBool dispatchEvent_Widget(iWidget *d, const SDL_Event *ev) {
-    //iAssert(d->root == get_Root());
     if (!d->parent) {
-        if (get_Window()->focus && get_Window()->focus->root == d->root && isKeyboardEvent_(ev)) {
+        if (window_Widget(d)->focus && window_Widget(d)->focus->root == d->root && isKeyboardEvent_(ev)) {
             /* Root dispatches keyboard events directly to the focused widget. */
-            if (dispatchEvent_Widget(get_Window()->focus, ev)) {
+            if (dispatchEvent_Widget(window_Widget(d)->focus, ev)) {
                 return iTrue;
             }
         }
@@ -1012,7 +1015,8 @@ iBool dispatchEvent_Widget(iWidget *d, const SDL_Event *ev) {
         }
     }
     else if (ev->type == SDL_MOUSEMOTION &&
-             (!get_Window()->hover || hasParent_Widget(d, get_Window()->hover)) &&
+             ev->motion.windowID == SDL_GetWindowID(window_Widget(d)->win) &&
+             (!window_Widget(d)->hover || hasParent_Widget(d, window_Widget(d)->hover)) &&
              flags_Widget(d) & hover_WidgetFlag && ~flags_Widget(d) & hidden_WidgetFlag &&
              ~flags_Widget(d) & disabled_WidgetFlag) {
         if (contains_Widget(d, init_I2(ev->motion.x, ev->motion.y))) {
@@ -1031,11 +1035,11 @@ iBool dispatchEvent_Widget(iWidget *d, const SDL_Event *ev) {
         iReverseForEach(ObjectList, i, d->children) {
             iWidget *child = as_Widget(i.object);
             //iAssert(child->root == d->root);
-            if (child == get_Window()->focus && isKeyboardEvent_(ev)) {
+            if (child == window_Widget(d)->focus && isKeyboardEvent_(ev)) {
                 continue; /* Already dispatched. */
             }
             if (isVisible_Widget(child) && child->flags & keepOnTop_WidgetFlag) {
-                /* Already dispatched. */
+            /* Already dispatched. */
                 continue;
             }
             if (dispatchEvent_Widget(child, ev)) {
@@ -1050,7 +1054,7 @@ iBool dispatchEvent_Widget(iWidget *d, const SDL_Event *ev) {
 #endif
 #if 0
                 if (ev->type == SDL_MOUSEMOTION) {
-                    printf("[%p] %s:'%s' (on top) ate the motion\n",
+                    printf("[%p] %s:'%s' ate the motion\n",
                            child, class_Widget(child)->name,
                            cstr_String(id_Widget(child)));
                     fflush(stdout);
@@ -1246,7 +1250,7 @@ iBool processEvent_Widget(iWidget *d, const SDL_Event *ev) {
                                ev->button.x,
                                ev->button.y);
         }
-        setCursor_Window(get_Window(), SDL_SYSTEM_CURSOR_ARROW);
+        setCursor_Window(window_Widget(d), SDL_SYSTEM_CURSOR_ARROW);
         return iTrue;
     }
     return iFalse;
@@ -1270,6 +1274,7 @@ iLocalDef iBool isDrawn_Widget_(const iWidget *d) {
 void drawLayerEffects_Widget(const iWidget *d) {
     /* Layered effects are not buffered, so they are drawn here separately. */
     iAssert(isDrawn_Widget_(d));
+    iAssert(window_Widget(d) == get_Window());
     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) {
@@ -1539,6 +1544,7 @@ static void endBufferDraw_Widget_(const iWidget *d) {
 }
 
 void draw_Widget(const iWidget *d) {
+    iAssert(window_Widget(d) == get_Window());
     if (!isDrawn_Widget_(d)) {
         if (d->drawBuf) {
 //            printf("[%p] drawBuffer released\n", d);
@@ -1820,7 +1826,17 @@ iBool equalWidget_Command(const char *cmd, const iWidget *widget, const char *ch
     if (equal_Command(cmd, checkCommand)) {
         const iWidget *src = pointer_Command(cmd);
         iAssert(!src || strstr(cmd, " ptr:"));
-        return src == widget || hasParent_Widget(src, widget);
+        if (src == widget || hasParent_Widget(src, widget)) {
+            return iTrue;
+        }
+//        if (src && type_Window(window_Widget(src)) == popup_WindowType) {
+//            /* Special case: command was emitted from a popup widget. The popup root widget actually
+//               belongs to someone else. */
+//            iWidget *realParent = userData_Object(src->root->widget);
+//            iAssert(realParent);
+//            iAssert(isInstance_Object(realParent, &Class_Widget));
+//            return realParent == widget || hasParent_Widget(realParent, widget);
+//        }
     }
     return iFalse;
 }
@@ -1962,6 +1978,10 @@ void postCommand_Widget(const iAnyObject *d, const char *cmd, ...) {
     }
     if (!isGlobal) {
         iAssert(isInstance_Object(d, &Class_Widget));
+        if (type_Window(window_Widget(d)) == popup_WindowType) {
+            postCommandf_Root(((const iWidget *) d)->root, "cancel popup:1 ptr:%p", d);
+            d = userData_Object(root_Widget(d));
+        }
         appendFormat_String(&str, " ptr:%p", d);
     }
     postCommandString_Root(((const iWidget *) d)->root, &str);
diff --git a/src/ui/widget.h b/src/ui/widget.h
index 7491cb79..0eab69c1 100644
--- a/src/ui/widget.h
+++ b/src/ui/widget.h
@@ -34,7 +34,8 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
 #include 
 #include 
 
-iDeclareType(Root) /* each widget is associated with a Root */
+iDeclareType(Root)   /* each widget is associated with a Root */
+iDeclareType(Window) /* each Root is inside a Window */
 
 #define iDeclareWidgetClass(className) \
     iDeclareType(className); \
@@ -185,6 +186,7 @@ void    releaseChildren_Widget  (iWidget *);
     - inner:  0,0 is at the top left corner of the widget */
 
 iWidget *       root_Widget             (const iWidget *);
+iWindow *       window_Widget           (const iAnyObject *);
 const iString * id_Widget               (const iWidget *);
 int64_t flags_Widget                    (const iWidget *);
 iRect   bounds_Widget                   (const iWidget *); /* outer bounds */
diff --git a/src/ui/window.c b/src/ui/window.c
index 92125d81..e9a34ace 100644
--- a/src/ui/window.c
+++ b/src/ui/window.c
@@ -57,7 +57,8 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
 #include "stb_image.h"
 #include "stb_image_resize.h"
 
-static iWindow *theWindow_ = NULL;
+static iWindow *    theWindow_;
+static iMainWindow *theMainWindow_;
 
 #if defined (iPlatformApple) || defined (iPlatformLinux) || defined (iPlatformOther)
 static float initialUiScale_ = 1.0f;
@@ -67,6 +68,9 @@ static float initialUiScale_ = 1.1f;
 
 static iBool isOpenGLRenderer_;
 
+iDefineTypeConstructionArgs(Window,
+                            (enum iWindowType type, iRect rect, uint32_t flags),
+                            type, rect, flags)
 iDefineTypeConstructionArgs(MainWindow, (iRect rect), rect)
 
 /* TODO: Define menus per platform. */
@@ -205,6 +209,7 @@ static void setupUserInterface_MainWindow(iMainWindow *d) {
 #endif
     /* One root is created by default. */
     d->base.roots[0] = new_Root();
+    d->base.roots[0]->window = as_Window(d);
     setCurrent_Root(d->base.roots[0]);
     createUserInterface_Root(d->base.roots[0]);
     setCurrent_Root(NULL);
@@ -409,7 +414,6 @@ void init_Window(iWindow *d, enum iWindowType type, iRect rect, uint32_t flags)
     d->mouseGrab     = NULL;
     d->focus         = NULL;
     d->pendingCursor = NULL;
-    d->isDrawFrozen  = iTrue;
     d->isExposed     = iFalse;
     d->isMinimized   = iFalse;
     d->isInvalidated = iFalse; /* set when posting event, to avoid repeated events */
@@ -441,9 +445,27 @@ void init_Window(iWindow *d, enum iWindowType type, iRect rect, uint32_t flags)
     d->uiScale      = initialUiScale_;
     /* TODO: Ratios, scales, and metrics must be window-specific, not global. */
     setScale_Metrics(d->pixelRatio * d->displayScale * d->uiScale);
+    d->text = new_Text(d->render);
+}
+
+static void deinitRoots_Window_(iWindow *d) {
+    iRecycle();
+    iForIndices(i, d->roots) {
+        if (d->roots[i]) {
+            setCurrent_Root(d->roots[i]);
+            delete_Root(d->roots[i]);
+            d->roots[i] = NULL;
+        }
+    }
+    setCurrent_Root(NULL);
 }
 
 void deinit_Window(iWindow *d) {
+    if (d->type == popup_WindowType) {
+        removePopup_App(d);
+    }
+    deinitRoots_Window_(d);
+    delete_Text(d->text);
     SDL_DestroyRenderer(d->render);
     SDL_DestroyWindow(d->win);
     iForIndices(i, d->cursors) {
@@ -455,6 +477,7 @@ void deinit_Window(iWindow *d) {
 
 void init_MainWindow(iMainWindow *d, iRect rect) {
     theWindow_ = &d->base;
+    theMainWindow_ = d;
     uint32_t flags = 0;
 #if defined (iPlatformAppleDesktop)
     SDL_SetHint(SDL_HINT_RENDER_DRIVER, shouldDefaultToMetalRenderer_MacOS() ? "metal" : "opengl");
@@ -465,13 +488,15 @@ void init_MainWindow(iMainWindow *d, iRect rect) {
 #endif
     SDL_SetHint(SDL_HINT_RENDER_VSYNC, "1");
     init_Window(&d->base, main_WindowType, rect, flags);
-    d->splitMode = d->pendingSplitMode = 0;
-    d->pendingSplitUrl = new_String();
-    d->place.initialPos = rect.pos;
-    d->place.normalRect = rect;
+    d->isDrawFrozen           = iTrue;
+    d->splitMode              = 0;
+    d->pendingSplitMode       = 0;
+    d->pendingSplitUrl        = new_String();
+    d->place.initialPos       = rect.pos;
+    d->place.normalRect       = rect;
     d->place.lastNotifiedSize = zero_I2();
-    d->place.snap = 0;
-    d->keyboardHeight = 0;
+    d->place.snap             = 0;
+    d->keyboardHeight         = 0;
 #if defined(iPlatformMobile)
     const iInt2 minSize = zero_I2(); /* windows aren't independently resizable */
 #else
@@ -510,9 +535,9 @@ void init_MainWindow(iMainWindow *d, iRect rect) {
     }
 #endif
 #if defined (iPlatformAppleMobile)
-    setupWindow_iOS(d);
+    setupWindow_iOS(as_Window(d));
 #endif
-    init_Text(d->base.render);
+    setCurrent_Text(d->base.text);
     SDL_GetRendererOutputSize(d->base.render, &d->base.size.x, &d->base.size.y);    
     setupUserInterface_MainWindow(d);
     postCommand_App("~bindings.changed"); /* update from bindings */
@@ -538,24 +563,15 @@ void init_MainWindow(iMainWindow *d, iRect rect) {
 #endif
 }
 
-static void deinitRoots_Window_(iWindow *d) {
-    iRecycle();
-    iForIndices(i, d->roots) {
-        if (d->roots[i]) {
-            setCurrent_Root(d->roots[i]);
-            deinit_Root(d->roots[i]);
-        }
-    }
-    setCurrent_Root(NULL);
-}
-
 void deinit_MainWindow(iMainWindow *d) {
     deinitRoots_Window_(as_Window(d));
     if (theWindow_ == as_Window(d)) {
         theWindow_ = NULL;
     }
+    if (theMainWindow_ == d) {
+        theMainWindow_ = NULL;
+    }
     delete_String(d->pendingSplitUrl);
-    deinit_Text();
     deinit_Window(&d->base);
 }
 
@@ -592,7 +608,7 @@ iRoot *otherRoot_Window(const iWindow *d, iRoot *root) {
 static void invalidate_MainWindow_(iMainWindow *d, iBool forced) {
     if (d && (!d->base.isInvalidated || forced)) {
         d->base.isInvalidated = iTrue;
-        resetFonts_Text();
+        resetFonts_Text(text_Window(d));
         postCommand_App("theme.changed auto:1"); /* forces UI invalidation */
     }
 }
@@ -607,7 +623,7 @@ void invalidate_Window(iAnyWindow *d) {
 }
 
 static iBool isNormalPlacement_MainWindow_(const iMainWindow *d) {
-    if (d->base.isDrawFrozen) return iFalse;
+    if (d->isDrawFrozen) return iFalse;
 #if defined (iPlatformApple)
     /* Maximized mode is not special on macOS. */
     if (snap_MainWindow(d) == maximized_WindowSnap) {
@@ -655,7 +671,7 @@ static iBool unsnap_MainWindow_(iMainWindow *d, const iInt2 *newPos) {
 static void notifyMetricsChange_Window_(const iWindow *d) {
     /* Dynamic UI metrics change. Widgets need to update themselves. */
     setScale_Metrics(d->pixelRatio * d->displayScale * d->uiScale);
-    resetFonts_Text();
+    resetFonts_Text(d->text);
     postCommand_App("metrics.changed");
 }
 
@@ -676,6 +692,41 @@ static void checkPixelRatioChange_Window_(iWindow *d) {
     }
 }
 
+static iBool handleWindowEvent_Window_(iWindow *d, const SDL_WindowEvent *ev) {
+    if (ev->windowID != SDL_GetWindowID(d->win)) {
+        return iFalse;
+    }
+    switch (ev->event) {
+        case SDL_WINDOWEVENT_EXPOSED:
+            d->isExposed = iTrue;
+            postRefresh_App();
+            return iTrue;
+        case SDL_WINDOWEVENT_RESTORED:
+        case SDL_WINDOWEVENT_SHOWN:
+            postRefresh_App();
+            return iTrue;
+        case SDL_WINDOWEVENT_FOCUS_LOST:
+            /* Popup windows are currently only used for menus. */
+            closeMenu_Widget(d->roots[0]->widget);
+            return iTrue;
+        case SDL_WINDOWEVENT_LEAVE:
+            unhover_Widget();
+            d->isMouseInside = iFalse;
+            //postCommand_App("window.mouse.exited");
+//            SDL_SetWindowInputFocus(mainWindow_App()->base.win);
+            printf("mouse leaves popup\n"); fflush(stdout);
+            //SDL_RaiseWindow(mainWindow_App()->base.win);
+            postRefresh_App();
+            return iTrue;
+        case SDL_WINDOWEVENT_ENTER:
+            d->isMouseInside = iTrue;
+            //postCommand_App("window.mouse.entered");
+            printf("mouse enters popup\n"); fflush(stdout);
+            return iTrue;
+    }
+    return iFalse;
+}
+
 static iBool handleWindowEvent_MainWindow_(iMainWindow *d, const SDL_WindowEvent *ev) {
     switch (ev->event) {
 #if defined(iPlatformDesktop)
@@ -795,6 +846,7 @@ static iBool handleWindowEvent_MainWindow_(iMainWindow *d, const SDL_WindowEvent
             return iTrue;
         case SDL_WINDOWEVENT_ENTER:
             d->base.isMouseInside = iTrue;
+            SDL_SetWindowInputFocus(d->base.win);
             postCommand_App("window.mouse.entered");
             return iTrue;
         case SDL_WINDOWEVENT_FOCUS_GAINED:
@@ -802,16 +854,16 @@ static iBool handleWindowEvent_MainWindow_(iMainWindow *d, const SDL_WindowEvent
             setCapsLockDown_Keys(iFalse);
             postCommand_App("window.focus.gained");
             d->base.isExposed = iTrue;
-#if !defined(iPlatformDesktop)
+#if !defined (iPlatformDesktop)
             /* Returned to foreground, may have lost buffered content. */
-            invalidate_Window_(d, iTrue);
+            invalidate_MainWindow_(d, iTrue);
             postCommand_App("window.unfreeze");
 #endif
             return iFalse;
         case SDL_WINDOWEVENT_FOCUS_LOST:
             postCommand_App("window.focus.lost");
-#if !defined(iPlatformDesktop)
-            setFreezeDraw_Window(d, iTrue);
+#if !defined (iPlatformDesktop)
+            setFreezeDraw_MainWindow(d, iTrue);
 #endif
             return iFalse;
         case SDL_WINDOWEVENT_TAKE_FOCUS:
@@ -831,8 +883,8 @@ static void applyCursor_Window_(iWindow *d) {
     }
 }
 
-iBool processEvent_MainWindow(iMainWindow *d, const SDL_Event *ev) {
-    iWindow *w = as_Window(d);
+iBool processEvent_Window(iWindow *d, const SDL_Event *ev) {
+    iMainWindow *mw = (type_Window(d) == main_WindowType ? as_MainWindow(d) : NULL);
     switch (ev->type) {
 #if defined (LAGRANGE_ENABLE_CUSTOM_FRAME)
         case SDL_SYSWMEVENT: {
@@ -845,19 +897,26 @@ iBool processEvent_MainWindow(iMainWindow *d, const SDL_Event *ev) {
         }
 #endif
         case SDL_WINDOWEVENT: {
-            return handleWindowEvent_MainWindow_(d, &ev->window);
+            if (mw) {
+                return handleWindowEvent_MainWindow_(mw, &ev->window);
+            }
+            else {
+                return handleWindowEvent_Window_(d, &ev->window);
+            }
         }
         case SDL_RENDER_TARGETS_RESET:
         case SDL_RENDER_DEVICE_RESET: {
-            invalidate_MainWindow_(d, iTrue /* force full reset */);
+            if (mw) {
+                invalidate_MainWindow_(mw, iTrue /* force full reset */);
+            }
             break;
         }
         default: {
             SDL_Event event = *ev;
             if (event.type == SDL_USEREVENT && isCommand_UserEvent(ev, "window.unfreeze")) {
-                d->base.isDrawFrozen = iFalse;
-                if (SDL_GetWindowFlags(w->win) & SDL_WINDOW_HIDDEN) {
-                    SDL_ShowWindow(w->win);
+                mw->isDrawFrozen = iFalse;
+                if (SDL_GetWindowFlags(d->win) & SDL_WINDOW_HIDDEN) {
+                    SDL_ShowWindow(d->win);
                 }
                 postRefresh_App();
                 postCommand_App("media.player.update"); /* in case a player needs updating */
@@ -866,35 +925,35 @@ iBool processEvent_MainWindow(iMainWindow *d, const SDL_Event *ev) {
             if (processEvent_Touch(&event)) {
                 return iTrue;
             }
-            if (event.type == SDL_KEYDOWN && SDL_GetTicks() - d->base.focusGainedAt < 10) {
+            if (event.type == SDL_KEYDOWN && SDL_GetTicks() - d->focusGainedAt < 10) {
                 /* Suspiciously close to when input focus was received. For example under openbox,
                    closing xterm with Ctrl+D will cause the keydown event to "spill" over to us.
                    As a workaround, ignore these events. */
                 return iTrue; /* won't go to bindings, either */
             }
-            if (event.type == SDL_MOUSEBUTTONDOWN && d->base.ignoreClick) {
-                d->base.ignoreClick = iFalse;
+            if (event.type == SDL_MOUSEBUTTONDOWN && d->ignoreClick) {
+                d->ignoreClick = iFalse;
                 return iTrue;
             }
             /* Map mouse pointer coordinate to our coordinate system. */
             if (event.type == SDL_MOUSEMOTION) {
-                setCursor_Window(w, SDL_SYSTEM_CURSOR_ARROW); /* default cursor */
-                const iInt2 pos = coord_Window(w, event.motion.x, event.motion.y);
+                setCursor_Window(d, SDL_SYSTEM_CURSOR_ARROW); /* default cursor */
+                const iInt2 pos = coord_Window(d, event.motion.x, event.motion.y);
                 event.motion.x = pos.x;
                 event.motion.y = pos.y;
             }
             else if (event.type == SDL_MOUSEBUTTONUP || event.type == SDL_MOUSEBUTTONDOWN) {
-                const iInt2 pos = coord_Window(w, event.button.x, event.button.y);
+                const iInt2 pos = coord_Window(d, event.button.x, event.button.y);
                 event.button.x = pos.x;
                 event.button.y = pos.y;
                 if (event.type == SDL_MOUSEBUTTONDOWN) {
                     /* Button clicks will change keyroot. */
-                    if (numRoots_Window(w) > 1) {
+                    if (numRoots_Window(d) > 1) {
                         const iInt2 click = init_I2(event.button.x, event.button.y);
-                        iForIndices(i, w->roots) {
-                            iRoot *root = w->roots[i];
-                            if (root != w->keyRoot && contains_Rect(rect_Root(root), click)) {
-                                setKeyRoot_Window(w, root);
+                        iForIndices(i, d->roots) {
+                            iRoot *root = d->roots[i];
+                            if (root != d->keyRoot && contains_Rect(rect_Root(root), click)) {
+                                setKeyRoot_Window(d, root);
                                 break;
                             }
                         }
@@ -909,13 +968,13 @@ iBool processEvent_MainWindow(iMainWindow *d, const SDL_Event *ev) {
                 event.type == SDL_MOUSEBUTTONUP || event.type == SDL_MOUSEBUTTONDOWN) {
                 if (mouseGrab_Widget()) {
                     iWidget *grabbed = mouseGrab_Widget();
-                    setCurrent_Root(findRoot_Window(w, grabbed));
+                    setCurrent_Root(findRoot_Window(d, grabbed));
                     wasUsed = dispatchEvent_Widget(grabbed, &event);
                 }
             }
             /* Dispatch the event to the tree of widgets. */
             if (!wasUsed) {
-                wasUsed = dispatchEvent_Window(w, &event);
+                wasUsed = dispatchEvent_Window(d, &event);
             }
             if (!wasUsed) {
                 /* As a special case, clicking the middle mouse button can be used for pasting
@@ -928,35 +987,35 @@ iBool processEvent_MainWindow(iMainWindow *d, const SDL_Event *ev) {
                     paste.key.keysym.mod = KMOD_PRIMARY;
                     paste.key.state      = SDL_PRESSED;
                     paste.key.timestamp  = SDL_GetTicks();
-                    wasUsed = dispatchEvent_Window(w, &paste);
+                    wasUsed = dispatchEvent_Window(d, &paste);
                 }
                 if (event.type == SDL_MOUSEBUTTONDOWN && event.button.button == SDL_BUTTON_RIGHT) {
-                    if (postContextClick_Window(w, &event.button)) {
+                    if (postContextClick_Window(d, &event.button)) {
                         wasUsed = iTrue;
                     }
                 }
             }
             if (isMetricsChange_UserEvent(&event)) {
-                iForIndices(i, w->roots) {
-                    updateMetrics_Root(w->roots[i]);
+                iForIndices(i, d->roots) {
+                    updateMetrics_Root(d->roots[i]);
                 }
             }
-            if (isCommand_UserEvent(&event, "lang.changed")) {
+            if (isCommand_UserEvent(&event, "lang.changed") && mw) {
 #if defined (iHaveNativeMenus)
                 /* Retranslate the menus. */
                 removeMacMenus_();
                 insertMacMenus_();
 #endif
-                invalidate_Window(w);
-                iForIndices(i, w->roots) {
-                    if (w->roots[i]) {
-                        updatePreferencesLayout_Widget(findChild_Widget(w->roots[i]->widget, "prefs"));
-                        arrange_Widget(w->roots[i]->widget);
+                invalidate_Window(d);
+                iForIndices(i, d->roots) {
+                    if (d->roots[i]) {
+                        updatePreferencesLayout_Widget(findChild_Widget(d->roots[i]->widget, "prefs"));
+                        arrange_Widget(d->roots[i]->widget);
                     }
                 }
             }
             if (event.type == SDL_MOUSEMOTION) {
-                applyCursor_Window_(w);
+                applyCursor_Window_(d);
             }
             return wasUsed;
         }
@@ -1003,6 +1062,9 @@ iBool dispatchEvent_Window(iWindow *d, const SDL_Event *ev) {
                                                              coord_MouseWheelEvent(&ev->wheel))) {
                 continue; /* Only process the event in the relevant split. */
             }
+            if (!root->widget) {
+                continue;
+            }
             setCurrent_Root(root);
             const iBool wasUsed = dispatchEvent_Widget(root->widget, ev);
             if (wasUsed) {
@@ -1044,11 +1106,40 @@ iBool postContextClick_Window(iWindow *d, const SDL_MouseButtonEvent *ev) {
     return iFalse;
 }
 
+void draw_Window(iWindow *d) {
+    if (SDL_GetWindowFlags(d->win) & SDL_WINDOW_HIDDEN) {
+        return;
+    }
+    iPaint p;
+    init_Paint(&p);
+    iRoot *root = d->roots[0];
+    setCurrent_Root(root);
+    unsetClip_Paint(&p); /* update clip to full window */
+    const iColor back = get_Color(uiBackground_ColorId);
+    SDL_SetRenderDrawColor(d->render, back.r, back.g, back.b, 255);
+    SDL_RenderClear(d->render);
+    d->frameTime = SDL_GetTicks();
+    if (isExposed_Window(d)) {
+        d->isInvalidated = iFalse;
+        extern int drawCount_;
+        drawRoot_Widget(root->widget);
+#if !defined (NDEBUG)
+        draw_Text(defaultBold_FontId, safeRect_Root(root).pos, red_ColorId, "%d", drawCount_);
+        drawCount_ = 0;
+#endif        
+    }
+//    drawRectThickness_Paint(&p, (iRect){ zero_I2(), sub_I2(d->size, one_I2()) }, gap_UI / 4, uiSeparator_ColorId);
+    setCurrent_Root(NULL);
+    SDL_RenderPresent(d->render);
+}
+
 void draw_MainWindow(iMainWindow *d) {
+    /* TODO: Try to make this a specialization of `draw_Window`? */
     iWindow *w = as_Window(d);
-    if (w->isDrawFrozen) {
+    if (d->isDrawFrozen) {
         return;
     }
+    setCurrent_Text(d->base.text);
     /* Check if root needs resizing. */ {
         iInt2 renderSize;
         SDL_GetRendererOutputSize(w->render, &renderSize.x, &renderSize.y);
@@ -1180,7 +1271,7 @@ void setUiScale_Window(iWindow *d, float uiScale) {
     }
 }
 
-void setFreezeDraw_Window(iWindow *d, iBool freezeDraw) {
+void setFreezeDraw_MainWindow(iMainWindow *d, iBool freezeDraw) {
     d->isDrawFrozen = freezeDraw;
 }
 
@@ -1231,8 +1322,23 @@ iWindow *get_Window(void) {
     return theWindow_;
 }
 
+void setCurrent_Window(iAnyWindow *d) {
+    theWindow_ = d;
+    if (type_Window(d) == main_WindowType) {
+        theMainWindow_ = d;
+    }
+    if (d) {
+        setCurrent_Text(theWindow_->text);
+        setCurrent_Root(theWindow_->keyRoot);
+    }
+    else {
+        setCurrent_Text(NULL);
+        setCurrent_Root(NULL);
+    }
+}
+
 iMainWindow *get_MainWindow(void) {
-    return as_MainWindow(theWindow_);
+    return theMainWindow_;
 }
 
 iBool isOpenGLRenderer_Window(void) {
@@ -1272,7 +1378,7 @@ void setSplitMode_MainWindow(iMainWindow *d, int splitFlags) {
     iAssert(current_Root() == NULL);
     if (d->splitMode != splitMode) {
         int oldCount = numRoots_Window(w);
-        setFreezeDraw_Window(w, iTrue);
+        setFreezeDraw_MainWindow(d, iTrue);
         if (oldCount == 2 && splitMode == 0) {
             /* Keep references to the tabs of the second root. */
             const iDocumentWidget *curPage = document_Root(w->keyRoot);
@@ -1311,6 +1417,7 @@ void setSplitMode_MainWindow(iMainWindow *d, int splitFlags) {
             }
             w->roots[newRootIndex] = new_Root();
             w->keyRoot             = w->roots[newRootIndex];
+            w->keyRoot->window     = w;
             setCurrent_Root(w->roots[newRootIndex]);
             createUserInterface_Root(w->roots[newRootIndex]);
             if (!isEmpty_String(d->pendingSplitUrl)) {
@@ -1471,3 +1578,25 @@ int snap_MainWindow(const iMainWindow *d) {
     }
     return d->place.snap;
 }
+
+/*----------------------------------------------------------------------------------------------*/
+
+iWindow *newPopup_Window(iInt2 screenPos, iWidget *rootWidget) {
+    arrange_Widget(rootWidget);
+    iWindow *win =
+        new_Window(popup_WindowType,
+                   (iRect){ screenPos, divf_I2(rootWidget->rect.size, get_Window()->pixelRatio) },
+                   SDL_WINDOW_ALWAYS_ON_TOP |
+                   SDL_WINDOW_POPUP_MENU |
+                   SDL_WINDOW_SKIP_TASKBAR);
+#if defined (iPlatformAppleDesktop)
+    hideTitleBar_MacOS(win); /* make it a borderless window */
+#endif
+    iRoot *root   = new_Root();
+    win->roots[0] = root;
+    win->keyRoot  = root;
+    root->widget  = rootWidget;
+    root->window  = win;
+    setRoot_Widget(rootWidget, root);
+    return win;
+}
diff --git a/src/ui/window.h b/src/ui/window.h
index 73e92391..f1827931 100644
--- a/src/ui/window.h
+++ b/src/ui/window.h
@@ -29,8 +29,16 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
 #include 
 #include 
 
+enum iWindowType {
+    main_WindowType,
+    popup_WindowType,
+};
+
 iDeclareType(MainWindow)
+iDeclareType(Text)
 iDeclareType(Window)
+    
+iDeclareTypeConstructionArgs(Window, enum iWindowType type, iRect rect, uint32_t flags)
 iDeclareTypeConstructionArgs(MainWindow, iRect rect)
     
 typedef iAny iAnyWindow;
@@ -71,15 +79,9 @@ enum iWindowSplit {
     noEvents_WindowSplit = iBit(11),
 };
 
-enum iWindowType {
-    main_WindowType,
-    popup_WindowType,
-};
-
 struct Impl_Window {
     enum iWindowType type;
     SDL_Window *  win;
-    iBool         isDrawFrozen; /* avoids premature draws while restoring window state */
     iBool         isExposed;
     iBool         isMinimized;
     iBool         isMouseInside;
@@ -102,11 +104,13 @@ struct Impl_Window {
     iRoot *       roots[2];     /* root widget and UI state; second one is for split mode */
     iRoot *       keyRoot;      /* root that has the current keyboard input focus */
     SDL_Texture * borderShadow;
+    iText *       text;
 };
 
 struct Impl_MainWindow {
     iWindow       base;
     iWindowPlacement place;
+    iBool         isDrawFrozen; /* avoids premature draws while restoring window state */
     int           splitMode;
     int           pendingSplitMode;
     iString *     pendingSplitUrl; /* URL to open in a newly opened split */
@@ -115,7 +119,10 @@ struct Impl_MainWindow {
 };
 
 iLocalDef enum iWindowType type_Window(const iAnyWindow *d) {
-    return ((const iWindow *) d)->type;
+    if (d) {
+        return ((const iWindow *) d)->type;
+    }
+    return main_WindowType;
 }
 
 uint32_t        id_Window               (const iWindow *);
@@ -131,11 +138,11 @@ int             numRoots_Window         (const iWindow *);
 iRoot *         findRoot_Window         (const iWindow *, const iWidget *widget);
 iRoot *         otherRoot_Window        (const iWindow *, iRoot *root);
 
+iBool       processEvent_Window     (iWindow *, const SDL_Event *);
 iBool       dispatchEvent_Window    (iWindow *, const SDL_Event *);
 void        invalidate_Window       (iAnyWindow *); /* discard all cached graphics */
 void        draw_Window             (iWindow *);
 void        setUiScale_Window       (iWindow *, float uiScale);
-void        setFreezeDraw_Window    (iWindow *, iBool freezeDraw);
 void        setCursor_Window        (iWindow *, int cursor);
 iBool       setKeyRoot_Window       (iWindow *, iRoot *root);
 iBool       postContextClick_Window (iWindow *, const SDL_MouseButtonEvent *);
@@ -143,6 +150,8 @@ iBool       postContextClick_Window (iWindow *, const SDL_MouseButtonEvent *);
 iWindow *   get_Window              (void);
 iBool       isOpenGLRenderer_Window (void);
 
+void        setCurrent_Window       (iAnyWindow *);
+
 iLocalDef iBool isExposed_Window(const iWindow *d) {
     iAssert(d);
     return d->isExposed;
@@ -158,6 +167,10 @@ iLocalDef const iWindow *constAs_Window(const iAnyWindow *d) {
     return (const iWindow *) d;
 }
 
+iLocalDef iText *text_Window(const iAnyWindow *d) {
+    return constAs_Window(d)->text;
+}
+
 /*----------------------------------------------------------------------------------------------*/
 
 iLocalDef iWindow *asWindow_MainWindow(iMainWindow *d) {
@@ -167,6 +180,7 @@ iLocalDef iWindow *asWindow_MainWindow(iMainWindow *d) {
 
 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);
 void        setSplitMode_MainWindow         (iMainWindow *, int splitMode);
 void        checkPendingSplit_MainWindow    (iMainWindow *);
@@ -196,3 +210,7 @@ iLocalDef const iMainWindow *constAs_MainWindow(const iAnyWindow *d) {
     iAssert(type_Window(d) == main_WindowType);
     return (const iMainWindow *) d;
 }
+
+/*----------------------------------------------------------------------------------------------*/
+
+iWindow *   newPopup_Window    (iInt2 screenPos, iWidget *rootWidget);
Proxy Information
Original URL
gemini://git.skyjake.fi/lagrange/work%2Fv1.7/cdiff/2d81addf78d6a8b0fb2f2959b04a385c4adffdf2
Status Code
Success (20)
Meta
text/gemini; charset=utf-8
Capsule Response Time
106.662881 milliseconds
Gemini-to-HTML Time
3.145036 milliseconds

This content has been proxied by September (ba2dc).