1 /* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi>
2 
3 Redistribution and use in source and binary forms, with or without
4 modification, are permitted provided that the following conditions are met:
5 
6 1. Redistributions of source code must retain the above copyright notice, this
7    list of conditions and the following disclaimer.
8 2. Redistributions in binary form must reproduce the above copyright notice,
9    this list of conditions and the following disclaimer in the documentation
10    and/or other materials provided with the distribution.
11 
12 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
13 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
14 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
15 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
16 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
17 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
18 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
19 ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
20 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
21 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22 
23 #include "window.h"
24 
25 #include "../app.h"
26 #include "bookmarks.h"
27 #include "command.h"
28 #include "defs.h"
29 #include "embedded.h"
30 #include "keys.h"
31 #include "labelwidget.h"
32 #include "documentwidget.h"
33 #include "sidebarwidget.h"
34 #include "paint.h"
35 #include "root.h"
36 #include "touch.h"
37 #include "util.h"
38 
39 #if defined (iPlatformMsys)
40 #   include "../win32.h"
41 #endif
42 #if defined (iPlatformAppleDesktop)
43 #   include "macos.h"
44 #endif
45 #if defined (iPlatformAppleMobile)
46 #   include "ios.h"
47 #endif
48 
49 #include <the_Foundation/file.h>
50 #include <the_Foundation/path.h>
51 #include <the_Foundation/regexp.h>
52 #include <SDL_hints.h>
53 #include <SDL_timer.h>
54 #include <SDL_syswm.h>
55 
56 #define STB_IMAGE_IMPLEMENTATION
57 #define STB_IMAGE_RESIZE_IMPLEMENTATION
58 #include "stb_image.h"
59 #include "stb_image_resize.h"
60 
61 static iWindow *    theWindow_;
62 static iMainWindow *theMainWindow_;
63 
64 #if defined (iPlatformApple) || defined (iPlatformLinux) || defined (iPlatformOther)
65 static float initialUiScale_ = 1.0f;
66 #else
67 static float initialUiScale_ = 1.1f;
68 #endif
69 
70 static iBool isOpenGLRenderer_;
71 
72 iDefineTypeConstructionArgs(Window,
73                             (enum iWindowType type, iRect rect, uint32_t flags),
74                             type, rect, flags)
75 iDefineTypeConstructionArgs(MainWindow, (iRect rect), rect)
76 
77 /* TODO: Define menus per platform. */
78 
79 #if defined (iHaveNativeMenus)
80 /* Using native menus. */
81 static const iMenuItem fileMenuItems_[] = {
82     { "${menu.newtab}", SDLK_t, KMOD_PRIMARY, "tabs.new" },
83     { "${menu.openlocation}", SDLK_l, KMOD_PRIMARY, "navigate.focus" },
84     { "---", 0, 0, NULL },
85     { saveToDownloads_Label, SDLK_s, KMOD_PRIMARY, "document.save" },
86     { "---", 0, 0, NULL },
87     { "${menu.downloads}", 0, 0, "downloads.open" },
88 };
89 
90 static const iMenuItem editMenuItems_[] = {
91     { "${menu.cut}", SDLK_x, KMOD_PRIMARY, "input.copy cut:1" },
92     { "${menu.copy}", SDLK_c, KMOD_PRIMARY, "copy" },
93     { "${menu.paste}", SDLK_v, KMOD_PRIMARY, "input.paste" },
94     { "---", 0, 0, NULL },
95     { "${menu.copy.pagelink}", SDLK_c, KMOD_PRIMARY | KMOD_SHIFT, "document.copylink" },
96     { "---", 0, 0, NULL },
97     { "${macos.menu.find}", SDLK_f, KMOD_PRIMARY, "focus.set id:find.input" },
98 };
99 
100 static const iMenuItem viewMenuItems_[] = {
101     { "${menu.show.bookmarks}", '1', KMOD_PRIMARY, "sidebar.mode arg:0 toggle:1" },
102     { "${menu.show.feeds}", '2', KMOD_PRIMARY, "sidebar.mode arg:1 toggle:1" },
103     { "${menu.show.history}", '3', KMOD_PRIMARY, "sidebar.mode arg:2 toggle:1" },
104     { "${menu.show.identities}", '4', KMOD_PRIMARY, "sidebar.mode arg:3 toggle:1" },
105     { "${menu.show.outline}", '5', KMOD_PRIMARY, "sidebar.mode arg:4 toggle:1" },
106     { "${menu.sidebar.left}", SDLK_l, KMOD_PRIMARY | KMOD_SHIFT, "sidebar.toggle" },
107     { "${menu.sidebar.right}", SDLK_p, KMOD_PRIMARY | KMOD_SHIFT, "sidebar2.toggle" },
108     { "---", 0, 0, NULL },
109     { "${menu.back}", SDLK_LEFTBRACKET, KMOD_PRIMARY, "navigate.back" },
110     { "${menu.forward}", SDLK_RIGHTBRACKET, KMOD_PRIMARY, "navigate.forward" },
111     { "${menu.parent}", navigateParent_KeyShortcut, "navigate.parent" },
112     { "${menu.root}", navigateRoot_KeyShortcut, "navigate.root" },
113     { "${menu.reload}", reload_KeyShortcut, "navigate.reload" },
114     { "---", 0, 0, NULL },
115     { "${menu.zoom.in}", SDLK_EQUALS, KMOD_PRIMARY, "zoom.delta arg:10" },
116     { "${menu.zoom.out}", SDLK_MINUS, KMOD_PRIMARY, "zoom.delta arg:-10" },
117     { "${menu.zoom.reset}", SDLK_0, KMOD_PRIMARY, "zoom.set arg:100" },
118     { "${menu.view.split}", SDLK_j, KMOD_PRIMARY, "splitmenu.open" },
119 };
120 
121 static iMenuItem bookmarksMenuItems_[] = {
122     { "${menu.page.bookmark}", SDLK_d, KMOD_PRIMARY, "bookmark.add" },
123     { "${menu.page.subscribe}", subscribeToPage_KeyModifier, "feeds.subscribe" },
124     { "${menu.newfolder}", 0, 0, "bookmarks.addfolder" },
125     { "---", 0, 0, NULL },
126     { "${menu.sort.alpha}", 0, 0, "bookmarks.sort" },
127     { "${menu.import.links}", 0, 0, "bookmark.links confirm:1" },
128     { "---", 0, 0, NULL },
129     { "${macos.menu.bookmarks.list}", 0, 0, "open url:about:bookmarks" },
130     { "${macos.menu.bookmarks.bytag}", 0, 0, "open url:about:bookmarks?tags" },
131     { "${macos.menu.bookmarks.bytime}", 0, 0, "open url:about:bookmarks?created" },
132     { "${menu.feeds.entrylist}", 0, 0, "open url:about:feeds" },
133     { "---", 0, 0, NULL },
134     { "---", 0, 0, NULL },
135     { "${menu.bookmarks.refresh}", 0, 0, "bookmarks.reload.remote" },
136     { "${menu.feeds.refresh}", SDLK_r, KMOD_PRIMARY | KMOD_SHIFT, "feeds.refresh" },
137 };
138 
139 static const iMenuItem identityMenuItems_[] = {
140     { "${menu.identity.new}", SDLK_n, KMOD_PRIMARY | KMOD_SHIFT, "ident.new" },
141     { "---", 0, 0, NULL },
142     { "${menu.identity.import}", SDLK_i, KMOD_PRIMARY | KMOD_SHIFT, "ident.import" },
143 };
144 
145 static const iMenuItem helpMenuItems_[] = {
146     { "${menu.help}", 0, 0, "!open url:about:help" },
147     { "${menu.releasenotes}", 0, 0, "!open url:about:version" },
148     { "---", 0, 0, NULL },
149     { "${menu.aboutpages}", 0, 0, "!open url:about:about" },
150     { "${menu.debug}", 0, 0, "!open url:about:debug" },
151 };
152 
insertMacMenus_(void)153 static void insertMacMenus_(void) {
154     insertMenuItems_MacOS("${menu.title.file}", 1, fileMenuItems_, iElemCount(fileMenuItems_));
155     insertMenuItems_MacOS("${menu.title.edit}", 2, editMenuItems_, iElemCount(editMenuItems_));
156     insertMenuItems_MacOS("${menu.title.view}", 3, viewMenuItems_, iElemCount(viewMenuItems_));
157     insertMenuItems_MacOS("${menu.title.bookmarks}", 4, bookmarksMenuItems_, iElemCount(bookmarksMenuItems_));
158     insertMenuItems_MacOS("${menu.title.identity}", 5, identityMenuItems_, iElemCount(identityMenuItems_));
159     insertMenuItems_MacOS("${menu.title.help}", 7, helpMenuItems_, iElemCount(helpMenuItems_));
160 }
161 
removeMacMenus_(void)162 static void removeMacMenus_(void) {
163     removeMenu_MacOS(7);
164     removeMenu_MacOS(5);
165     removeMenu_MacOS(4);
166     removeMenu_MacOS(3);
167     removeMenu_MacOS(2);
168     removeMenu_MacOS(1);
169 }
170 #endif
171 
numRoots_Window(const iWindow * d)172 int numRoots_Window(const iWindow *d) {
173     int num = 0;
174     iForIndices(i, d->roots) {
175         if (d->roots[i]) num++;
176     }
177     return num;
178 }
179 
windowSizeChanged_MainWindow_(iMainWindow * d)180 static void windowSizeChanged_MainWindow_(iMainWindow *d) {
181     const int numRoots = numRoots_Window(as_Window(d));
182     const iInt2 rootSize = d->base.size;
183     const int weights[2] = {
184         d->base.roots[0] ? (d->splitMode & twoToOne_WindowSplit ? 2 : 1) : 0,
185         d->base.roots[1] ? (d->splitMode & oneToTwo_WindowSplit ? 2 : 1) : 0,
186     };
187     const int totalWeight = weights[0] + weights[1];
188     int w = 0;
189     iForIndices(i, d->base.roots) {
190         iRoot *root = d->base.roots[i];
191         if (root) {
192             iRect *rect = &root->widget->rect;
193             /* Horizontal split frame. */
194             if (d->splitMode & vertical_WindowSplit) {
195                 rect->pos  = init_I2(0, rootSize.y * w / totalWeight);
196                 rect->size = init_I2(rootSize.x, rootSize.y * (w + weights[i]) / totalWeight - rect->pos.y);
197             }
198             else {
199                 rect->pos  = init_I2(rootSize.x * w / totalWeight, 0);
200                 rect->size = init_I2(rootSize.x * (w + weights[i]) / totalWeight - rect->pos.x, rootSize.y);
201             }
202             w += weights[i];
203             root->widget->minSize = rect->size;
204             updatePadding_Root(root);
205             arrange_Widget(root->widget);
206         }
207     }
208 }
209 
setupUserInterface_MainWindow(iMainWindow * d)210 static void setupUserInterface_MainWindow(iMainWindow *d) {
211 #if defined (iHaveNativeMenus)
212     insertMacMenus_();
213 #endif
214     /* One root is created by default. */
215     d->base.roots[0] = new_Root();
216     d->base.roots[0]->window = as_Window(d);
217     setCurrent_Root(d->base.roots[0]);
218     createUserInterface_Root(d->base.roots[0]);
219     setCurrent_Root(NULL);
220     /* One of the roots always has keyboard input focus. */
221     d->base.keyRoot = d->base.roots[0];
222 }
223 
updateSize_MainWindow_(iMainWindow * d,iBool notifyAlways)224 static void updateSize_MainWindow_(iMainWindow *d, iBool notifyAlways) {
225     iInt2 *size = &d->base.size;
226     const iInt2 oldSize = *size;
227     SDL_GetRendererOutputSize(d->base.render, &size->x, &size->y);
228     size->y -= d->keyboardHeight;
229     if (notifyAlways || !isEqual_I2(oldSize, *size)) {
230         windowSizeChanged_MainWindow_(d);
231         if (!isEqual_I2(*size, d->place.lastNotifiedSize)) {
232             const iBool isHoriz = (d->place.lastNotifiedSize.x != size->x);
233             const iBool isVert  = (d->place.lastNotifiedSize.y != size->y);
234             postCommandf_App("window.resized width:%d height:%d horiz:%d vert:%d",
235                              size->x,
236                              size->y,
237                              isHoriz,
238                              isVert);
239             postCommand_App("widget.overflow"); /* check bounds with updated sizes */
240         }
241         postRefresh_App();
242         d->place.lastNotifiedSize = *size;
243     }
244 }
245 
drawWhileResizing_MainWindow(iMainWindow * d,int w,int h)246 void drawWhileResizing_MainWindow(iMainWindow *d, int w, int h) {
247     draw_MainWindow(d);
248 }
249 
pixelRatio_Window_(const iWindow * d)250 static float pixelRatio_Window_(const iWindow *d) {
251     int dx, x;
252     SDL_GetRendererOutputSize(d->render, &dx, NULL);
253     SDL_GetWindowSize(d->win, &x, NULL);
254     return (float) dx / (float) x;
255 }
256 
257 #if defined (iPlatformApple)
258 #   define baseDPI_Window   113.5f
259 #else
260 #   define baseDPI_Window   96.0f
261 #endif
262 
displayScale_Window_(const iWindow * d)263 static float displayScale_Window_(const iWindow *d) {
264     /* The environment variable LAGRANGE_OVERRIDE_DPI can be used to override the automatic
265        display DPI detection. If not set, or is an empty string, ignore it.
266        Note: the same value used for all displays. */
267     const char *LAGRANGE_OVERRIDE_DPI = getenv("LAGRANGE_OVERRIDE_DPI");
268     if (LAGRANGE_OVERRIDE_DPI && *LAGRANGE_OVERRIDE_DPI) {
269         /* If the user has set the env var, but it is not an int, atoi */
270         /* will return 0, in which case we just guess and return 96 DPI.  */
271         const int envDpi = atoi(LAGRANGE_OVERRIDE_DPI);
272         if (envDpi > 0) {
273             return ((float) envDpi) / baseDPI_Window;
274         }
275         fprintf(stderr, "[window] WARNING: failed to parse LAGRANGE_OVERRIDE_DPI='%s', "
276                 "ignoring it\n", LAGRANGE_OVERRIDE_DPI);
277         /* To avoid showing the warning multiple times, overwrite
278          LAGRANGE_OVERRIDE_DPI with the empty string. */
279         setenv("LAGRANGE_OVERRIDE_DPI", "", 1);
280     }
281 #if defined (iPlatformApple)
282     iUnused(d);
283     /* Apple UI sizes are fixed and only scaled by pixel ratio. */
284     /* TODO: iOS text size setting? */
285     return 1.0f;
286 #elif defined (iPlatformMsys)
287     iUnused(d);
288     return desktopDPI_Win32();
289 #else
290     if (isRunningUnderWindowSystem_App()) {
291         float vdpi = 0.0f;
292         SDL_GetDisplayDPI(SDL_GetWindowDisplayIndex(d->win), NULL, NULL, &vdpi);
293 //      printf("DPI: %f\n", vdpi);
294         const float factor = vdpi / baseDPI_Window / pixelRatio_Window_(d);
295         return iMax(1.0f, factor);
296     }
297     return 1.0f;
298 #endif
299 }
300 
drawBlank_Window_(iWindow * d)301 static void drawBlank_Window_(iWindow *d) {
302 //    const iColor bg = get_Color(uiBackground_ColorId);
303     const iColor bg = { 128, 128, 128, 255 }; /* TODO: Have no root yet. */
304     SDL_SetRenderDrawColor(d->render, bg.r, bg.g, bg.b, 255);
305     SDL_RenderClear(d->render);
306     SDL_RenderPresent(d->render);
307 }
308 
rootAt_Window_(const iWindow * d,iInt2 coord)309 static iRoot *rootAt_Window_(const iWindow *d, iInt2 coord) {
310     iForIndices(i, d->roots) {
311         iRoot *root = d->roots[i];
312         if (root && contains_Rect(rect_Root(root), coord)) {
313             return root;
314         }
315     }
316     return d->roots[0];
317 }
318 
319 #if defined (LAGRANGE_ENABLE_CUSTOM_FRAME)
hitTest_MainWindow_(SDL_Window * win,const SDL_Point * pos,void * data)320 static SDL_HitTestResult hitTest_MainWindow_(SDL_Window *win, const SDL_Point *pos, void *data) {
321     iMainWindow *d = data;
322     iAssert(d->base.win == win);
323     if (SDL_GetWindowFlags(win) & (SDL_WINDOW_MOUSE_CAPTURE | SDL_WINDOW_FULLSCREEN_DESKTOP)) {
324         return SDL_HITTEST_NORMAL;
325     }
326     const int snap = snap_MainWindow(d);
327     int w, h;
328     SDL_GetWindowSize(win, &w, &h);
329     /* TODO: Check if inside the caption label widget. */
330     const iBool isLeft   = pos->x < gap_UI;
331     const iBool isRight  = pos->x >= w - gap_UI;
332     const iBool isTop    = pos->y < gap_UI && snap != yMaximized_WindowSnap;
333     const iBool isBottom = pos->y >= h - gap_UI && snap != yMaximized_WindowSnap;
334     const int   captionHeight = lineHeight_Text(uiContent_FontId) + gap_UI * 2;
335     const int   rightEdge     = left_Rect(bounds_Widget(findChild_Widget(
336                                     rootAt_Window_(as_Window(d), init_I2(pos->x, pos->y))->widget,
337                                     "winbar.min")));
338     d->place.lastHit = SDL_HITTEST_NORMAL;
339     if (snap != maximized_WindowSnap) {
340         if (isLeft) {
341             return pos->y < captionHeight       ? SDL_HITTEST_RESIZE_TOPLEFT
342                    : pos->y > h - captionHeight ? SDL_HITTEST_RESIZE_BOTTOMLEFT
343                                                 : (d->place.lastHit = SDL_HITTEST_RESIZE_LEFT);
344         }
345         if (isRight) {
346             return pos->y < captionHeight       ? SDL_HITTEST_RESIZE_TOPRIGHT
347                    : pos->y > h - captionHeight ? SDL_HITTEST_RESIZE_BOTTOMRIGHT
348                                                 : (d->place.lastHit = SDL_HITTEST_RESIZE_RIGHT);
349         }
350         if (isTop) {
351             return pos->x < captionHeight       ? SDL_HITTEST_RESIZE_TOPLEFT
352                    : pos->x > w - captionHeight ? SDL_HITTEST_RESIZE_TOPRIGHT
353                                                 : (d->place.lastHit = SDL_HITTEST_RESIZE_TOP);
354         }
355         if (isBottom) {
356             return pos->x < captionHeight       ? SDL_HITTEST_RESIZE_BOTTOMLEFT
357                    : pos->x > w - captionHeight ? SDL_HITTEST_RESIZE_BOTTOMRIGHT
358                                                 : (d->place.lastHit = SDL_HITTEST_RESIZE_BOTTOM);
359         }
360     }
361     if (pos->x < rightEdge && pos->y < captionHeight) {
362         return SDL_HITTEST_DRAGGABLE;
363     }
364     return SDL_HITTEST_NORMAL;
365 }
366 
hitTest_MainWindow(const iMainWindow * d,iInt2 pos)367 SDL_HitTestResult hitTest_MainWindow(const iMainWindow *d, iInt2 pos) {
368     return hitTest_MainWindow_(d->base.win, &(SDL_Point){ pos.x, pos.y }, iConstCast(void *, d));
369 }
370 #endif
371 
create_Window_(iWindow * d,iRect rect,uint32_t flags)372 void create_Window_(iWindow *d, iRect rect, uint32_t flags) {
373     flags |= SDL_WINDOW_ALLOW_HIGHDPI | SDL_WINDOW_HIDDEN;
374     if (d->type == main_WindowType) {
375         flags |= SDL_WINDOW_RESIZABLE;
376 #if defined (LAGRANGE_ENABLE_CUSTOM_FRAME)
377         if (prefs_App()->customFrame) {
378             /* We are drawing a custom frame so hide the default one. */
379             flags |= SDL_WINDOW_BORDERLESS;
380         }
381 #endif
382     }
383     const iBool setPos = left_Rect(rect) >= 0 || top_Rect(rect) >= 0;
384     d->win = SDL_CreateWindow("",
385                               setPos ? left_Rect(rect) : SDL_WINDOWPOS_CENTERED,
386                               setPos ? top_Rect(rect) : SDL_WINDOWPOS_CENTERED,
387                               width_Rect(rect),
388                               height_Rect(rect),
389                               flags);
390     if (!d->win) {
391         if (flags & SDL_WINDOW_OPENGL) {
392             /* Try without OpenGL support, then. */
393             setForceSoftwareRender_App(iTrue);
394             d->win = SDL_CreateWindow("",
395                                       setPos ? left_Rect(rect) : SDL_WINDOWPOS_CENTERED,
396                                       setPos ? top_Rect(rect) : SDL_WINDOWPOS_CENTERED,
397                                       width_Rect(rect),
398                                       height_Rect(rect),
399                                       flags & ~SDL_WINDOW_OPENGL);
400         }
401         if (!d->win) {
402             fprintf(stderr, "[window] failed to create window: %s\n", SDL_GetError());
403             exit(-3);
404         }
405     }
406     if (forceSoftwareRender_App()) {
407         SDL_SetHint(SDL_HINT_RENDER_DRIVER, "software");
408     }
409     d->render = SDL_CreateRenderer(
410         d->win,
411         -1,
412         (forceSoftwareRender_App() ? SDL_RENDERER_SOFTWARE : SDL_RENDERER_ACCELERATED) |
413             SDL_RENDERER_PRESENTVSYNC | SDL_RENDERER_TARGETTEXTURE);
414     if (!d->render) {
415         /* Try a basic software rendering instead. */
416         SDL_SetHint(SDL_HINT_RENDER_DRIVER, "software");
417         d->render = SDL_CreateRenderer(d->win, -1, SDL_RENDERER_SOFTWARE);
418         if (!d->render) {
419             /* This shouldn't fail.-..? */
420             fprintf(stderr, "[window] failed to create renderer: %s\n", SDL_GetError());
421             exit(-4);
422         }
423     }
424 #if defined(LAGRANGE_ENABLE_CUSTOM_FRAME)
425     if (type_Window(d) == main_WindowType && prefs_App()->customFrame) {
426         /* Register a handler for window hit testing (drag, resize). */
427         SDL_SetWindowHitTest(d->win, hitTest_MainWindow_, d);
428         SDL_SetWindowResizable(d->win, SDL_TRUE);
429     }
430 #endif
431 }
432 
loadImage_(const iBlock * data,int resized)433 static SDL_Surface *loadImage_(const iBlock *data, int resized) {
434     int      w = 0, h = 0, num = 4;
435     stbi_uc *pixels = stbi_load_from_memory(
436         constData_Block(data), size_Block(data), &w, &h, &num, STBI_rgb_alpha);
437     if (resized) {
438         stbi_uc *rsPixels = malloc(num * resized * resized);
439         stbir_resize_uint8(pixels, w, h, 0, rsPixels, resized, resized, 0, num);
440         free(pixels);
441         pixels = rsPixels;
442         w = h = resized;
443     }
444     return SDL_CreateRGBSurfaceWithFormatFrom(
445         pixels, w, h, 8 * num, w * num, SDL_PIXELFORMAT_RGBA32);
446 }
447 
init_Window(iWindow * d,enum iWindowType type,iRect rect,uint32_t flags)448 void init_Window(iWindow *d, enum iWindowType type, iRect rect, uint32_t flags) {
449     d->type          = type;
450     d->win           = NULL;
451     d->size          = zero_I2(); /* will be updated below */
452     d->hover         = NULL;
453     d->lastHover     = NULL;
454     d->mouseGrab     = NULL;
455     d->focus         = NULL;
456     d->pendingCursor = NULL;
457     d->isExposed     = iFalse;
458     d->isMinimized   = iFalse;
459     d->isInvalidated = iFalse; /* set when posting event, to avoid repeated events */
460     d->isMouseInside = iTrue;
461     d->ignoreClick   = iFalse;
462     d->focusGainedAt = SDL_GetTicks();
463     d->presentTime   = 0.0;
464     d->frameTime     = SDL_GetTicks();
465     d->keyRoot       = NULL;
466     d->borderShadow  = NULL;
467     iZap(d->roots);
468     iZap(d->cursors);
469     create_Window_(d, rect, flags);
470         /* No luck, maybe software only? This should always work as long as there is a display. */
471     //    SDL_SetHint(SDL_HINT_RENDER_DRIVER, "software");
472 //        flags &= ~SDL_WINDOW_OPENGL;
473 //        start_PerfTimer(create_Window_);
474 //        if (!create_Window_(d, rect, flags)) {
475 //            exit(-2);
476 //        }
477 //        stop_PerfTimer(create_Window_);
478 //    }
479 //    start_PerfTimer(setPos);
480 //    if (left_Rect(rect) >= 0 || top_Rect(rect) >= 0) {
481 //        SDL_SetWindowPosition(d->win, left_Rect(rect), top_Rect(rect));
482 //    }
483 //    stop_PerfTimer(setPos);
484     SDL_GetRendererOutputSize(d->render, &d->size.x, &d->size.y);
485     /* Renderer info. */ {
486         SDL_RendererInfo info;
487         SDL_GetRendererInfo(d->render, &info);
488         printf("[window] renderer: %s%s\n",
489                info.name,
490                info.flags & SDL_RENDERER_ACCELERATED ? " (accelerated)" : "");
491     }
492     drawBlank_Window_(d);
493     d->pixelRatio   = pixelRatio_Window_(d); /* point/pixel conversion */
494     d->displayScale = displayScale_Window_(d);
495     d->uiScale      = initialUiScale_;
496     /* TODO: Ratios, scales, and metrics must be window-specific, not global. */
497     if (d->type == main_WindowType) {
498         setScale_Metrics(d->pixelRatio * d->displayScale * d->uiScale);
499     }
500     d->text = new_Text(d->render);
501 }
502 
deinitRoots_Window_(iWindow * d)503 static void deinitRoots_Window_(iWindow *d) {
504     iRecycle();
505     iForIndices(i, d->roots) {
506         if (d->roots[i]) {
507             setCurrent_Root(d->roots[i]);
508             delete_Root(d->roots[i]);
509             d->roots[i] = NULL;
510         }
511     }
512     setCurrent_Root(NULL);
513 }
514 
deinit_Window(iWindow * d)515 void deinit_Window(iWindow *d) {
516     if (d->type == popup_WindowType) {
517         removePopup_App(d);
518     }
519     deinitRoots_Window_(d);
520     delete_Text(d->text);
521     SDL_DestroyRenderer(d->render);
522     SDL_DestroyWindow(d->win);
523     iForIndices(i, d->cursors) {
524         if (d->cursors[i]) {
525             SDL_FreeCursor(d->cursors[i]);
526         }
527     }
528 }
529 
init_MainWindow(iMainWindow * d,iRect rect)530 void init_MainWindow(iMainWindow *d, iRect rect) {
531     theWindow_ = &d->base;
532     theMainWindow_ = d;
533     uint32_t flags = 0;
534 #if defined (iPlatformAppleDesktop)
535     SDL_SetHint(SDL_HINT_RENDER_DRIVER, shouldDefaultToMetalRenderer_MacOS() ? "metal" : "opengl");
536     flags |= shouldDefaultToMetalRenderer_MacOS() ? SDL_WINDOW_METAL : SDL_WINDOW_OPENGL;
537 #elif defined (iPlatformAppleMobile)
538     SDL_SetHint(SDL_HINT_RENDER_DRIVER, "metal");
539     flags |= SDL_WINDOW_METAL;
540 #else
541     if (!forceSoftwareRender_App()) {
542         flags |= SDL_WINDOW_OPENGL;
543     }
544 #endif
545     SDL_SetHint(SDL_HINT_RENDER_VSYNC, "1");
546     init_Window(&d->base, main_WindowType, rect, flags);
547     d->isDrawFrozen           = iTrue;
548     d->splitMode              = 0;
549     d->pendingSplitMode       = 0;
550     d->pendingSplitUrl        = new_String();
551     d->place.initialPos       = rect.pos;
552     d->place.normalRect       = rect;
553     d->place.lastNotifiedSize = zero_I2();
554     d->place.snap             = 0;
555     d->keyboardHeight         = 0;
556 #if defined(iPlatformMobile)
557     const iInt2 minSize = zero_I2(); /* windows aren't independently resizable */
558 #else
559     const iInt2 minSize = init_I2(425, 325);
560 #endif
561     SDL_SetWindowMinimumSize(d->base.win, minSize.x, minSize.y);
562     SDL_SetWindowTitle(d->base.win, "Lagrange");
563     /* Some info. */ {
564         SDL_RendererInfo info;
565         SDL_GetRendererInfo(d->base.render, &info);
566         isOpenGLRenderer_ = !iCmpStr(info.name, "opengl");
567 #if !defined(NDEBUG)
568         printf("[window] max texture size: %d x %d\n",
569                info.max_texture_width,
570                info.max_texture_height);
571         for (size_t i = 0; i < info.num_texture_formats; ++i) {
572             printf("[window] supported texture format: %s\n",
573                    SDL_GetPixelFormatName(info.texture_formats[i]));
574         }
575 #endif
576     }
577 #if defined(iPlatformMsys)
578     SDL_SetWindowMinimumSize(d->base.win, minSize.x * d->base.displayScale, minSize.y * d->base.displayScale);
579     useExecutableIconResource_SDLWindow(d->base.win);
580     enableDarkMode_SDLWindow(d->base.win);
581 #endif
582 #if defined (iPlatformLinux)
583     SDL_SetWindowMinimumSize(d->base.win, minSize.x * d->base.pixelRatio, minSize.y * d->base.pixelRatio);
584     /* Load the window icon. */ {
585         SDL_Surface *surf = loadImage_(&imageLagrange64_Embedded, 0);
586         SDL_SetWindowIcon(d->base.win, surf);
587         free(surf->pixels);
588         SDL_FreeSurface(surf);
589     }
590 #endif
591 #if defined (iPlatformAppleMobile)
592     setupWindow_iOS(as_Window(d));
593 #endif
594     setCurrent_Text(d->base.text);
595     SDL_GetRendererOutputSize(d->base.render, &d->base.size.x, &d->base.size.y);
596     setupUserInterface_MainWindow(d);
597     postCommand_App("~bindings.changed"); /* update from bindings */
598     /* Load the border shadow texture. */ {
599         SDL_Surface *surf = loadImage_(&imageShadow_Embedded, 0);
600         d->base.borderShadow = SDL_CreateTextureFromSurface(d->base.render, surf);
601         SDL_SetTextureBlendMode(d->base.borderShadow, SDL_BLENDMODE_BLEND);
602         free(surf->pixels);
603         SDL_FreeSurface(surf);
604     }
605     d->appIcon = NULL;
606 #if defined (LAGRANGE_ENABLE_CUSTOM_FRAME)
607     /* Load the app icon for drawing in the title bar. */
608     if (prefs_App()->customFrame) {
609         SDL_Surface *surf = loadImage_(&imageLagrange64_Embedded, appIconSize_Root());
610         SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "0");
611         d->appIcon = SDL_CreateTextureFromSurface(d->base.render, surf);
612         free(surf->pixels);
613         SDL_FreeSurface(surf);
614         /* We need to observe non-client-area events. */
615         SDL_EventState(SDL_SYSWMEVENT, SDL_TRUE);
616     }
617 #endif
618     SDL_HideWindow(d->base.win);
619 }
620 
deinit_MainWindow(iMainWindow * d)621 void deinit_MainWindow(iMainWindow *d) {
622     deinitRoots_Window_(as_Window(d));
623     if (theWindow_ == as_Window(d)) {
624         theWindow_ = NULL;
625     }
626     if (theMainWindow_ == d) {
627         theMainWindow_ = NULL;
628     }
629     delete_String(d->pendingSplitUrl);
630     deinit_Window(&d->base);
631 }
632 
renderer_Window(const iWindow * d)633 SDL_Renderer *renderer_Window(const iWindow *d) {
634     return d->render;
635 }
636 
maxTextureSize_Window(const iWindow * d)637 iInt2 maxTextureSize_Window(const iWindow *d) {
638     SDL_RendererInfo info;
639     SDL_GetRendererInfo(d->render, &info);
640     return init_I2(info.max_texture_width, info.max_texture_height);
641 }
642 
isFullscreen_MainWindow(const iMainWindow * d)643 iBool isFullscreen_MainWindow(const iMainWindow *d) {
644     return snap_MainWindow(d) == fullscreen_WindowSnap;
645 }
646 
findRoot_Window(const iWindow * d,const iWidget * widget)647 iRoot *findRoot_Window(const iWindow *d, const iWidget *widget) {
648     while (widget->parent) {
649         widget = widget->parent;
650     }
651     iForIndices(i, d->roots) {
652         if (d->roots[i] && d->roots[i]->widget == widget) {
653             return d->roots[i];
654         }
655     }
656     return NULL;
657 }
658 
otherRoot_Window(const iWindow * d,iRoot * root)659 iRoot *otherRoot_Window(const iWindow *d, iRoot *root) {
660     return root == d->roots[0] && d->roots[1] ? d->roots[1] : d->roots[0];
661 }
662 
invalidate_MainWindow_(iMainWindow * d,iBool forced)663 static void invalidate_MainWindow_(iMainWindow *d, iBool forced) {
664     if (d && (!d->base.isInvalidated || forced)) {
665         d->base.isInvalidated = iTrue;
666         resetFonts_Text(text_Window(d));
667         postCommand_App("theme.changed auto:1"); /* forces UI invalidation */
668     }
669 }
670 
invalidate_Window(iAnyWindow * d)671 void invalidate_Window(iAnyWindow *d) {
672     if (type_Window(d) == main_WindowType) {
673         invalidate_MainWindow_(as_MainWindow(d), iFalse);
674     }
675     else {
676         iAssert(type_Window(d) == main_WindowType);
677     }
678 }
679 
isNormalPlacement_MainWindow_(const iMainWindow * d)680 static iBool isNormalPlacement_MainWindow_(const iMainWindow *d) {
681     if (d->isDrawFrozen) return iFalse;
682 #if defined (iPlatformApple)
683     /* Maximized mode is not special on macOS. */
684     if (snap_MainWindow(d) == maximized_WindowSnap) {
685         return iTrue;
686     }
687 #endif
688     if (snap_MainWindow(d)) return iFalse;
689     return !(SDL_GetWindowFlags(d->base.win) & SDL_WINDOW_MINIMIZED);
690 }
691 
unsnap_MainWindow_(iMainWindow * d,const iInt2 * newPos)692 static iBool unsnap_MainWindow_(iMainWindow *d, const iInt2 *newPos) {
693 #if defined (LAGRANGE_ENABLE_CUSTOM_FRAME)
694     if (!prefs_App()->customFrame) {
695         return iFalse;
696     }
697     const int snap = snap_MainWindow(d);
698     if (snap == yMaximized_WindowSnap || snap == left_WindowSnap || snap == right_WindowSnap) {
699         if (!newPos || (d->place.lastHit == SDL_HITTEST_RESIZE_LEFT ||
700                         d->place.lastHit == SDL_HITTEST_RESIZE_RIGHT)) {
701             return iFalse;
702         }
703         if (newPos) {
704             SDL_Rect usable;
705             SDL_GetDisplayUsableBounds(SDL_GetWindowDisplayIndex(d->base.win), &usable);
706             /* Snap to top. */
707             if (snap == yMaximized_WindowSnap &&
708                 iAbs(newPos->y - usable.y) < lineHeight_Text(uiContent_FontId) * 2) {
709                 setSnap_MainWindow(d, redo_WindowSnap | yMaximized_WindowSnap);
710                 return iFalse;
711             }
712         }
713     }
714     if (snap && snap != fullscreen_WindowSnap) {
715         if (snap_MainWindow(d) == yMaximized_WindowSnap && newPos) {
716             d->place.normalRect.pos = *newPos;
717         }
718         //printf("unsnap\n"); fflush(stdout);
719         setSnap_MainWindow(d, none_WindowSnap);
720         return iTrue;
721     }
722 #endif
723     return iFalse;
724 }
725 
notifyMetricsChange_Window_(const iWindow * d)726 static void notifyMetricsChange_Window_(const iWindow *d) {
727     /* Dynamic UI metrics change. Widgets need to update themselves. */
728     setScale_Metrics(d->pixelRatio * d->displayScale * d->uiScale);
729     resetFonts_Text(d->text);
730     postCommand_App("metrics.changed");
731 }
732 
checkPixelRatioChange_Window_(iWindow * d)733 static void checkPixelRatioChange_Window_(iWindow *d) {
734     iBool wasChanged = iFalse;
735     const float ratio = pixelRatio_Window_(d);
736     if (iAbs(ratio - d->pixelRatio) > 0.001f) {
737         d->pixelRatio = ratio;
738         wasChanged = iTrue;
739     }
740     const float scale = displayScale_Window_(d);
741     if (iAbs(scale - d->displayScale) > 0.001f) {
742         d->displayScale = scale;
743         wasChanged = iTrue;
744     }
745     if (wasChanged) {
746         notifyMetricsChange_Window_(d);
747     }
748 }
749 
handleWindowEvent_Window_(iWindow * d,const SDL_WindowEvent * ev)750 static iBool handleWindowEvent_Window_(iWindow *d, const SDL_WindowEvent *ev) {
751     if (ev->windowID != SDL_GetWindowID(d->win)) {
752         return iFalse;
753     }
754     switch (ev->event) {
755         case SDL_WINDOWEVENT_EXPOSED:
756             d->isExposed = iTrue;
757             postRefresh_App();
758             return iTrue;
759         case SDL_WINDOWEVENT_RESTORED:
760         case SDL_WINDOWEVENT_SHOWN:
761             postRefresh_App();
762             return iTrue;
763         case SDL_WINDOWEVENT_FOCUS_LOST:
764             /* Popup windows are currently only used for menus. */
765             closeMenu_Widget(d->roots[0]->widget);
766             return iTrue;
767         case SDL_WINDOWEVENT_LEAVE:
768             unhover_Widget();
769             d->isMouseInside = iFalse;
770             postRefresh_App();
771             return iTrue;
772         case SDL_WINDOWEVENT_ENTER:
773             d->isMouseInside = iTrue;
774             return iTrue;
775     }
776     return iFalse;
777 }
778 
savePlace_MainWindow_(iAny * mainWindow)779 static void savePlace_MainWindow_(iAny *mainWindow) {
780     iMainWindow *d = mainWindow;
781     if (isNormalPlacement_MainWindow_(d)) {
782         iInt2 newPos;
783         SDL_GetWindowPosition(d->base.win, &newPos.x, &newPos.y);
784         // printf("savePlace_MainWindow_ sets normalRect %d,%d\n", newPos.x, newPos.y);
785         d->place.normalRect.pos = newPos;
786         iInt2 border = zero_I2();
787 #if !defined(iPlatformApple)
788         SDL_GetWindowBordersSize(d->base.win, &border.y, &border.x, NULL, NULL);
789         iAssert(~SDL_GetWindowFlags(d->base.win) & SDL_WINDOW_MAXIMIZED);
790 #endif
791         d->place.normalRect.pos =
792             max_I2(zero_I2(), sub_I2(d->place.normalRect.pos, border));
793     }
794 }
795 
handleWindowEvent_MainWindow_(iMainWindow * d,const SDL_WindowEvent * ev)796 static iBool handleWindowEvent_MainWindow_(iMainWindow *d, const SDL_WindowEvent *ev) {
797     switch (ev->event) {
798 #if defined(iPlatformDesktop)
799         case SDL_WINDOWEVENT_EXPOSED:
800             d->base.isExposed = iTrue;
801             /* Since we are manually controlling when to redraw the window, we are responsible
802                for ensuring that window contents get redrawn after expose events. Under certain
803                circumstances (e.g., under openbox), not doing this would mean that the window
804                is missing contents until other events trigger a refresh. */
805             postRefresh_App();
806 #if defined(LAGRANGE_ENABLE_WINDOWPOS_FIX)
807             if (d->place.initialPos.x >= 0) {
808                 /* Must not move a maximized window. */
809                 if (snap_MainWindow(d) == 0) {
810                     int bx, by;
811                     SDL_GetWindowBordersSize(d->base.win, &by, &bx, NULL, NULL);
812                     // printf("EXPOSED sets position %d %d\n", d->place.initialPos.x, d->place.initialPos.y);
813                     SDL_SetWindowPosition(
814                         d->base.win, d->place.initialPos.x + bx, d->place.initialPos.y + by);
815                 }
816                 d->place.initialPos = init1_I2(-1);
817             }
818 #endif
819             return iFalse;
820         case SDL_WINDOWEVENT_MOVED: {
821             if (d->base.isMinimized) {
822                 return iFalse;
823             }
824             closePopups_App();
825             checkPixelRatioChange_Window_(as_Window(d));
826             const iInt2 newPos = init_I2(ev->data1, ev->data2);
827             if (isEqual_I2(newPos, init1_I2(-32000))) { /* magic! */
828                 /* Maybe minimized? Seems like a Windows constant of some kind. */
829                 d->base.isMinimized = iTrue;
830                 return iFalse;
831             }
832 #if defined(LAGRANGE_ENABLE_CUSTOM_FRAME)
833             /* Set the snap position depending on where the mouse cursor is. */
834             if (prefs_App()->customFrame) {
835                 SDL_Rect usable;
836                 iInt2    mouse = cursor_Win32(); /* SDL is unaware of the current cursor pos */
837                 SDL_GetDisplayUsableBounds(SDL_GetWindowDisplayIndex(d->base.win), &usable);
838                 const iBool isTop    = iAbs(mouse.y - usable.y) < gap_UI * 20;
839                 const iBool isBottom = iAbs(usable.y + usable.h - mouse.y) < gap_UI * 20;
840                 if (iAbs(mouse.x - usable.x) < gap_UI) {
841                     setSnap_MainWindow(d,
842                                        redo_WindowSnap | left_WindowSnap |
843                                            (isTop ? topBit_WindowSnap : 0) |
844                                            (isBottom ? bottomBit_WindowSnap : 0));
845                     return iTrue;
846                 }
847                 if (iAbs(mouse.x - usable.x - usable.w) < gap_UI) {
848                     setSnap_MainWindow(d,
849                                        redo_WindowSnap | right_WindowSnap |
850                                            (isTop ? topBit_WindowSnap : 0) |
851                                            (isBottom ? bottomBit_WindowSnap : 0));
852                     return iTrue;
853                 }
854                 if (iAbs(mouse.y - usable.y) < 2) {
855                     setSnap_MainWindow(d,
856                                        redo_WindowSnap | (d->place.lastHit == SDL_HITTEST_RESIZE_TOP
857                                                               ? yMaximized_WindowSnap
858                                                               : maximized_WindowSnap));
859                     return iTrue;
860                 }
861             }
862 #endif /* defined LAGRANGE_ENABLE_CUSTOM_FRAME */
863             if (unsnap_MainWindow_(d, &newPos)) {
864                 return iTrue;
865             }
866             addTicker_App(savePlace_MainWindow_, d);
867             return iTrue;
868         }
869         case SDL_WINDOWEVENT_RESIZED:
870             if (d->base.isMinimized) {
871                 // updateSize_Window_(d, iTrue);
872                 return iTrue;
873             }
874             closePopups_App();
875             if (unsnap_MainWindow_(d, NULL)) {
876                 return iTrue;
877             }
878             if (isNormalPlacement_MainWindow_(d)) {
879                 // printf("RESIZED sets normalRect\n");
880                 d->place.normalRect.size = init_I2(ev->data1, ev->data2);
881             }
882             checkPixelRatioChange_Window_(as_Window(d));
883             postRefresh_App();
884             return iTrue;
885         case SDL_WINDOWEVENT_RESTORED:
886         case SDL_WINDOWEVENT_SHOWN:
887             updateSize_MainWindow_(d, iTrue);
888             invalidate_MainWindow_(d, iTrue);
889             d->base.isMinimized = iFalse;
890             postRefresh_App();
891             return iTrue;
892         case SDL_WINDOWEVENT_MAXIMIZED:
893             return iTrue;
894         case SDL_WINDOWEVENT_MINIMIZED:
895             d->base.isMinimized = iTrue;
896             closePopups_App();
897             return iTrue;
898 #else /* if defined (!iPlatformDesktop) */
899         case SDL_WINDOWEVENT_RESIZED:
900             /* On mobile, this occurs when the display is rotated. */
901             invalidate_Window(d);
902             postRefresh_App();
903             return iTrue;
904 #endif
905         case SDL_WINDOWEVENT_LEAVE:
906             unhover_Widget();
907             d->base.isMouseInside = iFalse;
908             postCommand_App("window.mouse.exited");
909             return iTrue;
910         case SDL_WINDOWEVENT_ENTER:
911             d->base.isMouseInside = iTrue;
912             SDL_SetWindowInputFocus(d->base.win);
913             postCommand_App("window.mouse.entered");
914             return iTrue;
915         case SDL_WINDOWEVENT_FOCUS_GAINED:
916             d->base.focusGainedAt = SDL_GetTicks();
917             setCapsLockDown_Keys(iFalse);
918             postCommand_App("window.focus.gained");
919             d->base.isExposed = iTrue;
920 #if !defined (iPlatformDesktop)
921             /* Returned to foreground, may have lost buffered content. */
922             invalidate_MainWindow_(d, iTrue);
923             postCommand_App("window.unfreeze");
924 #endif
925             return iFalse;
926         case SDL_WINDOWEVENT_FOCUS_LOST:
927             postCommand_App("window.focus.lost");
928 #if !defined (iPlatformDesktop)
929             setFreezeDraw_MainWindow(d, iTrue);
930 #endif
931             closePopups_App();
932             return iFalse;
933         case SDL_WINDOWEVENT_TAKE_FOCUS:
934             SDL_SetWindowInputFocus(d->base.win);
935             postRefresh_App();
936             return iTrue;
937         default:
938             break;
939     }
940     return iFalse;
941 }
942 
applyCursor_Window_(iWindow * d)943 static void applyCursor_Window_(iWindow *d) {
944     if (d->pendingCursor) {
945         SDL_SetCursor(d->pendingCursor);
946         d->pendingCursor = NULL;
947     }
948 }
949 
processEvent_Window(iWindow * d,const SDL_Event * ev)950 iBool processEvent_Window(iWindow *d, const SDL_Event *ev) {
951     iMainWindow *mw = (type_Window(d) == main_WindowType ? as_MainWindow(d) : NULL);
952     switch (ev->type) {
953 #if defined (LAGRANGE_ENABLE_CUSTOM_FRAME)
954         case SDL_SYSWMEVENT: {
955             /* We observe native Win32 messages for better user interaction with the
956                window frame. Mouse clicks especially will not generate normal SDL
957                events if they happen on the custom hit-tested regions. These events
958                are processed only there; the UI widgets do not get involved. */
959             processNativeEvent_Win32(ev->syswm.msg, d);
960             break;
961         }
962 #endif
963         case SDL_WINDOWEVENT: {
964             if (mw) {
965                 return handleWindowEvent_MainWindow_(mw, &ev->window);
966             }
967             else {
968                 return handleWindowEvent_Window_(d, &ev->window);
969             }
970         }
971         case SDL_RENDER_TARGETS_RESET:
972         case SDL_RENDER_DEVICE_RESET: {
973             if (mw) {
974                 invalidate_MainWindow_(mw, iTrue /* force full reset */);
975             }
976             break;
977         }
978         default: {
979             SDL_Event event = *ev;
980             if (event.type == SDL_USEREVENT && isCommand_UserEvent(ev, "window.unfreeze") && mw) {
981                 mw->isDrawFrozen = iFalse;
982                 if (SDL_GetWindowFlags(d->win) & SDL_WINDOW_HIDDEN) {
983                     SDL_ShowWindow(d->win);
984                 }
985                 draw_MainWindow(mw); /* don't show a frame of placeholder content */
986                 postCommand_App("media.player.update"); /* in case a player needs updating */
987                 return iTrue;
988             }
989             if (processEvent_Touch(&event)) {
990                 return iTrue;
991             }
992             if (event.type == SDL_KEYDOWN && SDL_GetTicks() - d->focusGainedAt < 10) {
993                 /* Suspiciously close to when input focus was received. For example under openbox,
994                    closing xterm with Ctrl+D will cause the keydown event to "spill" over to us.
995                    As a workaround, ignore these events. */
996                 return iTrue; /* won't go to bindings, either */
997             }
998             if (event.type == SDL_MOUSEBUTTONDOWN && d->ignoreClick) {
999                 d->ignoreClick = iFalse;
1000                 return iTrue;
1001             }
1002             /* Map mouse pointer coordinate to our coordinate system. */
1003             if (event.type == SDL_MOUSEMOTION) {
1004                 setCursor_Window(d, SDL_SYSTEM_CURSOR_ARROW); /* default cursor */
1005                 const iInt2 pos = coord_Window(d, event.motion.x, event.motion.y);
1006                 event.motion.x = pos.x;
1007                 event.motion.y = pos.y;
1008             }
1009             else if (event.type == SDL_MOUSEBUTTONUP || event.type == SDL_MOUSEBUTTONDOWN) {
1010                 const iInt2 pos = coord_Window(d, event.button.x, event.button.y);
1011                 event.button.x = pos.x;
1012                 event.button.y = pos.y;
1013                 if (event.type == SDL_MOUSEBUTTONDOWN) {
1014                     /* Button clicks will change keyroot. */
1015                     if (numRoots_Window(d) > 1) {
1016                         const iInt2 click = init_I2(event.button.x, event.button.y);
1017                         iForIndices(i, d->roots) {
1018                             iRoot *root = d->roots[i];
1019                             if (root != d->keyRoot && contains_Rect(rect_Root(root), click)) {
1020                                 setKeyRoot_Window(d, root);
1021                                 break;
1022                             }
1023                         }
1024                     }
1025                 }
1026             }
1027 //            const iWidget *oldHover = d->hover;
1028             iBool wasUsed = iFalse;
1029             /* Dispatch first to the mouse-grabbed widget. */
1030 //            iWidget *widget = d->root.widget;
1031             if (event.type == SDL_MOUSEMOTION || event.type == SDL_MOUSEWHEEL ||
1032                 event.type == SDL_MOUSEBUTTONUP || event.type == SDL_MOUSEBUTTONDOWN) {
1033                 if (mouseGrab_Widget()) {
1034                     iWidget *grabbed = mouseGrab_Widget();
1035                     setCurrent_Root(findRoot_Window(d, grabbed));
1036                     wasUsed = dispatchEvent_Widget(grabbed, &event);
1037                 }
1038             }
1039             /* Dispatch the event to the tree of widgets. */
1040             if (!wasUsed) {
1041                 wasUsed = dispatchEvent_Window(d, &event);
1042             }
1043             if (!wasUsed) {
1044                 /* As a special case, clicking the middle mouse button can be used for pasting
1045                    from the clipboard. */
1046                 if (event.type == SDL_MOUSEBUTTONDOWN && event.button.button == SDL_BUTTON_MIDDLE) {
1047                     SDL_Event paste;
1048                     iZap(paste);
1049                     paste.type           = SDL_KEYDOWN;
1050                     paste.key.keysym.sym = SDLK_v;
1051                     paste.key.keysym.mod = KMOD_PRIMARY;
1052                     paste.key.state      = SDL_PRESSED;
1053                     paste.key.timestamp  = SDL_GetTicks();
1054                     wasUsed = dispatchEvent_Window(d, &paste);
1055                 }
1056                 if (event.type == SDL_MOUSEBUTTONDOWN && event.button.button == SDL_BUTTON_RIGHT) {
1057                     if (postContextClick_Window(d, &event.button)) {
1058                         wasUsed = iTrue;
1059                     }
1060                 }
1061             }
1062             if (isMetricsChange_UserEvent(&event)) {
1063                 iForIndices(i, d->roots) {
1064                     updateMetrics_Root(d->roots[i]);
1065                 }
1066             }
1067             if (isCommand_UserEvent(&event, "lang.changed") && mw) {
1068 #if defined (iHaveNativeMenus)
1069                 /* Retranslate the menus. */
1070                 removeMacMenus_();
1071                 insertMacMenus_();
1072 #endif
1073                 invalidate_Window(d);
1074                 iForIndices(i, d->roots) {
1075                     if (d->roots[i]) {
1076                         updatePreferencesLayout_Widget(findChild_Widget(d->roots[i]->widget, "prefs"));
1077                         arrange_Widget(d->roots[i]->widget);
1078                     }
1079                 }
1080             }
1081             if (event.type == SDL_MOUSEMOTION) {
1082                 applyCursor_Window_(d);
1083             }
1084             return wasUsed;
1085         }
1086     }
1087     return iFalse;
1088 }
1089 
setKeyRoot_Window(iWindow * d,iRoot * root)1090 iBool setKeyRoot_Window(iWindow *d, iRoot *root) {
1091     if (d->keyRoot != root) {
1092         d->keyRoot = root;
1093         postCommand_App("keyroot.changed");
1094         postRefresh_App();
1095         return iTrue;
1096     }
1097     return iFalse;
1098 }
1099 
isEscapeKeypress_(const SDL_Event * ev)1100 iLocalDef iBool isEscapeKeypress_(const SDL_Event *ev) {
1101     return (ev->type == SDL_KEYDOWN || ev->type == SDL_KEYUP) && ev->key.keysym.sym == SDLK_ESCAPE;
1102 }
1103 
dispatchEvent_Window(iWindow * d,const SDL_Event * ev)1104 iBool dispatchEvent_Window(iWindow *d, const SDL_Event *ev) {
1105     if (ev->type == SDL_MOUSEMOTION) {
1106         /* Hover widget may change. */
1107         setHover_Widget(NULL);
1108     }
1109     iRoot *order[2];
1110     rootOrder_App(order);
1111     iForIndices(i, order) {
1112         iRoot *root = order[i];
1113         if (root) {
1114             if (isCommand_SDLEvent(ev) && ev->user.data2 && ev->user.data2 != root) {
1115                 continue; /* Not meant for this root. */
1116             }
1117             if ((ev->type == SDL_KEYDOWN || ev->type == SDL_KEYUP || ev->type == SDL_TEXTINPUT)
1118                      && d->keyRoot != root) {
1119                 if (!isEscapeKeypress_(ev)) {
1120                     /* Key events go only to the root with keyboard focus, with the exception
1121                        of Escape that will also affect the entire window. */
1122                     continue;
1123                 }
1124             }
1125             if (ev->type == SDL_MOUSEWHEEL && !contains_Rect(rect_Root(root),
1126                                                              coord_MouseWheelEvent(&ev->wheel))) {
1127                 continue; /* Only process the event in the relevant split. */
1128             }
1129             if (!root->widget) {
1130                 continue;
1131             }
1132             setCurrent_Root(root);
1133             const iBool wasUsed = dispatchEvent_Widget(root->widget, ev);
1134             if (wasUsed) {
1135                 if (ev->type == SDL_MOUSEBUTTONDOWN ||
1136                     ev->type == SDL_MOUSEWHEEL) {
1137                     setKeyRoot_Window(d, root);
1138                 }
1139                 return iTrue;
1140             }
1141         }
1142     }
1143     return iFalse;
1144 }
1145 
hitChild_Window(const iWindow * d,iInt2 coord)1146 iAnyObject *hitChild_Window(const iWindow *d, iInt2 coord) {
1147     iForIndices(i, d->roots) {
1148         if (d->roots[i]) {
1149             iAnyObject *hit = hitChild_Widget(d->roots[i]->widget, coord);
1150             if (hit) {
1151                 return hit;
1152             }
1153         }
1154     }
1155     return NULL;
1156 }
1157 
postContextClick_Window(iWindow * d,const SDL_MouseButtonEvent * ev)1158 iBool postContextClick_Window(iWindow *d, const SDL_MouseButtonEvent *ev) {
1159     /* A context menu may still get triggered here. */
1160     const iWidget *hit = hitChild_Window(d, init_I2(ev->x, ev->y));
1161     while (hit && isEmpty_String(id_Widget(hit))) {
1162         hit = parent_Widget(hit);
1163     }
1164     if (hit) {
1165         postCommandf_App("contextclick id:%s ptr:%p coord:%d %d",
1166                          cstr_String(id_Widget(hit)), hit,
1167                          ev->x, ev->y);
1168         return iTrue;
1169     }
1170     return iFalse;
1171 }
1172 
draw_Window(iWindow * d)1173 void draw_Window(iWindow *d) {
1174     if (SDL_GetWindowFlags(d->win) & SDL_WINDOW_HIDDEN) {
1175         return;
1176     }
1177     iPaint p;
1178     init_Paint(&p);
1179     iRoot *root = d->roots[0];
1180     setCurrent_Root(root);
1181     unsetClip_Paint(&p); /* update clip to full window */
1182     const iColor back = get_Color(uiBackground_ColorId);
1183     SDL_SetRenderDrawColor(d->render, back.r, back.g, back.b, 255);
1184     SDL_RenderClear(d->render);
1185     d->frameTime = SDL_GetTicks();
1186     if (isExposed_Window(d)) {
1187         d->isInvalidated = iFalse;
1188         extern int drawCount_;
1189         drawRoot_Widget(root->widget);
1190 #if !defined (NDEBUG)
1191         draw_Text(defaultBold_FontId, safeRect_Root(root).pos, red_ColorId, "%d", drawCount_);
1192         drawCount_ = 0;
1193 #endif
1194     }
1195     drawRectThickness_Paint(
1196         &p, (iRect){ zero_I2(), sub_I2(d->size, one_I2()) }, gap_UI / 4, uiSeparator_ColorId);
1197     setCurrent_Root(NULL);
1198     SDL_RenderPresent(d->render);
1199 }
1200 
draw_MainWindow(iMainWindow * d)1201 void draw_MainWindow(iMainWindow *d) {
1202     /* TODO: Try to make this a specialization of `draw_Window`? */
1203     iWindow *w = as_Window(d);
1204     if (d->isDrawFrozen) {
1205         return;
1206     }
1207     setCurrent_Text(d->base.text);
1208     /* Check if root needs resizing. */ {
1209         iInt2 renderSize;
1210         SDL_GetRendererOutputSize(w->render, &renderSize.x, &renderSize.y);
1211         if (!isEqual_I2(renderSize, w->size)) {
1212             updateSize_MainWindow_(d, iTrue);
1213             processEvents_App(postedEventsOnly_AppEventMode);
1214         }
1215     }
1216     const int   winFlags = SDL_GetWindowFlags(d->base.win);
1217     const iBool gotFocus = (winFlags & SDL_WINDOW_INPUT_FOCUS) != 0;
1218     iPaint p;
1219     init_Paint(&p);
1220     /* Clear the window. The clear color is visible as a border around the window
1221        when the custom frame is being used. */ {
1222         setCurrent_Root(w->roots[0]);
1223 #if defined (iPlatformMobile)
1224         iColor back = get_Color(uiBackground_ColorId);
1225         if (deviceType_App() == phone_AppDeviceType) {
1226             /* Page background extends to safe area, so fill it completely. */
1227             back = get_Color(tmBackground_ColorId);
1228         }
1229 #else
1230         const iColor back = get_Color(gotFocus && d->place.snap != maximized_WindowSnap &&
1231                                               ~winFlags & SDL_WINDOW_FULLSCREEN_DESKTOP
1232                                           ? uiAnnotation_ColorId
1233                                           : uiSeparator_ColorId);
1234 #endif
1235         unsetClip_Paint(&p); /* update clip to full window */
1236         SDL_SetRenderDrawColor(w->render, back.r, back.g, back.b, 255);
1237         SDL_RenderClear(w->render);
1238     }
1239     /* Draw widgets. */
1240     w->frameTime = SDL_GetTicks();
1241     if (isExposed_Window(w)) {
1242         w->isInvalidated = iFalse;
1243         extern int drawCount_;
1244         iForIndices(i, w->roots) {
1245             iRoot *root = w->roots[i];
1246             if (root) {
1247                 setCurrent_Root(root);
1248                 unsetClip_Paint(&p); /* update clip to current root */
1249                 drawRoot_Widget(root->widget);
1250 #if defined (LAGRANGE_ENABLE_CUSTOM_FRAME)
1251                 /* App icon. */
1252                 const iWidget *appIcon = findChild_Widget(root->widget, "winbar.icon");
1253                 if (isVisible_Widget(appIcon)) {
1254                     const int   size    = appIconSize_Root();
1255                     const iRect rect    = bounds_Widget(appIcon);
1256                     const iInt2 mid     = mid_Rect(rect);
1257                     const iBool isLight = isLight_ColorTheme(colorTheme_App());
1258                     iColor iconColor    = get_Color(gotFocus || isLight ? white_ColorId : uiAnnotation_ColorId);
1259                     SDL_SetTextureColorMod(d->appIcon, iconColor.r, iconColor.g, iconColor.b);
1260                     SDL_SetTextureAlphaMod(d->appIcon, gotFocus || !isLight ? 255 : 92);
1261                     SDL_RenderCopy(
1262                         w->render,
1263                         d->appIcon,
1264                         NULL,
1265                         &(SDL_Rect){ left_Rect(rect) + gap_UI * 1.25f, mid.y - size / 2, size, size });
1266                 }
1267 #endif
1268                 /* Root separator and keyboard focus indicator. */
1269                 if (numRoots_Window(w) > 1){
1270                     const iRect bounds = bounds_Widget(root->widget);
1271                     if (i == 1) {
1272                         fillRect_Paint(&p, (iRect){
1273                             addX_I2(topLeft_Rect(bounds), -gap_UI / 8),
1274                             init_I2(gap_UI / 4, height_Rect(bounds))
1275                         }, uiSeparator_ColorId);
1276                     }
1277                     if (root == w->keyRoot) {
1278                         const iBool isDark = isDark_ColorTheme(colorTheme_App());
1279                         fillRect_Paint(&p, (iRect){
1280                             topLeft_Rect(bounds),
1281                             init_I2(width_Rect(bounds), gap_UI / 2)
1282                         }, isDark ? uiBackgroundSelected_ColorId
1283                                   : uiIcon_ColorId);
1284                     }
1285                 }
1286             }
1287         }
1288         setCurrent_Root(NULL);
1289 #if !defined (NDEBUG)
1290         draw_Text(defaultBold_FontId, safeRect_Root(w->roots[0]).pos, red_ColorId, "%d", drawCount_);
1291         drawCount_ = 0;
1292 #endif
1293     }
1294 #if 0
1295     /* Text cache debugging. */ {
1296         SDL_Rect rect = { d->roots[0]->widget->rect.size.x - 640, 0, 640, 2.5 * 640 };
1297         SDL_SetRenderDrawColor(d->render, 0, 0, 0, 255);
1298         SDL_RenderFillRect(d->render, &rect);
1299         SDL_RenderCopy(d->render, glyphCache_Text(), NULL, &rect);
1300     }
1301 #endif
1302     SDL_RenderPresent(w->render);
1303 }
1304 
resize_MainWindow(iMainWindow * d,int w,int h)1305 void resize_MainWindow(iMainWindow *d, int w, int h) {
1306     if (w > 0 && h > 0) {
1307         SDL_SetWindowSize(d->base.win, w, h);
1308         updateSize_MainWindow_(d, iFalse);
1309     }
1310     else {
1311         updateSize_MainWindow_(d, iTrue); /* notify always */
1312     }
1313 }
1314 
setTitle_MainWindow(iMainWindow * d,const iString * title)1315 void setTitle_MainWindow(iMainWindow *d, const iString *title) {
1316     SDL_SetWindowTitle(d->base.win, cstr_String(title));
1317     iLabelWidget *bar = findChild_Widget(get_Root()->widget, "winbar.title");
1318     if (bar) {
1319         updateText_LabelWidget(bar, title);
1320     }
1321 }
1322 
setUiScale_Window(iWindow * d,float uiScale)1323 void setUiScale_Window(iWindow *d, float uiScale) {
1324     if (uiScale <= 0.0f) {
1325         uiScale = 1.0f;
1326     }
1327     uiScale = iClamp(uiScale, 0.5f, 4.0f);
1328     if (d) {
1329         if (iAbs(d->uiScale - uiScale) > 0.0001f) {
1330             d->uiScale = uiScale;
1331             notifyMetricsChange_Window_(d);
1332         }
1333     }
1334     else {
1335         initialUiScale_ = uiScale;
1336     }
1337 }
1338 
setFreezeDraw_MainWindow(iMainWindow * d,iBool freezeDraw)1339 void setFreezeDraw_MainWindow(iMainWindow *d, iBool freezeDraw) {
1340     d->isDrawFrozen = freezeDraw;
1341 }
1342 
setCursor_Window(iWindow * d,int cursor)1343 void setCursor_Window(iWindow *d, int cursor) {
1344     if (!d->cursors[cursor]) {
1345         d->cursors[cursor] = SDL_CreateSystemCursor(cursor);
1346     }
1347     d->pendingCursor = d->cursors[cursor];
1348 }
1349 
id_Window(const iWindow * d)1350 uint32_t id_Window(const iWindow *d) {
1351     return d && d->win ? SDL_GetWindowID(d->win) : 0;
1352 }
1353 
size_Window(const iWindow * d)1354 iInt2 size_Window(const iWindow *d) {
1355     return d ? d->size : zero_I2();
1356 }
1357 
coord_Window(const iWindow * d,int x,int y)1358 iInt2 coord_Window(const iWindow *d, int x, int y) {
1359     return mulf_I2(init_I2(x, y), d->pixelRatio);
1360 }
1361 
mouseCoord_Window(const iWindow * d,int whichDevice)1362 iInt2 mouseCoord_Window(const iWindow *d, int whichDevice) {
1363     if (whichDevice == SDL_TOUCH_MOUSEID) {
1364         /* At least on iOS the coordinates returned by SDL_GetMouseState() do no match up with
1365            our touch coordinates on the Y axis (?). Maybe a pixel ratio thing? */
1366         iUnused(d);
1367         return latestPosition_Touch();
1368     }
1369     if (!d->isMouseInside) {
1370         return init_I2(-1000000, -1000000);
1371     }
1372     int x, y;
1373     SDL_GetMouseState(&x, &y);
1374     return coord_Window(d, x, y);
1375 }
1376 
uiScale_Window(const iWindow * d)1377 float uiScale_Window(const iWindow *d) {
1378     return d->uiScale;
1379 }
1380 
frameTime_Window(const iWindow * d)1381 uint32_t frameTime_Window(const iWindow *d) {
1382     return d->frameTime;
1383 }
1384 
get_Window(void)1385 iWindow *get_Window(void) {
1386     /* TODO: This should be thread-specific. */
1387     return theWindow_;
1388 }
1389 
setCurrent_Window(iAnyWindow * d)1390 void setCurrent_Window(iAnyWindow *d) {
1391     theWindow_ = d;
1392     if (type_Window(d) == main_WindowType) {
1393         theMainWindow_ = d;
1394     }
1395     if (d) {
1396         setCurrent_Text(theWindow_->text);
1397         setCurrent_Root(theWindow_->keyRoot);
1398     }
1399     else {
1400         setCurrent_Text(NULL);
1401         setCurrent_Root(NULL);
1402     }
1403 }
1404 
get_MainWindow(void)1405 iMainWindow *get_MainWindow(void) {
1406     return theMainWindow_;
1407 }
1408 
isOpenGLRenderer_Window(void)1409 iBool isOpenGLRenderer_Window(void) {
1410     return isOpenGLRenderer_;
1411 }
1412 
setKeyboardHeight_MainWindow(iMainWindow * d,int height)1413 void setKeyboardHeight_MainWindow(iMainWindow *d, int height) {
1414     if (d->keyboardHeight != height) {
1415         d->keyboardHeight = height;
1416         postCommandf_App("keyboard.changed arg:%d", height);
1417         postRefresh_App();
1418     }
1419 }
1420 
checkPendingSplit_MainWindow(iMainWindow * d)1421 void checkPendingSplit_MainWindow(iMainWindow *d) {
1422     if (d->splitMode != d->pendingSplitMode) {
1423         setSplitMode_MainWindow(d, d->pendingSplitMode);
1424     }
1425 }
1426 
swapRoots_MainWindow(iMainWindow * d)1427 void swapRoots_MainWindow(iMainWindow *d) {
1428     iWindow *w = as_Window(d);
1429     if (numRoots_Window(w) == 2) {
1430         iSwap(iRoot *, w->roots[0], w->roots[1]);
1431         updateSize_MainWindow_(d, iTrue);
1432     }
1433 }
1434 
setSplitMode_MainWindow(iMainWindow * d,int splitFlags)1435 void setSplitMode_MainWindow(iMainWindow *d, int splitFlags) {
1436     const int splitMode = splitFlags & mode_WindowSplit;
1437     if (deviceType_App() == phone_AppDeviceType) {
1438         /* There isn't enough room on the phone. */
1439         /* TODO: Maybe in landscape only? */
1440         return;
1441     }
1442     iWindow *w = as_Window(d);
1443     iAssert(current_Root() == NULL);
1444     if (d->splitMode != splitMode) {
1445         int oldCount = numRoots_Window(w);
1446         setFreezeDraw_MainWindow(d, iTrue);
1447         if (oldCount == 2 && splitMode == 0) {
1448             /* Keep references to the tabs of the second root. */
1449             const iDocumentWidget *curPage = document_Root(w->keyRoot);
1450             if (!curPage) {
1451                 /* All tabs closed on that side. */
1452                 curPage = document_Root(otherRoot_Window(w, w->keyRoot));
1453             }
1454             iObjectList *tabs = listDocuments_App(w->roots[1]);
1455             iForEach(ObjectList, i, tabs) {
1456                 setRoot_Widget(i.object, w->roots[0]);
1457             }
1458             setFocus_Widget(NULL);
1459             delete_Root(w->roots[1]);
1460             w->roots[1] = NULL;
1461             w->keyRoot = w->roots[0];
1462             /* Move the deleted root's tabs to the first root. */
1463             setCurrent_Root(w->roots[0]);
1464             iWidget *docTabs = findWidget_Root("doctabs");
1465             iForEach(ObjectList, j, tabs) {
1466                 appendTabPage_Widget(docTabs, j.object, "", 0, 0);
1467             }
1468             /* The last child is the [+] button for adding a tab. */
1469             moveTabButtonToEnd_Widget(findChild_Widget(docTabs, "newtab"));
1470             iRelease(tabs);
1471             postCommandf_App("tabs.switch id:%s", cstr_String(id_Widget(constAs_Widget(curPage))));
1472         }
1473         else if (splitMode && oldCount == 1) {
1474             /* Add a second root. */
1475             iDocumentWidget *moved = document_Root(w->roots[0]);
1476             iAssert(w->roots[1] == NULL);
1477             const iBool addToLeft = (prefs_App()->pinSplit == 2);
1478             size_t newRootIndex = 1;
1479             if (addToLeft) {
1480                 iSwap(iRoot *, w->roots[0], w->roots[1]);
1481                 newRootIndex = 0;
1482             }
1483             w->roots[newRootIndex] = new_Root();
1484             w->keyRoot             = w->roots[newRootIndex];
1485             w->keyRoot->window     = w;
1486             setCurrent_Root(w->roots[newRootIndex]);
1487             createUserInterface_Root(w->roots[newRootIndex]);
1488             /* Bookmark folder state will match the old root's state. */ {
1489                 for (int sb = 0; sb < 2; sb++) {
1490                     const char *sbId = (sb == 0 ? "sidebar" : "sidebar2");
1491                     setClosedFolders_SidebarWidget(
1492                         findChild_Widget(w->roots[newRootIndex]->widget, sbId),
1493                         closedFolders_SidebarWidget(
1494                             findChild_Widget(w->roots[newRootIndex ^ 1]->widget, sbId)));
1495                 }
1496             }
1497             if (!isEmpty_String(d->pendingSplitUrl)) {
1498                 postCommandf_Root(w->roots[newRootIndex], "open url:%s",
1499                                   cstr_String(d->pendingSplitUrl));
1500                 clear_String(d->pendingSplitUrl);
1501             }
1502             else if (~splitFlags & noEvents_WindowSplit) {
1503                 iWidget *docTabs0 = findChild_Widget(w->roots[newRootIndex ^ 1]->widget, "doctabs");
1504                 iWidget *docTabs1 = findChild_Widget(w->roots[newRootIndex]->widget, "doctabs");
1505                 /* If the old root has multiple tabs, move the current one to the new split. */
1506                 if (tabCount_Widget(docTabs0) >= 2) {
1507                     int movedIndex = tabPageIndex_Widget(docTabs0, moved);
1508                     removeTabPage_Widget(docTabs0, movedIndex);
1509                     showTabPage_Widget(docTabs0, tabPage_Widget(docTabs0, iMax(movedIndex - 1, 0)));
1510                     iRelease(removeTabPage_Widget(docTabs1, 0)); /* delete the default tab */
1511                     setRoot_Widget(as_Widget(moved), w->roots[newRootIndex]);
1512                     prependTabPage_Widget(docTabs1, iClob(moved), "", 0, 0);
1513                     postCommandf_App("tabs.switch page:%p", moved);
1514                 }
1515                 else {
1516                     postCommand_Root(w->roots[newRootIndex], "navigate.home");
1517                 }
1518             }
1519             setCurrent_Root(NULL);
1520         }
1521         d->splitMode = splitMode;
1522         postCommand_App("window.resized");
1523 #if defined (LAGRANGE_ENABLE_CUSTOM_FRAME)
1524         /* Update custom frame controls. */{
1525             const iBool hideCtl0 = numRoots_Window(as_Window(d)) != 1;
1526             iWidget *winBar = findChild_Widget(d->base.roots[0]->widget, "winbar");
1527             if (winBar) {
1528                 setFlags_Widget(
1529                     findChild_Widget(winBar, "winbar.min"), hidden_WidgetFlag, hideCtl0);
1530                 setFlags_Widget(
1531                     findChild_Widget(winBar, "winbar.max"), hidden_WidgetFlag, hideCtl0);
1532                 setFlags_Widget(
1533                     findChild_Widget(winBar, "winbar.close"), hidden_WidgetFlag, hideCtl0);
1534                 if (d->base.roots[1]) {
1535                     winBar = findChild_Widget(d->base.roots[1]->widget, "winbar");
1536                     setFlags_Widget(
1537                         findChild_Widget(winBar, "winbar.icon"), hidden_WidgetFlag, iTrue);
1538                     setFlags_Widget(
1539                         findChild_Widget(winBar, "winbar.app"), hidden_WidgetFlag, iTrue);
1540                 }
1541             }
1542         }
1543 #endif
1544         if (~splitFlags & noEvents_WindowSplit) {
1545             updateSize_MainWindow_(d, iTrue);
1546             postCommand_App("window.unfreeze");
1547         }
1548     }
1549 }
1550 
setSnap_MainWindow(iMainWindow * d,int snapMode)1551 void setSnap_MainWindow(iMainWindow *d, int snapMode) {
1552     if (!prefs_App()->customFrame) {
1553         if (snapMode == maximized_WindowSnap) {
1554             SDL_MaximizeWindow(d->base.win);
1555         }
1556         else if (snapMode == fullscreen_WindowSnap) {
1557             SDL_SetWindowFullscreen(d->base.win, SDL_WINDOW_FULLSCREEN_DESKTOP);
1558         }
1559         else {
1560             if (snap_MainWindow(d) == fullscreen_WindowSnap) {
1561                 SDL_SetWindowFullscreen(d->base.win, 0);
1562             }
1563             else {
1564                 SDL_RestoreWindow(d->base.win);
1565             }
1566         }
1567         return;
1568     }
1569 #if defined (LAGRANGE_ENABLE_CUSTOM_FRAME)
1570     if (d->place.snap == snapMode) {
1571         return;
1572     }
1573     const int snapDist = gap_UI * 4;
1574     iRect newRect = zero_Rect();
1575     SDL_Rect usable;
1576     SDL_GetDisplayUsableBounds(SDL_GetWindowDisplayIndex(d->base.win), &usable);
1577     if (d->place.snap == fullscreen_WindowSnap) {
1578         SDL_SetWindowFullscreen(d->base.win, 0);
1579     }
1580     d->place.snap = snapMode & ~redo_WindowSnap;
1581     switch (snapMode & mask_WindowSnap) {
1582         case none_WindowSnap:
1583             newRect = intersect_Rect(d->place.normalRect,
1584                                      init_Rect(usable.x, usable.y, usable.w, usable.h));
1585             break;
1586         case left_WindowSnap:
1587             newRect = init_Rect(usable.x, usable.y, usable.w / 2, usable.h);
1588             break;
1589         case right_WindowSnap:
1590             newRect =
1591                 init_Rect(usable.x + usable.w / 2, usable.y, usable.w - usable.w / 2, usable.h);
1592             break;
1593         case maximized_WindowSnap:
1594             newRect = init_Rect(usable.x, usable.y, usable.w, usable.h);
1595             break;
1596         case yMaximized_WindowSnap:
1597             newRect.pos.y = 0;
1598             newRect.size.y = usable.h;
1599             SDL_GetWindowSize(d->base.win, &newRect.size.x, NULL);
1600             SDL_GetWindowPosition(d->base.win, &newRect.pos.x, NULL);
1601             /* Snap the window to left/right edges, if close by. */
1602             if (iAbs(right_Rect(newRect) - (usable.x + usable.w)) < snapDist) {
1603                 newRect.pos.x = usable.x + usable.w - width_Rect(newRect);
1604             }
1605             if (iAbs(newRect.pos.x - usable.x) < snapDist) {
1606                 newRect.pos.x = usable.x;
1607             }
1608             break;
1609         case fullscreen_WindowSnap:
1610             SDL_SetWindowFullscreen(d->base.win, SDL_WINDOW_FULLSCREEN_DESKTOP);
1611             break;
1612     }
1613     if (snapMode & (topBit_WindowSnap | bottomBit_WindowSnap)) {
1614         newRect.size.y /= 2;
1615     }
1616     if (snapMode & bottomBit_WindowSnap) {
1617         newRect.pos.y += newRect.size.y;
1618     }
1619     if (newRect.size.x) {
1620         // printf("snap:%d newrect:%d,%d %dx%d\n", snapMode, newRect.pos.x, newRect.pos.y, newRect.size.x, newRect.size.y);
1621         SDL_SetWindowPosition(d->base.win, newRect.pos.x, newRect.pos.y);
1622         SDL_SetWindowSize(d->base.win, newRect.size.x, newRect.size.y);
1623         postCommand_App("window.resized");
1624     }
1625     /* Update window controls. */
1626     iForIndices(rootIndex, d->base.roots) {
1627         iRoot *root = d->base.roots[rootIndex];
1628         if (!root) continue;
1629         iWidget *winBar = findChild_Widget(root->widget, "winbar");
1630         updateTextCStr_LabelWidget(findChild_Widget(winBar, "winbar.max"),
1631                                    d->place.snap == maximized_WindowSnap ? "\u25a2" : "\u25a1");
1632         /* Show and hide the title bar. */
1633         const iBool wasVisible = isVisible_Widget(winBar);
1634         setFlags_Widget(winBar, hidden_WidgetFlag, d->place.snap == fullscreen_WindowSnap);
1635         if (wasVisible != isVisible_Widget(winBar)) {
1636             arrange_Widget(root->widget);
1637             postRefresh_App();
1638         }
1639     }
1640 #endif /* defined (LAGRANGE_ENABLE_CUSTOM_FRAME) */
1641 }
1642 
snap_MainWindow(const iMainWindow * d)1643 int snap_MainWindow(const iMainWindow *d) {
1644     if (!prefs_App()->customFrame) {
1645         const int flags = SDL_GetWindowFlags(d->base.win);
1646         if (flags & SDL_WINDOW_FULLSCREEN_DESKTOP) {
1647             return fullscreen_WindowSnap;
1648         }
1649         else if (flags & SDL_WINDOW_MAXIMIZED) {
1650             return maximized_WindowSnap;
1651         }
1652         return none_WindowSnap;
1653     }
1654     return d->place.snap;
1655 }
1656 
1657 /*----------------------------------------------------------------------------------------------*/
1658 
newPopup_Window(iInt2 screenPos,iWidget * rootWidget)1659 iWindow *newPopup_Window(iInt2 screenPos, iWidget *rootWidget) {
1660     start_PerfTimer(newPopup_Window);
1661     const iBool oldSw = forceSoftwareRender_App();
1662     /* On macOS, SDL seems to want to not use HiDPI with software rendering. */
1663 #if !defined (iPlatformApple)
1664     setForceSoftwareRender_App(iTrue);
1665 #endif
1666     iWindow *win =
1667         new_Window(popup_WindowType,
1668                    (iRect){ screenPos, divf_I2(rootWidget->rect.size, get_Window()->pixelRatio) },
1669                    SDL_WINDOW_ALWAYS_ON_TOP |
1670 #if !defined (iPlatformAppleDesktop)
1671                    SDL_WINDOW_BORDERLESS |
1672 #endif
1673                    SDL_WINDOW_POPUP_MENU |
1674                    SDL_WINDOW_SKIP_TASKBAR);
1675 #if defined (iPlatformAppleDesktop)
1676     hideTitleBar_MacOS(win); /* make it a borderless window, but retain shadow */
1677 #endif
1678     iRoot *root   = new_Root();
1679     win->roots[0] = root;
1680     win->keyRoot  = root;
1681     root->widget  = rootWidget;
1682     root->window  = win;
1683     rootWidget->rect.pos = zero_I2();
1684     setRoot_Widget(rootWidget, root);
1685     setForceSoftwareRender_App(oldSw);
1686 #if !defined (NDEBUG)
1687     stop_PerfTimer(newPopup_Window);
1688 #endif
1689     return win;
1690 }
1691