summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CMakeLists.txt2
-rw-r--r--src/app.c3
-rw-r--r--src/ui/documentwidget.c3
-rw-r--r--src/ui/root.c1224
-rw-r--r--src/ui/root.h13
-rw-r--r--src/ui/sidebarwidget.c3
-rw-r--r--src/ui/widget.c5
-rw-r--r--src/ui/window.c1186
-rw-r--r--src/ui/window.h1
9 files changed, 1265 insertions, 1175 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index bf624a1d..1813fd70 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -169,6 +169,8 @@ set (SOURCES
169 src/ui/metrics.h 169 src/ui/metrics.h
170 src/ui/paint.c 170 src/ui/paint.c
171 src/ui/paint.h 171 src/ui/paint.h
172 src/ui/root.c
173 src/ui/root.h
172 src/ui/mediaui.c 174 src/ui/mediaui.c
173 src/ui/mediaui.h 175 src/ui/mediaui.h
174 src/ui/scrollwidget.c 176 src/ui/scrollwidget.c
diff --git a/src/app.c b/src/app.c
index ce3f94f9..e6707b04 100644
--- a/src/app.c
+++ b/src/app.c
@@ -39,6 +39,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
39#include "ui/inputwidget.h" 39#include "ui/inputwidget.h"
40#include "ui/keys.h" 40#include "ui/keys.h"
41#include "ui/labelwidget.h" 41#include "ui/labelwidget.h"
42#include "ui/root.h"
42#include "ui/sidebarwidget.h" 43#include "ui/sidebarwidget.h"
43#include "ui/text.h" 44#include "ui/text.h"
44#include "ui/util.h" 45#include "ui/util.h"
@@ -1662,7 +1663,7 @@ iBool handleCommand_App(const char *cmd) {
1662 else if (equal_Command(cmd, "hidetoolbarscroll")) { 1663 else if (equal_Command(cmd, "hidetoolbarscroll")) {
1663 d->prefs.hideToolbarOnScroll = arg_Command(cmd); 1664 d->prefs.hideToolbarOnScroll = arg_Command(cmd);
1664 if (!d->prefs.hideToolbarOnScroll) { 1665 if (!d->prefs.hideToolbarOnScroll) {
1665 showToolbars_Window(d->window, iTrue); 1666 showToolbars_Root(get_Root(), iTrue);
1666 } 1667 }
1667 return iTrue; 1668 return iTrue;
1668 } 1669 }
diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c
index cdf3faa5..ee44933e 100644
--- a/src/ui/documentwidget.c
+++ b/src/ui/documentwidget.c
@@ -42,6 +42,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
42#include "media.h" 42#include "media.h"
43#include "paint.h" 43#include "paint.h"
44#include "periodic.h" 44#include "periodic.h"
45#include "root.h"
45#include "mediaui.h" 46#include "mediaui.h"
46#include "scrollwidget.h" 47#include "scrollwidget.h"
47#include "touch.h" 48#include "touch.h"
@@ -1353,7 +1354,7 @@ static void scrollBegan_DocumentWidget_(iAnyObject *any, int offset, uint32_t du
1353 /* Show and hide toolbar on scroll. */ 1354 /* Show and hide toolbar on scroll. */
1354 if (deviceType_App() == phone_AppDeviceType) { 1355 if (deviceType_App() == phone_AppDeviceType) {
1355 if (prefs_App()->hideToolbarOnScroll && iAbs(offset) > 5) { 1356 if (prefs_App()->hideToolbarOnScroll && iAbs(offset) > 5) {
1356 showToolbars_Window(get_Window(), offset < 0); 1357 showToolbars_Root(get_Root(), offset < 0);
1357 } 1358 }
1358 } 1359 }
1359 updateVisible_DocumentWidget_(d); 1360 updateVisible_DocumentWidget_(d);
diff --git a/src/ui/root.c b/src/ui/root.c
new file mode 100644
index 00000000..50c4992e
--- /dev/null
+++ b/src/ui/root.c
@@ -0,0 +1,1224 @@
1/* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi>
2
3Redistribution and use in source and binary forms, with or without
4modification, are permitted provided that the following conditions are met:
5
61. Redistributions of source code must retain the above copyright notice, this
7 list of conditions and the following disclaimer.
82. 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
12THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
13ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
14WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
15DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
16ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
17(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
18LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
19ANY 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
21SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22
23#include "root.h"
24
25#include "app.h"
26#include "bookmarks.h"
27#include "command.h"
28#include "defs.h"
29#include "documentwidget.h"
30#include "embedded.h"
31#include "inputwidget.h"
32#include "keys.h"
33#include "labelwidget.h"
34#include "lookupwidget.h"
35#include "sidebarwidget.h"
36#include "window.h"
37#include "../visited.h"
38#include "../history.h"
39#include "../gmcerts.h"
40#include "../gmutil.h"
41#include "../visited.h"
42
43#if defined (iPlatformMsys)
44# include "../win32.h"
45#endif
46#if defined (iPlatformAppleDesktop)
47# include "macos.h"
48#endif
49#if defined (iPlatformAppleMobile)
50# include "ios.h"
51#endif
52
53#include <SDL_timer.h>
54
55#if defined (iPlatformAppleDesktop)
56# define iHaveNativeMenus
57#endif
58
59#if defined (iPlatformPcDesktop)
60/* TODO: Submenus wouldn't hurt here. */
61static const iMenuItem navMenuItems_[] = {
62 { add_Icon " ${menu.newtab}", 't', KMOD_PRIMARY, "tabs.new" },
63 { "${menu.openlocation}", SDLK_l, KMOD_PRIMARY, "navigate.focus" },
64 { "---", 0, 0, NULL },
65 { download_Icon " " saveToDownloads_Label, SDLK_s, KMOD_PRIMARY, "document.save" },
66 { "${menu.downloads}", 0, 0, "downloads.open" },
67 { "${menu.page.copysource}", SDLK_c, KMOD_PRIMARY, "copy" },
68 { "---", 0, 0, NULL },
69 { leftHalf_Icon " ${menu.sidebar.left}", SDLK_l, KMOD_PRIMARY | KMOD_SHIFT, "sidebar.toggle" },
70 { rightHalf_Icon " ${menu.sidebar.right}", SDLK_p, KMOD_PRIMARY | KMOD_SHIFT, "sidebar2.toggle" },
71 { "${menu.zoom.in}", SDLK_EQUALS, KMOD_PRIMARY, "zoom.delta arg:10" },
72 { "${menu.zoom.out}", SDLK_MINUS, KMOD_PRIMARY, "zoom.delta arg:-10" },
73 { "${menu.zoom.reset}", SDLK_0, KMOD_PRIMARY, "zoom.set arg:100" },
74 { "---", 0, 0, NULL },
75 { book_Icon " ${menu.bookmarks.list}", 0, 0, "!open url:about:bookmarks" },
76 { "${menu.bookmarks.bytag}", 0, 0, "!open url:about:bookmarks?tags" },
77 { "${menu.bookmarks.bytime}", 0, 0, "!open url:about:bookmarks?created" },
78 { "---", 0, 0, NULL },
79 { "${menu.feeds.entrylist}", 0, 0, "!open url:about:feeds" },
80 { "---", 0, 0, NULL },
81 { gear_Icon " ${menu.preferences}", SDLK_COMMA, KMOD_PRIMARY, "preferences" },
82 { "${menu.help}", SDLK_F1, 0, "!open url:about:help" },
83 { "${menu.releasenotes}", 0, 0, "!open url:about:version" },
84 { "---", 0, 0, NULL },
85 { "${menu.quit}", 'q', KMOD_PRIMARY, "quit" }
86};
87#endif
88
89#if defined (iPlatformAppleMobile)
90/* Tablet menu. */
91static const iMenuItem tabletNavMenuItems_[] = {
92 { add_Icon " ${menu.newtab}", 't', KMOD_PRIMARY, "tabs.new" },
93 { close_Icon " ${menu.closetab}", 'w', KMOD_PRIMARY, "tabs.close" },
94 { "---", 0, 0, NULL },
95 { magnifyingGlass_Icon " ${menu.find}", 0, 0, "focus.set id:find.input" },
96 { leftHalf_Icon " ${menu.sidebar.left}", SDLK_l, KMOD_PRIMARY | KMOD_SHIFT, "sidebar.toggle" },
97 { rightHalf_Icon " ${menu.sidebar.right}", SDLK_p, KMOD_PRIMARY | KMOD_SHIFT, "sidebar2.toggle" },
98 { "---", 0, 0, NULL },
99 { book_Icon " ${menu.bookmarks.list}", 0, 0, "!open url:about:bookmarks" },
100 { "${menu.bookmarks.bytag}", 0, 0, "!open url:about:bookmarks?tags" },
101 { "${menu.feeds.entrylist}", 0, 0, "!open url:about:feeds" },
102 { "${menu.downloads}", 0, 0, "downloads.open" },
103 { "---", 0, 0, NULL },
104 { gear_Icon " ${menu.preferences}", SDLK_COMMA, KMOD_PRIMARY, "preferences" },
105 { "${menu.help}", SDLK_F1, 0, "!open url:about:help" },
106 { "${menu.releasenotes}", 0, 0, "!open url:about:version" },
107};
108
109/* Phone menu. */
110static const iMenuItem phoneNavMenuItems_[] = {
111 { add_Icon " ${menu.newtab}", 't', KMOD_PRIMARY, "tabs.new" },
112 { close_Icon " ${menu.closetab}", 'w', KMOD_PRIMARY, "tabs.close" },
113 { "---", 0, 0, NULL },
114 { magnifyingGlass_Icon " ${menu.find}", 0, 0, "focus.set id:find.input" },
115 { leftHalf_Icon " ${menu.sidebar}", SDLK_l, KMOD_PRIMARY | KMOD_SHIFT, "sidebar.toggle" },
116 { "---", 0, 0, NULL },
117 { book_Icon " ${menu.bookmarks.list}", 0, 0, "!open url:about:bookmarks" },
118 { "${menu.feeds.entrylist}", 0, 0, "!open url:about:feeds" },
119 { "${menu.downloads}", 0, 0, "downloads.open" },
120 { "---", 0, 0, NULL },
121 { gear_Icon " Settings...", SDLK_COMMA, KMOD_PRIMARY, "preferences" },
122};
123#endif /* AppleMobile */
124
125#if defined (iPlatformAppleMobile)
126static const iMenuItem identityButtonMenuItems_[] = {
127 { "${menu.identity.notactive}", 0, 0, "ident.showactive" },
128 { "---", 0, 0, NULL },
129 { add_Icon " ${menu.identity.new}", SDLK_n, KMOD_PRIMARY | KMOD_SHIFT, "ident.new" },
130 { "${menu.identity.import}", SDLK_i, KMOD_PRIMARY | KMOD_SHIFT, "ident.import" },
131 { "---", 0, 0, NULL },
132 { person_Icon " ${menu.show.identities}", 0, 0, "toolbar.showident" },
133};
134#else /* desktop */
135static const iMenuItem identityButtonMenuItems_[] = {
136 { "${menu.identity.notactive}", 0, 0, "ident.showactive" },
137 { "---", 0, 0, NULL },
138# if !defined (iPlatformAppleDesktop)
139 { add_Icon " ${menu.identity.new}", SDLK_n, KMOD_PRIMARY | KMOD_SHIFT, "ident.new" },
140 { "${menu.identity.import}", SDLK_i, KMOD_PRIMARY | KMOD_SHIFT, "ident.import" },
141 { "---", 0, 0, NULL },
142 { person_Icon " ${menu.show.identities}", '4', KMOD_PRIMARY, "sidebar.mode arg:3 show:1" },
143# else
144 { add_Icon " ${menu.identity.new}", 0, 0, "ident.new" },
145 { "---", 0, 0, NULL },
146 { person_Icon " ${menu.show.identities}", 0, 0, "sidebar.mode arg:3 show:1" },
147# endif
148};
149#endif
150
151static const char *reloadCStr_ = reload_Icon;
152static const char *pageMenuCStr_ = midEllipsis_Icon;
153
154/* TODO: A preference for these, maybe? */
155static const char *stopSeqCStr_[] = {
156 /* Corners */
157 uiTextCaution_ColorEscape "\U0000230c",
158 uiTextCaution_ColorEscape "\U0000230d",
159 uiTextCaution_ColorEscape "\U0000230f",
160 uiTextCaution_ColorEscape "\U0000230e",
161#if 0
162 /* Rotating arrow */
163 uiTextCaution_ColorEscape "\U00002b62",
164 uiTextCaution_ColorEscape "\U00002b68",
165 uiTextCaution_ColorEscape "\U00002b63",
166 uiTextCaution_ColorEscape "\U00002b69",
167 uiTextCaution_ColorEscape "\U00002b60",
168 uiTextCaution_ColorEscape "\U00002b66",
169 uiTextCaution_ColorEscape "\U00002b61",
170 uiTextCaution_ColorEscape "\U00002b67",
171#endif
172#if 0
173 /* Star */
174 uiTextCaution_ColorEscape "\u2bcc",
175 uiTextCaution_ColorEscape "\u2bcd",
176 uiTextCaution_ColorEscape "\u2bcc",
177 uiTextCaution_ColorEscape "\u2bcd",
178 uiTextCaution_ColorEscape "\u2bcc",
179 uiTextCaution_ColorEscape "\u2bcd",
180 uiTextCaution_ColorEscape "\u2bce",
181 uiTextCaution_ColorEscape "\u2bcf",
182 uiTextCaution_ColorEscape "\u2bce",
183 uiTextCaution_ColorEscape "\u2bcf",
184 uiTextCaution_ColorEscape "\u2bce",
185 uiTextCaution_ColorEscape "\u2bcf",
186#endif
187#if 0
188 /* Pulsing circle */
189 uiTextCaution_ColorEscape "\U0001f785",
190 uiTextCaution_ColorEscape "\U0001f786",
191 uiTextCaution_ColorEscape "\U0001f787",
192 uiTextCaution_ColorEscape "\U0001f788",
193 uiTextCaution_ColorEscape "\U0001f789",
194 uiTextCaution_ColorEscape "\U0001f789",
195 uiTextCaution_ColorEscape "\U0001f788",
196 uiTextCaution_ColorEscape "\U0001f787",
197 uiTextCaution_ColorEscape "\U0001f786",
198#endif
199#if 0
200 /* Dancing dots */
201 uiTextCaution_ColorEscape "\U0001fb00",
202 uiTextCaution_ColorEscape "\U0001fb01",
203 uiTextCaution_ColorEscape "\U0001fb07",
204 uiTextCaution_ColorEscape "\U0001fb1e",
205 uiTextCaution_ColorEscape "\U0001fb0f",
206 uiTextCaution_ColorEscape "\U0001fb03",
207 uiTextCaution_ColorEscape "\U0001fb00",
208 uiTextCaution_ColorEscape "\U0001fb01",
209 uiTextCaution_ColorEscape "\U0001fb07",
210 uiTextCaution_ColorEscape "\U0001fb1e",
211 uiTextCaution_ColorEscape "\U0001fb0f",
212 uiTextCaution_ColorEscape "\U0001fb03",
213
214 uiTextCaution_ColorEscape "\U0001fb7d",
215 uiTextCaution_ColorEscape "\U0001fb7e",
216 uiTextCaution_ColorEscape "\U0001fb7f",
217 uiTextCaution_ColorEscape "\U0001fb7c",
218 uiTextCaution_ColorEscape "\U0001fb7d",
219 uiTextCaution_ColorEscape "\U0001fb7e",
220 uiTextCaution_ColorEscape "\U0001fb7f",
221 uiTextCaution_ColorEscape "\U0001fb7c",
222
223 uiTextCaution_ColorEscape "\U0001fb00",
224 uiTextCaution_ColorEscape "\U0001fb01",
225 uiTextCaution_ColorEscape "\U0001fb07",
226 uiTextCaution_ColorEscape "\U0001fb03",
227 uiTextCaution_ColorEscape "\U0001fb0f",
228 uiTextCaution_ColorEscape "\U0001fb1e",
229 uiTextCaution_ColorEscape "\U0001fb07",
230 uiTextCaution_ColorEscape "\U0001fb03",
231#endif
232};
233
234static const int loadAnimIntervalMs_ = 133;
235static int loadAnimIndex_ = 0;
236
237static iWidget * activeRoot_ = NULL;
238
239void setCurrent_Root(iWidget *root) {
240 activeRoot_ = root;
241}
242
243iWidget *get_Root(void) {
244 return activeRoot_;
245}
246
247static iBool handleRootCommands_(iWidget *root, const char *cmd) {
248 iUnused(root);
249 if (equal_Command(cmd, "menu.open")) {
250 iWidget *button = pointer_Command(cmd);
251 iWidget *menu = findChild_Widget(button, "menu");
252 iAssert(menu);
253 if (!isVisible_Widget(menu)) {
254 openMenu_Widget(menu, init_I2(0, button->rect.size.y));
255 }
256 else {
257 closeMenu_Widget(menu);
258 }
259 return iTrue;
260 }
261 else if (equal_Command(cmd, "contextclick")) {
262 iBool showBarMenu = iFalse;
263 if (equal_Rangecc(range_Command(cmd, "id"), "buttons")) {
264 const iWidget *sidebar = findWidget_App("sidebar");
265 const iWidget *sidebar2 = findWidget_App("sidebar2");
266 const iWidget *buttons = pointer_Command(cmd);
267 if (hasParent_Widget(buttons, sidebar) ||
268 hasParent_Widget(buttons, sidebar2)) {
269 showBarMenu = iTrue;
270 }
271 }
272 if (equal_Rangecc(range_Command(cmd, "id"), "navbar")) {
273 showBarMenu = iTrue;
274 }
275 if (showBarMenu) {
276 openMenu_Widget(findWidget_App("barmenu"), coord_Command(cmd));
277 return iTrue;
278 }
279 return iFalse;
280 }
281 else if (equal_Command(cmd, "focus.set")) {
282 setFocus_Widget(findWidget_App(cstr_Rangecc(range_Command(cmd, "id"))));
283 return iTrue;
284 }
285 else if (equal_Command(cmd, "window.focus.lost")) {
286#if !defined (iPlatformMobile) /* apps don't share input focus on mobile */
287 setFocus_Widget(NULL);
288#endif
289 setTextColor_LabelWidget(findWidget_App("winbar.app"), uiAnnotation_ColorId);
290 setTextColor_LabelWidget(findWidget_App("winbar.title"), uiAnnotation_ColorId);
291 return iFalse;
292 }
293 else if (equal_Command(cmd, "window.focus.gained")) {
294 setTextColor_LabelWidget(findWidget_App("winbar.app"), uiTextAppTitle_ColorId);
295 setTextColor_LabelWidget(findWidget_App("winbar.title"), uiTextStrong_ColorId);
296 return iFalse;
297 }
298 else if (equal_Command(cmd, "window.setrect")) {
299 const int snap = argLabel_Command(cmd, "snap");
300 if (snap) {
301 iWindow *window = get_Window();
302 iInt2 coord = coord_Command(cmd);
303 iInt2 size = init_I2(argLabel_Command(cmd, "width"),
304 argLabel_Command(cmd, "height"));
305 SDL_SetWindowPosition(window->win, coord.x, coord.y);
306 SDL_SetWindowSize(window->win, size.x, size.y);
307 window->place.snap = snap;
308 return iTrue;
309 }
310 }
311 else if (equal_Command(cmd, "window.restore")) {
312 setSnap_Window(get_Window(), none_WindowSnap);
313 return iTrue;
314 }
315 else if (equal_Command(cmd, "window.minimize")) {
316 SDL_MinimizeWindow(get_Window()->win);
317 return iTrue;
318 }
319 else if (equal_Command(cmd, "window.close")) {
320 SDL_PushEvent(&(SDL_Event){ .type = SDL_QUIT });
321 return iTrue;
322 }
323 else if (deviceType_App() == phone_AppDeviceType && equal_Command(cmd, "window.resized")) {
324 /* Place the sidebar next to or under doctabs depending on orientation. */
325 iSidebarWidget *sidebar = findChild_Widget(root, "sidebar");
326 iSidebarWidget *sidebar2 = findChild_Widget(root, "sidebar2");
327 removeChild_Widget(parent_Widget(sidebar), sidebar);
328 setButtonFont_SidebarWidget(sidebar, isLandscape_App() ? uiLabel_FontId : defaultBig_FontId);
329 setButtonFont_SidebarWidget(sidebar2, isLandscape_App() ? uiLabel_FontId : defaultBig_FontId);
330 // setBackgroundColor_Widget(findChild_Widget(as_Widget(sidebar), "buttons"),
331 // isPortrait_App() ? uiBackgroundUnfocusedSelection_ColorId
332 // : uiBackgroundSidebar_ColorId);
333 setFlags_Widget(findChild_Widget(as_Widget(sidebar), "buttons"),
334 borderTop_WidgetFlag,
335 isPortrait_App());
336 if (isLandscape_App()) {
337 addChildPos_Widget(findChild_Widget(root, "tabs.content"), iClob(sidebar), front_WidgetAddPos);
338 setWidth_SidebarWidget(sidebar, 73.0f);
339 if (isVisible_Widget(findWidget_App("sidebar2"))) {
340 postCommand_App("sidebar2.toggle");
341 }
342 }
343 else {
344 addChildPos_Widget(findChild_Widget(root, "stack"), iClob(sidebar), back_WidgetAddPos);
345 setWidth_SidebarWidget(sidebar, (float) width_Widget(root) / (float) gap_UI);
346 }
347 return iFalse;
348 }
349 else if (handleCommand_App(cmd)) {
350 return iTrue;
351 }
352 return iFalse;
353}
354
355static void updateNavBarIdentity_(iWidget *navBar) {
356 const iGmIdentity *ident =
357 identityForUrl_GmCerts(certs_App(), url_DocumentWidget(document_App()));
358 iWidget *button = findChild_Widget(navBar, "navbar.ident");
359 iWidget *tool = findWidget_App("toolbar.ident");
360 setFlags_Widget(button, selected_WidgetFlag, ident != NULL);
361 setFlags_Widget(tool, selected_WidgetFlag, ident != NULL);
362 /* Update menu. */
363 iLabelWidget *idItem = child_Widget(findChild_Widget(button, "menu"), 0);
364 setTextCStr_LabelWidget(
365 idItem,
366 ident ? format_CStr(uiTextAction_ColorEscape "%s",
367 cstrCollect_String(subject_TlsCertificate(ident->cert)))
368 : "${menu.identity.notactive}");
369 setFlags_Widget(as_Widget(idItem), disabled_WidgetFlag, !ident);
370}
371
372static void updateNavDirButtons_(iWidget *navBar) {
373 const iHistory *history = history_DocumentWidget(document_App());
374 setFlags_Widget(findChild_Widget(navBar, "navbar.back"), disabled_WidgetFlag,
375 atOldest_History(history));
376 setFlags_Widget(findChild_Widget(navBar, "navbar.forward"), disabled_WidgetFlag,
377 atLatest_History(history));
378 setFlags_Widget(findWidget_App("toolbar.back"), disabled_WidgetFlag,
379 atOldest_History(history));
380 setFlags_Widget(findWidget_App("toolbar.forward"), disabled_WidgetFlag,
381 atLatest_History(history));
382}
383
384static const char *loadAnimationCStr_(void) {
385 return stopSeqCStr_[loadAnimIndex_ % iElemCount(stopSeqCStr_)];
386}
387
388static uint32_t updateReloadAnimation_Window_(uint32_t interval, void *window) {
389 iUnused(window);
390 loadAnimIndex_++;
391 postCommand_App("window.reload.update");
392 return interval;
393}
394
395static void setReloadLabel_Window_(iWindow *d, iBool animating) {
396 const iBool isMobile = deviceType_App() != desktop_AppDeviceType;
397 iLabelWidget *label = findChild_Widget(d->root, "reload");
398 updateTextCStr_LabelWidget(label, animating ? loadAnimationCStr_() :
399 (isMobile ? pageMenuCStr_ : reloadCStr_));
400 if (isMobile) {
401 setCommand_LabelWidget(label,
402 collectNewCStr_String(animating ? "navigate.reload" : "menu.open"));
403 }
404}
405
406static void checkLoadAnimation_Window_(iWindow *d) {
407 const iBool isOngoing = isRequestOngoing_DocumentWidget(document_App());
408 if (isOngoing && !d->loadAnimTimer) {
409 d->loadAnimTimer = SDL_AddTimer(loadAnimIntervalMs_, updateReloadAnimation_Window_, d);
410 }
411 else if (!isOngoing && d->loadAnimTimer) {
412 SDL_RemoveTimer(d->loadAnimTimer);
413 d->loadAnimTimer = 0;
414 }
415 setReloadLabel_Window_(d, isOngoing);
416}
417
418void updatePadding_Root(iWidget *d) {
419#if defined (iPlatformAppleMobile)
420 iWidget *toolBar = findChild_Widget(d, "toolbar");
421 float left, top, right, bottom;
422 safeAreaInsets_iOS(&left, &top, &right, &bottom);
423 /* Respect the safe area insets. */ {
424 setPadding_Widget(findChild_Widget(d, "navdiv"), left, top, right, 0);
425 if (toolBar) {
426 setPadding_Widget(toolBar, left, 0, right, bottom);
427 }
428 }
429 if (toolBar) {
430 /* TODO: get this from toolBar height, but it's buggy for some reason */
431 const int sidebarBottomPad = isPortrait_App() ? 11 * gap_UI + bottom : 0;
432 setPadding_Widget(findChild_Widget(d, "sidebar"), 0, 0, 0, sidebarBottomPad);
433 setPadding_Widget(findChild_Widget(d, "sidebar2"), 0, 0, 0, sidebarBottomPad);
434 /* TODO: There seems to be unrelated layout glitch in the sidebar where its children
435 are not arranged correctly until it's hidden and reshown. */
436 }
437 /* Note that `handleNavBarCommands_` also adjusts padding and spacing. */
438#else
439 iUnused(d);
440#endif
441}
442
443void dismissPortraitPhoneSidebars_Root(iWidget *root) {
444 if (deviceType_App() == phone_AppDeviceType && isPortrait_App()) {
445 iWidget *sidebar = findChild_Widget(root, "sidebar");
446 iWidget *sidebar2 = findChild_Widget(root, "sidebar2");
447 if (isVisible_Widget(sidebar)) {
448 postCommand_App("sidebar.toggle");
449 setVisualOffset_Widget(sidebar, height_Widget(sidebar), 250, easeIn_AnimFlag);
450 }
451 if (isVisible_Widget(sidebar2)) {
452 postCommand_App("sidebar2.toggle");
453 setVisualOffset_Widget(sidebar2, height_Widget(sidebar2), 250, easeIn_AnimFlag);
454 }
455 // setFlags_Widget(findWidget_App("toolbar.ident"), noBackground_WidgetFlag, iTrue);
456 // setFlags_Widget(findWidget_App("toolbar.view"), noBackground_WidgetFlag, iTrue);
457 }
458}
459
460static iBool willPerformSearchQuery_(const iString *userInput) {
461 const iString *clean = collect_String(trimmed_String(userInput));
462 if (isEmpty_String(clean)) {
463 return iFalse;
464 }
465 return !isEmpty_String(&prefs_App()->searchUrl) && !isLikelyUrl_String(userInput);
466}
467
468static void showSearchQueryIndicator_(iBool show) {
469 iWidget *indicator = findWidget_App("input.indicator.search");
470 showCollapsed_Widget(indicator, show);
471 iAssert(isInstance_Object(parent_Widget(parent_Widget(indicator)), &Class_InputWidget));
472 iInputWidget *url = (iInputWidget *) parent_Widget(parent_Widget(indicator));
473 setContentPadding_InputWidget(url, -1, contentPadding_InputWidget(url).left +
474 (show ? width_Widget(indicator) : 0));
475}
476
477static int navBarAvailableSpace_(iWidget *navBar) {
478 int avail = width_Rect(innerBounds_Widget(navBar));
479 iConstForEach(ObjectList, i, children_Widget(navBar)) {
480 const iWidget *child = i.object;
481 if (~flags_Widget(child) & expand_WidgetFlag &&
482 isVisible_Widget(child) &&
483 cmp_String(id_Widget(child), "url")) {
484 avail -= width_Widget(child);
485 }
486 }
487 return avail;
488}
489
490iBool isNarrow_Window(const iWindow *d) {
491 return width_Rect(safeRootRect_Window(d)) / gap_UI < 140;
492}
493
494static iBool handleNavBarCommands_(iWidget *navBar, const char *cmd) {
495 if (equal_Command(cmd, "window.resized") || equal_Command(cmd, "metrics.changed")) {
496 const iBool isPhone = deviceType_App() == phone_AppDeviceType;
497 const iBool isNarrow = !isPhone && isNarrow_Window(get_Window());
498 /* Adjust navbar padding. */ {
499 int hPad = isPhone && isPortrait_App() ? 0 : (isPhone || isNarrow) ? gap_UI / 2
500 : gap_UI * 3 / 2;
501 int vPad = gap_UI * 3 / 2;
502 int topPad = !findWidget_App("winbar") ? gap_UI / 2 : 0;
503 setPadding_Widget(navBar, hPad, vPad / 3 + topPad, hPad, vPad / 2);
504 }
505 /* Button sizing. */
506 if (isNarrow ^ ((flags_Widget(navBar) & tight_WidgetFlag) != 0)) {
507 setFlags_Widget(navBar, tight_WidgetFlag, isNarrow);
508 iForEach(ObjectList, i, navBar->children) {
509 iWidget *child = as_Widget(i.object);
510 setFlags_Widget(child, tight_WidgetFlag, isNarrow);
511 if (isInstance_Object(i.object, &Class_LabelWidget)) {
512 iLabelWidget *label = i.object;
513 updateSize_LabelWidget(label);
514 }
515 }
516 /* Note that InputWidget uses the `tight` flag to adjust its inner padding. */
517 /* TODO: Is this redundant? See `updateMetrics_Window_()`. */
518 const int embedButtonWidth = width_Widget(findChild_Widget(navBar, "navbar.lock"));
519 setContentPadding_InputWidget(findChild_Widget(navBar, "url"),
520 embedButtonWidth * 0.75f,
521 embedButtonWidth * 0.75f);
522 }
523 if (isPhone) {
524 static const char *buttons[] = { "navbar.back", "navbar.forward", "navbar.sidebar",
525 "navbar.ident", "navbar.home", "navbar.menu" };
526 iWidget *toolBar = findWidget_App("toolbar");
527 setVisualOffset_Widget(toolBar, 0, 0, 0);
528 setFlags_Widget(toolBar, hidden_WidgetFlag, isLandscape_App());
529 iForIndices(i, buttons) {
530 iLabelWidget *btn = findChild_Widget(navBar, buttons[i]);
531 setFlags_Widget(as_Widget(btn), hidden_WidgetFlag, isPortrait_App());
532 if (isLandscape_App()) {
533 /* Collapsing sets size to zero and the label doesn't know when to update
534 its own size automatically. */
535 updateSize_LabelWidget(btn);
536 }
537 }
538 arrange_Widget(get_Window()->root);
539 }
540 /* Resize the URL input field. */ {
541 iWidget *urlBar = findChild_Widget(navBar, "url");
542 urlBar->rect.size.x = iMini(navBarAvailableSpace_(navBar), 167 * gap_UI);
543 arrange_Widget(navBar);
544 }
545 refresh_Widget(navBar);
546 postCommand_Widget(navBar, "layout.changed id:navbar");
547 return iFalse;
548 }
549 else if (equal_Command(cmd, "window.reload.update")) {
550 checkLoadAnimation_Window_(get_Window());
551 return iTrue;
552 }
553 else if (equal_Command(cmd, "navigate.focus")) {
554 iWidget *url = findChild_Widget(navBar, "url");
555 if (focus_Widget() != url) {
556 setFocus_Widget(findChild_Widget(navBar, "url"));
557 }
558 else {
559 selectAll_InputWidget((iInputWidget *) url);
560 }
561 return iTrue;
562 }
563 else if (equal_Command(cmd, "input.edited")) {
564 iAnyObject * url = findChild_Widget(navBar, "url");
565 const iString *text = text_InputWidget(url);
566 const iBool show = willPerformSearchQuery_(text);
567 showSearchQueryIndicator_(show);
568 if (pointer_Command(cmd) == url) {
569 submit_LookupWidget(findWidget_App("lookup"), text);
570 return iTrue;
571 }
572 }
573 else if (startsWith_CStr(cmd, "input.ended id:url ")) {
574 iInputWidget *url = findChild_Widget(navBar, "url");
575 showSearchQueryIndicator_(iFalse);
576 if (isEmpty_String(text_InputWidget(url))) {
577 /* User entered nothing; restore the current URL. */
578 setText_InputWidget(url, url_DocumentWidget(document_App()));
579 return iTrue;
580 }
581 if (arg_Command(cmd) && argLabel_Command(cmd, "enter") &&
582 !isFocused_Widget(findWidget_App("lookup"))) {
583 iString *newUrl = copy_String(text_InputWidget(url));
584 trim_String(newUrl);
585 if (willPerformSearchQuery_(newUrl)) {
586 postCommandf_App("open url:%s", cstr_String(searchQueryUrl_App(newUrl)));
587 }
588 else {
589 postCommandf_App(
590 "open url:%s",
591 cstr_String(absoluteUrl_String(&iStringLiteral(""), collect_String(newUrl))));
592 }
593 return iTrue;
594 }
595 }
596 else if (startsWith_CStr(cmd, "document.")) {
597 /* React to the current document only. */
598 if (document_Command(cmd) == document_App()) {
599 if (equal_Command(cmd, "document.changed")) {
600 iInputWidget *url = findWidget_App("url");
601 const iString *urlStr = collect_String(suffix_Command(cmd, "url"));
602 trimCache_App();
603 visitUrl_Visited(visited_App(), withSpacesEncoded_String(urlStr), 0); /* TODO: internal URI normalization */
604 postCommand_App("visited.changed"); /* sidebar will update */
605 setText_InputWidget(url, urlStr);
606 checkLoadAnimation_Window_(get_Window());
607 dismissPortraitPhoneSidebars_Root(get_Root());
608 updateNavBarIdentity_(navBar);
609 updateNavDirButtons_(navBar);
610 /* Icon updates should be limited to automatically chosen icons if the user
611 is allowed to pick their own in the future. */
612 if (updateBookmarkIcon_Bookmarks(bookmarks_App(), urlStr,
613 siteIcon_GmDocument(document_DocumentWidget(document_App())))) {
614 postCommand_App("bookmarks.changed");
615 }
616 return iFalse;
617 }
618 else if (equal_Command(cmd, "document.request.cancelled")) {
619 checkLoadAnimation_Window_(get_Window());
620 return iFalse;
621 }
622 else if (equal_Command(cmd, "document.request.started")) {
623 iInputWidget *url = findChild_Widget(navBar, "url");
624 setTextCStr_InputWidget(url, suffixPtr_Command(cmd, "url"));
625 checkLoadAnimation_Window_(get_Window());
626 dismissPortraitPhoneSidebars_Root(get_Root());
627 return iFalse;
628 }
629 }
630 }
631 else if (equal_Command(cmd, "tabs.changed")) {
632 /* Update navbar according to the current tab. */
633 iDocumentWidget *doc = document_App();
634 if (doc) {
635 setText_InputWidget(findChild_Widget(navBar, "url"), url_DocumentWidget(doc));
636 checkLoadAnimation_Window_(get_Window());
637 updateNavBarIdentity_(navBar);
638 }
639 setFocus_Widget(NULL);
640 }
641 else if (equal_Command(cmd, "mouse.clicked") && arg_Command(cmd)) {
642 iWidget *widget = pointer_Command(cmd);
643 iWidget *menu = findWidget_App("doctabs.menu");
644 if (isTabButton_Widget(widget)) {
645 if (!isVisible_Widget(menu)) {
646 iWidget *tabs = findWidget_App("doctabs");
647 iWidget *page = tabPage_Widget(tabs, childIndex_Widget(widget->parent, widget));
648 if (argLabel_Command(cmd, "button") == SDL_BUTTON_MIDDLE) {
649 postCommandf_App("tabs.close id:%s", cstr_String(id_Widget(page)));
650 return iTrue;
651 }
652 showTabPage_Widget(tabs, page);
653 openMenu_Widget(menu, coord_Command(cmd));
654 }
655 }
656 }
657 else if (equal_Command(cmd, "navigate.reload")) {
658 iDocumentWidget *doc = document_Command(cmd);
659 if (isRequestOngoing_DocumentWidget(doc)) {
660 postCommand_App("document.stop");
661 }
662 else {
663 postCommand_App("document.reload");
664 }
665 return iTrue;
666 }
667 return iFalse;
668}
669
670static iBool handleSearchBarCommands_(iWidget *searchBar, const char *cmd) {
671 if (equal_Command(cmd, "input.ended") &&
672 equal_Rangecc(range_Command(cmd, "id"), "find.input")) {
673 iInputWidget *input = findChild_Widget(searchBar, "find.input");
674 if (arg_Command(cmd) && argLabel_Command(cmd, "enter") && isVisible_Widget(input)) {
675 postCommand_App("find.next");
676 /* Keep focus when pressing Enter. */
677 if (!isEmpty_String(text_InputWidget(input))) {
678 postCommand_App("focus.set id:find.input");
679 }
680 }
681 else {
682 postCommand_App("find.clearmark");
683 }
684 return iTrue;
685 }
686 else if (equal_Command(cmd, "focus.gained")) {
687 if (pointer_Command(cmd) == findChild_Widget(searchBar, "find.input")) {
688 if (!isVisible_Widget(searchBar)) {
689 setFlags_Widget(searchBar, hidden_WidgetFlag | disabled_WidgetFlag, iFalse);
690 arrange_Widget(get_Window()->root);
691 postRefresh_App();
692 }
693 }
694 }
695 else if (equal_Command(cmd, "find.close")) {
696 if (isVisible_Widget(searchBar)) {
697 setFlags_Widget(searchBar, hidden_WidgetFlag | disabled_WidgetFlag, iTrue);
698 arrange_Widget(searchBar->parent);
699 if (isFocused_Widget(findChild_Widget(searchBar, "find.input"))) {
700 setFocus_Widget(NULL);
701 }
702 refresh_Widget(searchBar->parent);
703 }
704 return iTrue;
705 }
706 return iFalse;
707}
708
709#if defined (iPlatformAppleMobile)
710static void dismissSidebar_(iWidget *sidebar, const char *toolButtonId) {
711 if (isVisible_Widget(sidebar)) {
712 postCommandf_App("%s.toggle", cstr_String(id_Widget(sidebar)));
713 if (toolButtonId) {
714 // setFlags_Widget(findWidget_App(toolButtonId), noBackground_WidgetFlag, iTrue);
715 }
716 setVisualOffset_Widget(sidebar, height_Widget(sidebar), 250, easeIn_AnimFlag);
717 }
718}
719
720static iBool handleToolBarCommands_(iWidget *toolBar, const char *cmd) {
721 if (equalWidget_Command(cmd, toolBar, "mouse.clicked") && arg_Command(cmd) &&
722 argLabel_Command(cmd, "button") == SDL_BUTTON_RIGHT) {
723 iWidget *menu = findChild_Widget(toolBar, "toolbar.menu");
724 arrange_Widget(menu);
725 openMenu_Widget(menu, init_I2(0, -height_Widget(menu)));
726 return iTrue;
727 }
728 else if (equal_Command(cmd, "toolbar.showview")) {
729 /* TODO: Clean this up. */
730 iWidget *sidebar = findWidget_App("sidebar");
731 iWidget *sidebar2 = findWidget_App("sidebar2");
732 dismissSidebar_(sidebar2, "toolbar.ident");
733 const iBool isVisible = isVisible_Widget(sidebar);
734 // setFlags_Widget(findChild_Widget(toolBar, "toolbar.view"), noBackground_WidgetFlag,
735 // isVisible);
736 /* If a sidebar hasn't been shown yet, it's height is zero. */
737 const int viewHeight = rootSize_Window(get_Window()).y;
738 if (arg_Command(cmd) >= 0) {
739 postCommandf_App("sidebar.mode arg:%d show:1", arg_Command(cmd));
740 if (!isVisible) {
741 setVisualOffset_Widget(sidebar, viewHeight, 0, 0);
742 setVisualOffset_Widget(sidebar, 0, 400, easeOut_AnimFlag | softer_AnimFlag);
743 }
744 }
745 else {
746 postCommandf_App("sidebar.toggle");
747 if (isVisible) {
748 setVisualOffset_Widget(sidebar, height_Widget(sidebar), 250, easeIn_AnimFlag);
749 }
750 else {
751 setVisualOffset_Widget(sidebar, viewHeight, 0, 0);
752 setVisualOffset_Widget(sidebar, 0, 400, easeOut_AnimFlag | softer_AnimFlag);
753 }
754 }
755 return iTrue;
756 }
757 else if (equal_Command(cmd, "toolbar.showident")) {
758 /* TODO: Clean this up. */
759 iWidget *sidebar = findWidget_App("sidebar");
760 iWidget *sidebar2 = findWidget_App("sidebar2");
761 dismissSidebar_(sidebar, "toolbar.view");
762 const iBool isVisible = isVisible_Widget(sidebar2);
763 // setFlags_Widget(findChild_Widget(toolBar, "toolbar.ident"), noBackground_WidgetFlag,
764 // isVisible);
765 /* If a sidebar hasn't been shown yet, it's height is zero. */
766 const int viewHeight = rootSize_Window(get_Window()).y;
767 if (isVisible) {
768 dismissSidebar_(sidebar2, NULL);
769 }
770 else {
771 postCommand_App("sidebar2.mode arg:3 show:1");
772 int offset = height_Widget(sidebar2);
773 if (offset == 0) offset = rootSize_Window(get_Window()).y;
774 setVisualOffset_Widget(sidebar2, offset, 0, 0);
775 setVisualOffset_Widget(sidebar2, 0, 400, easeOut_AnimFlag | softer_AnimFlag);
776 }
777 return iTrue;
778 }
779 else if (equal_Command(cmd, "sidebar.mode.changed")) {
780 iLabelWidget *viewTool = findChild_Widget(toolBar, "toolbar.view");
781 updateTextCStr_LabelWidget(viewTool, icon_SidebarMode(arg_Command(cmd)));
782 return iFalse;
783 }
784 return iFalse;
785}
786#endif /* defined (iPlatformAppleMobile) */
787
788static iLabelWidget *newLargeIcon_LabelWidget(const char *text, const char *cmd) {
789 iLabelWidget *lab = newIcon_LabelWidget(text, 0, 0, cmd);
790 setFont_LabelWidget(lab, uiLabelLarge_FontId);
791 return lab;
792}
793
794static int appIconSize_(void) {
795 return lineHeight_Text(uiContent_FontId);
796}
797
798void updateMetrics_Root(iWidget *d) {
799 /* Custom frame. */
800 iWidget *winBar = findChild_Widget(d, "winbar");
801 if (winBar) {
802 iWidget *appIcon = findChild_Widget(winBar, "winbar.icon");
803 iWidget *appTitle = findChild_Widget(winBar, "winbar.title");
804 iWidget *appMin = findChild_Widget(winBar, "winbar.min");
805 iWidget *appMax = findChild_Widget(winBar, "winbar.max");
806 iWidget *appClose = findChild_Widget(winBar, "winbar.close");
807 setPadding_Widget(winBar, 0, gap_UI / 3, 0, 0);
808 setFixedSize_Widget(appMin, init_I2(gap_UI * 11.5f, height_Widget(appTitle)));
809 setFixedSize_Widget(appMax, appMin->rect.size);
810 setFixedSize_Widget(appClose, appMin->rect.size);
811 setFixedSize_Widget(appIcon, init_I2(appIconSize_(), appMin->rect.size.y));
812 }
813 iWidget *navBar = findChild_Widget(d, "navbar");
814 iWidget *lock = findChild_Widget(navBar, "navbar.lock");
815 iWidget *url = findChild_Widget(d, "url");
816 iWidget *rightEmbed = findChild_Widget(navBar, "url.rightembed");
817 iWidget *embedPad = findChild_Widget(navBar, "url.embedpad");
818 setPadding_Widget(as_Widget(url), 0, gap_UI, 0, gap_UI);
819 navBar->rect.size.y = 0; /* recalculate height based on children (FIXME: shouldn't be needed) */
820 updateSize_LabelWidget((iLabelWidget *) lock);
821 setFixedSize_Widget(embedPad, init_I2(width_Widget(lock) + gap_UI / 2, 1));
822 setContentPadding_InputWidget((iInputWidget *) url, width_Widget(lock) * 0.75,
823 width_Widget(lock) * 0.75);
824 rightEmbed->rect.pos.y = gap_UI;
825 updatePadding_Root(d);
826 arrange_Widget(d);
827 postRefresh_App();
828}
829
830iWidget *createUserInterface_Root(void) {
831 iWidget *root = new_Widget();
832 setId_Widget(root, "root");
833 /* Children of root cover the entire window. */
834 setFlags_Widget(
835 root, resizeChildren_WidgetFlag | fixedSize_WidgetFlag | focusRoot_WidgetFlag, iTrue);
836 setCommandHandler_Widget(root, handleRootCommands_);
837
838 iWidget *div = makeVDiv_Widget();
839 setId_Widget(div, "navdiv");
840 addChild_Widget(root, iClob(div));
841
842#if defined (LAGRANGE_ENABLE_CUSTOM_FRAME)
843 /* Window title bar. */
844 if (prefs_App()->customFrame) {
845 setPadding1_Widget(div, 1);
846 iWidget *winBar = new_Widget();
847 setId_Widget(winBar, "winbar");
848 setFlags_Widget(winBar,
849 arrangeHeight_WidgetFlag | resizeChildren_WidgetFlag |
850 arrangeHorizontal_WidgetFlag | collapse_WidgetFlag,
851 iTrue);
852 iWidget *appIcon;
853 setId_Widget(
854 addChild_Widget(winBar, iClob(appIcon = makePadding_Widget(0))), "winbar.icon");
855 iLabelWidget *appButton =
856 addChildFlags_Widget(winBar,
857 iClob(new_LabelWidget("Lagrange", NULL)),
858 fixedHeight_WidgetFlag | frameless_WidgetFlag);
859 setTextColor_LabelWidget(appButton, uiTextAppTitle_ColorId);
860 setId_Widget(as_Widget(appButton), "winbar.app");
861 iLabelWidget *appTitle;
862 setFont_LabelWidget(appButton, uiContentBold_FontId);
863 setId_Widget(addChildFlags_Widget(winBar,
864 iClob(appTitle = new_LabelWidget("", NULL)),
865 expand_WidgetFlag | fixedHeight_WidgetFlag |
866 frameless_WidgetFlag | commandOnClick_WidgetFlag),
867 "winbar.title");
868 setTextColor_LabelWidget(appTitle, uiTextStrong_ColorId);
869 iLabelWidget *appMin, *appMax, *appClose;
870 setId_Widget(addChildFlags_Widget(
871 winBar,
872 iClob(appMin = newLargeIcon_LabelWidget("\u2013", "window.minimize")),
873 frameless_WidgetFlag),
874 "winbar.min");
875 addChildFlags_Widget(
876 winBar,
877 iClob(appMax = newLargeIcon_LabelWidget("\u25a1", "window.maximize toggle:1")),
878 frameless_WidgetFlag);
879 setId_Widget(as_Widget(appMax), "winbar.max");
880 addChildFlags_Widget(winBar,
881 iClob(appClose = newLargeIcon_LabelWidget(close_Icon, "window.close")),
882 frameless_WidgetFlag);
883 setId_Widget(as_Widget(appClose), "winbar.close");
884 setFont_LabelWidget(appClose, uiContent_FontId);
885 addChild_Widget(div, iClob(winBar));
886 setBackgroundColor_Widget(winBar, uiBackground_ColorId);
887 }
888#endif
889 /* Navigation bar. */ {
890 iWidget *navBar = new_Widget();
891 setId_Widget(navBar, "navbar");
892 setFlags_Widget(navBar,
893 hittable_WidgetFlag | /* context menu */
894 arrangeHeight_WidgetFlag |
895 resizeChildren_WidgetFlag |
896 arrangeHorizontal_WidgetFlag |
897 drawBackgroundToHorizontalSafeArea_WidgetFlag |
898 drawBackgroundToVerticalSafeArea_WidgetFlag,
899 iTrue);
900 addChild_Widget(div, iClob(navBar));
901 setBackgroundColor_Widget(navBar, uiBackground_ColorId);
902 setCommandHandler_Widget(navBar, handleNavBarCommands_);
903 setId_Widget(addChildFlags_Widget(navBar, iClob(newIcon_LabelWidget(backArrow_Icon, 0, 0, "navigate.back")), collapse_WidgetFlag), "navbar.back");
904 setId_Widget(addChildFlags_Widget(navBar, iClob(newIcon_LabelWidget(forwardArrow_Icon, 0, 0, "navigate.forward")), collapse_WidgetFlag), "navbar.forward");
905 /* Mobile devices have a button for easier access to the left sidebar. */
906 if (deviceType_App() != desktop_AppDeviceType) {
907 setId_Widget(addChildFlags_Widget(
908 navBar,
909 iClob(newIcon_LabelWidget(leftHalf_Icon, 0, 0, "sidebar.toggle")),
910 collapse_WidgetFlag),
911 "navbar.sidebar");
912 }
913 addChildFlags_Widget(navBar, iClob(new_Widget()), expand_WidgetFlag);
914 iLabelWidget *idMenu = makeMenuButton_LabelWidget(
915 "\U0001f464", identityButtonMenuItems_, iElemCount(identityButtonMenuItems_));
916 setAlignVisually_LabelWidget(idMenu, iTrue);
917 setId_Widget(addChildFlags_Widget(navBar, iClob(idMenu), collapse_WidgetFlag), "navbar.ident");
918 iInputWidget *url;
919 /* URL input field. */ {
920 url = new_InputWidget(0);
921 setFlags_Widget(as_Widget(url), resizeHeightOfChildren_WidgetFlag, iTrue);
922 setSelectAllOnFocus_InputWidget(url, iTrue);
923 setId_Widget(as_Widget(url), "url");
924 setUrlContent_InputWidget(url, iTrue);
925 setNotifyEdits_InputWidget(url, iTrue);
926 setTextCStr_InputWidget(url, "gemini://");
927 addChildFlags_Widget(navBar, iClob(url), 0);
928 const int64_t embedFlags =
929 noBackground_WidgetFlag | frameless_WidgetFlag | unpadded_WidgetFlag |
930 (deviceType_App() == desktop_AppDeviceType ? tight_WidgetFlag : 0);
931 /* Page information/certificate warning. */ {
932 iLabelWidget *lock = addChildFlags_Widget(
933 as_Widget(url),
934 iClob(newIcon_LabelWidget("\U0001f513", SDLK_i, KMOD_PRIMARY, "document.info")),
935 embedFlags | moveToParentLeftEdge_WidgetFlag);
936 setId_Widget(as_Widget(lock), "navbar.lock");
937 setFont_LabelWidget(lock, symbols_FontId + uiNormal_FontSize);
938 updateTextCStr_LabelWidget(lock, "\U0001f512");
939 }
940 iWidget *rightEmbed = new_Widget();
941 setId_Widget(rightEmbed, "url.rightembed");
942 addChildFlags_Widget(as_Widget(url),
943 iClob(rightEmbed),
944 arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag |
945 resizeHeightOfChildren_WidgetFlag |
946 moveToParentRightEdge_WidgetFlag);
947 /* Feeds refresh indicator is inside the input field. */ {
948 iLabelWidget *queryInd =
949 new_LabelWidget(uiTextAction_ColorEscape "\u21d2 ${status.query}", NULL);
950 setId_Widget(as_Widget(queryInd), "input.indicator.search");
951 setBackgroundColor_Widget(as_Widget(queryInd), uiBackground_ColorId);
952 setFrameColor_Widget(as_Widget(queryInd), uiTextAction_ColorId);
953 setAlignVisually_LabelWidget(queryInd, iTrue);
954 addChildFlags_Widget(rightEmbed,
955 iClob(queryInd),
956 collapse_WidgetFlag | hidden_WidgetFlag);
957 }
958 /* Feeds refresh indicator is inside the input field. */ {
959 iLabelWidget *fprog = new_LabelWidget(uiTextCaution_ColorEscape
960 "\u2605 ${status.feeds}", NULL);
961 setId_Widget(as_Widget(fprog), "feeds.progress");
962 setBackgroundColor_Widget(as_Widget(fprog), uiBackground_ColorId);
963 setAlignVisually_LabelWidget(fprog, iTrue);
964 addChildFlags_Widget(rightEmbed,
965 iClob(fprog),
966 collapse_WidgetFlag | frameless_WidgetFlag | hidden_WidgetFlag);
967 }
968 /* Download progress indicator is also inside the input field, but hidden normally. */ {
969 iLabelWidget *progress = new_LabelWidget(uiTextCaution_ColorEscape "00.000 ${mb}", NULL);
970 setId_Widget(as_Widget(progress), "document.progress");
971 setBackgroundColor_Widget(as_Widget(progress), uiBackground_ColorId);
972 setAlignVisually_LabelWidget(progress, iTrue);
973 addChildFlags_Widget(
974 rightEmbed, iClob(progress), collapse_WidgetFlag);
975 }
976 /* Reload button. */ {
977 iLabelWidget *reload;
978 if (deviceType_App() == desktop_AppDeviceType) {
979 reload = newIcon_LabelWidget(reloadCStr_, 0, 0, "navigate.reload");
980 }
981 else {
982 /* In a mobile layout, the reload button is replaced with the Page/Ellipsis menu. */
983 reload = makeMenuButton_LabelWidget(pageMenuCStr_,
984 (iMenuItem[]){
985 { reload_Icon " ${menu.reload}", reload_KeyShortcut, "navigate.reload" },
986 { timer_Icon " ${menu.autoreload}", 0, 0, "document.autoreload.menu" },
987 { "---", 0, 0, NULL },
988 { upArrow_Icon " ${menu.parent}", navigateParent_KeyShortcut, "navigate.parent" },
989 { upArrowBar_Icon " ${menu.root}", navigateRoot_KeyShortcut, "navigate.root" },
990 { "---", 0, 0, NULL },
991 { pin_Icon " ${menu.page.bookmark}", SDLK_d, KMOD_PRIMARY, "bookmark.add" },
992 { star_Icon " ${menu.page.subscribe}", subscribeToPage_KeyModifier, "feeds.subscribe" },
993 { book_Icon " ${menu.page.import}", 0, 0, "bookmark.links confirm:1" },
994 { globe_Icon " ${menu.page.translate}", 0, 0, "document.translate" },
995 { "---", 0, 0, NULL },
996 { "${menu.page.copyurl}", 0, 0, "document.copylink" },
997 { "${menu.page.copysource}", 'c', KMOD_PRIMARY, "copy" },
998 { download_Icon " " saveToDownloads_Label, SDLK_s, KMOD_PRIMARY, "document.save" } },
999 14);
1000 setFont_LabelWidget((iLabelWidget *) reload, uiContentBold_FontId);
1001 setAlignVisually_LabelWidget((iLabelWidget *) reload, iTrue);
1002 }
1003 setId_Widget(as_Widget(reload), "reload");
1004 addChildFlags_Widget(as_Widget(url), iClob(reload), embedFlags | moveToParentRightEdge_WidgetFlag);
1005 updateSize_LabelWidget(reload);
1006 }
1007 setId_Widget(addChild_Widget(rightEmbed, iClob(makePadding_Widget(0))), "url.embedpad");
1008 }
1009 if (deviceType_App() != desktop_AppDeviceType) {
1010 /* On mobile, the Identities button is on the right side of the URL bar. */
1011 iWidget *ident = removeChild_Widget(navBar, findChild_Widget(navBar, "navbar.ident"));
1012 addChild_Widget(navBar, iClob(ident));
1013 }
1014 addChildFlags_Widget(navBar, iClob(new_Widget()), expand_WidgetFlag);
1015 setId_Widget(addChildFlags_Widget(navBar,
1016 iClob(newIcon_LabelWidget(
1017 home_Icon, SDLK_h, KMOD_PRIMARY | KMOD_SHIFT, "navigate.home")),
1018 collapse_WidgetFlag),
1019 "navbar.home");
1020#if defined (iPlatformMobile)
1021 const iBool isPhone = (deviceType_App() == phone_AppDeviceType);
1022#endif
1023#if !defined (iHaveNativeMenus)
1024# if defined (iPlatformAppleMobile)
1025 iLabelWidget *navMenu =
1026 makeMenuButton_LabelWidget("\U0001d362", isPhone ? phoneNavMenuItems_ : tabletNavMenuItems_,
1027 isPhone ? iElemCount(phoneNavMenuItems_) : iElemCount(tabletNavMenuItems_));
1028# else
1029 iLabelWidget *navMenu =
1030 makeMenuButton_LabelWidget("\U0001d362", navMenuItems_, iElemCount(navMenuItems_));
1031# endif
1032 setAlignVisually_LabelWidget(navMenu, iTrue);
1033 setId_Widget(addChildFlags_Widget(navBar, iClob(navMenu), collapse_WidgetFlag), "navbar.menu");
1034#else
1035 insertMacMenus_();
1036#endif
1037 }
1038 /* Tab bar. */ {
1039 iWidget *mainStack = new_Widget();
1040 setId_Widget(mainStack, "stack");
1041 addChildFlags_Widget(div, iClob(mainStack), resizeChildren_WidgetFlag | expand_WidgetFlag |
1042 unhittable_WidgetFlag);
1043 iWidget *tabBar = makeTabs_Widget(mainStack);
1044 setId_Widget(tabBar, "doctabs");
1045 setBackgroundColor_Widget(tabBar, uiBackground_ColorId);
1046 appendTabPage_Widget(tabBar, iClob(new_DocumentWidget()), "Document", 0, 0);
1047 iWidget *buttons = findChild_Widget(tabBar, "tabs.buttons");
1048 setFlags_Widget(buttons, collapse_WidgetFlag | hidden_WidgetFlag |
1049 drawBackgroundToHorizontalSafeArea_WidgetFlag, iTrue);
1050 if (deviceType_App() == phone_AppDeviceType) {
1051 setBackgroundColor_Widget(buttons, uiBackground_ColorId);
1052 }
1053 setId_Widget(
1054 addChild_Widget(buttons, iClob(newIcon_LabelWidget(add_Icon, 0, 0, "tabs.new"))),
1055 "newtab");
1056 }
1057 /* Sidebars. */ {
1058 iWidget *content = findChild_Widget(root, "tabs.content");
1059 iSidebarWidget *sidebar1 = new_SidebarWidget(left_SideBarSide);
1060 addChildPos_Widget(content, iClob(sidebar1), front_WidgetAddPos);
1061 iSidebarWidget *sidebar2 = new_SidebarWidget(right_SideBarSide);
1062 if (deviceType_App() != phone_AppDeviceType) {
1063 addChildPos_Widget(content, iClob(sidebar2), back_WidgetAddPos);
1064 }
1065 else {
1066 /* The identities sidebar is always in the main area. */
1067 addChild_Widget(findChild_Widget(root, "stack"), iClob(sidebar2));
1068 setFlags_Widget(as_Widget(sidebar2), hidden_WidgetFlag, iTrue);
1069 }
1070 }
1071 /* Lookup results. */ {
1072 iLookupWidget *lookup = new_LookupWidget();
1073 addChildFlags_Widget(div, iClob(lookup), fixedPosition_WidgetFlag | hidden_WidgetFlag);
1074 }
1075 /* Search bar. */ {
1076 iWidget *searchBar = new_Widget();
1077 setId_Widget(searchBar, "search");
1078 setFlags_Widget(searchBar,
1079 hidden_WidgetFlag | disabled_WidgetFlag | collapse_WidgetFlag |
1080 arrangeHeight_WidgetFlag | resizeChildren_WidgetFlag |
1081 arrangeHorizontal_WidgetFlag,
1082 iTrue);
1083 if (deviceType_App() == desktop_AppDeviceType) {
1084 addChild_Widget(div, iClob(searchBar));
1085 }
1086 else {
1087 /* The search bar appears at the top on mobile, because there is a virtual keyboard
1088 covering the bottom. */
1089 insertChildAfter_Widget(div, iClob(searchBar),
1090 childIndex_Widget(div, findChild_Widget(div, "navbar")));
1091 }
1092 setBackgroundColor_Widget(searchBar, uiBackground_ColorId);
1093 setCommandHandler_Widget(searchBar, handleSearchBarCommands_);
1094 addChildFlags_Widget(
1095 searchBar, iClob(new_LabelWidget(magnifyingGlass_Icon, NULL)), frameless_WidgetFlag);
1096 iInputWidget *input = new_InputWidget(0);
1097 setHint_InputWidget(input, "${hint.findtext}");
1098 setSelectAllOnFocus_InputWidget(input, iTrue);
1099 setEatEscape_InputWidget(input, iFalse); /* unfocus and close with one keypress */
1100 setId_Widget(addChildFlags_Widget(searchBar, iClob(input), expand_WidgetFlag),
1101 "find.input");
1102 addChild_Widget(searchBar, iClob(newIcon_LabelWidget(" \u2b9f ", 'g', KMOD_PRIMARY, "find.next")));
1103 addChild_Widget(searchBar, iClob(newIcon_LabelWidget(" \u2b9d ", 'g', KMOD_PRIMARY | KMOD_SHIFT, "find.prev")));
1104 addChild_Widget(searchBar, iClob(newIcon_LabelWidget(close_Icon, SDLK_ESCAPE, 0, "find.close")));
1105 }
1106#if defined (iPlatformAppleMobile)
1107 /* Bottom toolbar. */
1108 if (isPhone_iOS()) {
1109 iWidget *toolBar = new_Widget();
1110 addChild_Widget(root, iClob(toolBar));
1111 setId_Widget(toolBar, "toolbar");
1112 setCommandHandler_Widget(toolBar, handleToolBarCommands_);
1113 setFlags_Widget(toolBar, moveToParentBottomEdge_WidgetFlag |
1114 parentCannotResizeHeight_WidgetFlag |
1115 resizeWidthOfChildren_WidgetFlag |
1116 arrangeHeight_WidgetFlag | arrangeHorizontal_WidgetFlag, iTrue);
1117 setBackgroundColor_Widget(toolBar, tmBannerBackground_ColorId);
1118 setId_Widget(addChildFlags_Widget(toolBar,
1119 iClob(newLargeIcon_LabelWidget("\U0001f870", "navigate.back")),
1120 frameless_WidgetFlag),
1121 "toolbar.back");
1122 setId_Widget(addChildFlags_Widget(toolBar,
1123 iClob(newLargeIcon_LabelWidget("\U0001f872", "navigate.forward")),
1124 frameless_WidgetFlag),
1125 "toolbar.forward");
1126 setId_Widget(addChildFlags_Widget(toolBar,
1127 iClob(newLargeIcon_LabelWidget("\U0001f464", "toolbar.showident")),
1128 frameless_WidgetFlag),
1129 "toolbar.ident");
1130 setId_Widget(addChildFlags_Widget(toolBar,
1131 iClob(newLargeIcon_LabelWidget("\U0001f588", "toolbar.showview arg:-1")),
1132 frameless_WidgetFlag | commandOnClick_WidgetFlag),
1133 "toolbar.view");
1134 iLabelWidget *menuButton = makeMenuButton_LabelWidget("\U0001d362", phoneNavMenuItems_,
1135 iElemCount(phoneNavMenuItems_));
1136 setFont_LabelWidget(menuButton, uiLabelLarge_FontId);
1137 setId_Widget(as_Widget(menuButton), "toolbar.navmenu");
1138 addChildFlags_Widget(toolBar, iClob(menuButton), frameless_WidgetFlag);
1139 iForEach(ObjectList, i, children_Widget(toolBar)) {
1140 iLabelWidget *btn = i.object;
1141 setFlags_Widget(i.object, noBackground_WidgetFlag, iTrue);
1142 setTextColor_LabelWidget(i.object, tmBannerIcon_ColorId);
1143 // setBackgroundColor_Widget(i.object, tmBannerSideTitle_ColorId);
1144 }
1145 const iMenuItem items[] = {
1146 { pin_Icon " ${sidebar.bookmarks}", 0, 0, "toolbar.showview arg:0" },
1147 { star_Icon " ${sidebar.feeds}", 0, 0, "toolbar.showview arg:1" },
1148 { clock_Icon " ${sidebar.history}", 0, 0, "toolbar.showview arg:2" },
1149 { page_Icon " ${toolbar.outline}", 0, 0, "toolbar.showview arg:4" },
1150 };
1151 iWidget *menu = makeMenu_Widget(findChild_Widget(toolBar, "toolbar.view"),
1152 items, iElemCount(items));
1153 setId_Widget(menu, "toolbar.menu"); /* view menu */
1154 }
1155#endif
1156 updatePadding_Root(root);
1157 /* Global context menus. */ {
1158 iWidget *tabsMenu = makeMenu_Widget(
1159 root,
1160 (iMenuItem[]){
1161 { close_Icon " ${menu.closetab}", 0, 0, "tabs.close" },
1162 { copy_Icon " ${menu.duptab}", 0, 0, "tabs.new duplicate:1" },
1163 { "---", 0, 0, NULL },
1164 { "${menu.closetab.other}", 0, 0, "tabs.close toleft:1 toright:1" },
1165 { barLeftArrow_Icon " ${menu.closetab.left}", 0, 0, "tabs.close toleft:1" },
1166 { barRightArrow_Icon " ${menu.closetab.right}", 0, 0, "tabs.close toright:1" },
1167 },
1168 6);
1169 iWidget *barMenu =
1170 makeMenu_Widget(root,
1171 (iMenuItem[]){
1172 { leftHalf_Icon " ${menu.sidebar.left}", 0, 0, "sidebar.toggle" },
1173 { rightHalf_Icon " ${menu.sidebar.right}", 0, 0, "sidebar2.toggle" },
1174 },
1175 deviceType_App() == phone_AppDeviceType ? 1 : 2);
1176 iWidget *clipMenu = makeMenu_Widget(root,
1177 (iMenuItem[]){
1178 { scissor_Icon " ${menu.cut}", 0, 0, "input.copy cut:1" },
1179 { clipCopy_Icon " ${menu.copy}", 0, 0, "input.copy" },
1180 { "---", 0, 0, NULL },
1181 { clipboard_Icon " ${menu.paste}", 0, 0, "input.paste" },
1182 },
1183 4);
1184 setId_Widget(tabsMenu, "doctabs.menu");
1185 setId_Widget(barMenu, "barmenu");
1186 setId_Widget(clipMenu, "clipmenu");
1187 }
1188 /* Global keyboard shortcuts. */ {
1189 addAction_Widget(root, 'l', KMOD_PRIMARY, "navigate.focus");
1190 addAction_Widget(root, 'f', KMOD_PRIMARY, "focus.set id:find.input");
1191 addAction_Widget(root, '1', KMOD_PRIMARY, "sidebar.mode arg:0 toggle:1");
1192 addAction_Widget(root, '2', KMOD_PRIMARY, "sidebar.mode arg:1 toggle:1");
1193 addAction_Widget(root, '3', KMOD_PRIMARY, "sidebar.mode arg:2 toggle:1");
1194 addAction_Widget(root, '4', KMOD_PRIMARY, "sidebar.mode arg:3 toggle:1");
1195 addAction_Widget(root, '5', KMOD_PRIMARY, "sidebar.mode arg:4 toggle:1");
1196 addAction_Widget(root, '1', rightSidebar_KeyModifier, "sidebar2.mode arg:0 toggle:1");
1197 addAction_Widget(root, '2', rightSidebar_KeyModifier, "sidebar2.mode arg:1 toggle:1");
1198 addAction_Widget(root, '3', rightSidebar_KeyModifier, "sidebar2.mode arg:2 toggle:1");
1199 addAction_Widget(root, '4', rightSidebar_KeyModifier, "sidebar2.mode arg:3 toggle:1");
1200 addAction_Widget(root, '5', rightSidebar_KeyModifier, "sidebar2.mode arg:4 toggle:1");
1201 }
1202 updateMetrics_Root(root);
1203 return root;
1204}
1205
1206void showToolbars_Root(iWidget *root, iBool show) {
1207 /* The toolbar is only used on phone portrait layout. */
1208 if (isLandscape_App()) return;
1209 iWidget *toolBar = findChild_Widget(root, "toolbar");
1210 if (!toolBar) return;
1211 const int height = rootSize_Window(get_Window()).y - top_Rect(boundsWithoutVisualOffset_Widget(toolBar));
1212 if (show && !isVisible_Widget(toolBar)) {
1213 setFlags_Widget(toolBar, hidden_WidgetFlag, iFalse);
1214 setVisualOffset_Widget(toolBar, 0, 200, easeOut_AnimFlag);
1215 }
1216 else if (!show && isVisible_Widget(toolBar)) {
1217 /* Close any menus that open via the toolbar. */
1218 closeMenu_Widget(findChild_Widget(findWidget_App("toolbar.navmenu"), "menu"));
1219 closeMenu_Widget(findChild_Widget(toolBar, "toolbar.menu"));
1220 setFlags_Widget(toolBar, hidden_WidgetFlag, iTrue);
1221 setVisualOffset_Widget(toolBar, height, 200, easeOut_AnimFlag);
1222 }
1223}
1224
diff --git a/src/ui/root.h b/src/ui/root.h
new file mode 100644
index 00000000..a3e595ba
--- /dev/null
+++ b/src/ui/root.h
@@ -0,0 +1,13 @@
1#pragma once
2
3#include "widget.h"
4
5iWidget * createUserInterface_Root (void);
6
7void setCurrent_Root (iWidget *root);
8iWidget * get_Root (void);
9
10void updateMetrics_Root (iWidget *);
11void updatePadding_Root (iWidget *); /* TODO: is part of metrics? */
12void dismissPortraitPhoneSidebars_Root (iWidget *);
13void showToolbars_Root (iWidget *, iBool show);
diff --git a/src/ui/sidebarwidget.c b/src/ui/sidebarwidget.c
index 5aa17140..683f6436 100644
--- a/src/ui/sidebarwidget.c
+++ b/src/ui/sidebarwidget.c
@@ -36,6 +36,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
36#include "listwidget.h" 36#include "listwidget.h"
37#include "keys.h" 37#include "keys.h"
38#include "paint.h" 38#include "paint.h"
39#include "root.h"
39#include "scrollwidget.h" 40#include "scrollwidget.h"
40#include "util.h" 41#include "util.h"
41#include "visited.h" 42#include "visited.h"
@@ -708,7 +709,7 @@ static void itemClicked_SidebarWidget_(iSidebarWidget *d, const iSidebarItem *it
708 const iGmDocument *doc = document_DocumentWidget(document_App()); 709 const iGmDocument *doc = document_DocumentWidget(document_App());
709 const iGmHeading *head = constAt_Array(headings_GmDocument(doc), item->id); 710 const iGmHeading *head = constAt_Array(headings_GmDocument(doc), item->id);
710 postCommandf_App("document.goto loc:%p", head->text.start); 711 postCommandf_App("document.goto loc:%p", head->text.start);
711 dismissPortraitPhoneSidebars_Window(get_Window()); 712 dismissPortraitPhoneSidebars_Root(get_Root());
712 break; 713 break;
713 } 714 }
714 case feeds_SidebarMode: { 715 case feeds_SidebarMode: {
diff --git a/src/ui/widget.c b/src/ui/widget.c
index b398b7f6..b7458528 100644
--- a/src/ui/widget.c
+++ b/src/ui/widget.c
@@ -26,6 +26,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
26#include "touch.h" 26#include "touch.h"
27#include "command.h" 27#include "command.h"
28#include "paint.h" 28#include "paint.h"
29#include "root.h"
29#include "util.h" 30#include "util.h"
30#include "window.h" 31#include "window.h"
31 32
@@ -39,7 +40,8 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
39#endif 40#endif
40 41
41iDeclareType(RootData) 42iDeclareType(RootData)
42 43
44/* TODO: Move to root.c, one instance per root widget. */
43struct Impl_RootData { 45struct Impl_RootData {
44 iWidget *hover; 46 iWidget *hover;
45 iWidget *mouseGrab; 47 iWidget *mouseGrab;
@@ -772,6 +774,7 @@ void unhover_Widget(void) {
772 774
773iBool dispatchEvent_Widget(iWidget *d, const SDL_Event *ev) { 775iBool dispatchEvent_Widget(iWidget *d, const SDL_Event *ev) {
774 if (!d->parent) { 776 if (!d->parent) {
777 setCurrent_Root(d);
775 if (ev->type == SDL_MOUSEMOTION) { 778 if (ev->type == SDL_MOUSEMOTION) {
776 /* Hover widget may change. */ 779 /* Hover widget may change. */
777 setHover_Widget(NULL); 780 setHover_Widget(NULL);
diff --git a/src/ui/window.c b/src/ui/window.c
index 10c7fc82..9732a4f9 100644
--- a/src/ui/window.c
+++ b/src/ui/window.c
@@ -22,25 +22,18 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22 22
23#include "window.h" 23#include "window.h"
24 24
25#include "defs.h" 25#include "../app.h"
26#include "labelwidget.h"
27#include "inputwidget.h"
28#include "documentwidget.h"
29#include "sidebarwidget.h"
30#include "lookupwidget.h"
31#include "bookmarks.h" 26#include "bookmarks.h"
32#include "embedded.h"
33#include "command.h" 27#include "command.h"
34#include "paint.h" 28#include "defs.h"
35#include "util.h" 29#include "embedded.h"
36#include "keys.h" 30#include "keys.h"
31#include "labelwidget.h"
32#include "paint.h"
33#include "root.h"
37#include "touch.h" 34#include "touch.h"
38#include "../app.h" 35#include "util.h"
39#include "../visited.h" 36
40#include "../history.h"
41#include "../gmcerts.h"
42#include "../gmutil.h"
43#include "../visited.h"
44#if defined (iPlatformMsys) 37#if defined (iPlatformMsys)
45# include "../win32.h" 38# include "../win32.h"
46#endif 39#endif
@@ -73,186 +66,12 @@ static float initialUiScale_ = 1.1f;
73 66
74iDefineTypeConstructionArgs(Window, (iRect rect), rect) 67iDefineTypeConstructionArgs(Window, (iRect rect), rect)
75 68
76static iBool handleRootCommands_(iWidget *root, const char *cmd) {
77 iUnused(root);
78 if (equal_Command(cmd, "menu.open")) {
79 iWidget *button = pointer_Command(cmd);
80 iWidget *menu = findChild_Widget(button, "menu");
81 iAssert(menu);
82 if (!isVisible_Widget(menu)) {
83 openMenu_Widget(menu, init_I2(0, button->rect.size.y));
84 }
85 else {
86 closeMenu_Widget(menu);
87 }
88 return iTrue;
89 }
90 else if (equal_Command(cmd, "contextclick")) {
91 iBool showBarMenu = iFalse;
92 if (equal_Rangecc(range_Command(cmd, "id"), "buttons")) {
93 const iWidget *sidebar = findWidget_App("sidebar");
94 const iWidget *sidebar2 = findWidget_App("sidebar2");
95 const iWidget *buttons = pointer_Command(cmd);
96 if (hasParent_Widget(buttons, sidebar) ||
97 hasParent_Widget(buttons, sidebar2)) {
98 showBarMenu = iTrue;
99 }
100 }
101 if (equal_Rangecc(range_Command(cmd, "id"), "navbar")) {
102 showBarMenu = iTrue;
103 }
104 if (showBarMenu) {
105 openMenu_Widget(findWidget_App("barmenu"), coord_Command(cmd));
106 return iTrue;
107 }
108 return iFalse;
109 }
110 else if (equal_Command(cmd, "focus.set")) {
111 setFocus_Widget(findWidget_App(cstr_Rangecc(range_Command(cmd, "id"))));
112 return iTrue;
113 }
114 else if (equal_Command(cmd, "window.focus.lost")) {
115#if !defined (iPlatformMobile) /* apps don't share input focus on mobile */
116 setFocus_Widget(NULL);
117#endif
118 setTextColor_LabelWidget(findWidget_App("winbar.app"), uiAnnotation_ColorId);
119 setTextColor_LabelWidget(findWidget_App("winbar.title"), uiAnnotation_ColorId);
120 return iFalse;
121 }
122 else if (equal_Command(cmd, "window.focus.gained")) {
123 setTextColor_LabelWidget(findWidget_App("winbar.app"), uiTextAppTitle_ColorId);
124 setTextColor_LabelWidget(findWidget_App("winbar.title"), uiTextStrong_ColorId);
125 return iFalse;
126 }
127 else if (equal_Command(cmd, "window.setrect")) {
128 const int snap = argLabel_Command(cmd, "snap");
129 if (snap) {
130 iWindow *window = get_Window();
131 iInt2 coord = coord_Command(cmd);
132 iInt2 size = init_I2(argLabel_Command(cmd, "width"),
133 argLabel_Command(cmd, "height"));
134 SDL_SetWindowPosition(window->win, coord.x, coord.y);
135 SDL_SetWindowSize(window->win, size.x, size.y);
136 window->place.snap = snap;
137 return iTrue;
138 }
139 }
140 else if (equal_Command(cmd, "window.restore")) {
141 setSnap_Window(get_Window(), none_WindowSnap);
142 return iTrue;
143 }
144 else if (equal_Command(cmd, "window.minimize")) {
145 SDL_MinimizeWindow(get_Window()->win);
146 return iTrue;
147 }
148 else if (equal_Command(cmd, "window.close")) {
149 SDL_PushEvent(&(SDL_Event){ .type = SDL_QUIT });
150 return iTrue;
151 }
152 else if (deviceType_App() == phone_AppDeviceType && equal_Command(cmd, "window.resized")) {
153 /* Place the sidebar next to or under doctabs depending on orientation. */
154 iSidebarWidget *sidebar = findChild_Widget(root, "sidebar");
155 iSidebarWidget *sidebar2 = findChild_Widget(root, "sidebar2");
156 removeChild_Widget(parent_Widget(sidebar), sidebar);
157 setButtonFont_SidebarWidget(sidebar, isLandscape_App() ? uiLabel_FontId : defaultBig_FontId);
158 setButtonFont_SidebarWidget(sidebar2, isLandscape_App() ? uiLabel_FontId : defaultBig_FontId);
159// setBackgroundColor_Widget(findChild_Widget(as_Widget(sidebar), "buttons"),
160// isPortrait_App() ? uiBackgroundUnfocusedSelection_ColorId
161// : uiBackgroundSidebar_ColorId);
162 setFlags_Widget(findChild_Widget(as_Widget(sidebar), "buttons"),
163 borderTop_WidgetFlag,
164 isPortrait_App());
165 if (isLandscape_App()) {
166 addChildPos_Widget(findChild_Widget(root, "tabs.content"), iClob(sidebar), front_WidgetAddPos);
167 setWidth_SidebarWidget(sidebar, 73.0f);
168 if (isVisible_Widget(findWidget_App("sidebar2"))) {
169 postCommand_App("sidebar2.toggle");
170 }
171 }
172 else {
173 addChildPos_Widget(findChild_Widget(root, "stack"), iClob(sidebar), back_WidgetAddPos);
174 setWidth_SidebarWidget(sidebar, (float) width_Widget(root) / (float) gap_UI);
175 }
176 return iFalse;
177 }
178 else if (handleCommand_App(cmd)) {
179 return iTrue;
180 }
181 return iFalse;
182}
183
184/* TODO: Define menus per platform. */ 69/* TODO: Define menus per platform. */
185 70
186#if defined (iPlatformAppleDesktop) 71#if defined (iPlatformAppleDesktop)
187# define iHaveNativeMenus 72# define iHaveNativeMenus
188#endif 73#endif
189 74
190#if !defined (iHaveNativeMenus)
191#if !defined (iPlatformAppleMobile)
192/* TODO: Submenus wouldn't hurt here. */
193static const iMenuItem navMenuItems_[] = {
194 { add_Icon " ${menu.newtab}", 't', KMOD_PRIMARY, "tabs.new" },
195 { "${menu.openlocation}", SDLK_l, KMOD_PRIMARY, "navigate.focus" },
196 { "---", 0, 0, NULL },
197 { download_Icon " " saveToDownloads_Label, SDLK_s, KMOD_PRIMARY, "document.save" },
198 { "${menu.downloads}", 0, 0, "downloads.open" },
199 { "${menu.page.copysource}", SDLK_c, KMOD_PRIMARY, "copy" },
200 { "---", 0, 0, NULL },
201 { leftHalf_Icon " ${menu.sidebar.left}", SDLK_l, KMOD_PRIMARY | KMOD_SHIFT, "sidebar.toggle" },
202 { rightHalf_Icon " ${menu.sidebar.right}", SDLK_p, KMOD_PRIMARY | KMOD_SHIFT, "sidebar2.toggle" },
203 { "${menu.zoom.in}", SDLK_EQUALS, KMOD_PRIMARY, "zoom.delta arg:10" },
204 { "${menu.zoom.out}", SDLK_MINUS, KMOD_PRIMARY, "zoom.delta arg:-10" },
205 { "${menu.zoom.reset}", SDLK_0, KMOD_PRIMARY, "zoom.set arg:100" },
206 { "---", 0, 0, NULL },
207 { book_Icon " ${menu.bookmarks.list}", 0, 0, "!open url:about:bookmarks" },
208 { "${menu.bookmarks.bytag}", 0, 0, "!open url:about:bookmarks?tags" },
209 { "${menu.bookmarks.bytime}", 0, 0, "!open url:about:bookmarks?created" },
210 { "---", 0, 0, NULL },
211 { "${menu.feeds.entrylist}", 0, 0, "!open url:about:feeds" },
212 { "---", 0, 0, NULL },
213 { gear_Icon " ${menu.preferences}", SDLK_COMMA, KMOD_PRIMARY, "preferences" },
214 { "${menu.help}", SDLK_F1, 0, "!open url:about:help" },
215 { "${menu.releasenotes}", 0, 0, "!open url:about:version" },
216 { "---", 0, 0, NULL },
217 { "${menu.quit}", 'q', KMOD_PRIMARY, "quit" }
218};
219#else
220/* Tablet menu. */
221static const iMenuItem tabletNavMenuItems_[] = {
222 { add_Icon " ${menu.newtab}", 't', KMOD_PRIMARY, "tabs.new" },
223 { close_Icon " ${menu.closetab}", 'w', KMOD_PRIMARY, "tabs.close" },
224 { "---", 0, 0, NULL },
225 { magnifyingGlass_Icon " ${menu.find}", 0, 0, "focus.set id:find.input" },
226 { leftHalf_Icon " ${menu.sidebar.left}", SDLK_l, KMOD_PRIMARY | KMOD_SHIFT, "sidebar.toggle" },
227 { rightHalf_Icon " ${menu.sidebar.right}", SDLK_p, KMOD_PRIMARY | KMOD_SHIFT, "sidebar2.toggle" },
228 { "---", 0, 0, NULL },
229 { book_Icon " ${menu.bookmarks.list}", 0, 0, "!open url:about:bookmarks" },
230 { "${menu.bookmarks.bytag}", 0, 0, "!open url:about:bookmarks?tags" },
231 { "${menu.feeds.entrylist}", 0, 0, "!open url:about:feeds" },
232 { "${menu.downloads}", 0, 0, "downloads.open" },
233 { "---", 0, 0, NULL },
234 { gear_Icon " ${menu.preferences}", SDLK_COMMA, KMOD_PRIMARY, "preferences" },
235 { "${menu.help}", SDLK_F1, 0, "!open url:about:help" },
236 { "${menu.releasenotes}", 0, 0, "!open url:about:version" },
237};
238
239/* Phone menu. */
240static const iMenuItem phoneNavMenuItems_[] = {
241 { add_Icon " ${menu.newtab}", 't', KMOD_PRIMARY, "tabs.new" },
242 { close_Icon " ${menu.closetab}", 'w', KMOD_PRIMARY, "tabs.close" },
243 { "---", 0, 0, NULL },
244 { magnifyingGlass_Icon " ${menu.find}", 0, 0, "focus.set id:find.input" },
245 { leftHalf_Icon " ${menu.sidebar}", SDLK_l, KMOD_PRIMARY | KMOD_SHIFT, "sidebar.toggle" },
246 { "---", 0, 0, NULL },
247 { book_Icon " ${menu.bookmarks.list}", 0, 0, "!open url:about:bookmarks" },
248 { "${menu.feeds.entrylist}", 0, 0, "!open url:about:feeds" },
249 { "${menu.downloads}", 0, 0, "downloads.open" },
250 { "---", 0, 0, NULL },
251 { gear_Icon " Settings...", SDLK_COMMA, KMOD_PRIMARY, "preferences" },
252};
253#endif /* AppleMobile */
254#endif
255
256#if defined (iHaveNativeMenus) 75#if defined (iHaveNativeMenus)
257/* Using native menus. */ 76/* Using native menus. */
258static const iMenuItem fileMenuItems_[] = { 77static const iMenuItem fileMenuItems_[] = {
@@ -342,980 +161,9 @@ static void removeMacMenus_(void) {
342} 161}
343#endif 162#endif
344 163
345#if defined (iPlatformAppleMobile)
346static const iMenuItem identityButtonMenuItems_[] = {
347 { "${menu.identity.notactive}", 0, 0, "ident.showactive" },
348 { "---", 0, 0, NULL },
349 { add_Icon " ${menu.identity.new}", SDLK_n, KMOD_PRIMARY | KMOD_SHIFT, "ident.new" },
350 { "${menu.identity.import}", SDLK_i, KMOD_PRIMARY | KMOD_SHIFT, "ident.import" },
351 { "---", 0, 0, NULL },
352 { person_Icon " ${menu.show.identities}", 0, 0, "toolbar.showident" },
353};
354#else /* desktop */
355static const iMenuItem identityButtonMenuItems_[] = {
356 { "${menu.identity.notactive}", 0, 0, "ident.showactive" },
357 { "---", 0, 0, NULL },
358# if !defined (iHaveNativeMenus)
359 { add_Icon " ${menu.identity.new}", SDLK_n, KMOD_PRIMARY | KMOD_SHIFT, "ident.new" },
360 { "${menu.identity.import}", SDLK_i, KMOD_PRIMARY | KMOD_SHIFT, "ident.import" },
361 { "---", 0, 0, NULL },
362 { person_Icon " ${menu.show.identities}", '4', KMOD_PRIMARY, "sidebar.mode arg:3 show:1" },
363# else
364 { add_Icon " ${menu.identity.new}", 0, 0, "ident.new" },
365 { "---", 0, 0, NULL },
366 { person_Icon " ${menu.show.identities}", 0, 0, "sidebar.mode arg:3 show:1" },
367#endif
368};
369#endif
370
371static const char *reloadCStr_ = reload_Icon;
372static const char *pageMenuCStr_ = midEllipsis_Icon;
373
374/* TODO: A preference for these, maybe? */
375static const char *stopSeqCStr_[] = {
376 /* Corners */
377 uiTextCaution_ColorEscape "\U0000230c",
378 uiTextCaution_ColorEscape "\U0000230d",
379 uiTextCaution_ColorEscape "\U0000230f",
380 uiTextCaution_ColorEscape "\U0000230e",
381#if 0
382 /* Rotating arrow */
383 uiTextCaution_ColorEscape "\U00002b62",
384 uiTextCaution_ColorEscape "\U00002b68",
385 uiTextCaution_ColorEscape "\U00002b63",
386 uiTextCaution_ColorEscape "\U00002b69",
387 uiTextCaution_ColorEscape "\U00002b60",
388 uiTextCaution_ColorEscape "\U00002b66",
389 uiTextCaution_ColorEscape "\U00002b61",
390 uiTextCaution_ColorEscape "\U00002b67",
391#endif
392#if 0
393 /* Star */
394 uiTextCaution_ColorEscape "\u2bcc",
395 uiTextCaution_ColorEscape "\u2bcd",
396 uiTextCaution_ColorEscape "\u2bcc",
397 uiTextCaution_ColorEscape "\u2bcd",
398 uiTextCaution_ColorEscape "\u2bcc",
399 uiTextCaution_ColorEscape "\u2bcd",
400 uiTextCaution_ColorEscape "\u2bce",
401 uiTextCaution_ColorEscape "\u2bcf",
402 uiTextCaution_ColorEscape "\u2bce",
403 uiTextCaution_ColorEscape "\u2bcf",
404 uiTextCaution_ColorEscape "\u2bce",
405 uiTextCaution_ColorEscape "\u2bcf",
406#endif
407#if 0
408 /* Pulsing circle */
409 uiTextCaution_ColorEscape "\U0001f785",
410 uiTextCaution_ColorEscape "\U0001f786",
411 uiTextCaution_ColorEscape "\U0001f787",
412 uiTextCaution_ColorEscape "\U0001f788",
413 uiTextCaution_ColorEscape "\U0001f789",
414 uiTextCaution_ColorEscape "\U0001f789",
415 uiTextCaution_ColorEscape "\U0001f788",
416 uiTextCaution_ColorEscape "\U0001f787",
417 uiTextCaution_ColorEscape "\U0001f786",
418#endif
419#if 0
420 /* Dancing dots */
421 uiTextCaution_ColorEscape "\U0001fb00",
422 uiTextCaution_ColorEscape "\U0001fb01",
423 uiTextCaution_ColorEscape "\U0001fb07",
424 uiTextCaution_ColorEscape "\U0001fb1e",
425 uiTextCaution_ColorEscape "\U0001fb0f",
426 uiTextCaution_ColorEscape "\U0001fb03",
427 uiTextCaution_ColorEscape "\U0001fb00",
428 uiTextCaution_ColorEscape "\U0001fb01",
429 uiTextCaution_ColorEscape "\U0001fb07",
430 uiTextCaution_ColorEscape "\U0001fb1e",
431 uiTextCaution_ColorEscape "\U0001fb0f",
432 uiTextCaution_ColorEscape "\U0001fb03",
433
434 uiTextCaution_ColorEscape "\U0001fb7d",
435 uiTextCaution_ColorEscape "\U0001fb7e",
436 uiTextCaution_ColorEscape "\U0001fb7f",
437 uiTextCaution_ColorEscape "\U0001fb7c",
438 uiTextCaution_ColorEscape "\U0001fb7d",
439 uiTextCaution_ColorEscape "\U0001fb7e",
440 uiTextCaution_ColorEscape "\U0001fb7f",
441 uiTextCaution_ColorEscape "\U0001fb7c",
442
443 uiTextCaution_ColorEscape "\U0001fb00",
444 uiTextCaution_ColorEscape "\U0001fb01",
445 uiTextCaution_ColorEscape "\U0001fb07",
446 uiTextCaution_ColorEscape "\U0001fb03",
447 uiTextCaution_ColorEscape "\U0001fb0f",
448 uiTextCaution_ColorEscape "\U0001fb1e",
449 uiTextCaution_ColorEscape "\U0001fb07",
450 uiTextCaution_ColorEscape "\U0001fb03",
451#endif
452};
453
454static void updateNavBarIdentity_(iWidget *navBar) {
455 const iGmIdentity *ident =
456 identityForUrl_GmCerts(certs_App(), url_DocumentWidget(document_App()));
457 iWidget *button = findChild_Widget(navBar, "navbar.ident");
458 iWidget *tool = findWidget_App("toolbar.ident");
459 setFlags_Widget(button, selected_WidgetFlag, ident != NULL);
460 setFlags_Widget(tool, selected_WidgetFlag, ident != NULL);
461 /* Update menu. */
462 iLabelWidget *idItem = child_Widget(findChild_Widget(button, "menu"), 0);
463 setTextCStr_LabelWidget(
464 idItem,
465 ident ? format_CStr(uiTextAction_ColorEscape "%s",
466 cstrCollect_String(subject_TlsCertificate(ident->cert)))
467 : "${menu.identity.notactive}");
468 setFlags_Widget(as_Widget(idItem), disabled_WidgetFlag, !ident);
469}
470
471static void updateNavDirButtons_(iWidget *navBar) {
472 const iHistory *history = history_DocumentWidget(document_App());
473 setFlags_Widget(findChild_Widget(navBar, "navbar.back"), disabled_WidgetFlag,
474 atOldest_History(history));
475 setFlags_Widget(findChild_Widget(navBar, "navbar.forward"), disabled_WidgetFlag,
476 atLatest_History(history));
477 setFlags_Widget(findWidget_App("toolbar.back"), disabled_WidgetFlag,
478 atOldest_History(history));
479 setFlags_Widget(findWidget_App("toolbar.forward"), disabled_WidgetFlag,
480 atLatest_History(history));
481}
482
483static const int loadAnimIntervalMs_ = 133;
484static int loadAnimIndex_ = 0;
485
486static const char *loadAnimationCStr_(void) {
487 return stopSeqCStr_[loadAnimIndex_ % iElemCount(stopSeqCStr_)];
488}
489
490static uint32_t updateReloadAnimation_Window_(uint32_t interval, void *window) {
491 iUnused(window);
492 loadAnimIndex_++;
493 postCommand_App("window.reload.update");
494 return interval;
495}
496
497static void setReloadLabel_Window_(iWindow *d, iBool animating) {
498 const iBool isMobile = deviceType_App() != desktop_AppDeviceType;
499 iLabelWidget *label = findChild_Widget(d->root, "reload");
500 updateTextCStr_LabelWidget(label, animating ? loadAnimationCStr_() :
501 (isMobile ? pageMenuCStr_ : reloadCStr_));
502 if (isMobile) {
503 setCommand_LabelWidget(label,
504 collectNewCStr_String(animating ? "navigate.reload" : "menu.open"));
505 }
506}
507
508static void checkLoadAnimation_Window_(iWindow *d) {
509 const iBool isOngoing = isRequestOngoing_DocumentWidget(document_App());
510 if (isOngoing && !d->loadAnimTimer) {
511 d->loadAnimTimer = SDL_AddTimer(loadAnimIntervalMs_, updateReloadAnimation_Window_, d);
512 }
513 else if (!isOngoing && d->loadAnimTimer) {
514 SDL_RemoveTimer(d->loadAnimTimer);
515 d->loadAnimTimer = 0;
516 }
517 setReloadLabel_Window_(d, isOngoing);
518}
519
520static void updatePadding_Window_(iWindow *d) {
521#if defined (iPlatformAppleMobile)
522 iWidget *toolBar = findChild_Widget(d->root, "toolbar");
523 float left, top, right, bottom;
524 safeAreaInsets_iOS(&left, &top, &right, &bottom);
525 /* Respect the safe area insets. */ {
526 setPadding_Widget(findChild_Widget(d->root, "navdiv"), left, top, right, 0);
527 if (toolBar) {
528 setPadding_Widget(toolBar, left, 0, right, bottom);
529 }
530 }
531 if (toolBar) {
532 /* TODO: get this from toolBar height, but it's buggy for some reason */
533 const int sidebarBottomPad = isPortrait_App() ? 11 * gap_UI + bottom : 0;
534 setPadding_Widget(findChild_Widget(d->root, "sidebar"), 0, 0, 0, sidebarBottomPad);
535 setPadding_Widget(findChild_Widget(d->root, "sidebar2"), 0, 0, 0, sidebarBottomPad);
536 /* TODO: There seems to be unrelated layout glitch in the sidebar where its children
537 are not arranged correctly until it's hidden and reshown. */
538 }
539 /* Note that `handleNavBarCommands_` also adjusts padding and spacing. */
540#endif
541}
542
543void dismissPortraitPhoneSidebars_Window(iWindow *d) {
544 if (deviceType_App() == phone_AppDeviceType && isPortrait_App()) {
545 iWidget *sidebar = findWidget_App("sidebar");
546 iWidget *sidebar2 = findWidget_App("sidebar2");
547 if (isVisible_Widget(sidebar)) {
548 postCommand_App("sidebar.toggle");
549 setVisualOffset_Widget(sidebar, height_Widget(sidebar), 250, easeIn_AnimFlag);
550 }
551 if (isVisible_Widget(sidebar2)) {
552 postCommand_App("sidebar2.toggle");
553 setVisualOffset_Widget(sidebar2, height_Widget(sidebar2), 250, easeIn_AnimFlag);
554 }
555// setFlags_Widget(findWidget_App("toolbar.ident"), noBackground_WidgetFlag, iTrue);
556// setFlags_Widget(findWidget_App("toolbar.view"), noBackground_WidgetFlag, iTrue);
557 }
558}
559
560static iBool willPerformSearchQuery_(const iString *userInput) {
561 const iString *clean = collect_String(trimmed_String(userInput));
562 if (isEmpty_String(clean)) {
563 return iFalse;
564 }
565 return !isEmpty_String(&prefs_App()->searchUrl) && !isLikelyUrl_String(userInput);
566}
567
568static void showSearchQueryIndicator_(iBool show) {
569 iWidget *indicator = findWidget_App("input.indicator.search");
570 showCollapsed_Widget(indicator, show);
571 iAssert(isInstance_Object(parent_Widget(parent_Widget(indicator)), &Class_InputWidget));
572 iInputWidget *url = (iInputWidget *) parent_Widget(parent_Widget(indicator));
573 setContentPadding_InputWidget(url, -1, contentPadding_InputWidget(url).left +
574 (show ? width_Widget(indicator) : 0));
575}
576
577static int navBarAvailableSpace_(iWidget *navBar) {
578 int avail = width_Rect(innerBounds_Widget(navBar));
579 iConstForEach(ObjectList, i, children_Widget(navBar)) {
580 const iWidget *child = i.object;
581 if (~flags_Widget(child) & expand_WidgetFlag &&
582 isVisible_Widget(child) &&
583 cmp_String(id_Widget(child), "url")) {
584 avail -= width_Widget(child);
585 }
586 }
587 return avail;
588}
589
590iBool isNarrow_Window(const iWindow *d) {
591 return width_Rect(safeRootRect_Window(d)) / gap_UI < 140;
592}
593
594static iBool handleNavBarCommands_(iWidget *navBar, const char *cmd) {
595 if (equal_Command(cmd, "window.resized") || equal_Command(cmd, "metrics.changed")) {
596 const iBool isPhone = deviceType_App() == phone_AppDeviceType;
597 const iBool isNarrow = !isPhone && isNarrow_Window(get_Window());
598 /* Adjust navbar padding. */ {
599 int hPad = isPhone && isPortrait_App() ? 0 : (isPhone || isNarrow) ? gap_UI / 2
600 : gap_UI * 3 / 2;
601 int vPad = gap_UI * 3 / 2;
602 int topPad = !findWidget_App("winbar") ? gap_UI / 2 : 0;
603 setPadding_Widget(navBar, hPad, vPad / 3 + topPad, hPad, vPad / 2);
604 }
605 /* Button sizing. */
606 if (isNarrow ^ ((flags_Widget(navBar) & tight_WidgetFlag) != 0)) {
607 setFlags_Widget(navBar, tight_WidgetFlag, isNarrow);
608 iForEach(ObjectList, i, navBar->children) {
609 iWidget *child = as_Widget(i.object);
610 setFlags_Widget(child, tight_WidgetFlag, isNarrow);
611 if (isInstance_Object(i.object, &Class_LabelWidget)) {
612 iLabelWidget *label = i.object;
613 updateSize_LabelWidget(label);
614 }
615 }
616 /* Note that InputWidget uses the `tight` flag to adjust its inner padding. */
617 /* TODO: Is this redundant? See `updateMetrics_Window_()`. */
618 const int embedButtonWidth = width_Widget(findChild_Widget(navBar, "navbar.lock"));
619 setContentPadding_InputWidget(findChild_Widget(navBar, "url"),
620 embedButtonWidth * 0.75f,
621 embedButtonWidth * 0.75f);
622 }
623 if (isPhone) {
624 static const char *buttons[] = { "navbar.back", "navbar.forward", "navbar.sidebar",
625 "navbar.ident", "navbar.home", "navbar.menu" };
626 iWidget *toolBar = findWidget_App("toolbar");
627 setVisualOffset_Widget(toolBar, 0, 0, 0);
628 setFlags_Widget(toolBar, hidden_WidgetFlag, isLandscape_App());
629 iForIndices(i, buttons) {
630 iLabelWidget *btn = findChild_Widget(navBar, buttons[i]);
631 setFlags_Widget(as_Widget(btn), hidden_WidgetFlag, isPortrait_App());
632 if (isLandscape_App()) {
633 /* Collapsing sets size to zero and the label doesn't know when to update
634 its own size automatically. */
635 updateSize_LabelWidget(btn);
636 }
637 }
638 arrange_Widget(get_Window()->root);
639 }
640 /* Resize the URL input field. */ {
641 iWidget *urlBar = findChild_Widget(navBar, "url");
642 urlBar->rect.size.x = iMini(navBarAvailableSpace_(navBar), 167 * gap_UI);
643 arrange_Widget(navBar);
644 }
645 refresh_Widget(navBar);
646 postCommand_Widget(navBar, "layout.changed id:navbar");
647 return iFalse;
648 }
649 else if (equal_Command(cmd, "window.reload.update")) {
650 checkLoadAnimation_Window_(get_Window());
651 return iTrue;
652 }
653 else if (equal_Command(cmd, "navigate.focus")) {
654 iWidget *url = findChild_Widget(navBar, "url");
655 if (focus_Widget() != url) {
656 setFocus_Widget(findChild_Widget(navBar, "url"));
657 }
658 else {
659 selectAll_InputWidget((iInputWidget *) url);
660 }
661 return iTrue;
662 }
663 else if (equal_Command(cmd, "input.edited")) {
664 iAnyObject * url = findChild_Widget(navBar, "url");
665 const iString *text = text_InputWidget(url);
666 const iBool show = willPerformSearchQuery_(text);
667 showSearchQueryIndicator_(show);
668 if (pointer_Command(cmd) == url) {
669 submit_LookupWidget(findWidget_App("lookup"), text);
670 return iTrue;
671 }
672 }
673 else if (startsWith_CStr(cmd, "input.ended id:url ")) {
674 iInputWidget *url = findChild_Widget(navBar, "url");
675 showSearchQueryIndicator_(iFalse);
676 if (isEmpty_String(text_InputWidget(url))) {
677 /* User entered nothing; restore the current URL. */
678 setText_InputWidget(url, url_DocumentWidget(document_App()));
679 return iTrue;
680 }
681 if (arg_Command(cmd) && argLabel_Command(cmd, "enter") &&
682 !isFocused_Widget(findWidget_App("lookup"))) {
683 iString *newUrl = copy_String(text_InputWidget(url));
684 trim_String(newUrl);
685 if (willPerformSearchQuery_(newUrl)) {
686 postCommandf_App("open url:%s", cstr_String(searchQueryUrl_App(newUrl)));
687 }
688 else {
689 postCommandf_App(
690 "open url:%s",
691 cstr_String(absoluteUrl_String(&iStringLiteral(""), collect_String(newUrl))));
692 }
693 return iTrue;
694 }
695 }
696 else if (startsWith_CStr(cmd, "document.")) {
697 /* React to the current document only. */
698 if (document_Command(cmd) == document_App()) {
699 if (equal_Command(cmd, "document.changed")) {
700 iInputWidget *url = findWidget_App("url");
701 const iString *urlStr = collect_String(suffix_Command(cmd, "url"));
702 trimCache_App();
703 visitUrl_Visited(visited_App(), withSpacesEncoded_String(urlStr), 0); /* TODO: internal URI normalization */
704 postCommand_App("visited.changed"); /* sidebar will update */
705 setText_InputWidget(url, urlStr);
706 checkLoadAnimation_Window_(get_Window());
707 dismissPortraitPhoneSidebars_Window(get_Window());
708 updateNavBarIdentity_(navBar);
709 updateNavDirButtons_(navBar);
710 /* Icon updates should be limited to automatically chosen icons if the user
711 is allowed to pick their own in the future. */
712 if (updateBookmarkIcon_Bookmarks(bookmarks_App(), urlStr,
713 siteIcon_GmDocument(document_DocumentWidget(document_App())))) {
714 postCommand_App("bookmarks.changed");
715 }
716 return iFalse;
717 }
718 else if (equal_Command(cmd, "document.request.cancelled")) {
719 checkLoadAnimation_Window_(get_Window());
720 return iFalse;
721 }
722 else if (equal_Command(cmd, "document.request.started")) {
723 iInputWidget *url = findChild_Widget(navBar, "url");
724 setTextCStr_InputWidget(url, suffixPtr_Command(cmd, "url"));
725 checkLoadAnimation_Window_(get_Window());
726 dismissPortraitPhoneSidebars_Window(get_Window());
727 return iFalse;
728 }
729 }
730 }
731 else if (equal_Command(cmd, "tabs.changed")) {
732 /* Update navbar according to the current tab. */
733 iDocumentWidget *doc = document_App();
734 if (doc) {
735 setText_InputWidget(findChild_Widget(navBar, "url"), url_DocumentWidget(doc));
736 checkLoadAnimation_Window_(get_Window());
737 updateNavBarIdentity_(navBar);
738 }
739 setFocus_Widget(NULL);
740 }
741 else if (equal_Command(cmd, "mouse.clicked") && arg_Command(cmd)) {
742 iWidget *widget = pointer_Command(cmd);
743 iWidget *menu = findWidget_App("doctabs.menu");
744 if (isTabButton_Widget(widget)) {
745 if (!isVisible_Widget(menu)) {
746 iWidget *tabs = findWidget_App("doctabs");
747 iWidget *page = tabPage_Widget(tabs, childIndex_Widget(widget->parent, widget));
748 if (argLabel_Command(cmd, "button") == SDL_BUTTON_MIDDLE) {
749 postCommandf_App("tabs.close id:%s", cstr_String(id_Widget(page)));
750 return iTrue;
751 }
752 showTabPage_Widget(tabs, page);
753 openMenu_Widget(menu, coord_Command(cmd));
754 }
755 }
756 }
757 else if (equal_Command(cmd, "navigate.reload")) {
758 iDocumentWidget *doc = document_Command(cmd);
759 if (isRequestOngoing_DocumentWidget(doc)) {
760 postCommand_App("document.stop");
761 }
762 else {
763 postCommand_App("document.reload");
764 }
765 return iTrue;
766 }
767 return iFalse;
768}
769
770static iBool handleSearchBarCommands_(iWidget *searchBar, const char *cmd) {
771 if (equal_Command(cmd, "input.ended") &&
772 equal_Rangecc(range_Command(cmd, "id"), "find.input")) {
773 iInputWidget *input = findChild_Widget(searchBar, "find.input");
774 if (arg_Command(cmd) && argLabel_Command(cmd, "enter") && isVisible_Widget(input)) {
775 postCommand_App("find.next");
776 /* Keep focus when pressing Enter. */
777 if (!isEmpty_String(text_InputWidget(input))) {
778 postCommand_App("focus.set id:find.input");
779 }
780 }
781 else {
782 postCommand_App("find.clearmark");
783 }
784 return iTrue;
785 }
786 else if (equal_Command(cmd, "focus.gained")) {
787 if (pointer_Command(cmd) == findChild_Widget(searchBar, "find.input")) {
788 if (!isVisible_Widget(searchBar)) {
789 setFlags_Widget(searchBar, hidden_WidgetFlag | disabled_WidgetFlag, iFalse);
790 arrange_Widget(get_Window()->root);
791 postRefresh_App();
792 }
793 }
794 }
795 else if (equal_Command(cmd, "find.close")) {
796 if (isVisible_Widget(searchBar)) {
797 setFlags_Widget(searchBar, hidden_WidgetFlag | disabled_WidgetFlag, iTrue);
798 arrange_Widget(searchBar->parent);
799 if (isFocused_Widget(findChild_Widget(searchBar, "find.input"))) {
800 setFocus_Widget(NULL);
801 }
802 refresh_Widget(searchBar->parent);
803 }
804 return iTrue;
805 }
806 return iFalse;
807}
808
809#if defined (iPlatformAppleMobile)
810static void dismissSidebar_(iWidget *sidebar, const char *toolButtonId) {
811 if (isVisible_Widget(sidebar)) {
812 postCommandf_App("%s.toggle", cstr_String(id_Widget(sidebar)));
813 if (toolButtonId) {
814// setFlags_Widget(findWidget_App(toolButtonId), noBackground_WidgetFlag, iTrue);
815 }
816 setVisualOffset_Widget(sidebar, height_Widget(sidebar), 250, easeIn_AnimFlag);
817 }
818}
819
820static iBool handleToolBarCommands_(iWidget *toolBar, const char *cmd) {
821 if (equalWidget_Command(cmd, toolBar, "mouse.clicked") && arg_Command(cmd) &&
822 argLabel_Command(cmd, "button") == SDL_BUTTON_RIGHT) {
823 iWidget *menu = findChild_Widget(toolBar, "toolbar.menu");
824 arrange_Widget(menu);
825 openMenu_Widget(menu, init_I2(0, -height_Widget(menu)));
826 return iTrue;
827 }
828 else if (equal_Command(cmd, "toolbar.showview")) {
829 /* TODO: Clean this up. */
830 iWidget *sidebar = findWidget_App("sidebar");
831 iWidget *sidebar2 = findWidget_App("sidebar2");
832 dismissSidebar_(sidebar2, "toolbar.ident");
833 const iBool isVisible = isVisible_Widget(sidebar);
834// setFlags_Widget(findChild_Widget(toolBar, "toolbar.view"), noBackground_WidgetFlag,
835// isVisible);
836 /* If a sidebar hasn't been shown yet, it's height is zero. */
837 const int viewHeight = rootSize_Window(get_Window()).y;
838 if (arg_Command(cmd) >= 0) {
839 postCommandf_App("sidebar.mode arg:%d show:1", arg_Command(cmd));
840 if (!isVisible) {
841 setVisualOffset_Widget(sidebar, viewHeight, 0, 0);
842 setVisualOffset_Widget(sidebar, 0, 400, easeOut_AnimFlag | softer_AnimFlag);
843 }
844 }
845 else {
846 postCommandf_App("sidebar.toggle");
847 if (isVisible) {
848 setVisualOffset_Widget(sidebar, height_Widget(sidebar), 250, easeIn_AnimFlag);
849 }
850 else {
851 setVisualOffset_Widget(sidebar, viewHeight, 0, 0);
852 setVisualOffset_Widget(sidebar, 0, 400, easeOut_AnimFlag | softer_AnimFlag);
853 }
854 }
855 return iTrue;
856 }
857 else if (equal_Command(cmd, "toolbar.showident")) {
858 /* TODO: Clean this up. */
859 iWidget *sidebar = findWidget_App("sidebar");
860 iWidget *sidebar2 = findWidget_App("sidebar2");
861 dismissSidebar_(sidebar, "toolbar.view");
862 const iBool isVisible = isVisible_Widget(sidebar2);
863// setFlags_Widget(findChild_Widget(toolBar, "toolbar.ident"), noBackground_WidgetFlag,
864// isVisible);
865 /* If a sidebar hasn't been shown yet, it's height is zero. */
866 const int viewHeight = rootSize_Window(get_Window()).y;
867 if (isVisible) {
868 dismissSidebar_(sidebar2, NULL);
869 }
870 else {
871 postCommand_App("sidebar2.mode arg:3 show:1");
872 int offset = height_Widget(sidebar2);
873 if (offset == 0) offset = rootSize_Window(get_Window()).y;
874 setVisualOffset_Widget(sidebar2, offset, 0, 0);
875 setVisualOffset_Widget(sidebar2, 0, 400, easeOut_AnimFlag | softer_AnimFlag);
876 }
877 return iTrue;
878 }
879 else if (equal_Command(cmd, "sidebar.mode.changed")) {
880 iLabelWidget *viewTool = findChild_Widget(toolBar, "toolbar.view");
881 updateTextCStr_LabelWidget(viewTool, icon_SidebarMode(arg_Command(cmd)));
882 return iFalse;
883 }
884 return iFalse;
885}
886#endif /* defined (iPlatformAppleMobile) */
887
888static iLabelWidget *newLargeIcon_LabelWidget(const char *text, const char *cmd) {
889 iLabelWidget *lab = newIcon_LabelWidget(text, 0, 0, cmd);
890 setFont_LabelWidget(lab, uiLabelLarge_FontId);
891 return lab;
892}
893
894static int appIconSize_(void) {
895 return lineHeight_Text(uiContent_FontId);
896}
897
898static void updateMetrics_Window_(iWindow *d) {
899 /* Custom frame. */
900 iWidget *winBar = findChild_Widget(d->root, "winbar");
901 if (winBar) {
902 iWidget *appIcon = findChild_Widget(winBar, "winbar.icon");
903 iWidget *appTitle = findChild_Widget(winBar, "winbar.title");
904 iWidget *appMin = findChild_Widget(winBar, "winbar.min");
905 iWidget *appMax = findChild_Widget(winBar, "winbar.max");
906 iWidget *appClose = findChild_Widget(winBar, "winbar.close");
907 setPadding_Widget(winBar, 0, gap_UI / 3, 0, 0);
908 setFixedSize_Widget(appMin, init_I2(gap_UI * 11.5f, height_Widget(appTitle)));
909 setFixedSize_Widget(appMax, appMin->rect.size);
910 setFixedSize_Widget(appClose, appMin->rect.size);
911 setFixedSize_Widget(appIcon, init_I2(appIconSize_(), appMin->rect.size.y));
912 }
913 iWidget *navBar = findChild_Widget(d->root, "navbar");
914 iWidget *lock = findChild_Widget(navBar, "navbar.lock");
915 iWidget *url = findChild_Widget(d->root, "url");
916 iWidget *rightEmbed = findChild_Widget(navBar, "url.rightembed");
917 iWidget *embedPad = findChild_Widget(navBar, "url.embedpad");
918 setPadding_Widget(as_Widget(url), 0, gap_UI, 0, gap_UI);
919 navBar->rect.size.y = 0; /* recalculate height based on children (FIXME: shouldn't be needed) */
920 updateSize_LabelWidget((iLabelWidget *) lock);
921 setFixedSize_Widget(embedPad, init_I2(width_Widget(lock) + gap_UI / 2, 1));
922 setContentPadding_InputWidget((iInputWidget *) url, width_Widget(lock) * 0.75,
923 width_Widget(lock) * 0.75);
924 rightEmbed->rect.pos.y = gap_UI;
925 updatePadding_Window_(d);
926 arrange_Widget(d->root);
927 postRefresh_App();
928}
929
930static void setupUserInterface_Window(iWindow *d) { 164static void setupUserInterface_Window(iWindow *d) {
931#if defined (iPlatformMobile) 165 d->root = createUserInterface_Root();
932 const iBool isPhone = (deviceType_App() == phone_AppDeviceType); 166 setCurrent_Root(d->root);
933#endif
934 /* Children of root cover the entire window. */
935 setFlags_Widget(d->root, resizeChildren_WidgetFlag, iTrue);
936 setCommandHandler_Widget(d->root, handleRootCommands_);
937
938 iWidget *div = makeVDiv_Widget();
939 setId_Widget(div, "navdiv");
940 addChild_Widget(d->root, iClob(div));
941
942#if defined (LAGRANGE_ENABLE_CUSTOM_FRAME)
943 /* Window title bar. */
944 if (prefs_App()->customFrame) {
945 setPadding1_Widget(div, 1);
946 iWidget *winBar = new_Widget();
947 setId_Widget(winBar, "winbar");
948 setFlags_Widget(winBar,
949 arrangeHeight_WidgetFlag | resizeChildren_WidgetFlag |
950 arrangeHorizontal_WidgetFlag | collapse_WidgetFlag,
951 iTrue);
952 iWidget *appIcon;
953 setId_Widget(
954 addChild_Widget(winBar, iClob(appIcon = makePadding_Widget(0))), "winbar.icon");
955 iLabelWidget *appButton =
956 addChildFlags_Widget(winBar,
957 iClob(new_LabelWidget("Lagrange", NULL)),
958 fixedHeight_WidgetFlag | frameless_WidgetFlag);
959 setTextColor_LabelWidget(appButton, uiTextAppTitle_ColorId);
960 setId_Widget(as_Widget(appButton), "winbar.app");
961 iLabelWidget *appTitle;
962 setFont_LabelWidget(appButton, uiContentBold_FontId);
963 setId_Widget(addChildFlags_Widget(winBar,
964 iClob(appTitle = new_LabelWidget("", NULL)),
965 expand_WidgetFlag | fixedHeight_WidgetFlag |
966 frameless_WidgetFlag | commandOnClick_WidgetFlag),
967 "winbar.title");
968 setTextColor_LabelWidget(appTitle, uiTextStrong_ColorId);
969 iLabelWidget *appMin, *appMax, *appClose;
970 setId_Widget(addChildFlags_Widget(
971 winBar,
972 iClob(appMin = newLargeIcon_LabelWidget("\u2013", "window.minimize")),
973 frameless_WidgetFlag),
974 "winbar.min");
975 addChildFlags_Widget(
976 winBar,
977 iClob(appMax = newLargeIcon_LabelWidget("\u25a1", "window.maximize toggle:1")),
978 frameless_WidgetFlag);
979 setId_Widget(as_Widget(appMax), "winbar.max");
980 addChildFlags_Widget(winBar,
981 iClob(appClose = newLargeIcon_LabelWidget(close_Icon, "window.close")),
982 frameless_WidgetFlag);
983 setId_Widget(as_Widget(appClose), "winbar.close");
984 setFont_LabelWidget(appClose, uiContent_FontId);
985 addChild_Widget(div, iClob(winBar));
986 setBackgroundColor_Widget(winBar, uiBackground_ColorId);
987 }
988#endif
989 /* Navigation bar. */ {
990 iWidget *navBar = new_Widget();
991 setId_Widget(navBar, "navbar");
992 setFlags_Widget(navBar,
993 hittable_WidgetFlag | /* context menu */
994 arrangeHeight_WidgetFlag |
995 resizeChildren_WidgetFlag |
996 arrangeHorizontal_WidgetFlag |
997 drawBackgroundToHorizontalSafeArea_WidgetFlag |
998 drawBackgroundToVerticalSafeArea_WidgetFlag,
999 iTrue);
1000 addChild_Widget(div, iClob(navBar));
1001 setBackgroundColor_Widget(navBar, uiBackground_ColorId);
1002 setCommandHandler_Widget(navBar, handleNavBarCommands_);
1003 setId_Widget(addChildFlags_Widget(navBar, iClob(newIcon_LabelWidget(backArrow_Icon, 0, 0, "navigate.back")), collapse_WidgetFlag), "navbar.back");
1004 setId_Widget(addChildFlags_Widget(navBar, iClob(newIcon_LabelWidget(forwardArrow_Icon, 0, 0, "navigate.forward")), collapse_WidgetFlag), "navbar.forward");
1005 /* Mobile devices have a button for easier access to the left sidebar. */
1006 if (deviceType_App() != desktop_AppDeviceType) {
1007 setId_Widget(addChildFlags_Widget(
1008 navBar,
1009 iClob(newIcon_LabelWidget(leftHalf_Icon, 0, 0, "sidebar.toggle")),
1010 collapse_WidgetFlag),
1011 "navbar.sidebar");
1012 }
1013 addChildFlags_Widget(navBar, iClob(new_Widget()), expand_WidgetFlag);
1014 iLabelWidget *idMenu = makeMenuButton_LabelWidget(
1015 "\U0001f464", identityButtonMenuItems_, iElemCount(identityButtonMenuItems_));
1016 setAlignVisually_LabelWidget(idMenu, iTrue);
1017 setId_Widget(addChildFlags_Widget(navBar, iClob(idMenu), collapse_WidgetFlag), "navbar.ident");
1018 iInputWidget *url;
1019 /* URL input field. */ {
1020 url = new_InputWidget(0);
1021 setFlags_Widget(as_Widget(url), resizeHeightOfChildren_WidgetFlag, iTrue);
1022 setSelectAllOnFocus_InputWidget(url, iTrue);
1023 setId_Widget(as_Widget(url), "url");
1024 setUrlContent_InputWidget(url, iTrue);
1025 setNotifyEdits_InputWidget(url, iTrue);
1026 setTextCStr_InputWidget(url, "gemini://");
1027 addChildFlags_Widget(navBar, iClob(url), 0);
1028 const int64_t embedFlags =
1029 noBackground_WidgetFlag | frameless_WidgetFlag | unpadded_WidgetFlag |
1030 (deviceType_App() == desktop_AppDeviceType ? tight_WidgetFlag : 0);
1031 /* Page information/certificate warning. */ {
1032 iLabelWidget *lock = addChildFlags_Widget(
1033 as_Widget(url),
1034 iClob(newIcon_LabelWidget("\U0001f513", SDLK_i, KMOD_PRIMARY, "document.info")),
1035 embedFlags | moveToParentLeftEdge_WidgetFlag);
1036 setId_Widget(as_Widget(lock), "navbar.lock");
1037 setFont_LabelWidget(lock, symbols_FontId + uiNormal_FontSize);
1038 updateTextCStr_LabelWidget(lock, "\U0001f512");
1039 }
1040 iWidget *rightEmbed = new_Widget();
1041 setId_Widget(rightEmbed, "url.rightembed");
1042 addChildFlags_Widget(as_Widget(url),
1043 iClob(rightEmbed),
1044 arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag |
1045 resizeHeightOfChildren_WidgetFlag |
1046 moveToParentRightEdge_WidgetFlag);
1047 /* Feeds refresh indicator is inside the input field. */ {
1048 iLabelWidget *queryInd =
1049 new_LabelWidget(uiTextAction_ColorEscape "\u21d2 ${status.query}", NULL);
1050 setId_Widget(as_Widget(queryInd), "input.indicator.search");
1051 setBackgroundColor_Widget(as_Widget(queryInd), uiBackground_ColorId);
1052 setFrameColor_Widget(as_Widget(queryInd), uiTextAction_ColorId);
1053 setAlignVisually_LabelWidget(queryInd, iTrue);
1054 addChildFlags_Widget(rightEmbed,
1055 iClob(queryInd),
1056 collapse_WidgetFlag | hidden_WidgetFlag);
1057 }
1058 /* Feeds refresh indicator is inside the input field. */ {
1059 iLabelWidget *fprog = new_LabelWidget(uiTextCaution_ColorEscape
1060 "\u2605 ${status.feeds}", NULL);
1061 setId_Widget(as_Widget(fprog), "feeds.progress");
1062 setBackgroundColor_Widget(as_Widget(fprog), uiBackground_ColorId);
1063 setAlignVisually_LabelWidget(fprog, iTrue);
1064 addChildFlags_Widget(rightEmbed,
1065 iClob(fprog),
1066 collapse_WidgetFlag | frameless_WidgetFlag | hidden_WidgetFlag);
1067 }
1068 /* Download progress indicator is also inside the input field, but hidden normally. */ {
1069 iLabelWidget *progress = new_LabelWidget(uiTextCaution_ColorEscape "00.000 ${mb}", NULL);
1070 setId_Widget(as_Widget(progress), "document.progress");
1071 setBackgroundColor_Widget(as_Widget(progress), uiBackground_ColorId);
1072 setAlignVisually_LabelWidget(progress, iTrue);
1073 addChildFlags_Widget(
1074 rightEmbed, iClob(progress), collapse_WidgetFlag);
1075 }
1076 /* Reload button. */ {
1077 iLabelWidget *reload;
1078 if (deviceType_App() == desktop_AppDeviceType) {
1079 reload = newIcon_LabelWidget(reloadCStr_, 0, 0, "navigate.reload");
1080 }
1081 else {
1082 /* In a mobile layout, the reload button is replaced with the Page/Ellipsis menu. */
1083 reload = makeMenuButton_LabelWidget(pageMenuCStr_,
1084 (iMenuItem[]){
1085 { reload_Icon " ${menu.reload}", reload_KeyShortcut, "navigate.reload" },
1086 { timer_Icon " ${menu.autoreload}", 0, 0, "document.autoreload.menu" },
1087 { "---", 0, 0, NULL },
1088 { upArrow_Icon " ${menu.parent}", navigateParent_KeyShortcut, "navigate.parent" },
1089 { upArrowBar_Icon " ${menu.root}", navigateRoot_KeyShortcut, "navigate.root" },
1090 { "---", 0, 0, NULL },
1091 { pin_Icon " ${menu.page.bookmark}", SDLK_d, KMOD_PRIMARY, "bookmark.add" },
1092 { star_Icon " ${menu.page.subscribe}", subscribeToPage_KeyModifier, "feeds.subscribe" },
1093 { book_Icon " ${menu.page.import}", 0, 0, "bookmark.links confirm:1" },
1094 { globe_Icon " ${menu.page.translate}", 0, 0, "document.translate" },
1095 { "---", 0, 0, NULL },
1096 { "${menu.page.copyurl}", 0, 0, "document.copylink" },
1097 { "${menu.page.copysource}", 'c', KMOD_PRIMARY, "copy" },
1098 { download_Icon " " saveToDownloads_Label, SDLK_s, KMOD_PRIMARY, "document.save" } },
1099 14);
1100 setFont_LabelWidget((iLabelWidget *) reload, uiContentBold_FontId);
1101 setAlignVisually_LabelWidget((iLabelWidget *) reload, iTrue);
1102 }
1103 setId_Widget(as_Widget(reload), "reload");
1104 addChildFlags_Widget(as_Widget(url), iClob(reload), embedFlags | moveToParentRightEdge_WidgetFlag);
1105 updateSize_LabelWidget(reload);
1106 }
1107 setId_Widget(addChild_Widget(rightEmbed, iClob(makePadding_Widget(0))), "url.embedpad");
1108 }
1109 if (deviceType_App() != desktop_AppDeviceType) {
1110 /* On mobile, the Identities button is on the right side of the URL bar. */
1111 iWidget *ident = removeChild_Widget(navBar, findChild_Widget(navBar, "navbar.ident"));
1112 addChild_Widget(navBar, iClob(ident));
1113 }
1114 addChildFlags_Widget(navBar, iClob(new_Widget()), expand_WidgetFlag);
1115 setId_Widget(addChildFlags_Widget(navBar,
1116 iClob(newIcon_LabelWidget(
1117 home_Icon, SDLK_h, KMOD_PRIMARY | KMOD_SHIFT, "navigate.home")),
1118 collapse_WidgetFlag),
1119 "navbar.home");
1120#if !defined (iHaveNativeMenus)
1121# if defined (iPlatformAppleMobile)
1122 iLabelWidget *navMenu =
1123 makeMenuButton_LabelWidget("\U0001d362", isPhone ? phoneNavMenuItems_ : tabletNavMenuItems_,
1124 isPhone ? iElemCount(phoneNavMenuItems_) : iElemCount(tabletNavMenuItems_));
1125# else
1126 iLabelWidget *navMenu =
1127 makeMenuButton_LabelWidget("\U0001d362", navMenuItems_, iElemCount(navMenuItems_));
1128# endif
1129 setAlignVisually_LabelWidget(navMenu, iTrue);
1130 setId_Widget(addChildFlags_Widget(navBar, iClob(navMenu), collapse_WidgetFlag), "navbar.menu");
1131#else
1132 insertMacMenus_();
1133#endif
1134 }
1135 /* Tab bar. */ {
1136 iWidget *mainStack = new_Widget();
1137 setId_Widget(mainStack, "stack");
1138 addChildFlags_Widget(div, iClob(mainStack), resizeChildren_WidgetFlag | expand_WidgetFlag |
1139 unhittable_WidgetFlag);
1140 iWidget *tabBar = makeTabs_Widget(mainStack);
1141 setId_Widget(tabBar, "doctabs");
1142 setBackgroundColor_Widget(tabBar, uiBackground_ColorId);
1143 appendTabPage_Widget(tabBar, iClob(new_DocumentWidget()), "Document", 0, 0);
1144 iWidget *buttons = findChild_Widget(tabBar, "tabs.buttons");
1145 setFlags_Widget(buttons, collapse_WidgetFlag | hidden_WidgetFlag |
1146 drawBackgroundToHorizontalSafeArea_WidgetFlag, iTrue);
1147 if (deviceType_App() == phone_AppDeviceType) {
1148 setBackgroundColor_Widget(buttons, uiBackground_ColorId);
1149 }
1150 setId_Widget(
1151 addChild_Widget(buttons, iClob(newIcon_LabelWidget(add_Icon, 0, 0, "tabs.new"))),
1152 "newtab");
1153 }
1154 /* Sidebars. */ {
1155 iWidget *content = findChild_Widget(d->root, "tabs.content");
1156 iSidebarWidget *sidebar1 = new_SidebarWidget(left_SideBarSide);
1157 addChildPos_Widget(content, iClob(sidebar1), front_WidgetAddPos);
1158 iSidebarWidget *sidebar2 = new_SidebarWidget(right_SideBarSide);
1159 if (deviceType_App() != phone_AppDeviceType) {
1160 addChildPos_Widget(content, iClob(sidebar2), back_WidgetAddPos);
1161 }
1162 else {
1163 /* The identities sidebar is always in the main area. */
1164 addChild_Widget(findChild_Widget(d->root, "stack"), iClob(sidebar2));
1165 setFlags_Widget(as_Widget(sidebar2), hidden_WidgetFlag, iTrue);
1166 }
1167 }
1168 /* Lookup results. */ {
1169 iLookupWidget *lookup = new_LookupWidget();
1170 addChildFlags_Widget(div, iClob(lookup), fixedPosition_WidgetFlag | hidden_WidgetFlag);
1171 }
1172 /* Search bar. */ {
1173 iWidget *searchBar = new_Widget();
1174 setId_Widget(searchBar, "search");
1175 setFlags_Widget(searchBar,
1176 hidden_WidgetFlag | disabled_WidgetFlag | collapse_WidgetFlag |
1177 arrangeHeight_WidgetFlag | resizeChildren_WidgetFlag |
1178 arrangeHorizontal_WidgetFlag,
1179 iTrue);
1180 if (deviceType_App() == desktop_AppDeviceType) {
1181 addChild_Widget(div, iClob(searchBar));
1182 }
1183 else {
1184 /* The search bar appears at the top on mobile, because there is a virtual keyboard
1185 covering the bottom. */
1186 insertChildAfter_Widget(div, iClob(searchBar),
1187 childIndex_Widget(div, findChild_Widget(div, "navbar")));
1188 }
1189 setBackgroundColor_Widget(searchBar, uiBackground_ColorId);
1190 setCommandHandler_Widget(searchBar, handleSearchBarCommands_);
1191 addChildFlags_Widget(
1192 searchBar, iClob(new_LabelWidget(magnifyingGlass_Icon, NULL)), frameless_WidgetFlag);
1193 iInputWidget *input = new_InputWidget(0);
1194 setHint_InputWidget(input, "${hint.findtext}");
1195 setSelectAllOnFocus_InputWidget(input, iTrue);
1196 setEatEscape_InputWidget(input, iFalse); /* unfocus and close with one keypress */
1197 setId_Widget(addChildFlags_Widget(searchBar, iClob(input), expand_WidgetFlag),
1198 "find.input");
1199 addChild_Widget(searchBar, iClob(newIcon_LabelWidget(" \u2b9f ", 'g', KMOD_PRIMARY, "find.next")));
1200 addChild_Widget(searchBar, iClob(newIcon_LabelWidget(" \u2b9d ", 'g', KMOD_PRIMARY | KMOD_SHIFT, "find.prev")));
1201 addChild_Widget(searchBar, iClob(newIcon_LabelWidget(close_Icon, SDLK_ESCAPE, 0, "find.close")));
1202 }
1203#if defined (iPlatformAppleMobile)
1204 /* Bottom toolbar. */
1205 if (isPhone_iOS()) {
1206 iWidget *toolBar = new_Widget();
1207 addChild_Widget(d->root, iClob(toolBar));
1208 setId_Widget(toolBar, "toolbar");
1209 setCommandHandler_Widget(toolBar, handleToolBarCommands_);
1210 setFlags_Widget(toolBar, moveToParentBottomEdge_WidgetFlag |
1211 parentCannotResizeHeight_WidgetFlag |
1212 resizeWidthOfChildren_WidgetFlag |
1213 arrangeHeight_WidgetFlag | arrangeHorizontal_WidgetFlag, iTrue);
1214 setBackgroundColor_Widget(toolBar, tmBannerBackground_ColorId);
1215 setId_Widget(addChildFlags_Widget(toolBar,
1216 iClob(newLargeIcon_LabelWidget("\U0001f870", "navigate.back")),
1217 frameless_WidgetFlag),
1218 "toolbar.back");
1219 setId_Widget(addChildFlags_Widget(toolBar,
1220 iClob(newLargeIcon_LabelWidget("\U0001f872", "navigate.forward")),
1221 frameless_WidgetFlag),
1222 "toolbar.forward");
1223 setId_Widget(addChildFlags_Widget(toolBar,
1224 iClob(newLargeIcon_LabelWidget("\U0001f464", "toolbar.showident")),
1225 frameless_WidgetFlag),
1226 "toolbar.ident");
1227 setId_Widget(addChildFlags_Widget(toolBar,
1228 iClob(newLargeIcon_LabelWidget("\U0001f588", "toolbar.showview arg:-1")),
1229 frameless_WidgetFlag | commandOnClick_WidgetFlag),
1230 "toolbar.view");
1231 iLabelWidget *menuButton = makeMenuButton_LabelWidget("\U0001d362", phoneNavMenuItems_,
1232 iElemCount(phoneNavMenuItems_));
1233 setFont_LabelWidget(menuButton, uiLabelLarge_FontId);
1234 setId_Widget(as_Widget(menuButton), "toolbar.navmenu");
1235 addChildFlags_Widget(toolBar, iClob(menuButton), frameless_WidgetFlag);
1236 iForEach(ObjectList, i, children_Widget(toolBar)) {
1237 iLabelWidget *btn = i.object;
1238 setFlags_Widget(i.object, noBackground_WidgetFlag, iTrue);
1239 setTextColor_LabelWidget(i.object, tmBannerIcon_ColorId);
1240// setBackgroundColor_Widget(i.object, tmBannerSideTitle_ColorId);
1241 }
1242 const iMenuItem items[] = {
1243 { pin_Icon " ${sidebar.bookmarks}", 0, 0, "toolbar.showview arg:0" },
1244 { star_Icon " ${sidebar.feeds}", 0, 0, "toolbar.showview arg:1" },
1245 { clock_Icon " ${sidebar.history}", 0, 0, "toolbar.showview arg:2" },
1246 { page_Icon " ${toolbar.outline}", 0, 0, "toolbar.showview arg:4" },
1247 };
1248 iWidget *menu = makeMenu_Widget(findChild_Widget(toolBar, "toolbar.view"),
1249 items, iElemCount(items));
1250 setId_Widget(menu, "toolbar.menu"); /* view menu */
1251 }
1252#endif
1253 updatePadding_Window_(d);
1254 /* Global context menus. */ {
1255 iWidget *tabsMenu = makeMenu_Widget(
1256 d->root,
1257 (iMenuItem[]){
1258 { close_Icon " ${menu.closetab}", 0, 0, "tabs.close" },
1259 { copy_Icon " ${menu.duptab}", 0, 0, "tabs.new duplicate:1" },
1260 { "---", 0, 0, NULL },
1261 { "${menu.closetab.other}", 0, 0, "tabs.close toleft:1 toright:1" },
1262 { barLeftArrow_Icon " ${menu.closetab.left}", 0, 0, "tabs.close toleft:1" },
1263 { barRightArrow_Icon " ${menu.closetab.right}", 0, 0, "tabs.close toright:1" },
1264 },
1265 6);
1266 iWidget *barMenu =
1267 makeMenu_Widget(d->root,
1268 (iMenuItem[]){
1269 { leftHalf_Icon " ${menu.sidebar.left}", 0, 0, "sidebar.toggle" },
1270 { rightHalf_Icon " ${menu.sidebar.right}", 0, 0, "sidebar2.toggle" },
1271 },
1272 deviceType_App() == phone_AppDeviceType ? 1 : 2);
1273 iWidget *clipMenu = makeMenu_Widget(d->root,
1274 (iMenuItem[]){
1275 { scissor_Icon " ${menu.cut}", 0, 0, "input.copy cut:1" },
1276 { clipCopy_Icon " ${menu.copy}", 0, 0, "input.copy" },
1277 { "---", 0, 0, NULL },
1278 { clipboard_Icon " ${menu.paste}", 0, 0, "input.paste" },
1279 },
1280 4);
1281 setId_Widget(tabsMenu, "doctabs.menu");
1282 setId_Widget(barMenu, "barmenu");
1283 setId_Widget(clipMenu, "clipmenu");
1284 }
1285 /* Global keyboard shortcuts. */ {
1286 addAction_Widget(d->root, 'l', KMOD_PRIMARY, "navigate.focus");
1287 addAction_Widget(d->root, 'f', KMOD_PRIMARY, "focus.set id:find.input");
1288 addAction_Widget(d->root, '1', KMOD_PRIMARY, "sidebar.mode arg:0 toggle:1");
1289 addAction_Widget(d->root, '2', KMOD_PRIMARY, "sidebar.mode arg:1 toggle:1");
1290 addAction_Widget(d->root, '3', KMOD_PRIMARY, "sidebar.mode arg:2 toggle:1");
1291 addAction_Widget(d->root, '4', KMOD_PRIMARY, "sidebar.mode arg:3 toggle:1");
1292 addAction_Widget(d->root, '5', KMOD_PRIMARY, "sidebar.mode arg:4 toggle:1");
1293 addAction_Widget(d->root, '1', rightSidebar_KeyModifier, "sidebar2.mode arg:0 toggle:1");
1294 addAction_Widget(d->root, '2', rightSidebar_KeyModifier, "sidebar2.mode arg:1 toggle:1");
1295 addAction_Widget(d->root, '3', rightSidebar_KeyModifier, "sidebar2.mode arg:2 toggle:1");
1296 addAction_Widget(d->root, '4', rightSidebar_KeyModifier, "sidebar2.mode arg:3 toggle:1");
1297 addAction_Widget(d->root, '5', rightSidebar_KeyModifier, "sidebar2.mode arg:4 toggle:1");
1298 }
1299 updateMetrics_Window_(d);
1300}
1301
1302void showToolbars_Window(iWindow *d, iBool show) {
1303 /* The toolbar is only used on phone portrait layout. */
1304 if (isLandscape_App()) return;
1305 iWidget *toolBar = findChild_Widget(d->root, "toolbar");
1306 if (!toolBar) return;
1307 const int height = rootSize_Window(d).y - top_Rect(boundsWithoutVisualOffset_Widget(toolBar));
1308 if (show && !isVisible_Widget(toolBar)) {
1309 setFlags_Widget(toolBar, hidden_WidgetFlag, iFalse);
1310 setVisualOffset_Widget(toolBar, 0, 200, easeOut_AnimFlag);
1311 }
1312 else if (!show && isVisible_Widget(toolBar)) {
1313 /* Close any menus that open via the toolbar. */
1314 closeMenu_Widget(findChild_Widget(findWidget_App("toolbar.navmenu"), "menu"));
1315 closeMenu_Widget(findChild_Widget(toolBar, "toolbar.menu"));
1316 setFlags_Widget(toolBar, hidden_WidgetFlag, iTrue);
1317 setVisualOffset_Widget(toolBar, height, 200, easeOut_AnimFlag);
1318 }
1319} 167}
1320 168
1321static void updateRootSize_Window_(iWindow *d, iBool notifyAlways) { 169static void updateRootSize_Window_(iWindow *d, iBool notifyAlways) {
@@ -1325,7 +173,7 @@ static void updateRootSize_Window_(iWindow *d, iBool notifyAlways) {
1325 size->y -= d->keyboardHeight; 173 size->y -= d->keyboardHeight;
1326 d->root->minSize = *size; 174 d->root->minSize = *size;
1327 if (notifyAlways || !isEqual_I2(oldSize, *size)) { 175 if (notifyAlways || !isEqual_I2(oldSize, *size)) {
1328 updatePadding_Window_(d); 176 updatePadding_Root(d->root);
1329 const iBool isHoriz = (d->place.lastNotifiedSize.x != size->x); 177 const iBool isHoriz = (d->place.lastNotifiedSize.x != size->x);
1330 const iBool isVert = (d->place.lastNotifiedSize.y != size->y); 178 const iBool isVert = (d->place.lastNotifiedSize.y != size->y);
1331 arrange_Widget(d->root); 179 arrange_Widget(d->root);
@@ -1494,6 +342,7 @@ static SDL_Surface *loadImage_(const iBlock *data, int resized) {
1494void init_Window(iWindow *d, iRect rect) { 342void init_Window(iWindow *d, iRect rect) {
1495 theWindow_ = d; 343 theWindow_ = d;
1496 d->win = NULL; 344 d->win = NULL;
345 d->root = NULL;
1497 iZap(d->cursors); 346 iZap(d->cursors);
1498 d->place.initialPos = rect.pos; 347 d->place.initialPos = rect.pos;
1499 d->place.normalRect = rect; 348 d->place.normalRect = rect;
@@ -1567,12 +416,9 @@ void init_Window(iWindow *d, iRect rect) {
1567#if defined (iPlatformAppleMobile) 416#if defined (iPlatformAppleMobile)
1568 setupWindow_iOS(d); 417 setupWindow_iOS(d);
1569#endif 418#endif
1570 d->root = new_Widget();
1571 setFlags_Widget(d->root, fixedSize_WidgetFlag | focusRoot_WidgetFlag, iTrue);
1572 d->presentTime = 0.0; 419 d->presentTime = 0.0;
1573 d->frameTime = SDL_GetTicks(); 420 d->frameTime = SDL_GetTicks();
1574 d->loadAnimTimer = 0; 421 d->loadAnimTimer = 0;
1575 setId_Widget(d->root, "root");
1576 init_Text(d->render); 422 init_Text(d->render);
1577 setupUserInterface_Window(d); 423 setupUserInterface_Window(d);
1578 postCommand_App("~bindings.changed"); /* update from bindings */ 424 postCommand_App("~bindings.changed"); /* update from bindings */
@@ -1785,7 +631,7 @@ static iBool handleWindowEvent_Window_(iWindow *d, const SDL_WindowEvent *ev) {
1785 return iTrue; 631 return iTrue;
1786 } 632 }
1787 case SDL_WINDOWEVENT_RESIZED: 633 case SDL_WINDOWEVENT_RESIZED:
1788 updatePadding_Window_(d); 634 updatePadding_Root(d->root);
1789 if (d->isMinimized) { 635 if (d->isMinimized) {
1790 updateRootSize_Window_(d, iTrue); 636 updateRootSize_Window_(d, iTrue);
1791 return iTrue; 637 return iTrue;
@@ -1945,7 +791,7 @@ iBool processEvent_Window(iWindow *d, const SDL_Event *ev) {
1945 } 791 }
1946 } 792 }
1947 if (isMetricsChange_UserEvent(&event)) { 793 if (isMetricsChange_UserEvent(&event)) {
1948 updateMetrics_Window_(d); 794 updateMetrics_Root(d->root);
1949 } 795 }
1950 if (isCommand_UserEvent(&event, "lang.changed")) { 796 if (isCommand_UserEvent(&event, "lang.changed")) {
1951#if defined (iPlatformAppleDesktop) 797#if defined (iPlatformAppleDesktop)
@@ -2091,11 +937,11 @@ uint32_t id_Window(const iWindow *d) {
2091} 937}
2092 938
2093iInt2 rootSize_Window(const iWindow *d) { 939iInt2 rootSize_Window(const iWindow *d) {
2094 return d ? d->root->rect.size : zero_I2(); 940 return d && d->root ? d->root->rect.size : zero_I2();
2095} 941}
2096 942
2097iRect safeRootRect_Window(const iWindow *d) { 943iRect safeRootRect_Window(const iWindow *d) {
2098 iRect rect = { zero_I2(), d->root->rect.size }; 944 iRect rect = { zero_I2(), rootSize_Window(d) };
2099#if defined (iPlatformAppleMobile) 945#if defined (iPlatformAppleMobile)
2100 float left, top, right, bottom; 946 float left, top, right, bottom;
2101 safeAreaInsets_iOS(&left, &top, &right, &bottom); 947 safeAreaInsets_iOS(&left, &top, &right, &bottom);
diff --git a/src/ui/window.h b/src/ui/window.h
index 052f8828..9d9a6aeb 100644
--- a/src/ui/window.h
+++ b/src/ui/window.h
@@ -91,7 +91,6 @@ void setFreezeDraw_Window (iWindow *, iBool freezeDraw);
91void setCursor_Window (iWindow *, int cursor); 91void setCursor_Window (iWindow *, int cursor);
92void setSnap_Window (iWindow *, int snapMode); 92void setSnap_Window (iWindow *, int snapMode);
93void setKeyboardHeight_Window(iWindow *, int height); 93void setKeyboardHeight_Window(iWindow *, int height);
94void dismissPortraitPhoneSidebars_Window (iWindow *);
95void showToolbars_Window (iWindow *, iBool show); 94void showToolbars_Window (iWindow *, iBool show);
96iBool postContextClick_Window (iWindow *, const SDL_MouseButtonEvent *); 95iBool postContextClick_Window (iWindow *, const SDL_MouseButtonEvent *);
97 96