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