summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSolidHal <hal@halemmerich.com>2020-12-08 20:46:57 -0600
committerGitHub <noreply@github.com>2020-12-08 20:46:57 -0600
commitcd848c14d8c5d1fd66acded70cc17d2a8009383f (patch)
treeb8c932a745f77845f79aea464d5195ce36109ee9
parent9fde33bb6f8149cc8dee7ac626b8b56f9f1cd14e (diff)
parenteae5e61bba99c97af3d8b27dab39d50aedeb3b04 (diff)
Merge pull request #1 from skyjake/dev
bring up to date
-rw-r--r--CMakeLists.txt6
-rwxr-xr-xdebian/rules2
m---------lib/the_Foundation0
-rw-r--r--res/MacOSXBundleInfo.plist.in22
-rw-r--r--res/about/help.gmi102
-rw-r--r--res/about/version.gmi29
-rw-r--r--src/app.c106
-rw-r--r--src/app.h1
-rw-r--r--src/gmrequest.c14
-rw-r--r--src/gmutil.c5
-rw-r--r--src/mimehooks.c71
-rw-r--r--src/mimehooks.h4
-rw-r--r--src/ui/documentwidget.c24
-rw-r--r--src/ui/keys.h2
-rw-r--r--src/ui/sidebarwidget.c218
-rw-r--r--src/ui/sidebarwidget.h7
-rw-r--r--src/ui/text.c71
-rw-r--r--src/ui/text.h16
-rw-r--r--src/ui/util.c8
-rw-r--r--src/ui/widget.c13
-rw-r--r--src/ui/widget.h9
-rw-r--r--src/ui/window.c21
22 files changed, 562 insertions, 189 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 0c200e21..1033772e 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -18,7 +18,7 @@
18cmake_minimum_required (VERSION 3.9) 18cmake_minimum_required (VERSION 3.9)
19 19
20project (Lagrange 20project (Lagrange
21 VERSION 0.12.0 21 VERSION 0.12.2
22 DESCRIPTION "A Beautiful Gemini Client" 22 DESCRIPTION "A Beautiful Gemini Client"
23 LANGUAGES C 23 LANGUAGES C
24) 24)
@@ -30,6 +30,7 @@ option (ENABLE_X11_SWRENDER "Use software rendering under X11" OFF)
30option (ENABLE_KERNING "Enable kerning in font renderer (slower)" ON) 30option (ENABLE_KERNING "Enable kerning in font renderer (slower)" ON)
31option (ENABLE_RESOURCE_EMBED "Embed resources inside the executable" OFF) 31option (ENABLE_RESOURCE_EMBED "Embed resources inside the executable" OFF)
32option (ENABLE_WINDOWPOS_FIX "Set position after showing window (workaround for SDL bug)" OFF) 32option (ENABLE_WINDOWPOS_FIX "Set position after showing window (workaround for SDL bug)" OFF)
33option (ENABLE_IDLE_SLEEP "While idle, sleep in the main thread instead of waiting for events" ON)
33 34
34include (BuildType.cmake) 35include (BuildType.cmake)
35include (res/Embed.cmake) 36include (res/Embed.cmake)
@@ -219,6 +220,9 @@ if (ENABLE_MPG123 AND MPG123_FOUND)
219 target_compile_definitions (app PUBLIC LAGRANGE_ENABLE_MPG123=1) 220 target_compile_definitions (app PUBLIC LAGRANGE_ENABLE_MPG123=1)
220 target_link_libraries (app PUBLIC PkgConfig::MPG123) 221 target_link_libraries (app PUBLIC PkgConfig::MPG123)
221endif () 222endif ()
223if (ENABLE_IDLE_SLEEP)
224 target_compile_definitions (app PUBLIC LAGRANGE_IDLE_SLEEP=1)
225endif ()
222target_link_libraries (app PUBLIC the_Foundation::the_Foundation) 226target_link_libraries (app PUBLIC the_Foundation::the_Foundation)
223target_link_libraries (app PUBLIC ${SDL2_LDFLAGS}) 227target_link_libraries (app PUBLIC ${SDL2_LDFLAGS})
224if (APPLE) 228if (APPLE)
diff --git a/debian/rules b/debian/rules
index f4578eea..9e306b0b 100755
--- a/debian/rules
+++ b/debian/rules
@@ -6,7 +6,7 @@ export DEB_BUILD_MAINT_OPTIONS=hardening=-format
6 dh $@ 6 dh $@
7 7
8override_dh_auto_configure: 8override_dh_auto_configure:
9 cmake .. -DENABLE_WINDOWPOS_FIX=YES -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=$(pwd)/../usr 9 cmake .. -DCMAKE_BUILD_TYPE=Release -DENABLE_WINDOWPOS_FIX=YES -DTFDN_ENABLE_SSE41=NO -DCMAKE_INSTALL_PREFIX=$(pwd)/../usr
10 10
11override_dh_build_configure: 11override_dh_build_configure:
12 cmake --build . 12 cmake --build .
diff --git a/lib/the_Foundation b/lib/the_Foundation
Subproject 17fd22771266a562eecd861519d1587885b26c2 Subproject 4514a181458dfa02d73aafd6498e101b63eaa47
diff --git a/res/MacOSXBundleInfo.plist.in b/res/MacOSXBundleInfo.plist.in
index 79cf7627..daa71574 100644
--- a/res/MacOSXBundleInfo.plist.in
+++ b/res/MacOSXBundleInfo.plist.in
@@ -54,16 +54,16 @@
54 <false/> 54 <false/>
55 </dict> 55 </dict>
56 </array> 56 </array>
57 <key>CFBundleURLTypes</key> 57 <key>CFBundleURLTypes</key>
58 <array> 58 <array>
59 <dict> 59 <dict>
60 <key>CFBundleURLName</key> 60 <key>CFBundleURLName</key>
61 <string>Gemini</string> 61 <string>Gemini</string>
62 <key>CFBundleURLSchemes</key> 62 <key>CFBundleURLSchemes</key>
63 <array> 63 <array>
64 <string>gemini</string> 64 <string>gemini</string>
65 </array> 65 </array>
66 </dict> 66 </dict>
67 </array> 67 </array>
68</dict> 68</dict>
69</plist> 69</plist>
diff --git a/res/about/help.gmi b/res/about/help.gmi
index 3f21adb8..f71f5164 100644
--- a/res/about/help.gmi
+++ b/res/about/help.gmi
@@ -63,7 +63,7 @@ Lagrange's user interface is modeled after web browsers:
63 63
64* There is a navigation bar at the top with Back and Forward buttons. 64* There is a navigation bar at the top with Back and Forward buttons.
65* There is a tab bar for switching tabs. The tab bar is hidden if there is only one tab open. 65* There is a tab bar for switching tabs. The tab bar is hidden if there is only one tab open.
66* There is a sidebar for managing bookmarks and TLS identities, and viewing history and the page outline. The sidebar is hidden by default. 66* There is a sidebar for managing bookmarks and TLS identities, and viewing history and the page outline. Sidebars are hidden by default.
67* There is a search bar that appears at the bottom when searching text on the page. 67* There is a search bar that appears at the bottom when searching text on the page.
68 68
69Tip: Try pressing ${CTRL+}5 now to see the page outline. 69Tip: Try pressing ${CTRL+}5 now to see the page outline.
@@ -126,17 +126,17 @@ Press ${CTRL+}T to open a new tab, and ${CTRL+}W to close the current tab. Right
126 126
127The set of open tabs is restored when you launch Lagrange. 127The set of open tabs is restored when you launch Lagrange.
128 128
129## Sidebar 129## Sidebars
130 130
131The sidebar can be toggled via menus or by pressing ${SHIFT+}${CTRL+}L. It has five tabs: 131The sidebars can be toggled via menus or by pressing ${SHIFT+}${CTRL+}L or ${SHIFT+}${CTRL+}P (for the left/right sidebar, respectively). Both sidebars have five tabs:
132 132
133* Bookmarks: List of bookmarks that you've created. These appear first in search results for quick and easy access. 133* Bookmarks: List of bookmarks that you've created. These appear first in search results for quick and easy access.
134* Feeds: Entries found on subscribed feed index pages. 134* Feeds: Entries found on subscribed pages.
135* History: Chronological list of visited URLs. This is not a full history of all the URLs you've accessed over time — only unique URLs are shown at the latest access time. 135* History: Chronological list of visited URLs. This is not a full history of all the URLs you've accessed over time — only unique URLs are shown at the latest access time.
136* Identities: TLS client certificates. 136* Identities: TLS client certificates.
137* Outline: List of the headings in the currently open tab. Useful when reading longer documents. 137* Outline: List of the headings in the currently open tab. Useful when reading longer documents.
138 138
139${CTRL+}1 through ${CTRL+}5 switch between the sidebar tabs, or hide the sidebar if the current tab's key is pressed. You can also press Escape to dismiss the sidebar. 139${CTRL+}1 through ${CTRL+}5 switch between the left sidebar tabs, or hide the sidebar if the current tab's key is pressed. You can also press Escape to dismiss sidebars.
140 140
141## Bookmarks 141## Bookmarks
142 142
@@ -150,10 +150,11 @@ In addition to a title, bookmarks can have tags. Some tags have a special meanin
150 150
151* Set a "homepage" tag on a bookmark to make it one of the pages that will be opened when pressing the 🏠 button. If multiple bookmarks are tagged as homepages, a random one is selected. 151* Set a "homepage" tag on a bookmark to make it one of the pages that will be opened when pressing the 🏠 button. If multiple bookmarks are tagged as homepages, a random one is selected.
152* A "subscribed" tag means that Lagrange will periodically fetch the bookmarked page and look for Gemini feed links. All the found links that match the required style ("YYYY-MM-DD Entry title") will appear in the Feeds sidebar tab. 152* A "subscribed" tag means that Lagrange will periodically fetch the bookmarked page and look for Gemini feed links. All the found links that match the required style ("YYYY-MM-DD Entry title") will appear in the Feeds sidebar tab.
153* "headings" can be used together with "subscribed" to subscribe to new headings instead of Gemini feed links.
153 154
154## Feeds 155## Feeds
155 156
156You may be familiar with RSS and Atom XML feeds from the web. Lagrange does _not_ support RSS/Atom, only Gemini feeds. A Gemini feed is simply a regular 'text/gemini' page that contains one or more links whose labels are formatted in a particular way. 157You may be familiar with RSS and Atom XML feeds from the web. Lagrange does _not_ support RSS or Atom, only Gemini feeds. A Gemini feed is simply a regular 'text/gemini' page that contains one or more links whose labels are formatted in a particular way.
157 158
158=> gemini://gemini.circumlunar.space/docs/companion/subscription.gmi See "Subscribing to Gemini pages" for more information. 159=> gemini://gemini.circumlunar.space/docs/companion/subscription.gmi See "Subscribing to Gemini pages" for more information.
159 160
@@ -213,8 +214,6 @@ You can find a number of settings in Preferences to customize the user interface
213 214
214### Browsing behavior 215### Browsing behavior
215 216
216The "Outline on scrollbar" option shows the page outline when the mouse cursor is moved over the scrollbar, allowing one to better navigate long documents. The outline will disappear after the mouse cursor is moved away. If you need a persistent outline, one is always available in the sidebar.
217
218One important characteristic of Gemini is that you remain in control of what gets loaded and when. The browser will not suddenly fetch a ton of images, autoplay videos, or make surreptitious connections to any tracking servers — each network request is purposeful and manually triggered. With this in mind, the "Load image on scroll" option is provided to assist keyboard-only browsing and to facilitate a focused reading experience. When enabled, if there is an image link visible on the page and you press Space or ↓, it will be loaded and shown inline _instead_ of the view scrolling down. This allows you to read and see all the content of the page while only tapping on a single key on the keyboard. 217One important characteristic of Gemini is that you remain in control of what gets loaded and when. The browser will not suddenly fetch a ton of images, autoplay videos, or make surreptitious connections to any tracking servers — each network request is purposeful and manually triggered. With this in mind, the "Load image on scroll" option is provided to assist keyboard-only browsing and to facilitate a focused reading experience. When enabled, if there is an image link visible on the page and you press Space or ↓, it will be loaded and shown inline _instead_ of the view scrolling down. This allows you to read and see all the content of the page while only tapping on a single key on the keyboard.
219 218
220### Window and UI options 219### Window and UI options
@@ -315,12 +314,99 @@ Other Unix : ~/.config/lagrange/
315 314
316* bindings.txt 315* bindings.txt
317* bookmarks.txt 316* bookmarks.txt
317* feeds.txt
318* idents.binary and idents/ 318* idents.binary and idents/
319* mimehooks.txt
319* prefs.cfg 320* prefs.cfg
320* state.binary 321* state.binary
321* trusted.txt 322* trusted.txt
322* visited.txt 323* visited.txt
323 324
325# MIME hooks
326
327MIME hooks enable piping Gemini responses through external programs for arbitrary processing. This allows leveraging scripting languages and binary executables to perform format conversions, content transformation, and data analysis.
328
329Hooks are configured using the file "mimehooks.txt" in Lagrange's config directory. Each hook has a regexp that is matched against the response MIME type and parameters, and each matching hook is offered the response body via stdin. The called external programs are free to rewrite the entire response, including the MIME type. If one of the hooks returns a valid response, it is used as the final response of the Gemini request.
330
331Example use cases:
332* Parsing an Atom XML feed and generating a Gemini feed index page
333* Rendering a MIDI file using Timidity as PCM WAV
334* Generate a graph PNG based on values in a CSV
335* Converting HTML or Markdown to 'text/gemini'
336* Language translations
337
338## Interface
339
340When a hook is called, it is given the MIME type and parameters via command line arguments. The response body is provided via stdin. The request's URL is available in the REQUEST_URL environment variable.
341
342The MIME type and parameters are split at semicolons, so "text/gemini; lang=ja" would be called as:
343```
344hookprogram text/gemini lang=ja
345```
346
347Output from the program (via stdout) must be a valid "20" Gemini response that includes the new MIME type:
348```
34920 text/gemini; lang=en\r\n
350{...body...}
351```
352
353Any output that does not follow this format is considered to mean that the hook refused to process the contents. The next hook will be offered the response instead.
354
355## mimehooks.txt syntax
356
357Like other Lagrange configuration lines, mimehooks.txt has a simple line-oriented syntax. Lagrange must be restarted for changes to the configuration file to take effect.
358
359Each hook is specified as three lines:
360* A human-readable label (for reporting to the user)
361* MIME type/parameter regular expression
362* Command to execute, plus additional arguments each separated with semicolons
363
364For example:
365```
366Convert Atom to Gemini feed
367application/xml
368/usr/bin/python3;/home/jaakko/atomconv.py
369```
370
371The hook program is executed directly without involving the shell. This means scripts must be invoked via the interpreter executable.
372
373## Example: Converting from Atom to Gemini
374
375The following simple Python script demonstrates how a MIME hook could be used to parse an Atom XML document using Python 3 and output a Gemini feed index page based on the parsed entries. This is just a simple example; a more robust script could include more content from the Atom feed and handle errors, too.
376```
377import sys
378import xml.etree.ElementTree as ET
379
380def atomtag(n):
381 return '{http://www.w3.org/2005/Atom}' + n
382
383root = ET.fromstring(sys.stdin.read())
384if root.tag != atomtag('feed'):
385 sys.exit(0)
386feed_title = ''
387feed_author = ''
388feed_entries = []
389for child in root:
390 if child.tag == atomtag('title'):
391 feed_title = child.text
392 elif child.tag == atomtag('entry'):
393 feed_entries.append(child)
394print("20 text/gemini\r")
395print(f'# {feed_title}')
396for entry in feed_entries:
397 entry_date = ''
398 entry_title = ''
399 entry_link = ''
400 for child in entry:
401 if child.tag == atomtag('updated'):
402 entry_date = child.text[:10]
403 elif child.tag == atomtag('title'):
404 entry_title = child.text
405 elif child.tag == atomtag('link'):
406 entry_link = child.attrib['href']
407 print(f'=> {entry_link} {entry_date} {entry_title}')
408```
409
324# Open source licenses 410# Open source licenses
325 411
326=> about:license 412=> about:license
diff --git a/res/about/version.gmi b/res/about/version.gmi
index 85490dc7..2020b46c 100644
--- a/res/about/version.gmi
+++ b/res/about/version.gmi
@@ -6,8 +6,35 @@
6``` 6```
7# Release notes 7# Release notes
8 8
9## 0.12.2
10
11## 0.12.1
12* 'text/*' content falls back to plain text.
13* Minimized visual artifacts in Unicode box-drawing characters (overlapping/gaps) by fine-tuning glyph scaling.
14* Fixed truncated tab titles when opening tabs in background.
15* Fixed possible exit if hook program not found (SIGPIPE).
16* REQUEST_URL is set in the environment when running MIME hooks.
17* "about:debug" lists the configured MIME hooks.
18* macOS: Fixed excessive CPU usage while idling.
19
9## 0.12 20## 0.12
10* Subscribe to new headings on pages. 21* Added MIME hooks: pipe Gemini responses through external programs for arbitrary processing. (See "about:help" for usage.)
22* Added a right-hand sidebar; have a sidebar on the right or on both sides at once.
23* Added a clear warning banner when there is an issue with the server's TLS certificate.
24* Follow Weiph/pikkulogs — subscribe to new headings on pages.
25* Added UI for subscribing: feed name, entry type (Gemini feed or new headings).
26* Added keyboard shortcut ${SHIFT+}${CTRL+}D for subscribing to page.
27* Feeds sidebar is capped to 100 entries. "about:feeds" shows all known entries.
28* Network connections have a timeout in case server doesn't respond at all.
29* Adjusted spacing before/after links to reflect use of empty lines in the source.
30* Clicking on page area unfocuses URL input field.
31* Added keybindings for switching tabs.
32* Gopher: Query links have a 🔍 icon.
33* Fixed handling of "file:///" URIs on Windows.
34* Fixed misaligned Unicode box-drawing characters.
35* Fixed missing error page if status code is unknown (torture test 34).
36* Fixed detection of invalid headers (torture test 39).
37* Fixed rendering of soft hyphens (torture test 50).
11 38
12## 0.11 39## 0.11
13* Added feed subscriptions. A subscription is any bookmark with the "subscribed" tag. Subscribed feeds are refreshed in the background while Lagrange is running. 40* Added feed subscriptions. A subscription is any bookmark with the "subscribed" tag. Subscribed feeds are refreshed in the background while Lagrange is running.
diff --git a/src/app.c b/src/app.c
index b799b627..8a6b2a66 100644
--- a/src/app.c
+++ b/src/app.c
@@ -89,6 +89,8 @@ static const char *prefsFileName_App_ = "prefs.cfg";
89static const char *stateFileName_App_ = "state.binary"; 89static const char *stateFileName_App_ = "state.binary";
90static const char *downloadDir_App_ = "~/Downloads"; 90static const char *downloadDir_App_ = "~/Downloads";
91 91
92static const int idleThreshold_App_ = 1000; /* ms */
93
92struct Impl_App { 94struct Impl_App {
93 iCommandLine args; 95 iCommandLine args;
94 iString * execPath; 96 iString * execPath;
@@ -100,7 +102,12 @@ struct Impl_App {
100 iSortedArray tickers; 102 iSortedArray tickers;
101 uint32_t lastTickerTime; 103 uint32_t lastTickerTime;
102 uint32_t elapsedSinceLastTicker; 104 uint32_t elapsedSinceLastTicker;
103 iBool running; 105 iBool isRunning;
106#if defined (LAGRANGE_IDLE_SLEEP)
107 iBool isIdling;
108 uint32_t lastEventTime;
109 int sleepTimer;
110#endif
104 iAtomicInt pendingRefresh; 111 iAtomicInt pendingRefresh;
105 int tabEnum; 112 int tabEnum;
106 iStringList *launchCommands; 113 iStringList *launchCommands;
@@ -139,7 +146,8 @@ const iString *dateStr_(const iDate *date) {
139 146
140static iString *serializePrefs_App_(const iApp *d) { 147static iString *serializePrefs_App_(const iApp *d) {
141 iString *str = new_String(); 148 iString *str = new_String();
142 const iSidebarWidget *sidebar = findWidget_App("sidebar"); 149 const iSidebarWidget *sidebar = findWidget_App("sidebar");
150 const iSidebarWidget *sidebar2 = findWidget_App("sidebar2");
143 appendFormat_String(str, "window.retain arg:%d\n", d->prefs.retainWindowSize); 151 appendFormat_String(str, "window.retain arg:%d\n", d->prefs.retainWindowSize);
144 if (d->prefs.retainWindowSize) { 152 if (d->prefs.retainWindowSize) {
145 const iBool isMaximized = (SDL_GetWindowFlags(d->window->win) & SDL_WINDOW_MAXIMIZED) != 0; 153 const iBool isMaximized = (SDL_GetWindowFlags(d->window->win) & SDL_WINDOW_MAXIMIZED) != 0;
@@ -150,6 +158,7 @@ static iString *serializePrefs_App_(const iApp *d) {
150 h = d->window->lastRect.size.y; 158 h = d->window->lastRect.size.y;
151 appendFormat_String(str, "window.setrect width:%d height:%d coord:%d %d\n", w, h, x, y); 159 appendFormat_String(str, "window.setrect width:%d height:%d coord:%d %d\n", w, h, x, y);
152 appendFormat_String(str, "sidebar.width arg:%d\n", width_SidebarWidget(sidebar)); 160 appendFormat_String(str, "sidebar.width arg:%d\n", width_SidebarWidget(sidebar));
161 appendFormat_String(str, "sidebar2.width arg:%d\n", width_SidebarWidget(sidebar2));
153 /* On macOS, maximization should be applied at creation time or the window will take 162 /* On macOS, maximization should be applied at creation time or the window will take
154 a moment to animate to its maximized size. */ 163 a moment to animate to its maximized size. */
155#if !defined (iPlatformApple) 164#if !defined (iPlatformApple)
@@ -160,10 +169,16 @@ static iString *serializePrefs_App_(const iApp *d) {
160 iUnused(isMaximized); 169 iUnused(isMaximized);
161#endif 170#endif
162 } 171 }
163 if (isVisible_Widget(sidebar)) { 172 /* Sidebars. */ {
164 appendCStr_String(str, "sidebar.toggle\n"); 173 if (isVisible_Widget(sidebar)) {
174 appendCStr_String(str, "sidebar.toggle\n");
175 }
176 appendFormat_String(str, "sidebar.mode arg:%d\n", mode_SidebarWidget(sidebar));
177 if (isVisible_Widget(sidebar2)) {
178 appendCStr_String(str, "sidebar2.toggle\n");
179 }
180 appendFormat_String(str, "sidebar2.mode arg:%d\n", mode_SidebarWidget(sidebar2));
165 } 181 }
166 appendFormat_String(str, "sidebar.mode arg:%d\n", mode_SidebarWidget(sidebar));
167 appendFormat_String(str, "uiscale arg:%f\n", uiScale_Window(d->window)); 182 appendFormat_String(str, "uiscale arg:%f\n", uiScale_Window(d->window));
168 appendFormat_String(str, "prefs.dialogtab arg:%d\n", d->prefs.dialogTab); 183 appendFormat_String(str, "prefs.dialogtab arg:%d\n", d->prefs.dialogTab);
169 appendFormat_String(str, "font.set arg:%d\n", d->prefs.font); 184 appendFormat_String(str, "font.set arg:%d\n", d->prefs.font);
@@ -312,6 +327,16 @@ static void saveState_App_(const iApp *d) {
312 iRelease(f); 327 iRelease(f);
313} 328}
314 329
330#if defined (LAGRANGE_IDLE_SLEEP)
331static uint32_t checkAsleep_App_(uint32_t interval, void *param) {
332 iApp *d = param;
333 SDL_Event ev = { .type = SDL_USEREVENT };
334 ev.user.code = asleep_UserEventCode;
335 SDL_PushEvent(&ev);
336 return interval;
337}
338#endif
339
315static void init_App_(iApp *d, int argc, char **argv) { 340static void init_App_(iApp *d, int argc, char **argv) {
316 const iBool isFirstRun = !fileExistsCStr_FileInfo(cleanedPath_CStr(dataDir_App_)); 341 const iBool isFirstRun = !fileExistsCStr_FileInfo(cleanedPath_CStr(dataDir_App_));
317 d->isFinishedLaunching = iFalse; 342 d->isFinishedLaunching = iFalse;
@@ -341,7 +366,7 @@ static void init_App_(iApp *d, int argc, char **argv) {
341#endif 366#endif
342 init_Prefs(&d->prefs); 367 init_Prefs(&d->prefs);
343 setCStr_String(&d->prefs.downloadDir, downloadDir_App_); 368 setCStr_String(&d->prefs.downloadDir, downloadDir_App_);
344 d->running = iFalse; 369 d->isRunning = iFalse;
345 d->window = NULL; 370 d->window = NULL;
346 set_Atomic(&d->pendingRefresh, iFalse); 371 set_Atomic(&d->pendingRefresh, iFalse);
347 d->mimehooks = new_MimeHooks(); 372 d->mimehooks = new_MimeHooks();
@@ -350,6 +375,11 @@ static void init_App_(iApp *d, int argc, char **argv) {
350 d->bookmarks = new_Bookmarks(); 375 d->bookmarks = new_Bookmarks();
351 d->tabEnum = 0; /* generates unique IDs for tab pages */ 376 d->tabEnum = 0; /* generates unique IDs for tab pages */
352 setThemePalette_Color(d->prefs.theme); 377 setThemePalette_Color(d->prefs.theme);
378#if defined (LAGRANGE_IDLE_SLEEP)
379 d->isIdling = iFalse;
380 d->lastEventTime = 0;
381 d->sleepTimer = SDL_AddTimer(1000, checkAsleep_App_, d);
382#endif
353#if defined (iPlatformApple) 383#if defined (iPlatformApple)
354 setupApplication_MacOS(); 384 setupApplication_MacOS();
355#endif 385#endif
@@ -472,23 +502,31 @@ const iString *debugInfo_App(void) {
472 iConstForEach(StringList, j, d->launchCommands) { 502 iConstForEach(StringList, j, d->launchCommands) {
473 appendFormat_String(msg, "%s\n", cstr_String(j.value)); 503 appendFormat_String(msg, "%s\n", cstr_String(j.value));
474 } 504 }
505 appendFormat_String(msg, "## MIME hooks\n");
506 append_String(msg, debugInfo_MimeHooks(d->mimehooks));
475 return msg; 507 return msg;
476} 508}
477 509
478iLocalDef iBool isWaitingAllowed_App_(iApp *d) { 510iLocalDef iBool isWaitingAllowed_App_(iApp *d) {
511#if defined (LAGRANGE_IDLE_SLEEP)
512 if (d->isIdling) {
513 return iFalse;
514 }
515#endif
479 return !value_Atomic(&d->pendingRefresh) && isEmpty_SortedArray(&d->tickers); 516 return !value_Atomic(&d->pendingRefresh) && isEmpty_SortedArray(&d->tickers);
480} 517}
481 518
482void processEvents_App(enum iAppEventMode eventMode) { 519void processEvents_App(enum iAppEventMode eventMode) {
483 iApp *d = &app_; 520 iApp *d = &app_;
484 SDL_Event ev; 521 SDL_Event ev;
522 iBool gotEvents = iFalse;
485 while ((isWaitingAllowed_App_(d) && eventMode == waitForNewEvents_AppEventMode && 523 while ((isWaitingAllowed_App_(d) && eventMode == waitForNewEvents_AppEventMode &&
486 SDL_WaitEvent(&ev)) || 524 SDL_WaitEvent(&ev)) ||
487 ((!isWaitingAllowed_App_(d) || eventMode == postedEventsOnly_AppEventMode) && 525 ((!isWaitingAllowed_App_(d) || eventMode == postedEventsOnly_AppEventMode) &&
488 SDL_PollEvent(&ev))) { 526 SDL_PollEvent(&ev))) {
489 switch (ev.type) { 527 switch (ev.type) {
490 case SDL_QUIT: 528 case SDL_QUIT:
491 d->running = iFalse; 529 d->isRunning = iFalse;
492 goto backToMainLoop; 530 goto backToMainLoop;
493 case SDL_DROPFILE: { 531 case SDL_DROPFILE: {
494 iBool newTab = iFalse; 532 iBool newTab = iFalse;
@@ -508,6 +546,25 @@ void processEvents_App(enum iAppEventMode eventMode) {
508 break; 546 break;
509 } 547 }
510 default: { 548 default: {
549#if defined (LAGRANGE_IDLE_SLEEP)
550 if (ev.type == SDL_USEREVENT && ev.user.code == asleep_UserEventCode) {
551 if (SDL_GetTicks() - d->lastEventTime > idleThreshold_App_) {
552 if (!d->isIdling) {
553// printf("[App] idling...\n");
554 fflush(stdout);
555 }
556 d->isIdling = iTrue;
557 }
558 continue;
559 }
560 d->lastEventTime = SDL_GetTicks();
561 if (d->isIdling) {
562// printf("[App] ...woke up\n");
563 fflush(stdout);
564 }
565 d->isIdling = iFalse;
566#endif
567 gotEvents = iTrue;
511 iBool wasUsed = processEvent_Window(d->window, &ev); 568 iBool wasUsed = processEvent_Window(d->window, &ev);
512 if (!wasUsed) { 569 if (!wasUsed) {
513 /* There may be a key bindings for this. */ 570 /* There may be a key bindings for this. */
@@ -531,6 +588,14 @@ void processEvents_App(enum iAppEventMode eventMode) {
531 } 588 }
532 } 589 }
533 } 590 }
591#if defined (LAGRANGE_IDLE_SLEEP)
592 if (d->isIdling && !gotEvents) {
593 /* This is where we spend most of our time when idle. 60 Hz still quite a lot but we
594 can't wait too long after the user tries to interact again with the app. In any
595 case, on macOS SDL_WaitEvent() seems to use 10x more CPU time than sleeping. */
596 SDL_Delay(1000 / 60);
597 }
598#endif
534backToMainLoop:; 599backToMainLoop:;
535} 600}
536 601
@@ -578,10 +643,10 @@ static int resizeWatcher_(void *user, SDL_Event *event) {
578 643
579static int run_App_(iApp *d) { 644static int run_App_(iApp *d) {
580 arrange_Widget(findWidget_App("root")); 645 arrange_Widget(findWidget_App("root"));
581 d->running = iTrue; 646 d->isRunning = iTrue;
582 SDL_EventState(SDL_DROPFILE, SDL_ENABLE); /* open files via drag'n'drop */ 647 SDL_EventState(SDL_DROPFILE, SDL_ENABLE); /* open files via drag'n'drop */
583 SDL_AddEventWatch(resizeWatcher_, d); 648 SDL_AddEventWatch(resizeWatcher_, d);
584 while (d->running) { 649 while (d->isRunning) {
585 processEvents_App(waitForNewEvents_AppEventMode); 650 processEvents_App(waitForNewEvents_AppEventMode);
586 runTickers_App_(d); 651 runTickers_App_(d);
587 refresh_App(); 652 refresh_App();
@@ -592,6 +657,9 @@ static int run_App_(iApp *d) {
592 657
593void refresh_App(void) { 658void refresh_App(void) {
594 iApp *d = &app_; 659 iApp *d = &app_;
660#if defined (LAGRANGE_IDLE_SLEEP)
661 if (d->isIdling) return;
662#endif
595 destroyPending_Widget(); 663 destroyPending_Widget();
596 draw_Window(d->window); 664 draw_Window(d->window);
597 set_Atomic(&d->pendingRefresh, iFalse); 665 set_Atomic(&d->pendingRefresh, iFalse);
@@ -649,6 +717,9 @@ int run_App(int argc, char **argv) {
649 717
650void postRefresh_App(void) { 718void postRefresh_App(void) {
651 iApp *d = &app_; 719 iApp *d = &app_;
720#if defined (LAGRANGE_IDLE_SLEEP)
721 d->isIdling = iFalse;
722#endif
652 const iBool wasPending = exchange_Atomic(&d->pendingRefresh, iTrue); 723 const iBool wasPending = exchange_Atomic(&d->pendingRefresh, iTrue);
653 if (!wasPending) { 724 if (!wasPending) {
654 SDL_Event ev; 725 SDL_Event ev;
@@ -844,6 +915,7 @@ iDocumentWidget *newTab_App(const iDocumentWidget *duplicateOf, iBool switchToNe
844 } 915 }
845 arrange_Widget(tabs); 916 arrange_Widget(tabs);
846 refresh_Widget(tabs); 917 refresh_Widget(tabs);
918 postCommandf_App("tab.created id:%s", cstr_String(id_Widget(as_Widget(doc))));
847 return doc; 919 return doc;
848} 920}
849 921
@@ -964,7 +1036,13 @@ iBool willUseProxy_App(const iRangecc scheme) {
964 1036
965iBool handleCommand_App(const char *cmd) { 1037iBool handleCommand_App(const char *cmd) {
966 iApp *d = &app_; 1038 iApp *d = &app_;
967 if (equal_Command(cmd, "prefs.dialogtab")) { 1039 if (equal_Command(cmd, "config.error")) {
1040 makeMessage_Widget(uiTextCaution_ColorEscape "CONFIG ERROR",
1041 format_CStr("Error in config file: %s\nSee \"about:debug\" for details.",
1042 suffixPtr_Command(cmd, "where")));
1043 return iTrue;
1044 }
1045 else if (equal_Command(cmd, "prefs.dialogtab")) {
968 d->prefs.dialogTab = arg_Command(cmd); 1046 d->prefs.dialogTab = arg_Command(cmd);
969 return iTrue; 1047 return iTrue;
970 } 1048 }
@@ -1076,12 +1154,12 @@ iBool handleCommand_App(const char *cmd) {
1076 } 1154 }
1077 else if (equal_Command(cmd, "prefs.sideicon.changed")) { 1155 else if (equal_Command(cmd, "prefs.sideicon.changed")) {
1078 d->prefs.sideIcon = arg_Command(cmd) != 0; 1156 d->prefs.sideIcon = arg_Command(cmd) != 0;
1079 refresh_App(); 1157 postRefresh_App();
1080 return iTrue; 1158 return iTrue;
1081 } 1159 }
1082 else if (equal_Command(cmd, "prefs.hoveroutline.changed")) { 1160 else if (equal_Command(cmd, "prefs.hoveroutline.changed")) {
1083 d->prefs.hoverOutline = arg_Command(cmd) != 0; 1161 d->prefs.hoverOutline = arg_Command(cmd) != 0;
1084 refresh_App(); 1162 postRefresh_App();
1085 return iTrue; 1163 return iTrue;
1086 } 1164 }
1087 else if (equal_Command(cmd, "saturation.set")) { 1165 else if (equal_Command(cmd, "saturation.set")) {
@@ -1364,13 +1442,13 @@ iBool handleCommand_App(const char *cmd) {
1364 } 1442 }
1365 else if (equal_Command(cmd, "feeds.update.started")) { 1443 else if (equal_Command(cmd, "feeds.update.started")) {
1366 setFlags_Widget(findWidget_App("feeds.progress"), hidden_WidgetFlag, iFalse); 1444 setFlags_Widget(findWidget_App("feeds.progress"), hidden_WidgetFlag, iFalse);
1367 refresh_App(); 1445 postRefresh_App();
1368 return iFalse; 1446 return iFalse;
1369 } 1447 }
1370 else if (equal_Command(cmd, "feeds.update.finished")) { 1448 else if (equal_Command(cmd, "feeds.update.finished")) {
1371 setFlags_Widget(findWidget_App("feeds.progress"), hidden_WidgetFlag, iTrue); 1449 setFlags_Widget(findWidget_App("feeds.progress"), hidden_WidgetFlag, iTrue);
1372 refreshFinished_Feeds(); 1450 refreshFinished_Feeds();
1373 refresh_App(); 1451 postRefresh_App();
1374 return iFalse; 1452 return iFalse;
1375 } 1453 }
1376 else if (equal_Command(cmd, "visited.changed")) { 1454 else if (equal_Command(cmd, "visited.changed")) {
diff --git a/src/app.h b/src/app.h
index bc086dfe..743484a5 100644
--- a/src/app.h
+++ b/src/app.h
@@ -46,6 +46,7 @@ enum iAppEventMode {
46enum iUserEventCode { 46enum iUserEventCode {
47 command_UserEventCode = 1, 47 command_UserEventCode = 1,
48 refresh_UserEventCode = 2, 48 refresh_UserEventCode = 2,
49 asleep_UserEventCode = 3,
49}; 50};
50 51
51const iString *execPath_App (void); 52const iString *execPath_App (void);
diff --git a/src/gmrequest.c b/src/gmrequest.c
index a1284078..884486b3 100644
--- a/src/gmrequest.c
+++ b/src/gmrequest.c
@@ -262,8 +262,9 @@ static void requestFinished_GmRequest_(iGmRequest *d, iTlsRequest *req) {
262 checkServerCertificate_GmRequest_(d); 262 checkServerCertificate_GmRequest_(d);
263 unlock_Mutex(d->mtx); 263 unlock_Mutex(d->mtx);
264 /* Check for mimehooks. */ 264 /* Check for mimehooks. */
265 if (d->isRespFiltered && d->state == finished_GmRequestState) { 265 if (d->isRespFiltered && d->state == finished_GmRequestState) {
266 iBlock *xbody = tryFilter_MimeHooks(mimeHooks_App(), &d->resp->meta, &d->resp->body); 266 iBlock *xbody =
267 tryFilter_MimeHooks(mimeHooks_App(), &d->resp->meta, &d->resp->body, &d->url);
267 if (xbody) { 268 if (xbody) {
268 lock_Mutex(d->mtx); 269 lock_Mutex(d->mtx);
269 clear_String(&d->resp->meta); 270 clear_String(&d->resp->meta);
@@ -526,6 +527,12 @@ void submit_GmRequest(iGmRequest *d) {
526 } 527 }
527 else if (equalCase_Rangecc(url.scheme, "file")) { 528 else if (equalCase_Rangecc(url.scheme, "file")) {
528 iString *path = collect_String(urlDecode_String(collect_String(newRange_String(url.path)))); 529 iString *path = collect_String(urlDecode_String(collect_String(newRange_String(url.path))));
530#if defined (iPlatformMsys)
531 /* Remove the extra slash from the beginning. */
532 if (startsWith_String(path, "/")) {
533 remove_Block(&path->chars, 0, 1);
534 }
535#endif
529 iFile * f = new_File(path); 536 iFile * f = new_File(path);
530 if (open_File(f, readOnly_FileMode)) { 537 if (open_File(f, readOnly_FileMode)) {
531 /* TODO: Check supported file types: images, audio */ 538 /* TODO: Check supported file types: images, audio */
@@ -555,6 +562,9 @@ void submit_GmRequest(iGmRequest *d) {
555 else if (endsWithCase_String(path, ".mp3")) { 562 else if (endsWithCase_String(path, ".mp3")) {
556 setCStr_String(&resp->meta, "audio/mpeg"); 563 setCStr_String(&resp->meta, "audio/mpeg");
557 } 564 }
565 else if (endsWithCase_String(path, ".mid")) {
566 setCStr_String(&resp->meta, "audio/midi");
567 }
558 else { 568 else {
559 setCStr_String(&resp->meta, "application/octet-stream"); 569 setCStr_String(&resp->meta, "application/octet-stream");
560 } 570 }
diff --git a/src/gmutil.c b/src/gmutil.c
index 557a82f8..94f00ce1 100644
--- a/src/gmutil.c
+++ b/src/gmutil.c
@@ -192,7 +192,10 @@ const iString *absoluteUrl_String(const iString *d, const iString *urlMaybeRelat
192iString *makeFileUrl_String(const iString *localFilePath) { 192iString *makeFileUrl_String(const iString *localFilePath) {
193 iString *url = cleaned_Path(localFilePath); 193 iString *url = cleaned_Path(localFilePath);
194 replace_Block(&url->chars, '\\', '/'); /* in case it's a Windows path */ 194 replace_Block(&url->chars, '\\', '/'); /* in case it's a Windows path */
195 set_String(url, collect_String(urlEncodeExclude_String(url, "/"))); 195 set_String(url, collect_String(urlEncodeExclude_String(url, "/:")));
196#if defined (iPlatformMsys)
197 prependChar_String(url, '/'); /* three slashes */
198#endif
196 prependCStr_String(url, "file://"); 199 prependCStr_String(url, "file://");
197 return url; 200 return url;
198} 201}
diff --git a/src/mimehooks.c b/src/mimehooks.c
index 8bb838ef..f4ec6bf4 100644
--- a/src/mimehooks.c
+++ b/src/mimehooks.c
@@ -1,6 +1,8 @@
1#include "mimehooks.h" 1#include "mimehooks.h"
2#include "app.h"
2 3
3#include <the_Foundation/file.h> 4#include <the_Foundation/file.h>
5#include <the_Foundation/fileinfo.h>
4#include <the_Foundation/path.h> 6#include <the_Foundation/path.h>
5#include <the_Foundation/process.h> 7#include <the_Foundation/process.h>
6#include <the_Foundation/stringlist.h> 8#include <the_Foundation/stringlist.h>
@@ -23,6 +25,7 @@ void deinit_FilterHook(iFilterHook *d) {
23 25
24void setMimePattern_FilterHook(iFilterHook *d, const iString *pattern) { 26void setMimePattern_FilterHook(iFilterHook *d, const iString *pattern) {
25 iReleasePtr(&d->mimeRegex); 27 iReleasePtr(&d->mimeRegex);
28 set_String(&d->mimePattern, pattern);
26 d->mimeRegex = new_RegExp(cstr_String(pattern), caseInsensitive_RegExpOption); 29 d->mimeRegex = new_RegExp(cstr_String(pattern), caseInsensitive_RegExpOption);
27} 30}
28 31
@@ -30,7 +33,8 @@ void setCommand_FilterHook(iFilterHook *d, const iString *command) {
30 set_String(&d->command, command); 33 set_String(&d->command, command);
31} 34}
32 35
33iBlock *run_FilterHook_(const iFilterHook *d, const iString *mime, const iBlock *body) { 36iBlock *run_FilterHook_(const iFilterHook *d, const iString *mime, const iBlock *body,
37 const iString *requestUrl) {
34 iProcess * proc = new_Process(); 38 iProcess * proc = new_Process();
35 iStringList *args = new_StringList(); 39 iStringList *args = new_StringList();
36 iRangecc seg = iNullRange; 40 iRangecc seg = iNullRange;
@@ -43,13 +47,21 @@ iBlock *run_FilterHook_(const iFilterHook *d, const iString *mime, const iBlock
43 } 47 }
44 setArguments_Process(proc, args); 48 setArguments_Process(proc, args);
45 iRelease(args); 49 iRelease(args);
46 start_Process(proc); 50 if (!isEmpty_String(requestUrl)) {
47 writeInput_Process(proc, body); 51 setEnvironment_Process(
48 iBlock *output = readOutputUntilClosed_Process(proc); 52 proc,
49 if (!startsWith_Rangecc(range_Block(output), "20")) { 53 iClob(newStrings_StringList(
50 /* Didn't produce valid output. */ 54 collectNewFormat_String("REQUEST_URL=%s", cstr_String(requestUrl)), NULL)));
51 delete_Block(output); 55 }
52 output = NULL; 56 iBlock *output = NULL;
57 if (start_Process(proc)) {
58 writeInput_Process(proc, body);
59 output = readOutputUntilClosed_Process(proc);
60 if (!startsWith_Rangecc(range_Block(output), "20")) {
61 /* Didn't produce valid output. */
62 delete_Block(output);
63 output = NULL;
64 }
53 } 65 }
54 iRelease(proc); 66 iRelease(proc);
55 return output; 67 return output;
@@ -87,13 +99,14 @@ iBool willTryFilter_MimeHooks(const iMimeHooks *d, const iString *mime) {
87 return iFalse; 99 return iFalse;
88} 100}
89 101
90iBlock *tryFilter_MimeHooks(const iMimeHooks *d, const iString *mime, const iBlock *body) { 102iBlock *tryFilter_MimeHooks(const iMimeHooks *d, const iString *mime, const iBlock *body,
103 const iString *requestUrl) {
91 iRegExpMatch m; 104 iRegExpMatch m;
92 iConstForEach(PtrArray, i, &d->filters) { 105 iConstForEach(PtrArray, i, &d->filters) {
93 const iFilterHook *xc = i.ptr; 106 const iFilterHook *xc = i.ptr;
94 init_RegExpMatch(&m); 107 init_RegExpMatch(&m);
95 if (matchString_RegExp(xc->mimeRegex, mime, &m)) { 108 if (matchString_RegExp(xc->mimeRegex, mime, &m)) {
96 iBlock *result = run_FilterHook_(xc, mime, body); 109 iBlock *result = run_FilterHook_(xc, mime, body, requestUrl);
97 if (result) { 110 if (result) {
98 return result; 111 return result;
99 } 112 }
@@ -105,6 +118,7 @@ iBlock *tryFilter_MimeHooks(const iMimeHooks *d, const iString *mime, const iBlo
105static const char *mimeHooksFilename_MimeHooks_ = "mimehooks.txt"; 118static const char *mimeHooksFilename_MimeHooks_ = "mimehooks.txt";
106 119
107void load_MimeHooks(iMimeHooks *d, const char *saveDir) { 120void load_MimeHooks(iMimeHooks *d, const char *saveDir) {
121 iBool reportError = iFalse;
108 iFile *f = newCStr_File(concatPath_CStr(saveDir, mimeHooksFilename_MimeHooks_)); 122 iFile *f = newCStr_File(concatPath_CStr(saveDir, mimeHooksFilename_MimeHooks_));
109 if (open_File(f, read_FileMode | text_FileMode)) { 123 if (open_File(f, read_FileMode | text_FileMode)) {
110 iBlock * src = readAll_File(f); 124 iBlock * src = readAll_File(f);
@@ -124,6 +138,15 @@ void load_MimeHooks(iMimeHooks *d, const char *saveDir) {
124 setRange_String(&hook->label, lines[0]); 138 setRange_String(&hook->label, lines[0]);
125 setMimePattern_FilterHook(hook, collect_String(newRange_String(lines[1]))); 139 setMimePattern_FilterHook(hook, collect_String(newRange_String(lines[1])));
126 setCommand_FilterHook(hook, collect_String(newRange_String(lines[2]))); 140 setCommand_FilterHook(hook, collect_String(newRange_String(lines[2])));
141 /* Check if commmand is valid. */ {
142 iRangecc seg = iNullRange;
143 while (nextSplit_Rangecc(range_String(&hook->command), ";", &seg)) {
144 if (!fileExistsCStr_FileInfo(cstr_Rangecc(seg))) {
145 reportError = iTrue;
146 }
147 break;
148 }
149 }
127 pushBack_PtrArray(&d->filters, hook); 150 pushBack_PtrArray(&d->filters, hook);
128 pos = 0; 151 pos = 0;
129 } 152 }
@@ -131,9 +154,37 @@ void load_MimeHooks(iMimeHooks *d, const char *saveDir) {
131 delete_Block(src); 154 delete_Block(src);
132 } 155 }
133 iRelease(f); 156 iRelease(f);
157 if (reportError) {
158 postCommand_App("~config.error where:mimehooks.txt");
159 }
134} 160}
135 161
136void save_MimeHooks(const iMimeHooks *d) { 162void save_MimeHooks(const iMimeHooks *d) {
137 iUnused(d); 163 iUnused(d);
138} 164}
139 165
166const iString *debugInfo_MimeHooks(const iMimeHooks *d) {
167 iString *str = collectNew_String();
168 size_t index = 0;
169 iConstForEach(PtrArray, i, &d->filters) {
170 const iFilterHook *filter = i.ptr;
171 appendFormat_String(str, "### %d: %s\n", index, cstr_String(&filter->label));
172 appendFormat_String(str, "MIME regex:\n```\n%s\n```\n", cstr_String(&filter->mimePattern));
173 iStringList *args = iClob(split_String(&filter->command, ";"));
174 if (isEmpty_StringList(args)) {
175 appendFormat_String(str, "\u26a0 Command not specified!\n");
176 continue;
177 }
178 const iString *exec = constAt_StringList(args, 0);
179 if (isEmpty_String(exec)) {
180 appendFormat_String(str, "\u26a0 Command not specified!\n");
181 }
182 else {
183 appendFormat_String(str, "Executable: %s\n```\n%s\n```\n",
184 fileExists_FileInfo(exec) ? "" : "\u26a0 FILE NOT FOUND",
185 cstr_String(exec));
186 }
187 index++;
188 }
189 return str;
190}
diff --git a/src/mimehooks.h b/src/mimehooks.h
index c78a3c86..6da14fdf 100644
--- a/src/mimehooks.h
+++ b/src/mimehooks.h
@@ -25,7 +25,9 @@ iDeclareTypeConstruction(MimeHooks)
25 25
26iBool willTryFilter_MimeHooks (const iMimeHooks *, const iString *mime); 26iBool willTryFilter_MimeHooks (const iMimeHooks *, const iString *mime);
27iBlock * tryFilter_MimeHooks (const iMimeHooks *, const iString *mime, 27iBlock * tryFilter_MimeHooks (const iMimeHooks *, const iString *mime,
28 const iBlock *body); 28 const iBlock *body, const iString *requestUrl);
29 29
30void load_MimeHooks (iMimeHooks *, const char *saveDir); 30void load_MimeHooks (iMimeHooks *, const char *saveDir);
31void save_MimeHooks (const iMimeHooks *); 31void save_MimeHooks (const iMimeHooks *);
32
33const iString *debugInfo_MimeHooks (const iMimeHooks *);
diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c
index a6cc8187..7a297aa7 100644
--- a/src/ui/documentwidget.c
+++ b/src/ui/documentwidget.c
@@ -302,7 +302,7 @@ static int documentWidth_DocumentWidget_(const iDocumentWidget *d) {
302 const iWidget *w = constAs_Widget(d); 302 const iWidget *w = constAs_Widget(d);
303 const iRect bounds = bounds_Widget(w); 303 const iRect bounds = bounds_Widget(w);
304 const iPrefs * prefs = prefs_App(); 304 const iPrefs * prefs = prefs_App();
305 return iMini(bounds.size.x - gap_UI * d->pageMargin * 2, 305 return iMini(iMax(50 * gap_UI, bounds.size.x - gap_UI * d->pageMargin * 2),
306 fontSize_UI * prefs->lineWidth * prefs->zoomPercent / 100); 306 fontSize_UI * prefs->lineWidth * prefs->zoomPercent / 100);
307} 307}
308 308
@@ -859,12 +859,13 @@ static void updateDocument_DocumentWidget_(iDocumentWidget *d, const iGmResponse
859 while (nextSplit_Rangecc(mime, ";", &seg)) { 859 while (nextSplit_Rangecc(mime, ";", &seg)) {
860 iRangecc param = seg; 860 iRangecc param = seg;
861 trim_Rangecc(&param); 861 trim_Rangecc(&param);
862 if (equal_Rangecc(param, "text/plain")) { 862 if (equal_Rangecc(param, "text/gemini")) {
863 docFormat = plainText_GmDocumentFormat; 863 docFormat = gemini_GmDocumentFormat;
864 setRange_String(&d->sourceMime, param); 864 setRange_String(&d->sourceMime, param);
865 } 865 }
866 else if (equal_Rangecc(param, "text/gemini")) { 866 else if (startsWith_Rangecc(param, "text/") ||
867 docFormat = gemini_GmDocumentFormat; 867 equal_Rangecc(param, "application/json")) {
868 docFormat = plainText_GmDocumentFormat;
868 setRange_String(&d->sourceMime, param); 869 setRange_String(&d->sourceMime, param);
869 } 870 }
870 else if (startsWith_Rangecc(param, "image/") || 871 else if (startsWith_Rangecc(param, "image/") ||
@@ -1528,6 +1529,11 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
1528 animatePlayers_DocumentWidget_(d); 1529 animatePlayers_DocumentWidget_(d);
1529 return iFalse; 1530 return iFalse;
1530 } 1531 }
1532 else if (equal_Command(cmd, "tab.created")) {
1533 /* Space for tab buttons has changed. */
1534 updateWindowTitle_DocumentWidget_(d);
1535 return iFalse;
1536 }
1531 else if (equal_Command(cmd, "server.showcert") && d == document_App()) { 1537 else if (equal_Command(cmd, "server.showcert") && d == document_App()) {
1532 const char *unchecked = red_ColorEscape "\u2610"; 1538 const char *unchecked = red_ColorEscape "\u2610";
1533 const char *checked = green_ColorEscape "\u2611"; 1539 const char *checked = green_ColorEscape "\u2611";
@@ -2227,6 +2233,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
2227 if (d->menu) { 2233 if (d->menu) {
2228 destroy_Widget(d->menu); 2234 destroy_Widget(d->menu);
2229 } 2235 }
2236 setFocus_Widget(NULL);
2230 iArray items; 2237 iArray items;
2231 init_Array(&items, sizeof(iMenuItem)); 2238 init_Array(&items, sizeof(iMenuItem));
2232 if (d->contextLink) { 2239 if (d->contextLink) {
@@ -2909,6 +2916,7 @@ static void drawSideElements_DocumentWidget_(const iDocumentWidget *d) {
2909 iMax(0, scrollMax_DocumentWidget_(d) - value_Anim(&d->scrollY)))), 2916 iMax(0, scrollMax_DocumentWidget_(d) - value_Anim(&d->scrollY)))),
2910 tmQuoteIcon_ColorId); 2917 tmQuoteIcon_ColorId);
2911 } 2918 }
2919#if 0
2912 /* Outline on the right side. */ 2920 /* Outline on the right side. */
2913 const float outlineOpacity = value_Anim(&d->outlineOpacity); 2921 const float outlineOpacity = value_Anim(&d->outlineOpacity);
2914 if (prefs_App()->hoverOutline && !isEmpty_Array(&d->outline) && outlineOpacity > 0.0f) { 2922 if (prefs_App()->hoverOutline && !isEmpty_Array(&d->outline) && outlineOpacity > 0.0f) {
@@ -2964,8 +2972,9 @@ static void drawSideElements_DocumentWidget_(const iDocumentWidget *d) {
2964 setOpacity_Text(1.0f); 2972 setOpacity_Text(1.0f);
2965 SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_NONE); 2973 SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_NONE);
2966 } 2974 }
2975#endif
2967 unsetClip_Paint(&p); 2976 unsetClip_Paint(&p);
2968 } 2977}
2969 2978
2970static void drawPlayers_DocumentWidget_(const iDocumentWidget *d, iPaint *p) { 2979static void drawPlayers_DocumentWidget_(const iDocumentWidget *d, iPaint *p) {
2971 iConstForEach(PtrArray, i, &d->visiblePlayers) { 2980 iConstForEach(PtrArray, i, &d->visiblePlayers) {
@@ -2982,6 +2991,9 @@ static void draw_DocumentWidget_(const iDocumentWidget *d) {
2982 const iWidget *w = constAs_Widget(d); 2991 const iWidget *w = constAs_Widget(d);
2983 const iRect bounds = bounds_Widget(w); 2992 const iRect bounds = bounds_Widget(w);
2984 iVisBuf * visBuf = d->visBuf; /* will be updated now */ 2993 iVisBuf * visBuf = d->visBuf; /* will be updated now */
2994 if (width_Rect(bounds) <= 0) {
2995 return;
2996 }
2985 draw_Widget(w); 2997 draw_Widget(w);
2986 allocVisBuffer_DocumentWidget_(d); 2998 allocVisBuffer_DocumentWidget_(d);
2987 const iRect ctxWidgetBounds = init_Rect( 2999 const iRect ctxWidgetBounds = init_Rect(
diff --git a/src/ui/keys.h b/src/ui/keys.h
index cc56f8d1..8bcd4f53 100644
--- a/src/ui/keys.h
+++ b/src/ui/keys.h
@@ -36,6 +36,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
36# define navigateRoot_KeyShortcut SDLK_UP, KMOD_SHIFT | KMOD_PRIMARY 36# define navigateRoot_KeyShortcut SDLK_UP, KMOD_SHIFT | KMOD_PRIMARY
37# define byWord_KeyModifier KMOD_ALT 37# define byWord_KeyModifier KMOD_ALT
38# define byLine_KeyModifier KMOD_PRIMARY 38# define byLine_KeyModifier KMOD_PRIMARY
39# define rightSidebar_KeyModifier KMOD_CTRL
39# define subscribeToPage_KeyModifier SDLK_d, KMOD_SHIFT | KMOD_PRIMARY 40# define subscribeToPage_KeyModifier SDLK_d, KMOD_SHIFT | KMOD_PRIMARY
40#else 41#else
41# define reload_KeyShortcut SDLK_r, KMOD_PRIMARY 42# define reload_KeyShortcut SDLK_r, KMOD_PRIMARY
@@ -47,6 +48,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
47# define navigateRoot_KeyShortcut SDLK_UP, KMOD_SHIFT | KMOD_ALT 48# define navigateRoot_KeyShortcut SDLK_UP, KMOD_SHIFT | KMOD_ALT
48# define byWord_KeyModifier KMOD_CTRL 49# define byWord_KeyModifier KMOD_CTRL
49# define byLine_KeyModifier 0 50# define byLine_KeyModifier 0
51# define rightSidebar_KeyModifier KMOD_SHIFT | KMOD_CTRL
50# define subscribeToPage_KeyModifier SDLK_d, KMOD_SHIFT | KMOD_PRIMARY 52# define subscribeToPage_KeyModifier SDLK_d, KMOD_SHIFT | KMOD_PRIMARY
51#endif 53#endif
52 54
diff --git a/src/ui/sidebarwidget.c b/src/ui/sidebarwidget.c
index 5baa08f7..e6144744 100644
--- a/src/ui/sidebarwidget.c
+++ b/src/ui/sidebarwidget.c
@@ -84,7 +84,9 @@ iDefineObjectConstruction(SidebarItem)
84 84
85struct Impl_SidebarWidget { 85struct Impl_SidebarWidget {
86 iWidget widget; 86 iWidget widget;
87 enum iSidebarSide side;
87 enum iSidebarMode mode; 88 enum iSidebarMode mode;
89 iString cmdPrefix;
88 iWidget * blank; 90 iWidget * blank;
89 iListWidget * list; 91 iListWidget * list;
90 int modeScroll[max_SidebarMode]; 92 int modeScroll[max_SidebarMode];
@@ -92,12 +94,11 @@ struct Impl_SidebarWidget {
92 int maxButtonLabelWidth; 94 int maxButtonLabelWidth;
93 int width; 95 int width;
94 iWidget * resizer; 96 iWidget * resizer;
95 SDL_Cursor * resizeCursor;
96 iWidget * menu; 97 iWidget * menu;
97 iSidebarItem * contextItem; /* list item accessed in the context menu */ 98 iSidebarItem * contextItem; /* list item accessed in the context menu */
98}; 99};
99 100
100iDefineObjectConstruction(SidebarWidget) 101iDefineObjectConstructionArgs(SidebarWidget, (enum iSidebarSide side), side)
101 102
102static iBool isResizing_SidebarWidget_(const iSidebarWidget *d) { 103static iBool isResizing_SidebarWidget_(const iSidebarWidget *d) {
103 return (flags_Widget(d->resizer) & pressed_WidgetFlag) != 0; 104 return (flags_Widget(d->resizer) & pressed_WidgetFlag) != 0;
@@ -408,16 +409,19 @@ static const char *tightModeLabels_[max_SidebarMode] = {
408 "\U0001f5b9", 409 "\U0001f5b9",
409}; 410};
410 411
411void init_SidebarWidget(iSidebarWidget *d) { 412void init_SidebarWidget(iSidebarWidget *d, enum iSidebarSide side) {
412 iWidget *w = as_Widget(d); 413 iWidget *w = as_Widget(d);
413 init_Widget(w); 414 init_Widget(w);
414 setId_Widget(w, "sidebar"); 415 setId_Widget(w, side == left_SideBarSide ? "sidebar" : "sidebar2");
416 initCopy_String(&d->cmdPrefix, id_Widget(w));
417 appendChar_String(&d->cmdPrefix, '.');
415 setBackgroundColor_Widget(w, none_ColorId); 418 setBackgroundColor_Widget(w, none_ColorId);
416 setFlags_Widget(w, 419 setFlags_Widget(w,
417 collapse_WidgetFlag | hidden_WidgetFlag | arrangeHorizontal_WidgetFlag | 420 collapse_WidgetFlag | hidden_WidgetFlag | arrangeHorizontal_WidgetFlag |
418 resizeWidthOfChildren_WidgetFlag, 421 resizeWidthOfChildren_WidgetFlag,
419 iTrue); 422 iTrue);
420 iZap(d->modeScroll); 423 iZap(d->modeScroll);
424 d->side = side;
421 d->mode = -1; 425 d->mode = -1;
422 d->width = 60 * gap_UI; 426 d->width = 60 * gap_UI;
423 setFlags_Widget(w, fixedWidth_WidgetFlag, iTrue); 427 setFlags_Widget(w, fixedWidth_WidgetFlag, iTrue);
@@ -428,8 +432,9 @@ void init_SidebarWidget(iSidebarWidget *d) {
428 for (int i = 0; i < max_SidebarMode; i++) { 432 for (int i = 0; i < max_SidebarMode; i++) {
429 d->modeButtons[i] = addChildFlags_Widget( 433 d->modeButtons[i] = addChildFlags_Widget(
430 buttons, 434 buttons,
431 iClob( 435 iClob(new_LabelWidget(
432 new_LabelWidget(tightModeLabels_[i], format_CStr("sidebar.mode arg:%d", i))), 436 tightModeLabels_[i],
437 format_CStr("%s.mode arg:%d", cstr_String(id_Widget(w)), i))),
433 frameless_WidgetFlag); 438 frameless_WidgetFlag);
434 d->maxButtonLabelWidth = 439 d->maxButtonLabelWidth =
435 iMaxi(d->maxButtonLabelWidth, 440 iMaxi(d->maxButtonLabelWidth,
@@ -448,21 +453,22 @@ void init_SidebarWidget(iSidebarWidget *d) {
448 addChildFlags_Widget(content, iClob(d->blank), resizeChildren_WidgetFlag); 453 addChildFlags_Widget(content, iClob(d->blank), resizeChildren_WidgetFlag);
449 addChildFlags_Widget(vdiv, iClob(content), expand_WidgetFlag); 454 addChildFlags_Widget(vdiv, iClob(content), expand_WidgetFlag);
450 setMode_SidebarWidget(d, bookmarks_SidebarMode); 455 setMode_SidebarWidget(d, bookmarks_SidebarMode);
451 d->resizer = addChildFlags_Widget( 456 d->resizer =
452 w, 457 addChildFlags_Widget(w,
453 iClob(new_Widget()), 458 iClob(new_Widget()),
454 hover_WidgetFlag | commandOnClick_WidgetFlag | fixedWidth_WidgetFlag | 459 hover_WidgetFlag | commandOnClick_WidgetFlag | fixedWidth_WidgetFlag |
455 resizeToParentHeight_WidgetFlag | moveToParentRightEdge_WidgetFlag); 460 resizeToParentHeight_WidgetFlag |
456 setId_Widget(d->resizer, "sidebar.grab"); 461 (side == left_SideBarSide ? moveToParentRightEdge_WidgetFlag
462 : moveToParentLeftEdge_WidgetFlag));
463 setId_Widget(d->resizer, side == left_SideBarSide ? "sidebar.grab" : "sidebar2.grab");
457 d->resizer->rect.size.x = gap_UI; 464 d->resizer->rect.size.x = gap_UI;
458 setBackgroundColor_Widget(d->resizer, none_ColorId); 465 setBackgroundColor_Widget(d->resizer, none_ColorId);
459 d->resizeCursor = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_SIZEWE);
460 d->menu = NULL; 466 d->menu = NULL;
461 addAction_Widget(w, SDLK_r, KMOD_PRIMARY | KMOD_SHIFT, "feeds.refresh"); 467 addAction_Widget(w, SDLK_r, KMOD_PRIMARY | KMOD_SHIFT, "feeds.refresh");
462} 468}
463 469
464void deinit_SidebarWidget(iSidebarWidget *d) { 470void deinit_SidebarWidget(iSidebarWidget *d) {
465 SDL_FreeCursor(d->resizeCursor); 471 deinit_String(&d->cmdPrefix);
466} 472}
467 473
468static const iGmIdentity *constHoverIdentity_SidebarWidget_(const iSidebarWidget *d) { 474static const iGmIdentity *constHoverIdentity_SidebarWidget_(const iSidebarWidget *d) {
@@ -558,8 +564,11 @@ static void checkModeButtonLayout_SidebarWidget_(iSidebarWidget *d) {
558} 564}
559 565
560void setWidth_SidebarWidget(iSidebarWidget *d, int width) { 566void setWidth_SidebarWidget(iSidebarWidget *d, int width) {
561 iWidget *w = as_Widget(d); 567 iWidget * w = as_Widget(d);
562 width = iClamp(width, 30 * gap_UI, rootSize_Window(get_Window()).x - 50 * gap_UI); 568 /* Even less space if the other sidebar is visible, too. */
569 const int otherWidth =
570 width_Widget(findWidget_App(d->side == left_SideBarSide ? "sidebar2" : "sidebar"));
571 width = iClamp(width, 30 * gap_UI, rootSize_Window(get_Window()).x - 50 * gap_UI - otherWidth);
563 d->width = width; 572 d->width = width;
564 if (isVisible_Widget(w)) { 573 if (isVisible_Widget(w)) {
565 w->rect.size.x = width; 574 w->rect.size.x = width;
@@ -574,7 +583,8 @@ void setWidth_SidebarWidget(iSidebarWidget *d, int width) {
574 583
575iBool handleBookmarkEditorCommands_SidebarWidget_(iWidget *editor, const char *cmd) { 584iBool handleBookmarkEditorCommands_SidebarWidget_(iWidget *editor, const char *cmd) {
576 if (equal_Command(cmd, "bmed.accept") || equal_Command(cmd, "cancel")) { 585 if (equal_Command(cmd, "bmed.accept") || equal_Command(cmd, "cancel")) {
577 iSidebarWidget *d = findWidget_App("sidebar"); 586 iAssert(startsWith_String(id_Widget(editor), "bmed."));
587 iSidebarWidget *d = findWidget_App(cstr_String(id_Widget(editor)) + 5); /* bmed.sidebar */
578 if (equal_Command(cmd, "bmed.accept")) { 588 if (equal_Command(cmd, "bmed.accept")) {
579 const iString *title = text_InputWidget(findChild_Widget(editor, "bmed.title")); 589 const iString *title = text_InputWidget(findChild_Widget(editor, "bmed.title"));
580 const iString *url = text_InputWidget(findChild_Widget(editor, "bmed.url")); 590 const iString *url = text_InputWidget(findChild_Widget(editor, "bmed.url"));
@@ -594,6 +604,43 @@ iBool handleBookmarkEditorCommands_SidebarWidget_(iWidget *editor, const char *c
594 return iFalse; 604 return iFalse;
595} 605}
596 606
607static iBool handleSidebarCommand_SidebarWidget_(iSidebarWidget *d, const char *cmd) {
608 iWidget *w = as_Widget(d);
609 if (equal_Command(cmd, "width")) {
610 setWidth_SidebarWidget(d, arg_Command(cmd));
611 return iTrue;
612 }
613 else if (equal_Command(cmd, "mode")) {
614 const iBool wasChanged = setMode_SidebarWidget(d, arg_Command(cmd));
615 updateItems_SidebarWidget_(d);
616 if ((argLabel_Command(cmd, "show") && !isVisible_Widget(w)) ||
617 (argLabel_Command(cmd, "toggle") && (!isVisible_Widget(w) || !wasChanged))) {
618 postCommandf_App("%s.toggle", cstr_String(id_Widget(w)));
619 }
620 scrollOffset_ListWidget(d->list, 0);
621 return iTrue;
622 }
623 else if (equal_Command(cmd, "toggle")) {
624 if (arg_Command(cmd) && isVisible_Widget(w)) {
625 return iTrue;
626 }
627 setFlags_Widget(w, hidden_WidgetFlag, isVisible_Widget(w));
628 if (isVisible_Widget(w)) {
629 w->rect.size.x = d->width;
630 invalidate_ListWidget(d->list);
631 }
632 arrange_Widget(w->parent);
633 updateSize_DocumentWidget(document_App());
634 if (isVisible_Widget(w)) {
635 updateItems_SidebarWidget_(d);
636 scrollOffset_ListWidget(d->list, 0);
637 }
638 refresh_Widget(w->parent);
639 return iTrue;
640 }
641 return iFalse;
642}
643
597static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev) { 644static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev) {
598 iWidget *w = as_Widget(d); 645 iWidget *w = as_Widget(d);
599 /* Handle commands. */ 646 /* Handle commands. */
@@ -602,7 +649,27 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev)
602 } 649 }
603 else if (ev->type == SDL_USEREVENT && ev->user.code == command_UserEventCode) { 650 else if (ev->type == SDL_USEREVENT && ev->user.code == command_UserEventCode) {
604 const char *cmd = command_UserEvent(ev); 651 const char *cmd = command_UserEvent(ev);
605 if (isCommand_Widget(w, ev, "mouse.clicked")) { 652 if (equal_Command(cmd, "tabs.changed") || equal_Command(cmd, "document.changed")) {
653 updateItems_SidebarWidget_(d);
654 scrollOffset_ListWidget(d->list, 0);
655 }
656 else if (equal_Command(cmd, "visited.changed") &&
657 (d->mode == history_SidebarMode || d->mode == feeds_SidebarMode)) {
658 updateItems_SidebarWidget_(d);
659 }
660 else if (equal_Command(cmd, "bookmarks.changed") && (d->mode == bookmarks_SidebarMode ||
661 d->mode == feeds_SidebarMode)) {
662 updateItems_SidebarWidget_(d);
663 }
664 else if (equal_Command(cmd, "idents.changed") && d->mode == identities_SidebarMode) {
665 updateItems_SidebarWidget_(d);
666 }
667 else if (startsWith_CStr(cmd, cstr_String(&d->cmdPrefix))) {
668 if (handleSidebarCommand_SidebarWidget_(d, cmd + size_String(&d->cmdPrefix))) {
669 return iTrue;
670 }
671 }
672 else if (isCommand_Widget(w, ev, "mouse.clicked")) {
606 if (argLabel_Command(cmd, "button") == SDL_BUTTON_LEFT) { 673 if (argLabel_Command(cmd, "button") == SDL_BUTTON_LEFT) {
607 if (arg_Command(cmd)) { 674 if (arg_Command(cmd)) {
608 setFlags_Widget(d->resizer, pressed_WidgetFlag, iTrue); 675 setFlags_Widget(d->resizer, pressed_WidgetFlag, iTrue);
@@ -624,7 +691,13 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev)
624 else if (isCommand_Widget(w, ev, "mouse.moved")) { 691 else if (isCommand_Widget(w, ev, "mouse.moved")) {
625 if (isResizing_SidebarWidget_(d)) { 692 if (isResizing_SidebarWidget_(d)) {
626 const iInt2 local = localCoord_Widget(w, coord_Command(cmd)); 693 const iInt2 local = localCoord_Widget(w, coord_Command(cmd));
627 setWidth_SidebarWidget(d, local.x + d->resizer->rect.size.x / 2); 694 const int resMid = d->resizer->rect.size.x / 2;
695 setWidth_SidebarWidget(
696 d,
697 (d->side == left_SideBarSide
698 ? local.x
699 : (rootSize_Window(get_Window()).x - coord_Command(cmd).x)) +
700 resMid);
628 } 701 }
629 return iTrue; 702 return iTrue;
630 } 703 }
@@ -638,54 +711,19 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev)
638 else if (isCommand_Widget(w, ev, "menu.closed")) { 711 else if (isCommand_Widget(w, ev, "menu.closed")) {
639 setFlags_Widget(as_Widget(d->list), disabled_WidgetFlag, iFalse); 712 setFlags_Widget(as_Widget(d->list), disabled_WidgetFlag, iFalse);
640 } 713 }
641 else if (equal_Command(cmd, "sidebar.width")) { 714 else if (isCommand_Widget(w, ev, "bookmark.copy")) {
642 setWidth_SidebarWidget(d, arg_Command(cmd));
643 return iTrue;
644 }
645 else if (equal_Command(cmd, "sidebar.mode")) {
646 const iBool wasChanged = setMode_SidebarWidget(d, arg_Command(cmd));
647 updateItems_SidebarWidget_(d);
648 if ((argLabel_Command(cmd, "show") && !isVisible_Widget(w)) ||
649 (argLabel_Command(cmd, "toggle") && (!isVisible_Widget(w) || !wasChanged))) {
650 postCommand_App("sidebar.toggle");
651 }
652 scrollOffset_ListWidget(d->list, 0);
653 return iTrue;
654 }
655 else if (equal_Command(cmd, "sidebar.toggle")) {
656 if (arg_Command(cmd) && isVisible_Widget(w)) {
657 return iTrue;
658 }
659 setFlags_Widget(w, hidden_WidgetFlag, isVisible_Widget(w));
660 if (isVisible_Widget(w)) {
661 w->rect.size.x = d->width;
662 invalidate_ListWidget(d->list);
663 }
664 arrange_Widget(w->parent);
665 updateSize_DocumentWidget(document_App());
666 if (isVisible_Widget(w)) {
667 updateItems_SidebarWidget_(d);
668 scrollOffset_ListWidget(d->list, 0);
669 }
670 refresh_Widget(w->parent);
671 return iTrue;
672 }
673 else if (equal_Command(cmd, "tabs.changed") || equal_Command(cmd, "document.changed")) {
674 updateItems_SidebarWidget_(d);
675 scrollOffset_ListWidget(d->list, 0);
676 }
677 else if (equal_Command(cmd, "bookmark.copy")) {
678 const iSidebarItem *item = d->contextItem; 715 const iSidebarItem *item = d->contextItem;
679 if (d->mode == bookmarks_SidebarMode && item) { 716 if (d->mode == bookmarks_SidebarMode && item) {
680 SDL_SetClipboardText(cstr_String(&item->url)); 717 SDL_SetClipboardText(cstr_String(&item->url));
681 } 718 }
682 return iTrue; 719 return iTrue;
683 } 720 }
684 else if (equal_Command(cmd, "bookmark.edit")) { 721 else if (isCommand_Widget(w, ev, "bookmark.edit")) {
685 const iSidebarItem *item = d->contextItem; 722 const iSidebarItem *item = d->contextItem;
686 if (d->mode == bookmarks_SidebarMode && item) { 723 if (d->mode == bookmarks_SidebarMode && item) {
687 setFlags_Widget(w, disabled_WidgetFlag, iTrue); 724 setFlags_Widget(w, disabled_WidgetFlag, iTrue);
688 iWidget *dlg = makeBookmarkEditor_Widget(); 725 iWidget *dlg = makeBookmarkEditor_Widget();
726 setId_Widget(dlg, format_CStr("bmed.%s", cstr_String(id_Widget(w))));
689 iBookmark *bm = get_Bookmarks(bookmarks_App(), item->id); 727 iBookmark *bm = get_Bookmarks(bookmarks_App(), item->id);
690 setText_InputWidget(findChild_Widget(dlg, "bmed.title"), &bm->title); 728 setText_InputWidget(findChild_Widget(dlg, "bmed.title"), &bm->title);
691 setText_InputWidget(findChild_Widget(dlg, "bmed.url"), &bm->url); 729 setText_InputWidget(findChild_Widget(dlg, "bmed.url"), &bm->url);
@@ -695,7 +733,7 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev)
695 } 733 }
696 return iTrue; 734 return iTrue;
697 } 735 }
698 else if (equal_Command(cmd, "bookmark.tag")) { 736 else if (isCommand_Widget(w, ev, "bookmark.tag")) {
699 const iSidebarItem *item = d->contextItem; 737 const iSidebarItem *item = d->contextItem;
700 if (d->mode == bookmarks_SidebarMode && item) { 738 if (d->mode == bookmarks_SidebarMode && item) {
701 const char *tag = cstr_String(string_Command(cmd, "tag")); 739 const char *tag = cstr_String(string_Command(cmd, "tag"));
@@ -713,7 +751,7 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev)
713 } 751 }
714 return iTrue; 752 return iTrue;
715 } 753 }
716 else if (equal_Command(cmd, "bookmark.delete")) { 754 else if (isCommand_Widget(w, ev, "bookmark.delete")) {
717 const iSidebarItem *item = d->contextItem; 755 const iSidebarItem *item = d->contextItem;
718 if (d->mode == bookmarks_SidebarMode && item && remove_Bookmarks(bookmarks_App(), item->id)) { 756 if (d->mode == bookmarks_SidebarMode && item && remove_Bookmarks(bookmarks_App(), item->id)) {
719 removeEntries_Feeds(item->id); 757 removeEntries_Feeds(item->id);
@@ -721,10 +759,6 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev)
721 } 759 }
722 return iTrue; 760 return iTrue;
723 } 761 }
724 else if (equal_Command(cmd, "visited.changed") &&
725 (d->mode == history_SidebarMode || d->mode == feeds_SidebarMode)) {
726 updateItems_SidebarWidget_(d);
727 }
728 else if (equal_Command(cmd, "feeds.update.finished") && d->mode == feeds_SidebarMode) { 762 else if (equal_Command(cmd, "feeds.update.finished") && d->mode == feeds_SidebarMode) {
729 updateItems_SidebarWidget_(d); 763 updateItems_SidebarWidget_(d);
730 } 764 }
@@ -742,11 +776,11 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev)
742 else if (startsWith_CStr(cmd, "feed.entry.") && d->mode == feeds_SidebarMode) { 776 else if (startsWith_CStr(cmd, "feed.entry.") && d->mode == feeds_SidebarMode) {
743 const iSidebarItem *item = d->contextItem; 777 const iSidebarItem *item = d->contextItem;
744 if (item) { 778 if (item) {
745 if (equal_Command(cmd, "feed.entry.opentab")) { 779 if (isCommand_Widget(w, ev, "feed.entry.opentab")) {
746 postCommandf_App("open newtab:1 url:%s", cstr_String(&item->url)); 780 postCommandf_App("open newtab:1 url:%s", cstr_String(&item->url));
747 return iTrue; 781 return iTrue;
748 } 782 }
749 if (equal_Command(cmd, "feed.entry.toggleread")) { 783 if (isCommand_Widget(w, ev, "feed.entry.toggleread")) {
750 iVisited *vis = visited_App(); 784 iVisited *vis = visited_App();
751 if (containsUrl_Visited(vis, &item->url)) { 785 if (containsUrl_Visited(vis, &item->url)) {
752 removeUrl_Visited(vis, &item->url); 786 removeUrl_Visited(vis, &item->url);
@@ -757,20 +791,21 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev)
757 postCommand_App("visited.changed"); 791 postCommand_App("visited.changed");
758 return iTrue; 792 return iTrue;
759 } 793 }
760 if (equal_Command(cmd, "feed.entry.bookmark")) { 794 if (isCommand_Widget(w, ev, "feed.entry.bookmark")) {
761 makeBookmarkCreation_Widget(&item->url, &item->label, item->icon); 795 makeBookmarkCreation_Widget(&item->url, &item->label, item->icon);
762 postCommand_App("focus.set id:bmed.title"); 796 postCommand_App("focus.set id:bmed.title");
763 return iTrue; 797 return iTrue;
764 } 798 }
765 iBookmark *feedBookmark = get_Bookmarks(bookmarks_App(), item->id); 799 iBookmark *feedBookmark = get_Bookmarks(bookmarks_App(), item->id);
766 if (feedBookmark) { 800 if (feedBookmark) {
767 if (equal_Command(cmd, "feed.entry.openfeed")) { 801 if (isCommand_Widget(w, ev, "feed.entry.openfeed")) {
768 postCommandf_App("open url:%s", cstr_String(&feedBookmark->url)); 802 postCommandf_App("open url:%s", cstr_String(&feedBookmark->url));
769 return iTrue; 803 return iTrue;
770 } 804 }
771 if (equal_Command(cmd, "feed.entry.edit")) { 805 if (isCommand_Widget(w, ev, "feed.entry.edit")) {
772 setFlags_Widget(w, disabled_WidgetFlag, iTrue); 806 setFlags_Widget(w, disabled_WidgetFlag, iTrue);
773 iWidget *dlg = makeBookmarkEditor_Widget(); 807 iWidget *dlg = makeBookmarkEditor_Widget();
808 setId_Widget(dlg, format_CStr("bmed.%s", cstr_String(id_Widget(w))));
774 setText_InputWidget(findChild_Widget(dlg, "bmed.title"), &feedBookmark->title); 809 setText_InputWidget(findChild_Widget(dlg, "bmed.title"), &feedBookmark->title);
775 setText_InputWidget(findChild_Widget(dlg, "bmed.url"), &feedBookmark->url); 810 setText_InputWidget(findChild_Widget(dlg, "bmed.url"), &feedBookmark->url);
776 setText_InputWidget(findChild_Widget(dlg, "bmed.tags"), &feedBookmark->tags); 811 setText_InputWidget(findChild_Widget(dlg, "bmed.tags"), &feedBookmark->tags);
@@ -778,7 +813,7 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev)
778 setFocus_Widget(findChild_Widget(dlg, "bmed.title")); 813 setFocus_Widget(findChild_Widget(dlg, "bmed.title"));
779 return iTrue; 814 return iTrue;
780 } 815 }
781 if (equal_Command(cmd, "feed.entry.unsubscribe")) { 816 if (isCommand_Widget(w, ev, "feed.entry.unsubscribe")) {
782 if (arg_Command(cmd)) { 817 if (arg_Command(cmd)) {
783 removeTag_Bookmark(feedBookmark, "subscribed"); 818 removeTag_Bookmark(feedBookmark, "subscribed");
784 removeEntries_Feeds(id_Bookmark(feedBookmark)); 819 removeEntries_Feeds(id_Bookmark(feedBookmark));
@@ -791,7 +826,9 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev)
791 cstr_String(&feedBookmark->title)), 826 cstr_String(&feedBookmark->title)),
792 (const char *[]){ "Cancel", 827 (const char *[]){ "Cancel",
793 uiTextCaution_ColorEscape "Unsubscribe" }, 828 uiTextCaution_ColorEscape "Unsubscribe" },
794 (const char *[]){ "cancel", "feed.entry.unsubscribe arg:1" }, 829 (const char *[]){
830 "cancel",
831 format_CStr("!feed.entry.unsubscribe arg:1 ptr:%p", d) },
795 2); 832 2);
796 } 833 }
797 return iTrue; 834 return iTrue;
@@ -799,13 +836,6 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev)
799 } 836 }
800 } 837 }
801 } 838 }
802 else if (equal_Command(cmd, "bookmarks.changed") && (d->mode == bookmarks_SidebarMode ||
803 d->mode == feeds_SidebarMode)) {
804 updateItems_SidebarWidget_(d);
805 }
806 else if (equal_Command(cmd, "idents.changed") && d->mode == identities_SidebarMode) {
807 updateItems_SidebarWidget_(d);
808 }
809 else if (isCommand_Widget(w, ev, "ident.use")) { 839 else if (isCommand_Widget(w, ev, "ident.use")) {
810 iGmIdentity * ident = menuIdentity_SidebarWidget_(d); 840 iGmIdentity * ident = menuIdentity_SidebarWidget_(d);
811 const iString *tabUrl = url_DocumentWidget(document_App()); 841 const iString *tabUrl = url_DocumentWidget(document_App());
@@ -843,7 +873,7 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev)
843 } 873 }
844 return iTrue; 874 return iTrue;
845 } 875 }
846 else if (equal_Command(cmd, "ident.setnotes")) { 876 else if (isCommand_Widget(w, ev, "ident.setnotes")) {
847 iGmIdentity *ident = pointerLabel_Command(cmd, "ident"); 877 iGmIdentity *ident = pointerLabel_Command(cmd, "ident");
848 if (ident) { 878 if (ident) {
849 setCStr_String(&ident->notes, suffixPtr_Command(cmd, "value")); 879 setCStr_String(&ident->notes, suffixPtr_Command(cmd, "value"));
@@ -864,27 +894,27 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev)
864 } 894 }
865 return iTrue; 895 return iTrue;
866 } 896 }
867 else if (equal_Command(cmd, "ident.delete")) { 897 else if (isCommand_Widget(w, ev, "ident.delete")) {
868 iSidebarItem *item = d->contextItem; 898 iSidebarItem *item = d->contextItem;
869 if (argLabel_Command(cmd, "confirm")) { 899 if (argLabel_Command(cmd, "confirm")) {
870 makeQuestion_Widget(uiTextCaution_ColorEscape "DELETE IDENTITY", 900 makeQuestion_Widget(
871 format_CStr("Do you really want to delete the identity\n" 901 uiTextCaution_ColorEscape "DELETE IDENTITY",
872 uiTextAction_ColorEscape "%s\n" 902 format_CStr(
873 uiText_ColorEscape 903 "Do you really want to delete the identity\n" uiTextAction_ColorEscape
874 "including its certificate and private key files?", 904 "%s\n" uiText_ColorEscape
875 cstr_String(&item->label)), 905 "including its certificate and private key files?",
876 (const char *[]){ "Cancel", 906 cstr_String(&item->label)),
877 uiTextCaution_ColorEscape 907 (const char *[]){ "Cancel",
878 "Delete Identity and Files" }, 908 uiTextCaution_ColorEscape "Delete Identity and Files" },
879 (const char *[]){ "cancel", "ident.delete confirm:0" }, 909 (const char *[]){ "cancel", format_CStr("!ident.delete confirm:0 ptr:%p", d) },
880 2); 910 2);
881 return iTrue; 911 return iTrue;
882 } 912 }
883 deleteIdentity_GmCerts(certs_App(), hoverIdentity_SidebarWidget_(d)); 913 deleteIdentity_GmCerts(certs_App(), hoverIdentity_SidebarWidget_(d));
884 updateItems_SidebarWidget_(d); 914 postCommand_App("idents.changed");
885 return iTrue; 915 return iTrue;
886 } 916 }
887 else if (equal_Command(cmd, "history.delete")) { 917 else if (isCommand_Widget(w, ev, "history.delete")) {
888 if (d->contextItem && !isEmpty_String(&d->contextItem->url)) { 918 if (d->contextItem && !isEmpty_String(&d->contextItem->url)) {
889 removeUrl_Visited(visited_App(), &d->contextItem->url); 919 removeUrl_Visited(visited_App(), &d->contextItem->url);
890 updateItems_SidebarWidget_(d); 920 updateItems_SidebarWidget_(d);
@@ -892,14 +922,14 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev)
892 } 922 }
893 return iTrue; 923 return iTrue;
894 } 924 }
895 else if (equal_Command(cmd, "history.copy")) { 925 else if (isCommand_Widget(w, ev, "history.copy")) {
896 const iSidebarItem *item = d->contextItem; 926 const iSidebarItem *item = d->contextItem;
897 if (item && !isEmpty_String(&item->url)) { 927 if (item && !isEmpty_String(&item->url)) {
898 SDL_SetClipboardText(cstr_String(&item->url)); 928 SDL_SetClipboardText(cstr_String(&item->url));
899 } 929 }
900 return iTrue; 930 return iTrue;
901 } 931 }
902 else if (equal_Command(cmd, "history.addbookmark")) { 932 else if (isCommand_Widget(w, ev, "history.addbookmark")) {
903 const iSidebarItem *item = d->contextItem; 933 const iSidebarItem *item = d->contextItem;
904 if (!isEmpty_String(&item->url)) { 934 if (!isEmpty_String(&item->url)) {
905 makeBookmarkCreation_Widget( 935 makeBookmarkCreation_Widget(
diff --git a/src/ui/sidebarwidget.h b/src/ui/sidebarwidget.h
index 0d4ed9c8..fa74e049 100644
--- a/src/ui/sidebarwidget.h
+++ b/src/ui/sidebarwidget.h
@@ -33,8 +33,13 @@ enum iSidebarMode {
33 max_SidebarMode 33 max_SidebarMode
34}; 34};
35 35
36enum iSidebarSide {
37 left_SideBarSide,
38 right_SideBarSide,
39};
40
36iDeclareWidgetClass(SidebarWidget) 41iDeclareWidgetClass(SidebarWidget)
37iDeclareObjectConstruction(SidebarWidget) 42iDeclareObjectConstructionArgs(SidebarWidget, enum iSidebarSide side)
38 43
39iBool setMode_SidebarWidget (iSidebarWidget *, enum iSidebarMode mode); 44iBool setMode_SidebarWidget (iSidebarWidget *, enum iSidebarMode mode);
40 45
diff --git a/src/ui/text.c b/src/ui/text.c
index 686927b1..ae4b1714 100644
--- a/src/ui/text.c
+++ b/src/ui/text.c
@@ -85,7 +85,7 @@ iDefineTypeConstructionArgs(Glyph, (iChar ch), ch)
85struct Impl_Font { 85struct Impl_Font {
86 iBlock * data; 86 iBlock * data;
87 stbtt_fontinfo font; 87 stbtt_fontinfo font;
88 float scale; 88 float xScale, yScale;
89 int vertOffset; /* offset due to scaling */ 89 int vertOffset; /* offset due to scaling */
90 int height; 90 int height;
91 int baseline; 91 int baseline;
@@ -100,21 +100,33 @@ struct Impl_Font {
100 100
101static iFont *font_Text_(enum iFontId id); 101static iFont *font_Text_(enum iFontId id);
102 102
103static void init_Font(iFont *d, const iBlock *data, int height, float scale, enum iFontId symbolsFont) { 103static void init_Font(iFont *d, const iBlock *data, int height, float scale,
104 enum iFontId symbolsFont, iBool isMonospaced) {
104 init_Hash(&d->glyphs); 105 init_Hash(&d->glyphs);
105 d->data = NULL; 106 d->data = NULL;
107 d->isMonospaced = isMonospaced;
106 d->height = height; 108 d->height = height;
107 iZap(d->font); 109 iZap(d->font);
108 stbtt_InitFont(&d->font, constData_Block(data), 0); 110 stbtt_InitFont(&d->font, constData_Block(data), 0);
109 d->scale = stbtt_ScaleForPixelHeight(&d->font, height) * scale; 111 d->xScale = d->yScale = stbtt_ScaleForPixelHeight(&d->font, height) * scale;
112 if (d->isMonospaced) {
113 /* It is important that monospaced fonts align 1:1 with the pixel grid so that
114 box-drawing characters don't have partially occupied edge pixels, leading to seams
115 between adjacent glyphs. */
116 int adv;
117 stbtt_GetCodepointHMetrics(&d->font, 'M', &adv, NULL);
118 const float advance = (float) adv * d->xScale;
119 if (advance > 4) { /* not too tiny */
120 d->xScale *= floorf(advance) / advance;
121 }
122 }
110 d->vertOffset = height * (1.0f - scale) / 2; 123 d->vertOffset = height * (1.0f - scale) / 2;
111 int ascent; 124 int ascent;
112 stbtt_GetFontVMetrics(&d->font, &ascent, NULL, NULL); 125 stbtt_GetFontVMetrics(&d->font, &ascent, NULL, NULL);
113 d->baseline = (int) ascent * d->scale; 126 d->baseline = ceil(ascent * d->yScale);
114 d->symbolsFont = symbolsFont; 127 d->symbolsFont = symbolsFont;
115 d->japaneseFont = regularJapanese_FontId; 128 d->japaneseFont = regularJapanese_FontId;
116 d->koreanFont = regularKorean_FontId; 129 d->koreanFont = regularKorean_FontId;
117 d->isMonospaced = iFalse;
118 memset(d->indexTable, 0xff, sizeof(d->indexTable)); 130 memset(d->indexTable, 0xff, sizeof(d->indexTable));
119} 131}
120 132
@@ -271,11 +283,12 @@ static void initFonts_Text_(iText *d) {
271 }; 283 };
272 iForIndices(i, fontData) { 284 iForIndices(i, fontData) {
273 iFont *font = &d->fonts[i]; 285 iFont *font = &d->fonts[i];
274 init_Font( 286 init_Font(font,
275 font, fontData[i].ttf, fontData[i].size, fontData[i].scaling, fontData[i].symbolsFont); 287 fontData[i].ttf,
276 if (fontData[i].ttf == &fontFiraMonoRegular_Embedded) { 288 fontData[i].size,
277 font->isMonospaced = iTrue; 289 fontData[i].scaling,
278 } 290 fontData[i].symbolsFont,
291 fontData[i].ttf == &fontFiraMonoRegular_Embedded);
279 if (i == default_FontId || i == defaultMedium_FontId) { 292 if (i == default_FontId || i == defaultMedium_FontId) {
280 font->manualKernOnly = iTrue; 293 font->manualKernOnly = iTrue;
281 } 294 }
@@ -423,7 +436,7 @@ static void freeBmp_(void *ptr) {
423static SDL_Surface *rasterizeGlyph_Font_(const iFont *d, uint32_t glyphIndex, float xShift) { 436static SDL_Surface *rasterizeGlyph_Font_(const iFont *d, uint32_t glyphIndex, float xShift) {
424 int w, h; 437 int w, h;
425 uint8_t *bmp = stbtt_GetGlyphBitmapSubpixel( 438 uint8_t *bmp = stbtt_GetGlyphBitmapSubpixel(
426 &d->font, d->scale, d->scale, xShift, 0.0f, glyphIndex, &w, &h, 0, 0); 439 &d->font, d->xScale, d->yScale, xShift, 0.0f, glyphIndex, &w, &h, 0, 0);
427 collect_Garbage(bmp, freeBmp_); /* `bmp` must be freed afterwards. */ 440 collect_Garbage(bmp, freeBmp_); /* `bmp` must be freed afterwards. */
428 SDL_Surface *surface8 = 441 SDL_Surface *surface8 =
429 SDL_CreateRGBSurfaceWithFormatFrom(bmp, w, h, 8, w, SDL_PIXELFORMAT_INDEX8); 442 SDL_CreateRGBSurfaceWithFormatFrom(bmp, w, h, 8, w, SDL_PIXELFORMAT_INDEX8);
@@ -478,12 +491,12 @@ static void cache_Font_(iFont *d, iGlyph *glyph, int hoff) {
478 int adv; 491 int adv;
479 const uint32_t gIndex = glyph->glyphIndex; 492 const uint32_t gIndex = glyph->glyphIndex;
480 stbtt_GetGlyphHMetrics(&d->font, gIndex, &adv, NULL); 493 stbtt_GetGlyphHMetrics(&d->font, gIndex, &adv, NULL);
481 glyph->advance = d->scale * adv; 494 glyph->advance = d->xScale * adv;
482 } 495 }
483 stbtt_GetGlyphBitmapBoxSubpixel(&d->font, 496 stbtt_GetGlyphBitmapBoxSubpixel(&d->font,
484 glyph->glyphIndex, 497 glyph->glyphIndex,
485 d->scale, 498 d->xScale,
486 d->scale, 499 d->yScale,
487 hoff * 0.5f, 500 hoff * 0.5f,
488 0.0f, 501 0.0f,
489 &glyph->d[hoff].x, 502 &glyph->d[hoff].x,
@@ -652,6 +665,17 @@ static iRect run_Font_(iFont *d, enum iRunMode mode, iRangecc text, size_t maxLe
652 } 665 }
653 } 666 }
654 iChar ch = nextChar_(&chPos, text.end); 667 iChar ch = nextChar_(&chPos, text.end);
668 if (ch == 0x200d) { /* zero-width joiner */
669 /* We don't have the composited Emojis. */
670 if (isEmoji_Char(prevCh)) {
671 /* skip */
672 ch = nextChar_(&chPos, text.end);
673 ch = nextChar_(&chPos, text.end);
674 }
675 else {
676 printf("it's %x\n", prevCh);
677 }
678 }
655 if (isVariationSelector_Char(ch)) { 679 if (isVariationSelector_Char(ch)) {
656 /* TODO: VS15: Should peek ahead for this and prefer the Emoji font. */ 680 /* TODO: VS15: Should peek ahead for this and prefer the Emoji font. */
657 ch = nextChar_(&chPos, text.end); /* just ignore */ 681 ch = nextChar_(&chPos, text.end); /* just ignore */
@@ -699,6 +723,9 @@ static iRect run_Font_(iFont *d, enum iRunMode mode, iRangecc text, size_t maxLe
699 prevCh = 0; 723 prevCh = 0;
700 continue; 724 continue;
701 } 725 }
726 if (isDefaultIgnorable_Char(ch) || isFitzpatrickType_Char(ch)) {
727 continue;
728 }
702 } 729 }
703 const iGlyph *glyph = glyph_Font_(d, ch); 730 const iGlyph *glyph = glyph_Font_(d, ch);
704 int x1 = xpos; 731 int x1 = xpos;
@@ -714,6 +741,7 @@ static iRect run_Font_(iFont *d, enum iRunMode mode, iRangecc text, size_t maxLe
714 } 741 }
715 break; 742 break;
716 } 743 }
744 const int yLineMax = pos.y + d->height;
717 SDL_Rect dst = { x1 + glyph->d[hoff].x, 745 SDL_Rect dst = { x1 + glyph->d[hoff].x,
718 pos.y + glyph->font->baseline + glyph->d[hoff].y, 746 pos.y + glyph->font->baseline + glyph->d[hoff].y,
719 glyph->rect[hoff].size.x, 747 glyph->rect[hoff].size.x,
@@ -739,7 +767,16 @@ static iRect run_Font_(iFont *d, enum iRunMode mode, iRangecc text, size_t maxLe
739 /* Glyphs from a different font may need recentering to look better. */ 767 /* Glyphs from a different font may need recentering to look better. */
740 dst.x -= (dst.w - advance) / 2; 768 dst.x -= (dst.w - advance) / 2;
741 } 769 }
742 SDL_RenderCopy(text_.render, text_.cache, (const SDL_Rect *) &glyph->rect[hoff], &dst); 770 SDL_Rect src;
771 memcpy(&src, &glyph->rect[hoff], sizeof(SDL_Rect));
772 /* Clip the glyphs to the font's height. This is useful when the font's line spacing
773 has been reduced or when the glyph is from a different font. */
774 if (dst.y + dst.h > yLineMax) {
775 const int over = dst.y + dst.h - yLineMax;
776 src.h -= over;
777 dst.h -= over;
778 }
779 SDL_RenderCopy(text_.render, text_.cache, &src, &dst);
743 } 780 }
744 /* Symbols and emojis are NOT monospaced, so must conform when the primary font 781 /* Symbols and emojis are NOT monospaced, so must conform when the primary font
745 is monospaced. Except with Japanese script, that's larger than the normal monospace. */ 782 is monospaced. Except with Japanese script, that's larger than the normal monospace. */
@@ -755,7 +792,7 @@ static iRect run_Font_(iFont *d, enum iRunMode mode, iRangecc text, size_t maxLe
755 const char *peek = chPos; 792 const char *peek = chPos;
756 const iChar next = nextChar_(&peek, text.end); 793 const iChar next = nextChar_(&peek, text.end);
757 if (enableKerning_Text && !d->manualKernOnly && next) { 794 if (enableKerning_Text && !d->manualKernOnly && next) {
758 xpos += d->scale * stbtt_GetGlyphKernAdvance(&d->font, glyph->glyphIndex, next); 795 xpos += d->xScale * stbtt_GetGlyphKernAdvance(&d->font, glyph->glyphIndex, next);
759 } 796 }
760 } 797 }
761#endif 798#endif
@@ -977,7 +1014,7 @@ iString *renderBlockChars_Text(const iBlock *fontData, int height, enum iTextBlo
977 size_t strRemain = length_String(text); 1014 size_t strRemain = length_String(text);
978 iConstForEach(String, i, text) { 1015 iConstForEach(String, i, text) {
979 if (!strRemain) break; 1016 if (!strRemain) break;
980 if (i.value == variationSelectorEmoji_Char) { 1017 if (isVariationSelector_Char(i.value) || isDefaultIgnorable_Char(i.value)) {
981 strRemain--; 1018 strRemain--;
982 continue; 1019 continue;
983 } 1020 }
diff --git a/src/ui/text.h b/src/ui/text.h
index 5bae8e2a..a0b2dc1a 100644
--- a/src/ui/text.h
+++ b/src/ui/text.h
@@ -114,11 +114,21 @@ enum iFontId {
114iLocalDef iBool isJapanese_FontId(enum iFontId id) { 114iLocalDef iBool isJapanese_FontId(enum iFontId id) {
115 return id >= defaultJapanese_FontId && id <= hugeJapanese_FontId; 115 return id >= defaultJapanese_FontId && id <= hugeJapanese_FontId;
116} 116}
117iLocalDef iBool isVariationSelector_Char(iChar ch) { 117iLocalDef iBool isVariationSelector_Char(iChar c) {
118 return ch >= 0xfe00 && ch <= 0xfe0f; 118 return (c >= 0xfe00 && c <= 0xfe0f) || (c >= 0xe0100 && c <= 0xe0121);
119}
120iLocalDef iBool isFitzpatrickType_Char(iChar c) {
121 return c >= 0x1f3fb && c <= 0x1f3ff;
122}
123iLocalDef iBool isDefaultIgnorable_Char(iChar c) {
124 return c == 0x115f || (c >= 0x200b && c <= 0x200e) || c == 0x2060 || c == 0x2061 ||
125 c == 0xfeff;
126}
127iLocalDef iBool isEmoji_Char(iChar c) {
128 return (c >= 0x1f300 && c < 0x1f700) || (c >= 0x1f900 && c <= 0x1f9ff);
119} 129}
120 130
121#define variationSelectorEmoji_Char ((iChar) 0xfe0f) 131#define emojiVariationSelector_Char ((iChar) 0xfe0f)
122 132
123enum iTextFont { 133enum iTextFont {
124 nunito_TextFont, 134 nunito_TextFont,
diff --git a/src/ui/util.c b/src/ui/util.c
index d9997004..1ad3f30e 100644
--- a/src/ui/util.c
+++ b/src/ui/util.c
@@ -456,14 +456,14 @@ void openMenu_Widget(iWidget *d, iInt2 coord) {
456 if (leftExcess > 0) { 456 if (leftExcess > 0) {
457 d->rect.pos.x += leftExcess; 457 d->rect.pos.x += leftExcess;
458 } 458 }
459 refresh_App(); 459 postRefresh_App();
460 postCommand_Widget(d, "menu.opened"); 460 postCommand_Widget(d, "menu.opened");
461} 461}
462 462
463void closeMenu_Widget(iWidget *d) { 463void closeMenu_Widget(iWidget *d) {
464 setFlags_Widget(d, hidden_WidgetFlag, iTrue); 464 setFlags_Widget(d, hidden_WidgetFlag, iTrue);
465 setFlags_Widget(findChild_Widget(d, "menu.cancel"), disabled_WidgetFlag, iTrue); 465 setFlags_Widget(findChild_Widget(d, "menu.cancel"), disabled_WidgetFlag, iTrue);
466 refresh_App(); 466 postRefresh_App();
467 postCommand_Widget(d, "menu.closed"); 467 postCommand_Widget(d, "menu.closed");
468} 468}
469 469
@@ -1031,8 +1031,8 @@ iWidget *makePreferences_Widget(void) {
1031 appendTwoColumnPage_(tabs, "General", '1', &headings, &values); 1031 appendTwoColumnPage_(tabs, "General", '1', &headings, &values);
1032 addChild_Widget(headings, iClob(makeHeading_Widget("Downloads folder:"))); 1032 addChild_Widget(headings, iClob(makeHeading_Widget("Downloads folder:")));
1033 setId_Widget(addChild_Widget(values, iClob(new_InputWidget(0))), "prefs.downloads"); 1033 setId_Widget(addChild_Widget(values, iClob(new_InputWidget(0))), "prefs.downloads");
1034 addChild_Widget(headings, iClob(makeHeading_Widget("Outline on scrollbar:"))); 1034 /*addChild_Widget(headings, iClob(makeHeading_Widget("Outline on scrollbar:")));
1035 addChild_Widget(values, iClob(makeToggle_Widget("prefs.hoveroutline"))); 1035 addChild_Widget(values, iClob(makeToggle_Widget("prefs.hoveroutline")));*/
1036 addChild_Widget(headings, iClob(makeHeading_Widget("Smooth scrolling:"))); 1036 addChild_Widget(headings, iClob(makeHeading_Widget("Smooth scrolling:")));
1037 addChild_Widget(values, iClob(makeToggle_Widget("prefs.smoothscroll"))); 1037 addChild_Widget(values, iClob(makeToggle_Widget("prefs.smoothscroll")));
1038 addChild_Widget(headings, iClob(makeHeading_Widget("Load image on scroll:"))); 1038 addChild_Widget(headings, iClob(makeHeading_Widget("Load image on scroll:")));
diff --git a/src/ui/widget.c b/src/ui/widget.c
index 4d50da38..f3e73ee7 100644
--- a/src/ui/widget.c
+++ b/src/ui/widget.c
@@ -263,10 +263,13 @@ void arrange_Widget(iWidget *d) {
263 setFlags_Widget(d, wasCollapsed_WidgetFlag, iTrue); 263 setFlags_Widget(d, wasCollapsed_WidgetFlag, iTrue);
264 return; 264 return;
265 } 265 }
266 if (d->flags & moveToParentRightEdge_WidgetFlag) { 266 if (d->flags & moveToParentLeftEdge_WidgetFlag) {
267 d->rect.pos.x = d->padding[0];
268 }
269 else if (d->flags & moveToParentRightEdge_WidgetFlag) {
267 d->rect.pos.x = width_Rect(innerRect_Widget_(d->parent)) - width_Rect(d->rect); 270 d->rect.pos.x = width_Rect(innerRect_Widget_(d->parent)) - width_Rect(d->rect);
268 } 271 }
269 if (d->flags & centerHorizontal_WidgetFlag) { 272 else if (d->flags & centerHorizontal_WidgetFlag) {
270 centerHorizontal_Widget_(d); 273 centerHorizontal_Widget_(d);
271 } 274 }
272 if (d->flags & resizeToParentWidth_WidgetFlag) { 275 if (d->flags & resizeToParentWidth_WidgetFlag) {
@@ -388,7 +391,8 @@ void arrange_Widget(iWidget *d) {
388 continue; 391 continue;
389 } 392 }
390 if (d->flags & (arrangeHorizontal_WidgetFlag | arrangeVertical_WidgetFlag)) { 393 if (d->flags & (arrangeHorizontal_WidgetFlag | arrangeVertical_WidgetFlag)) {
391 if (child->flags & moveToParentRightEdge_WidgetFlag) { 394 if (child->flags &
395 (moveToParentLeftEdge_WidgetFlag | moveToParentRightEdge_WidgetFlag)) {
392 continue; /* Not part of the sequential arrangement .*/ 396 continue; /* Not part of the sequential arrangement .*/
393 } 397 }
394 child->rect.pos = pos; 398 child->rect.pos = pos;
@@ -422,7 +426,8 @@ void arrange_Widget(iWidget *d) {
422 iForEach(ObjectList, j, d->children) { 426 iForEach(ObjectList, j, d->children) {
423 iWidget *child = as_Widget(j.object); 427 iWidget *child = as_Widget(j.object);
424 if (child->flags & 428 if (child->flags &
425 (resizeToParentWidth_WidgetFlag | moveToParentRightEdge_WidgetFlag)) { 429 (resizeToParentWidth_WidgetFlag | moveToParentLeftEdge_WidgetFlag |
430 moveToParentRightEdge_WidgetFlag)) {
426 arrange_Widget(child); 431 arrange_Widget(child);
427 } 432 }
428 } 433 }
diff --git a/src/ui/widget.h b/src/ui/widget.h
index 278ae081..f06a6607 100644
--- a/src/ui/widget.h
+++ b/src/ui/widget.h
@@ -85,10 +85,11 @@ enum iWidgetFlag {
85/* 64-bit extended flags */ 85/* 64-bit extended flags */
86#define wasCollapsed_WidgetFlag iBit64(32) 86#define wasCollapsed_WidgetFlag iBit64(32)
87#define centerHorizontal_WidgetFlag iBit64(33) 87#define centerHorizontal_WidgetFlag iBit64(33)
88#define moveToParentRightEdge_WidgetFlag iBit64(34) 88#define moveToParentLeftEdge_WidgetFlag iBit64(34)
89#define wrapText_WidgetFlag iBit64(35) 89#define moveToParentRightEdge_WidgetFlag iBit64(35)
90#define borderTop_WidgetFlag iBit64(36) 90#define wrapText_WidgetFlag iBit64(36)
91#define overflowScrollable_WidgetFlag iBit64(37) 91#define borderTop_WidgetFlag iBit64(37)
92#define overflowScrollable_WidgetFlag iBit64(38)
92 93
93enum iWidgetAddPos { 94enum iWidgetAddPos {
94 back_WidgetAddPos, 95 back_WidgetAddPos,
diff --git a/src/ui/window.c b/src/ui/window.c
index 199e35a7..1bf9065e 100644
--- a/src/ui/window.c
+++ b/src/ui/window.c
@@ -109,7 +109,8 @@ static const iMenuItem navMenuItems_[] = {
109 { "---", 0, 0, NULL }, 109 { "---", 0, 0, NULL },
110 { "Show Feed Entries", 0, 0, "!open url:about:feeds" }, 110 { "Show Feed Entries", 0, 0, "!open url:about:feeds" },
111 { "---", 0, 0, NULL }, 111 { "---", 0, 0, NULL },
112 { "Toggle Sidebar", SDLK_l, KMOD_PRIMARY | KMOD_SHIFT, "sidebar.toggle" }, 112 { "Toggle Left Sidebar", SDLK_l, KMOD_PRIMARY | KMOD_SHIFT, "sidebar.toggle" },
113 { "Toggle Right Sidebar", SDLK_p, KMOD_PRIMARY | KMOD_SHIFT, "sidebar2.toggle" },
113 { "Zoom In", SDLK_EQUALS, KMOD_PRIMARY, "zoom.delta arg:10" }, 114 { "Zoom In", SDLK_EQUALS, KMOD_PRIMARY, "zoom.delta arg:10" },
114 { "Zoom Out", SDLK_MINUS, KMOD_PRIMARY, "zoom.delta arg:-10" }, 115 { "Zoom Out", SDLK_MINUS, KMOD_PRIMARY, "zoom.delta arg:-10" },
115 { "Reset Zoom", SDLK_0, KMOD_PRIMARY, "zoom.set arg:100" }, 116 { "Reset Zoom", SDLK_0, KMOD_PRIMARY, "zoom.set arg:100" },
@@ -144,7 +145,8 @@ static const iMenuItem viewMenuItems_[] = {
144 { "Show History", '3', KMOD_PRIMARY, "sidebar.mode arg:2 toggle:1" }, 145 { "Show History", '3', KMOD_PRIMARY, "sidebar.mode arg:2 toggle:1" },
145 { "Show Identities", '4', KMOD_PRIMARY, "sidebar.mode arg:3 toggle:1" }, 146 { "Show Identities", '4', KMOD_PRIMARY, "sidebar.mode arg:3 toggle:1" },
146 { "Show Page Outline", '5', KMOD_PRIMARY, "sidebar.mode arg:4 toggle:1" }, 147 { "Show Page Outline", '5', KMOD_PRIMARY, "sidebar.mode arg:4 toggle:1" },
147 { "Toggle Sidebar", SDLK_l, KMOD_PRIMARY | KMOD_SHIFT, "sidebar.toggle" }, 148 { "Toggle Left Sidebar", SDLK_l, KMOD_PRIMARY | KMOD_SHIFT, "sidebar.toggle" },
149 { "Toggle Right Sidebar", SDLK_p, KMOD_PRIMARY | KMOD_SHIFT, "sidebar2.toggle" },
148 { "---", 0, 0, NULL }, 150 { "---", 0, 0, NULL },
149 { "Go Back", SDLK_LEFTBRACKET, KMOD_PRIMARY, "navigate.back" }, 151 { "Go Back", SDLK_LEFTBRACKET, KMOD_PRIMARY, "navigate.back" },
150 { "Go Forward", SDLK_RIGHTBRACKET, KMOD_PRIMARY, "navigate.forward" }, 152 { "Go Forward", SDLK_RIGHTBRACKET, KMOD_PRIMARY, "navigate.forward" },
@@ -343,7 +345,7 @@ static iBool handleSearchBarCommands_(iWidget *searchBar, const char *cmd) {
343 if (!isVisible_Widget(searchBar)) { 345 if (!isVisible_Widget(searchBar)) {
344 setFlags_Widget(searchBar, hidden_WidgetFlag | disabled_WidgetFlag, iFalse); 346 setFlags_Widget(searchBar, hidden_WidgetFlag | disabled_WidgetFlag, iFalse);
345 arrange_Widget(get_Window()->root); 347 arrange_Widget(get_Window()->root);
346 refresh_App(); 348 postRefresh_App();
347 } 349 }
348 } 350 }
349 } 351 }
@@ -459,10 +461,12 @@ static void setupUserInterface_Window(iWindow *d) {
459 addChild_Widget(buttons, iClob(newIcon_LabelWidget("\u2795", 0, 0, "tabs.new"))), 461 addChild_Widget(buttons, iClob(newIcon_LabelWidget("\u2795", 0, 0, "tabs.new"))),
460 "newtab"); 462 "newtab");
461 } 463 }
462 /* Side bar. */ { 464 /* Side bars. */ {
463 iWidget *content = findChild_Widget(d->root, "tabs.content"); 465 iWidget *content = findChild_Widget(d->root, "tabs.content");
464 iSidebarWidget *sidebar = new_SidebarWidget(); 466 iSidebarWidget *sidebar1 = new_SidebarWidget(left_SideBarSide);
465 addChildPos_Widget(content, iClob(sidebar), front_WidgetAddPos); 467 addChildPos_Widget(content, iClob(sidebar1), front_WidgetAddPos);
468 iSidebarWidget *sidebar2 = new_SidebarWidget(right_SideBarSide);
469 addChildPos_Widget(content, iClob(sidebar2), back_WidgetAddPos);
466 } 470 }
467 /* Lookup results. */ { 471 /* Lookup results. */ {
468 iLookupWidget *lookup = new_LookupWidget(); 472 iLookupWidget *lookup = new_LookupWidget();
@@ -507,6 +511,11 @@ static void setupUserInterface_Window(iWindow *d) {
507 addAction_Widget(d->root, '3', KMOD_PRIMARY, "sidebar.mode arg:2 toggle:1"); 511 addAction_Widget(d->root, '3', KMOD_PRIMARY, "sidebar.mode arg:2 toggle:1");
508 addAction_Widget(d->root, '4', KMOD_PRIMARY, "sidebar.mode arg:3 toggle:1"); 512 addAction_Widget(d->root, '4', KMOD_PRIMARY, "sidebar.mode arg:3 toggle:1");
509 addAction_Widget(d->root, '5', KMOD_PRIMARY, "sidebar.mode arg:4 toggle:1"); 513 addAction_Widget(d->root, '5', KMOD_PRIMARY, "sidebar.mode arg:4 toggle:1");
514 addAction_Widget(d->root, '1', rightSidebar_KeyModifier, "sidebar2.mode arg:0 toggle:1");
515 addAction_Widget(d->root, '2', rightSidebar_KeyModifier, "sidebar2.mode arg:1 toggle:1");
516 addAction_Widget(d->root, '3', rightSidebar_KeyModifier, "sidebar2.mode arg:2 toggle:1");
517 addAction_Widget(d->root, '4', rightSidebar_KeyModifier, "sidebar2.mode arg:3 toggle:1");
518 addAction_Widget(d->root, '5', rightSidebar_KeyModifier, "sidebar2.mode arg:4 toggle:1");
510 } 519 }
511} 520}
512 521