summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--.gitmodules3
-rw-r--r--CMakeLists.txt37
-rw-r--r--README.md28
-rw-r--r--lagrange_about.pngbin493681 -> 492436 bytes
m---------lib/the_Foundation0
-rw-r--r--res/about/help.gmi173
-rw-r--r--res/about/license.gmi105
-rw-r--r--res/about/version.gmi15
-rw-r--r--res/urlopen.bat2
-rw-r--r--sdl2-macos-mouse-scrolling-patch.diff28
-rw-r--r--src/app.c70
-rw-r--r--src/app.h4
-rw-r--r--src/bookmarks.c7
-rw-r--r--src/bookmarks.h2
-rw-r--r--src/gmdocument.c84
-rw-r--r--src/gmdocument.h10
-rw-r--r--src/gmrequest.c4
-rw-r--r--src/gmutil.c6
-rw-r--r--src/macos.m145
-rw-r--r--src/ui/color.h3
-rw-r--r--src/ui/documentwidget.c268
-rw-r--r--src/ui/inputwidget.c2
-rw-r--r--src/ui/labelwidget.c49
-rw-r--r--src/ui/lookupwidget.c6
-rw-r--r--src/ui/util.c80
-rw-r--r--src/ui/util.h1
-rw-r--r--src/ui/visbuf.c1
-rw-r--r--src/ui/widget.c9
-rw-r--r--src/ui/widget.h1
-rw-r--r--src/ui/window.c69
-rw-r--r--src/ui/window.h1
32 files changed, 853 insertions, 362 deletions
diff --git a/.gitignore b/.gitignore
index a7738f1d..05eba91e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,5 +4,5 @@
4build-* 4build-*
5/.vsbuild 5/.vsbuild
6/.vscode 6/.vscode
7/lib 7
8 8
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 00000000..f869c7ae
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
1[submodule "lib/the_Foundation"]
2 path = lib/the_Foundation
3 url = https://git.skyjake.fi/skyjake/the_Foundation
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 8065cd25..b18ead27 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1,20 +1,40 @@
1# Lagrange - A Beautiful Gemini Client
2# Copyright: 2020 Jaakko Keränen <jaakko.keranen@iki.fi>
3#
4# Notes:
5# - Required dependencies: SDL 2, OpenSSL 1.1.1, libpcre,
6# GNU libunistring, zlib.
7# - the_Foundation is built as a static library from 'lib/the_Foundation',
8# if it exists in that location. The Git repository has it as a submodule.
9# - Windows builds require MSYS2. In theory, Clang could be set up on
10# Windows for compiling everything, but the_Foundation still lacks
11# native Win32 implementations for the Socket and Process classes.
12# - Windows builds should use the SDL 2 library precompiled for native
13# Windows (MSVC variant) instead the version from MSYS2 (get it from
14# https://libsdl.org/). To make configuration easier, consider writing
15# for your personal use a pkg-config sdl2.pc file that uses the Windows
16# version of the library.
17# - `cat` is relied upon for merging all the resource files together.
18
1cmake_minimum_required (VERSION 3.9) 19cmake_minimum_required (VERSION 3.9)
2set (CMAKE_OSX_DEPLOYMENT_TARGET 10.14) 20set (CMAKE_OSX_DEPLOYMENT_TARGET 10.14)
3 21
4project (Lagrange 22project (Lagrange
5 VERSION 0.1.1 23 VERSION 0.2.0
6 DESCRIPTION "Beautiful Gemini Client" 24 DESCRIPTION "Beautiful Gemini Client"
7 LANGUAGES C 25 LANGUAGES C
8) 26)
9set (COPYRIGHT_YEAR 2020) 27set (COPYRIGHT_YEAR 2020)
10 28
11# Build configuration. 29# Build configuration.
30option (ENABLE_X11_SWRENDER "Use software rendering under X11" OFF)
12option (ENABLE_KERNING "Enable kerning in font renderer (slower)" ON) 31option (ENABLE_KERNING "Enable kerning in font renderer (slower)" ON)
13option (ENABLE_RESOURCE_EMBED "Embed resources inside the executable" OFF) 32option (ENABLE_RESOURCE_EMBED "Embed resources inside the executable" OFF)
33option (ENABLE_WINDOWPOS_FIX "Set position after showing window (workaround for SDL bug)" OFF)
14 34
15include (BuildType.cmake) 35include (BuildType.cmake)
16include (Embed.cmake) 36include (Embed.cmake)
17if (NOT EXISTS ${CMAKE_SOURCE_DIR}/lib/the_Foundation) 37if (NOT EXISTS ${CMAKE_SOURCE_DIR}/lib/the_Foundation/CMakeLists.txt)
18 set (INSTALL_THE_FOUNDATION YES) 38 set (INSTALL_THE_FOUNDATION YES)
19 find_package (the_Foundation REQUIRED) 39 find_package (the_Foundation REQUIRED)
20else () 40else ()
@@ -35,6 +55,7 @@ message (STATUS "Preparing embedded resources...")
35set (EMBED_RESOURCES 55set (EMBED_RESOURCES
36 res/about/help.gmi 56 res/about/help.gmi
37 res/about/lagrange.gmi 57 res/about/lagrange.gmi
58 res/about/license.gmi
38 res/about/version.gmi 59 res/about/version.gmi
39 res/FiraMono-Regular.ttf 60 res/FiraMono-Regular.ttf
40 res/FiraSans-Bold.ttf 61 res/FiraSans-Bold.ttf
@@ -151,8 +172,14 @@ target_compile_options (app PUBLIC
151 ${SDL2_CFLAGS} 172 ${SDL2_CFLAGS}
152) 173)
153target_compile_definitions (app PUBLIC LAGRANGE_APP_VERSION="${PROJECT_VERSION}") 174target_compile_definitions (app PUBLIC LAGRANGE_APP_VERSION="${PROJECT_VERSION}")
175if (ENABLE_X11_SWRENDER)
176 target_compile_definitions (app PUBLIC LAGRANGE_ENABLE_X11_SWRENDER=1)
177endif ()
154if (ENABLE_KERNING) 178if (ENABLE_KERNING)
155 target_compile_definitions (app PUBLIC LAGRANGE_ENABLE_KERNING=1) 179 target_compile_definitions (app PUBLIC LAGRANGE_ENABLE_KERNING=1)
180if (ENABLE_WINDOWPOS_FIX)
181endif ()
182 target_compile_definitions (app PUBLIC LAGRANGE_ENABLE_WINDOWPOS_FIX=1)
156endif () 183endif ()
157target_link_libraries (app PUBLIC the_Foundation::the_Foundation) 184target_link_libraries (app PUBLIC the_Foundation::the_Foundation)
158target_link_libraries (app PUBLIC ${SDL2_LDFLAGS}) 185target_link_libraries (app PUBLIC ${SDL2_LDFLAGS})
@@ -188,7 +215,11 @@ if (MSYS)
188 if (NOT ENABLE_RESOURCE_EMBED) 215 if (NOT ENABLE_RESOURCE_EMBED)
189 install (FILES ${EMB_BIN} DESTINATION .) 216 install (FILES ${EMB_BIN} DESTINATION .)
190 endif () 217 endif ()
191 install (PROGRAMS ${SDL2_LIBDIR}/SDL2.dll DESTINATION .) 218 install (PROGRAMS
219 ${SDL2_LIBDIR}/SDL2.dll
220 res/urlopen.bat
221 DESTINATION .
222 )
192 if (INSTALL_THE_FOUNDATION) 223 if (INSTALL_THE_FOUNDATION)
193 install (PROGRAMS $<TARGET_FILE:the_Foundation::the_Foundation> DESTINATION .) 224 install (PROGRAMS $<TARGET_FILE:the_Foundation::the_Foundation> DESTINATION .)
194 endif () 225 endif ()
diff --git a/README.md b/README.md
index 83e9a6e9..5b577aee 100644
--- a/README.md
+++ b/README.md
@@ -24,11 +24,11 @@ Prebuilt binaries for Windows and macOS can be found in [Releases][rel].
24 24
25This is how to build Lagrange in a Unix-like environment. The required tools are a C11 compiler (e.g., Clang or GCC), CMake and `pkg-config`. 25This is how to build Lagrange in a Unix-like environment. The required tools are a C11 compiler (e.g., Clang or GCC), CMake and `pkg-config`.
26 26
271. Download and extract a source tarball from [Releases][rel]. (If you just clone this Git repository, [the_Foundation][tf] is expected to be already available on the system.) 271. Download and extract a source tarball from [Releases][rel]. Alternatively, you may also clone the repository and its submodules: `git clone --recursive --branch release https://git.skyjake.fi/skyjake/lagrange`
282. Check that you have the dependencies installed: SDL2, OpenSSL, libpcre, zlib, libunistring. For example, on macOS this would do the trick (using Homebrew): ```brew install sdl2 openssl@1.1 pcre libunistring``` Or on Ubuntu: ```sudo apt install libsdl2-dev libssl-dev libpcre3-dev zlib1g-dev libunistring-dev``` 282. Check that you have the dependencies installed: CMake, SDL 2, OpenSSL 1.1.1, libpcre, zlib, libunistring. For example, on macOS this would do the trick (using Homebrew): ```brew install cmake sdl2 openssl@1.1 pcre libunistring``` Or on Ubuntu: ```sudo apt install cmake libsdl2-dev libssl-dev libpcre3-dev zlib1g-dev libunistring-dev```
293. Create a build directory. 293. Create a build directory.
304. In your empty build directory, run CMake: ```cmake {path_of_lagrange_sources}``` 304. In your empty build directory, run CMake: ```cmake {path_of_lagrange_sources} -DCMAKE_BUILD_TYPE=Release```
315. Built it: ```cmake --build .``` 315. Build it: ```cmake --build .```
326. Now you can run `lagrange`, `lagrange.exe`, or `Lagrange.app`. 326. Now you can run `lagrange`, `lagrange.exe`, or `Lagrange.app`.
33 33
34### Installing to a directory 34### Installing to a directory
@@ -40,5 +40,25 @@ To install to "/dest/path":
40 40
41This will also install an XDG .desktop file for launching the app. 41This will also install an XDG .desktop file for launching the app.
42 42
43### macOS-specific notes
44
45When using OpenSSL 1.1.1 from Homebrew, you must add its pkgconfig path to your `PKG_CONFIG_PATH` environment variable, for example:
46
47 export PKG_CONFIG_PATH=/usr/local/Cellar/openssl@1.1/1.1.1g/lib/pkgconfig
48
49Also, SDL's trackpad scrolling behavior on macOS is not optimal for regular GUI apps because it emulates a physical mouse wheel. This may change in a future release of SDL, but at least in 2.0.12 a [small patch](https://git.skyjake.fi/skyjake/lagrange/raw/branch/dev/sdl2-macos-mouse-scrolling-patch.diff) is required to allow momentum scrolling to come through as single-pixel mouse wheel events.
50
51### Raspberry Pi notes
52
53You should use a version of SDL that is compiled to take advantage of the Broadcom VideoCore OpenGL ES hardware. This provides the best performance when running Lagrange in a console.
54
55When running under X11, software rendering is the best choice and in that case the SDL from Raspbian etc. is sufficient.
56
57The following build options are recommended on Raspberry Pi:
58
59* `ENABLE_KERNING=NO`: faster text rendering without noticeable loss of quality
60* `ENABLE_WINDOWPOS_FIX=YES`: workaround for window position restore issues (SDL bug)
61* `ENABLE_X11_SWRENDER=YES`: use software rendering under X11
62
43[rel]: https://git.skyjake.fi/skyjake/lagrange/releases 63[rel]: https://git.skyjake.fi/skyjake/lagrange/releases
44[tf]: https://git.skyjake.fi/skyjake/the_Foundation 64[tf]: https://git.skyjake.fi/skyjake/the_Foundation
diff --git a/lagrange_about.png b/lagrange_about.png
index d342262b..7867344d 100644
--- a/lagrange_about.png
+++ b/lagrange_about.png
Binary files differ
diff --git a/lib/the_Foundation b/lib/the_Foundation
new file mode 160000
Subproject ab8f6e0769db2ec74846beea0f36561b85bf2c7
diff --git a/res/about/help.gmi b/res/about/help.gmi
index dccef9f8..ca5e164f 100644
--- a/res/about/help.gmi
+++ b/res/about/help.gmi
@@ -12,6 +12,7 @@ Lagrange is a GUI client for browsing Geminispace. It offers modern conveniences
12 12
13Like Gemini, Lagrange has been designed with minimalism in mind. It depends on a small number of essential libraries. It is written in C and uses SDL for hardware-accelerated graphics. OpenSSL is used for secure communications. 13Like Gemini, Lagrange has been designed with minimalism in mind. It depends on a small number of essential libraries. It is written in C and uses SDL for hardware-accelerated graphics. OpenSSL is used for secure communications.
14 14
15=> about:lagrange About Lagrange
15=> https://www.libsdl.org SDL: Simple DirectMedia Layer 16=> https://www.libsdl.org SDL: Simple DirectMedia Layer
16=> https://www.openssl.org OpenSSL: Cryptography and SSL/TLS Toolkit 17=> https://www.openssl.org OpenSSL: Cryptography and SSL/TLS Toolkit
17 18
@@ -59,13 +60,17 @@ Lagrange's user interface is modeled after web browsers:
59* There is a sidebar for managing bookmarks and TLS identities, and viewing history and the page outline. The sidebar is hidden by default. 60* There is a sidebar for managing bookmarks and TLS identities, and viewing history and the page outline. The sidebar is hidden by default.
60* There is a search bar that appears at the bottom when searching text on the page. 61* There is a search bar that appears at the bottom when searching text on the page.
61 62
63Tip: Try pressing ${CTRL+}4 now to see the page outline.
64
62## URL entry and quick search 65## URL entry and quick search
63 66
64The URL input field is in its typical location in the navigation bar. It can be accessed quickly by pressing ${CTRL+}L. 67The URL input field is in its typical location in the navigation bar. It can be accessed quickly by pressing ${CTRL+}L.
65 68
66As you enter text, Lagrange starts looking for matches in bookmarks, history, content of cached pages, and identities. Search terms are case insensitive, and if many words are entered, they are all required to appear in the specified order in any matched content. Search of cached pages is limited to the (small) set of pages that Lagrange keeps in memory for back navigation. 69As you enter text, Lagrange starts looking for matches in bookmarks, history, content of cached pages, and identities. Search results appear below the URL input field in a popup. Press Tab or ↓ to switch input focus to the results.
70
71Search within cached pages is limited to the (small) set of pages that Lagrange keeps in memory for back navigation. Search terms are case insensitive, and if many words are entered, they are all required to appear in the specified order in any matched content.
67 72
68Press Tab or ↓ to switch input focus to the search results. 73Note that the navigation stack is saved to a file when Lagrange is shut down and restored on the next launch. This means the next time you launch Lagrange, you can still search the contents of past pages. However, navigation stacks are tab-specific, so closing a tab will delete its history as well.
69 74
70## Tabs 75## Tabs
71 76
@@ -86,6 +91,8 @@ ${CTRL+}1 through ${CTRL+}4 switch between the sidebar tabs, or hide the sidebar
86 91
87## Navigation 92## Navigation
88 93
94When navigating to a new page, the old page is cached in memory. If you navigate back, the cached copy of the page is restored. Think of it as rewinding time — you return to a past time as if nothing had happened. The same applies to forward navigation; cached pages are loaded if available. This allows back and forward navigation to happen instantly, without any network requests.
95
89### Link icons 96### Link icons
90 97
91The type and destination of a link are indicated by the link's icon and color: ➤ links to the same domain, and 🌐 to a different domain. The colors are: 98The type and destination of a link are indicated by the link's icon and color: ➤ links to the same domain, and 🌐 to a different domain. The colors are:
@@ -103,17 +110,60 @@ When navigating via keyboard, hold down ${ALT} to see link shortcut keys. Try do
103 110
104Each visible link on the page gets an alphanumeric shortcut. For example, the first link can be opened by pressing ${ALT+}1. The tenth link is ${ALT+}A. Additionally hold down ${CTRL} to open the link in a new tab. 111Each visible link on the page gets an alphanumeric shortcut. For example, the first link can be opened by pressing ${ALT+}1. The tenth link is ${ALT+}A. Additionally hold down ${CTRL} to open the link in a new tab.
105 112
113## Downloads
114
115Press ${CTRL+}S to save the contents of the current page to the Downloads folder. This works on all pages regardless of whether Lagrange can display the contents or not.
116
117The location where downloaded files are saved can be set in Preferences.
118
119The 🔃 button on the right side of the URL input field is the Reload/Stop button — it reloads the current page or stops an ongoing download. During large downloads, an additional progress indicator appears next to the Stop button.
120
106## Bookmarks 121## Bookmarks
107 122
108## Managing and using identities 123Press ${CTRL+}D to bookmark the currently open URL.
124
125In addition to a title, bookmarks can have tags. Some tags have a special meaning, but you are free to enter whatever you want in the tags field. In quick search results, tags are given extra weight so they appear higher in results.
126
127### Special tags
128
129* Set a "homepage" tag on a bookmark to make it one of the pages that will be opened when pressing the 🏠 button.
130
131## Identities (TLS client certificates)
132
133Gemini uses TLS client certificates for manual user/session identification purposes. This is analogous to logging into a web site, except you are in full control of the information. The term "Identity" is used in Lagrange to refer to client certificates.
134
135Lagrange can easily create a new identity. The shortcut for this is ${SHIFT+}${CTRL+}N. Consider any information you enter in the certificate as public; only the Common Name is required and will appear as the issuer and subject of the certificate.
136
137Clicking on an identity in the sidebar will toggle it on/off for the currently open URL. On subsequent page loads, the certificate will then be sent to the server when the URL or any URL under it is fetched. You can click on the 👤 button in the navigation bar to see which identity is being used for the current page.
109 138
110TLS client certificates that you can identify yourself with. Consider any information you enter in the certificate as public; only the Common Name is required and will appear as the issuer and subject of the certificate. 139### Importing existing certificates
140
141At launch, Lagrange looks through its "idents" directory to see if any new certificates have been copied there. (See "Runtime files" below for the location.) The file format must be PEM. Both a certificate (.crt) and its private key (.key) must be found in "idents" and they must have matching file names. For example:
142* mycert.crt
143* mycert.key
144Lagrange will add a note to the imported identities to mark them as "Imported".
145
146# OS integration
147
148## Opening Gemini URLs (macOS)
149
150Lagrange registers itself as a "gemini:" URL scheme handler, so you can click on Gemini links in any application to open Lagrange with that URL.
151
152Likewise, .gmi/.gemini file extensions are registered as file formats that Lagrange can view so Finder will know how to open those automatically using Lagrange.
111 153
112## Drop and drop 154## Drop and drop
113 155
114You can drag and drop .gmi files on the Lagrange window to open them in the current tab. Dropping multiple files opens them in separate tabs. This is the recommended way to view local files, because there is no "Open File" menu item. You may also type "file://" URLs in the URL field. 156You can drag and drop .gmi files on the Lagrange window to open them in the current tab. Dropping multiple files opens them in separate tabs. This is the recommended way to view local files, because there is no "Open File" menu item. You may also type "file://" URLs in the URL field.
115 157
116# Runtime files 158## Runtime files
159
160Lagrange stores user-specific persistent files in one of the following locations (depending on the operating system):
161
162```
163 Windows : C:\Users\Name\AppData\Roaming\fi.skyjake.Lagrange\
164 macOS : ~/Library/Application Support/fi.skyjake.Lagrange/
165Other Unix : ~/.config/lagrange/
166```
117 167
118* bookmarks.txt 168* bookmarks.txt
119* idents.binary and idents/ 169* idents.binary and idents/
@@ -122,108 +172,13 @@ You can drag and drop .gmi files on the Lagrange window to open them in the curr
122* trusted.txt 172* trusted.txt
123* visited.txt 173* visited.txt
124 174
125# Open source licenses 175## Command line options
126 176
127Lagrange itself is distributed under the BSD 2-clause license: 177### --echo
128=> https://opensource.org/licenses/BSD-2-Clause The 2-Clause BSD License 178Debugging utility: internal events are printed to stdout.
129=> https://git.skyjake.fi/skyjake/lagrange.git Lagrange Git Repository 179
130 180### --sw
131> Copyright 2020 Jaakko Keränen 181Disable hardware accelerated graphics. Note that software rendering is anyway used as a fallback, so usually this option should not be necessary.
132> 182
133> Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 183# Open source licenses
134> 184=> about:license
135> 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
136>
137> 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
138>
139> THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
140
141## SDL
142
143SDL 2.0 and newer are available under the zlib license:
144=> https://www.zlib.net/zlib_license.html ZLIB License
145
146> This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software.
147>
148> Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions:
149>
150> 1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required.
151>
152> 2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software.
153>
154> 3. This notice may not be removed or altered from any source distribution.
155
156## the_Foundation
157
158the_Foundation is an opinionated C11 library by Jaakko Keränen. It is under the BSD 2-clause license. Note that the rest of the libraries in this section are used by the_Foundation, and not directly by Lagrange.
159=> https://opensource.org/licenses/BSD-2-Clause The 2-Clause BSD License
160=> https://git.skyjake.fi/skyjake/the_Foundation.git the_Foundation Git Repository
161
162## OpenSSL
163
164OpenSSL 1.1.1 is under a double license, which both apply to the library.
165=> https://www.openssl.org/source/license-openssl-ssleay.txt OpenSSL and SSLeay Licenses
166
167> Copyright (c) 1998-2019 The OpenSSL Project. All rights reserved.
168>
169> Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
170>
171> 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
172>
173> 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
174>
175> 3. All advertising materials mentioning features or use of this software must display the following acknowledgment: "This product includes software developed by the OpenSSL Project for use in the OpenSSL Toolkit. (http://www.openssl.org/)"
176>
177> 4. The names "OpenSSL Toolkit" and "OpenSSL Project" must not be used to endorse or promote products derived from this software without prior written permission. For written permission, please contact openssl-core@openssl.org.
178>
179> 5. Products derived from this software may not be called "OpenSSL" nor may "OpenSSL" appear in their names without prior written permission of the OpenSSL Project.
180>
181> 6. Redistributions of any form whatsoever must retain the following acknowledgment: "This product includes software developed by the OpenSSL Project for use in the OpenSSL Toolkit (http://www.openssl.org/)"
182>
183> THIS SOFTWARE IS PROVIDED BY THE OpenSSL PROJECT ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE OpenSSL PROJECT OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
184>
185> This product includes cryptographic software written by Eric Young (eay@cryptsoft.com). This product includes software written by Tim Hudson (tjh@cryptsoft.com).
186
187SSLeay license:
188
189> Copyright (C) 1995-1998 Eric Young (eay@cryptsoft.com)
190> All rights reserved.
191>
192> This package is an SSL implementation written by Eric Young (eay@cryptsoft.com). The implementation was written so as to conform with Netscapes SSL.
193>
194> This library is free for commercial and non-commercial use as long as the following conditions are aheared to. The following conditions apply to all code found in this distribution, be it the RC4, RSA, lhash, DES, etc., code; not just the SSL code. The SSL documentation included with this distribution is covered by the same copyright terms except that the holder is Tim Hudson (tjh@cryptsoft.com).
195>
196> Copyright remains Eric Young's, and as such any Copyright notices in the code are not to be removed. If this package is used in a product, Eric Young should be given attribution as the author of the parts of the library used. This can be in the form of a textual message at program startup or in documentation (online or textual) provided with the package.
197>
198> Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
199>
200> 1. Redistributions of source code must retain the copyright notice, this list of conditions and the following disclaimer.
201>
202> 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
203>
204> 3. All advertising materials mentioning features or use of this software must display the following acknowledgement: "This product includes cryptographic software written by Eric Young (eay@cryptsoft.com)" The word 'cryptographic' can be left out if the rouines from the library being used are not cryptographic related :-).
205>
206> 4. If you include any Windows specific code (or a derivative thereof) from the apps directory (application code) you must include an acknowledgement: "This product includes software written by Tim Hudson (tjh@cryptsoft.com)"
207>
208> THIS SOFTWARE IS PROVIDED BY ERIC YOUNG ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
209>
210> The licence and distribution terms for any publically available version or derivative of this code cannot be changed. i.e. this code cannot simply be copied and put under another distribution licence [including the GNU Public Licence.]
211
212## GNU libunistring
213
214The libunistring library is covered by the GNU Lesser General Public License (LGPL):
215=> https://www.gnu.org/software/libunistring/manual/libunistring.html#GNU-LGPL GNU LGPL License
216
217## Fonts
218
219This application uses fonts licensed under the Open Font License.
220
221=> https://github.com/mozilla/Fira/blob/master/LICENSE Fira Sans, Fira Mono
222=> https://github.com/googlefonts/nunito/blob/master/OFL.txt Nunito
223=> https://github.com/adobe-fonts/source-sans-pro/blob/release/LICENSE.md Source Sans Pro
224
225Additional fonts:
226
227=> https://fonts.google.com/specimen/Kosugi+Maru#license Kosugi Maru (Apache License 2.0)
228=> https://github.com/googlefonts/noto-emoji/blob/master/LICENSE Noto Emoji (Apache License 2.0)
229=> https://dn-works.com/ufas/ Symbola (Public Domain) \ No newline at end of file
diff --git a/res/about/license.gmi b/res/about/license.gmi
new file mode 100644
index 00000000..4f654540
--- /dev/null
+++ b/res/about/license.gmi
@@ -0,0 +1,105 @@
1# Open source licenses
2
3Lagrange is distributed under the BSD 2-clause license.
4=> https://opensource.org/licenses/BSD-2-Clause The 2-Clause BSD License
5=> https://git.skyjake.fi/skyjake/lagrange.git Lagrange Git Repository
6
7> Copyright 2020 Jaakko Keränen
8>
9> Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
10>
11> 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
12>
13> 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
14>
15> THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
16
17## SDL
18
19SDL 2.0 and newer are available under the zlib license:
20=> https://www.zlib.net/zlib_license.html ZLIB License
21
22> This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software.
23>
24> Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions:
25>
26> 1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required.
27>
28> 2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software.
29>
30> 3. This notice may not be removed or altered from any source distribution.
31
32## the_Foundation
33
34the_Foundation is an opinionated C11 library by Jaakko Keränen. It is under the BSD 2-clause license. Note that the rest of the libraries in this section are used by the_Foundation, and not directly by Lagrange.
35=> https://opensource.org/licenses/BSD-2-Clause The 2-Clause BSD License
36=> https://git.skyjake.fi/skyjake/the_Foundation.git the_Foundation Git Repository
37
38## OpenSSL
39
40OpenSSL 1.1.1 is under a double license, which both apply to the library.
41=> https://www.openssl.org/source/license-openssl-ssleay.txt OpenSSL and SSLeay Licenses
42
43> Copyright (c) 1998-2019 The OpenSSL Project. All rights reserved.
44>
45> Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
46>
47> 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
48>
49> 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
50>
51> 3. All advertising materials mentioning features or use of this software must display the following acknowledgment: "This product includes software developed by the OpenSSL Project for use in the OpenSSL Toolkit. (http://www.openssl.org/)"
52>
53> 4. The names "OpenSSL Toolkit" and "OpenSSL Project" must not be used to endorse or promote products derived from this software without prior written permission. For written permission, please contact openssl-core@openssl.org.
54>
55> 5. Products derived from this software may not be called "OpenSSL" nor may "OpenSSL" appear in their names without prior written permission of the OpenSSL Project.
56>
57> 6. Redistributions of any form whatsoever must retain the following acknowledgment: "This product includes software developed by the OpenSSL Project for use in the OpenSSL Toolkit (http://www.openssl.org/)"
58>
59> THIS SOFTWARE IS PROVIDED BY THE OpenSSL PROJECT ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE OpenSSL PROJECT OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
60>
61> This product includes cryptographic software written by Eric Young (eay@cryptsoft.com). This product includes software written by Tim Hudson (tjh@cryptsoft.com).
62
63SSLeay license:
64
65> Copyright (C) 1995-1998 Eric Young (eay@cryptsoft.com)
66> All rights reserved.
67>
68> This package is an SSL implementation written by Eric Young (eay@cryptsoft.com). The implementation was written so as to conform with Netscapes SSL.
69>
70> This library is free for commercial and non-commercial use as long as the following conditions are aheared to. The following conditions apply to all code found in this distribution, be it the RC4, RSA, lhash, DES, etc., code; not just the SSL code. The SSL documentation included with this distribution is covered by the same copyright terms except that the holder is Tim Hudson (tjh@cryptsoft.com).
71>
72> Copyright remains Eric Young's, and as such any Copyright notices in the code are not to be removed. If this package is used in a product, Eric Young should be given attribution as the author of the parts of the library used. This can be in the form of a textual message at program startup or in documentation (online or textual) provided with the package.
73>
74> Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
75>
76> 1. Redistributions of source code must retain the copyright notice, this list of conditions and the following disclaimer.
77>
78> 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
79>
80> 3. All advertising materials mentioning features or use of this software must display the following acknowledgement: "This product includes cryptographic software written by Eric Young (eay@cryptsoft.com)" The word 'cryptographic' can be left out if the rouines from the library being used are not cryptographic related :-).
81>
82> 4. If you include any Windows specific code (or a derivative thereof) from the apps directory (application code) you must include an acknowledgement: "This product includes software written by Tim Hudson (tjh@cryptsoft.com)"
83>
84> THIS SOFTWARE IS PROVIDED BY ERIC YOUNG ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
85>
86> The licence and distribution terms for any publically available version or derivative of this code cannot be changed. i.e. this code cannot simply be copied and put under another distribution licence [including the GNU Public Licence.]
87
88## GNU libunistring
89
90The libunistring library is covered by the GNU Lesser General Public License (LGPL):
91=> https://www.gnu.org/software/libunistring/manual/libunistring.html#GNU-LGPL GNU LGPL License
92
93## Fonts
94
95This application uses fonts licensed under the Open Font License.
96
97=> https://github.com/mozilla/Fira/blob/master/LICENSE Fira Sans, Fira Mono
98=> https://github.com/googlefonts/nunito/blob/master/OFL.txt Nunito
99=> https://github.com/adobe-fonts/source-sans-pro/blob/release/LICENSE.md Source Sans Pro
100
101Additional fonts:
102
103=> https://fonts.google.com/specimen/Kosugi+Maru#license Kosugi Maru (Apache License 2.0)
104=> https://github.com/googlefonts/noto-emoji/blob/master/LICENSE Noto Emoji (Apache License 2.0)
105=> https://dn-works.com/ufas/ Symbola (Public Domain)
diff --git a/res/about/version.gmi b/res/about/version.gmi
index 2bbf673a..32f1e49f 100644
--- a/res/about/version.gmi
+++ b/res/about/version.gmi
@@ -6,6 +6,21 @@
6``` 6```
7# Release notes 7# Release notes
8 8
9## 0.2
10* Added an icon for quote paragraphs.
11* Added Downloads folder to Preferences.
12* Added "Save to Downloads" menu item (${CTRL+}S) for saving page contents.
13* Added a download progress indicator in the URL input field.
14* Added a progress indicator for inline image fetching.
15* Added `--sw` option to force software rendering.
16* Added macOS touch bar buttons for Back, Forward, Find, New Tab, and sidebar modes.
17* Home button opens a random bookmark with the "homepage" tag.
18* Improved context menu when right-clicking on links or the page.
19* Recognize and handle "mailto:" links.
20* Fixed behavior of images on single-image pages; cannot be hidden like inline images.
21* Fall back to software rendering automatically if accelerated graphics are not available.
22* Minor bug fixes.
23
9## 0.1.1 24## 0.1.1
10* Fixed a potential crash at startup. 25* Fixed a potential crash at startup.
11* Fixed bug where user's query input is handled by all tabs. 26* Fixed bug where user's query input is handled by all tabs.
diff --git a/res/urlopen.bat b/res/urlopen.bat
new file mode 100644
index 00000000..82935824
--- /dev/null
+++ b/res/urlopen.bat
@@ -0,0 +1,2 @@
1@echo off
2start %1
diff --git a/sdl2-macos-mouse-scrolling-patch.diff b/sdl2-macos-mouse-scrolling-patch.diff
new file mode 100644
index 00000000..633243ed
--- /dev/null
+++ b/sdl2-macos-mouse-scrolling-patch.diff
@@ -0,0 +1,28 @@
1diff -r 4f06c06b6d19 src/events/SDL_mouse.c
2--- a/src/events/SDL_mouse.c Wed Aug 05 15:28:51 2020 +0200
3+++ b/src/events/SDL_mouse.c Tue Sep 15 07:54:17 2020 +0300
4@@ -642,8 +642,8 @@
5 event.wheel.preciseX = x;
6 event.wheel.preciseY = y;
7 #endif
8- event.wheel.x = integral_x;
9- event.wheel.y = integral_y;
10+ event.wheel.x = x; //integral_x;
11+ event.wheel.y = y; //integral_y;
12 event.wheel.direction = (Uint32)direction;
13 posted = (SDL_PushEvent(&event) > 0);
14 }
15diff -r 4f06c06b6d19 src/video/cocoa/SDL_cocoamouse.m
16--- a/src/video/cocoa/SDL_cocoamouse.m Wed Aug 05 15:28:51 2020 +0200
17+++ b/src/video/cocoa/SDL_cocoamouse.m Tue Sep 15 07:54:17 2020 +0300
18@@ -424,8 +424,8 @@
19 }
20
21 SDL_MouseID mouseID = mouse->mouseID;
22- CGFloat x = -[event deltaX];
23- CGFloat y = [event deltaY];
24+ CGFloat x = -[event scrollingDeltaX];
25+ CGFloat y = [event scrollingDeltaY];
26 SDL_MouseWheelDirection direction = SDL_MOUSEWHEEL_NORMAL;
27
28 if ([event respondsToSelector:@selector(isDirectionInvertedFromDevice)]) {
diff --git a/src/app.c b/src/app.c
index 5eceebec..57fadf65 100644
--- a/src/app.c
+++ b/src/app.c
@@ -73,8 +73,9 @@ static const char *dataDir_App_ = "~/AppData/Roaming/fi.skyjake.Lagrange";
73static const char *dataDir_App_ = "~/.config/lagrange"; 73static const char *dataDir_App_ = "~/.config/lagrange";
74#endif 74#endif
75#define EMB_BIN2 "../resources.binary" /* fallback from build/executable dir */ 75#define EMB_BIN2 "../resources.binary" /* fallback from build/executable dir */
76static const char *prefsFileName_App_ = "prefs.cfg"; 76static const char *prefsFileName_App_ = "prefs.cfg";
77static const char *stateFileName_App_ = "state.binary"; 77static const char *stateFileName_App_ = "state.binary";
78static const char *downloadDir_App_ = "~/Downloads";
78 79
79struct Impl_App { 80struct Impl_App {
80 iCommandLine args; 81 iCommandLine args;
@@ -98,10 +99,12 @@ struct Impl_App {
98 float uiScale; 99 float uiScale;
99 int zoomPercent; 100 int zoomPercent;
100 iBool forceWrap; 101 iBool forceWrap;
102 iBool forceSoftwareRender;
101 enum iColorTheme theme; 103 enum iColorTheme theme;
102 iBool useSystemTheme; 104 iBool useSystemTheme;
103 iString gopherProxy; 105 iString gopherProxy;
104 iString httpProxy; 106 iString httpProxy;
107 iString downloadDir;
105}; 108};
106 109
107static iApp app_; 110static iApp app_;
@@ -160,6 +163,7 @@ static iString *serializePrefs_App_(const iApp *d) {
160 appendFormat_String(str, "ostheme arg:%d\n", d->useSystemTheme); 163 appendFormat_String(str, "ostheme arg:%d\n", d->useSystemTheme);
161 appendFormat_String(str, "proxy.gopher address:%s\n", cstr_String(&d->gopherProxy)); 164 appendFormat_String(str, "proxy.gopher address:%s\n", cstr_String(&d->gopherProxy));
162 appendFormat_String(str, "proxy.http address:%s\n", cstr_String(&d->httpProxy)); 165 appendFormat_String(str, "proxy.http address:%s\n", cstr_String(&d->httpProxy));
166 appendFormat_String(str, "downloads path:%s\n", cstr_String(&d->downloadDir));
163 return str; 167 return str;
164} 168}
165 169
@@ -304,12 +308,14 @@ static void init_App_(iApp *d, int argc, char **argv) {
304 d->pendingRefresh = iFalse; 308 d->pendingRefresh = iFalse;
305 d->zoomPercent = 100; 309 d->zoomPercent = 100;
306 d->forceWrap = iFalse; 310 d->forceWrap = iFalse;
311 d->forceSoftwareRender = checkArgument_CommandLine(&d->args, "sw") != NULL;
307 d->certs = new_GmCerts(dataDir_App_); 312 d->certs = new_GmCerts(dataDir_App_);
308 d->visited = new_Visited(); 313 d->visited = new_Visited();
309 d->bookmarks = new_Bookmarks(); 314 d->bookmarks = new_Bookmarks();
310 d->tabEnum = 0; /* generates unique IDs for tab pages */ 315 d->tabEnum = 0; /* generates unique IDs for tab pages */
311 init_String(&d->gopherProxy); 316 init_String(&d->gopherProxy);
312 init_String(&d->httpProxy); 317 init_String(&d->httpProxy);
318 initCStr_String(&d->downloadDir, downloadDir_App_);
313 setThemePalette_Color(d->theme); 319 setThemePalette_Color(d->theme);
314#if defined (iPlatformApple) 320#if defined (iPlatformApple)
315 setupApplication_MacOS(); 321 setupApplication_MacOS();
@@ -384,6 +390,7 @@ static void init_App_(iApp *d, int argc, char **argv) {
384static void deinit_App(iApp *d) { 390static void deinit_App(iApp *d) {
385 saveState_App_(d); 391 saveState_App_(d);
386 savePrefs_App_(d); 392 savePrefs_App_(d);
393 deinit_String(&d->downloadDir);
387 deinit_String(&d->httpProxy); 394 deinit_String(&d->httpProxy);
388 deinit_String(&d->gopherProxy); 395 deinit_String(&d->gopherProxy);
389 save_Bookmarks(d->bookmarks, dataDir_App_); 396 save_Bookmarks(d->bookmarks, dataDir_App_);
@@ -406,6 +413,10 @@ const iString *dataDir_App(void) {
406 return collect_String(cleanedCStr_Path(dataDir_App_)); 413 return collect_String(cleanedCStr_Path(dataDir_App_));
407} 414}
408 415
416const iString *downloadDir_App(void) {
417 return collect_String(cleaned_Path(&app_.downloadDir));
418}
419
409const iString *debugInfo_App(void) { 420const iString *debugInfo_App(void) {
410 iApp *d = &app_; 421 iApp *d = &app_;
411 iString *msg = collectNew_String(); 422 iString *msg = collectNew_String();
@@ -531,8 +542,20 @@ int zoom_App(void) {
531 return app_.zoomPercent; 542 return app_.zoomPercent;
532} 543}
533 544
534iBool isLineWrapForced_App(void) { 545iBool forceLineWrap_App(void) {
535 return app_.forceWrap; 546 return app_.forceWrap;
547iBool forceSoftwareRender_App(void) {
548}
549
550 if (app_.forceSoftwareRender) {
551 return iTrue;
552 }
553#if defined (LAGRANGE_ENABLE_X11_SWRENDER)
554 if (getenv("DISPLAY")) {
555 return iTrue;
556 }
557#endif
558 return iFalse;
536} 559}
537 560
538enum iColorTheme colorTheme_App(void) { 561enum iColorTheme colorTheme_App(void) {
@@ -644,6 +667,8 @@ static iBool handlePrefsCommands_(iWidget *d, const char *cmd) {
644 if (equal_Command(cmd, "prefs.dismiss") || equal_Command(cmd, "preferences")) { 667 if (equal_Command(cmd, "prefs.dismiss") || equal_Command(cmd, "preferences")) {
645 setUiScale_Window(get_Window(), 668 setUiScale_Window(get_Window(),
646 toFloat_String(text_InputWidget(findChild_Widget(d, "prefs.uiscale")))); 669 toFloat_String(text_InputWidget(findChild_Widget(d, "prefs.uiscale"))));
670 postCommandf_App("downloads path:%s",
671 cstr_String(text_InputWidget(findChild_Widget(d, "prefs.downloads"))));
647 postCommandf_App("window.retain arg:%d", 672 postCommandf_App("window.retain arg:%d",
648 isSelected_Widget(findChild_Widget(d, "prefs.retainwindow"))); 673 isSelected_Widget(findChild_Widget(d, "prefs.retainwindow")));
649 postCommandf_App("ostheme arg:%d", 674 postCommandf_App("ostheme arg:%d",
@@ -777,12 +802,17 @@ iBool handleCommand_App(const char *cmd) {
777 d->retainWindowSize = arg_Command(cmd); 802 d->retainWindowSize = arg_Command(cmd);
778 return iTrue; 803 return iTrue;
779 } 804 }
805 else if (equal_Command(cmd, "downloads")) {
806 setCStr_String(&d->downloadDir, suffixPtr_Command(cmd, "path"));
807 return iTrue;
808 }
780 else if (equal_Command(cmd, "open")) { 809 else if (equal_Command(cmd, "open")) {
781 const iString *url = collectNewCStr_String(suffixPtr_Command(cmd, "url")); 810 const iString *url = collectNewCStr_String(suffixPtr_Command(cmd, "url"));
782 iUrl parts; 811 iUrl parts;
783 init_Url(&parts, url); 812 init_Url(&parts, url);
784 if (isEmpty_String(&d->httpProxy) && 813 if (equalCase_Rangecc(parts.scheme, "mailto") ||
785 (equalCase_Rangecc(parts.scheme, "http") || equalCase_Rangecc(parts.scheme, "https"))) { 814 (isEmpty_String(&d->httpProxy) && (equalCase_Rangecc(parts.scheme, "http") ||
815 equalCase_Rangecc(parts.scheme, "https")))) {
786 openInDefaultBrowser_App(url); 816 openInDefaultBrowser_App(url);
787 return iTrue; 817 return iTrue;
788 } 818 }
@@ -872,6 +902,7 @@ iBool handleCommand_App(const char *cmd) {
872 else if (equal_Command(cmd, "preferences")) { 902 else if (equal_Command(cmd, "preferences")) {
873 iWidget *dlg = makePreferences_Widget(); 903 iWidget *dlg = makePreferences_Widget();
874 updatePrefsThemeButtons_(dlg); 904 updatePrefsThemeButtons_(dlg);
905 setText_InputWidget(findChild_Widget(dlg, "prefs.downloads"), &d->downloadDir);
875 setToggle_Widget(findChild_Widget(dlg, "prefs.ostheme"), d->useSystemTheme); 906 setToggle_Widget(findChild_Widget(dlg, "prefs.ostheme"), d->useSystemTheme);
876 setToggle_Widget(findChild_Widget(dlg, "prefs.retainwindow"), d->retainWindowSize); 907 setToggle_Widget(findChild_Widget(dlg, "prefs.retainwindow"), d->retainWindowSize);
877 setText_InputWidget(findChild_Widget(dlg, "prefs.uiscale"), 908 setText_InputWidget(findChild_Widget(dlg, "prefs.uiscale"),
@@ -883,8 +914,28 @@ iBool handleCommand_App(const char *cmd) {
883 setCommandHandler_Widget(dlg, handlePrefsCommands_); 914 setCommandHandler_Widget(dlg, handlePrefsCommands_);
884 } 915 }
885 else if (equal_Command(cmd, "navigate.home")) { 916 else if (equal_Command(cmd, "navigate.home")) {
886 /* TODO: Look for bookmarks tagged homepage, or use the URL set in Preferences. */ 917 /* Look for bookmarks tagged "homepage". */
918 iRegExp *pattern = iClob(new_RegExp("\\bhomepage\\b", caseInsensitive_RegExpOption));
919 const iPtrArray *homepages =
920 list_Bookmarks(d->bookmarks, NULL, filterTagsRegExp_Bookmarks, pattern);
921 if (isEmpty_PtrArray(homepages)) {
887 postCommand_App("open url:about:lagrange"); 922 postCommand_App("open url:about:lagrange");
923 }
924 else {
925 iStringSet *urls = iClob(new_StringSet());
926 iConstForEach(PtrArray, i, homepages) {
927 const iBookmark *bm = i.ptr;
928 /* Try to switch to a different bookmark. */
929 if (cmpStringCase_String(url_DocumentWidget(document_App()), &bm->url)) {
930 insert_StringSet(urls, &bm->url);
931 }
932 }
933 if (!isEmpty_StringSet(urls)) {
934 postCommandf_App(
935 "open url:%s",
936 cstr_String(constAt_StringSet(urls, iRandoms(0, size_StringSet(urls)))));
937 }
938 }
888 return iTrue; 939 return iTrue;
889 } 940 }
890 else if (equal_Command(cmd, "zoom.set")) { 941 else if (equal_Command(cmd, "zoom.set")) {
@@ -993,9 +1044,10 @@ void openInDefaultBrowser_App(const iString *url) {
993 iClob(newStringsCStr_StringList("/usr/bin/x-www-browser", cstr_String(url), NULL)) 1044 iClob(newStringsCStr_StringList("/usr/bin/x-www-browser", cstr_String(url), NULL))
994#elif defined (iPlatformMsys) 1045#elif defined (iPlatformMsys)
995 iClob(newStringsCStr_StringList( 1046 iClob(newStringsCStr_StringList(
996 "c:\\Windows\\System32\\cmd.exe", "/q", "/c", "start", cstr_String(url), NULL)) 1047 concatPath_CStr(cstr_String(execPath_App()), "../urlopen.bat"),
997 /* TODO: Should consult environment variables to find the 1048 cstr_String(url),
998 right cmd.exe. Also, the prompt window is shown momentarily... */ 1049 NULL))
1050 /* TODO: The prompt window is shown momentarily... */
999#endif 1051#endif
1000 ); 1052 );
1001 start_Process(proc); 1053 start_Process(proc);
diff --git a/src/app.h b/src/app.h
index 69b8d274..acc3e952 100644
--- a/src/app.h
+++ b/src/app.h
@@ -48,6 +48,7 @@ enum iUserEventCode {
48 48
49const iString *execPath_App (void); 49const iString *execPath_App (void);
50const iString *dataDir_App (void); 50const iString *dataDir_App (void);
51const iString *downloadDir_App (void);
51const iString *debugInfo_App (void); 52const iString *debugInfo_App (void);
52 53
53int run_App (int argc, char **argv); 54int run_App (int argc, char **argv);
@@ -58,7 +59,8 @@ iBool isRefreshPending_App (void);
58uint32_t elapsedSinceLastTicker_App (void); /* milliseconds */ 59uint32_t elapsedSinceLastTicker_App (void); /* milliseconds */
59 60
60int zoom_App (void); 61int zoom_App (void);
61iBool isLineWrapForced_App(void); 62iBool forceLineWrap_App (void);
63iBool forceSoftwareRender_App(void);
62enum iColorTheme colorTheme_App (void); 64enum iColorTheme colorTheme_App (void);
63const iString * schemeProxy_App (iRangecc scheme); 65const iString * schemeProxy_App (iRangecc scheme);
64 66
diff --git a/src/bookmarks.c b/src/bookmarks.c
index 8fe7d109..7e98fb27 100644
--- a/src/bookmarks.c
+++ b/src/bookmarks.c
@@ -26,6 +26,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
26#include <the_Foundation/hash.h> 26#include <the_Foundation/hash.h>
27#include <the_Foundation/mutex.h> 27#include <the_Foundation/mutex.h>
28#include <the_Foundation/path.h> 28#include <the_Foundation/path.h>
29#include <the_Foundation/regexp.h>
29 30
30void init_Bookmark(iBookmark *d) { 31void init_Bookmark(iBookmark *d) {
31 init_String(&d->url); 32 init_String(&d->url);
@@ -166,6 +167,12 @@ iBookmark *get_Bookmarks(iBookmarks *d, uint32_t id) {
166 return (iBookmark *) value_Hash(&d->bookmarks, id); 167 return (iBookmark *) value_Hash(&d->bookmarks, id);
167} 168}
168 169
170iBool filterTagsRegExp_Bookmarks(void *regExp, const iBookmark *bm) {
171 iRegExpMatch m;
172 init_RegExpMatch(&m);
173 return matchString_RegExp(regExp, &bm->tags, &m);
174}
175
169const iPtrArray *list_Bookmarks(const iBookmarks *d, iBookmarksCompareFunc cmp, 176const iPtrArray *list_Bookmarks(const iBookmarks *d, iBookmarksCompareFunc cmp,
170 iBookmarksFilterFunc filter, void *context) { 177 iBookmarksFilterFunc filter, void *context) {
171 lock_Mutex(d->mtx); 178 lock_Mutex(d->mtx);
diff --git a/src/bookmarks.h b/src/bookmarks.h
index aac83be1..4889e7b5 100644
--- a/src/bookmarks.h
+++ b/src/bookmarks.h
@@ -55,6 +55,8 @@ iBookmark *get_Bookmarks (iBookmarks *, uint32_t id);
55typedef iBool (*iBookmarksFilterFunc) (void *context, const iBookmark *); 55typedef iBool (*iBookmarksFilterFunc) (void *context, const iBookmark *);
56typedef int (*iBookmarksCompareFunc)(const iBookmark **, const iBookmark **); 56typedef int (*iBookmarksCompareFunc)(const iBookmark **, const iBookmark **);
57 57
58iBool filterTagsRegExp_Bookmarks (void *regExp, const iBookmark *);
59
58/** 60/**
59 * Lists all or a subset of the bookmarks in a sorted array of Bookmark pointers. 61 * Lists all or a subset of the bookmarks in a sorted array of Bookmark pointers.
60 * 62 *
diff --git a/src/gmdocument.c b/src/gmdocument.c
index e2832ae2..4d4ba603 100644
--- a/src/gmdocument.c
+++ b/src/gmdocument.c
@@ -64,11 +64,13 @@ struct Impl_GmImage {
64 size_t numBytes; 64 size_t numBytes;
65 iString mime; 65 iString mime;
66 iGmLinkId linkId; 66 iGmLinkId linkId;
67 iBool isPermanent;
67 SDL_Texture *texture; 68 SDL_Texture *texture;
68}; 69};
69 70
70void init_GmImage(iGmImage *d, const iBlock *data) { 71void init_GmImage(iGmImage *d, const iBlock *data) {
71 init_String(&d->mime); 72 init_String(&d->mime);
73 d->isPermanent = iFalse;
72 d->numBytes = size_Block(data); 74 d->numBytes = size_Block(data);
73 uint8_t *imgData = stbi_load_from_memory( 75 uint8_t *imgData = stbi_load_from_memory(
74 constData_Block(data), size_Block(data), &d->size.x, &d->size.y, NULL, 4); 76 constData_Block(data), size_Block(data), &d->size.x, &d->size.y, NULL, 4);
@@ -224,6 +226,12 @@ static iRangecc addLink_GmDocument_(iGmDocument *d, iRangecc line, iGmLinkId *li
224 else if (equalCase_Rangecc(parts.scheme, "data")) { 226 else if (equalCase_Rangecc(parts.scheme, "data")) {
225 link->flags |= data_GmLinkFlag; 227 link->flags |= data_GmLinkFlag;
226 } 228 }
229 else if (equalCase_Rangecc(parts.scheme, "about")) {
230 link->flags |= about_GmLinkFlag;
231 }
232 else if (equalCase_Rangecc(parts.scheme, "mailto")) {
233 link->flags |= mailto_GmLinkFlag;
234 }
227 /* Check the file name extension, if present. */ 235 /* Check the file name extension, if present. */
228 if (!isEmpty_Range(&parts.path)) { 236 if (!isEmpty_Range(&parts.path)) {
229 iString *path = newRange_String(parts.path); 237 iString *path = newRange_String(parts.path);
@@ -253,7 +261,7 @@ static iRangecc addLink_GmDocument_(iGmDocument *d, iRangecc line, iGmLinkId *li
253 trim_Rangecc(&desc); 261 trim_Rangecc(&desc);
254 if (!isEmpty_Range(&desc)) { 262 if (!isEmpty_Range(&desc)) {
255 line = desc; /* Just show the description. */ 263 line = desc; /* Just show the description. */
256 link->flags |= userFriendly_GmLinkFlag; 264 link->flags |= humanReadable_GmLinkFlag;
257 } 265 }
258 else { 266 else {
259 line = capturedRange_RegExpMatch(&m, 1); /* Show the URL. */ 267 line = capturedRange_RegExpMatch(&m, 1); /* Show the URL. */
@@ -316,10 +324,12 @@ static void doLayout_GmDocument_(iGmDocument *d) {
316 static const float bottomMargin[max_GmLineType] = { 324 static const float bottomMargin[max_GmLineType] = {
317 0.0f, 0.5f, 1.0f, 0.5f, 0.5f, 0.5f, 0.5f, 1.0f 325 0.0f, 0.5f, 1.0f, 0.5f, 0.5f, 0.5f, 0.5f, 1.0f
318 }; 326 };
319 static const char *arrow = "\u27a4"; // "\u2192"; 327 static const char *arrow = "\u27a4";
320 static const char *bullet = "\u2022"; 328 static const char *envelope = "\U0001f4e7";
321 static const char *folder = "\U0001f4c1"; 329 static const char *bullet = "\u2022";
322 static const char *globe = "\U0001f310"; 330 static const char *folder = "\U0001f4c1";
331 static const char *globe = "\U0001f310";
332 static const char *quote = "\u201c";
323 const float midRunSkip = 0; /*0.120f;*/ /* extra space between wrapped text/quote lines */ 333 const float midRunSkip = 0; /*0.120f;*/ /* extra space between wrapped text/quote lines */
324 clear_Array(&d->layout); 334 clear_Array(&d->layout);
325 clearLinks_GmDocument_(d); 335 clearLinks_GmDocument_(d);
@@ -332,12 +342,13 @@ static void doLayout_GmDocument_(iGmDocument *d) {
332 iRangecc contentLine = iNullRange; 342 iRangecc contentLine = iNullRange;
333 iInt2 pos = zero_I2(); 343 iInt2 pos = zero_I2();
334 iBool isFirstText = isGemini; 344 iBool isFirstText = isGemini;
345 iBool addQuoteIcon = iTrue;
335 iBool isPreformat = iFalse; 346 iBool isPreformat = iFalse;
336 iRangecc preAltText = iNullRange; 347 iRangecc preAltText = iNullRange;
337 int preFont = preformatted_FontId; 348 int preFont = preformatted_FontId;
338 iBool enableIndents = iFalse; 349 iBool enableIndents = iFalse;
339 iBool addSiteBanner = iTrue; 350 iBool addSiteBanner = iTrue;
340 enum iGmLineType prevType; 351 enum iGmLineType prevType = text_GmLineType;
341 if (d->format == plainText_GmDocumentFormat) { 352 if (d->format == plainText_GmDocumentFormat) {
342 isPreformat = iTrue; 353 isPreformat = iTrue;
343 isFirstText = iFalse; 354 isFirstText = iFalse;
@@ -351,9 +362,10 @@ static void doLayout_GmDocument_(iGmDocument *d) {
351 run.imageId = 0; 362 run.imageId = 0;
352 enum iGmLineType type; 363 enum iGmLineType type;
353 int indent = 0; 364 int indent = 0;
365 /* Detect the type of the line. */
354 if (!isPreformat) { 366 if (!isPreformat) {
355 type = lineType_GmDocument_(d, line); 367 type = lineType_GmDocument_(d, line);
356 if (line.start == content.start) { 368 if (contentLine.start == content.start) {
357 prevType = type; 369 prevType = type;
358 } 370 }
359 indent = indents[type]; 371 indent = indents[type];
@@ -389,6 +401,9 @@ static void doLayout_GmDocument_(iGmDocument *d) {
389 else { 401 else {
390 /* Preformatted line. */ 402 /* Preformatted line. */
391 type = preformatted_GmLineType; 403 type = preformatted_GmLineType;
404 if (contentLine.start == content.start) {
405 prevType = type;
406 }
392 if (d->format == gemini_GmDocumentFormat && 407 if (d->format == gemini_GmDocumentFormat &&
393 startsWithSc_Rangecc(line, "```", &iCaseSensitive)) { 408 startsWithSc_Rangecc(line, "```", &iCaseSensitive)) {
394 isPreformat = iFalse; 409 isPreformat = iFalse;
@@ -458,10 +473,30 @@ static void doLayout_GmDocument_(iGmDocument *d) {
458 bulRun.visBounds.size = advance_Text(run.font, bullet); 473 bulRun.visBounds.size = advance_Text(run.font, bullet);
459 bulRun.visBounds.pos.x -= 4 * gap_Text - width_Rect(bulRun.visBounds) / 2; 474 bulRun.visBounds.pos.x -= 4 * gap_Text - width_Rect(bulRun.visBounds) / 2;
460 bulRun.bounds = zero_Rect(); /* just visual */ 475 bulRun.bounds = zero_Rect(); /* just visual */
461 bulRun.text = range_CStr(bullet); 476 bulRun.text = range_CStr(bullet);
462 bulRun.flags |= decoration_GmRunFlag; 477 bulRun.flags |= decoration_GmRunFlag;
463 pushBack_Array(&d->layout, &bulRun); 478 pushBack_Array(&d->layout, &bulRun);
464 } 479 }
480 /* Quote icon. */
481 if (type == quote_GmLineType && addQuoteIcon) {
482 addQuoteIcon = iFalse;
483 iGmRun quoteRun = run;
484 quoteRun.font = heading1_FontId;
485 quoteRun.text = range_CStr(quote);
486 quoteRun.color = tmQuoteIcon_ColorId;
487 iRect vis = visualBounds_Text(quoteRun.font, quoteRun.text);
488 quoteRun.visBounds.size = advance_Text(quoteRun.font, quote);
489 quoteRun.visBounds.pos =
490 add_I2(pos,
491 init_I2(indents[text_GmLineType] * gap_Text,
492 lineHeight_Text(quote_FontId) / 2 - bottom_Rect(vis)));
493 quoteRun.bounds = zero_Rect(); /* just visual */
494 quoteRun.flags |= decoration_GmRunFlag;
495 pushBack_Array(&d->layout, &quoteRun);
496 }
497 else if (type != quote_GmLineType) {
498 addQuoteIcon = iTrue;
499 }
465 /* Link icon. */ 500 /* Link icon. */
466 if (type == link_GmLineType) { 501 if (type == link_GmLineType) {
467 iGmRun icon = run; 502 iGmRun icon = run;
@@ -471,7 +506,9 @@ static void doLayout_GmDocument_(iGmDocument *d) {
471 const iGmLink *link = constAt_PtrArray(&d->links, run.linkId - 1); 506 const iGmLink *link = constAt_PtrArray(&d->links, run.linkId - 1);
472 icon.text = range_CStr(link->flags & file_GmLinkFlag 507 icon.text = range_CStr(link->flags & file_GmLinkFlag
473 ? folder 508 ? folder
474 : link->flags & remote_GmLinkFlag ? globe : arrow); 509 : link->flags & mailto_GmLinkFlag
510 ? envelope
511 : link->flags & remote_GmLinkFlag ? globe : arrow);
475 if (link->flags & remote_GmLinkFlag) { 512 if (link->flags & remote_GmLinkFlag) {
476 icon.visBounds.pos.x -= gap_Text / 2; 513 icon.visBounds.pos.x -= gap_Text / 2;
477 } 514 }
@@ -536,8 +573,14 @@ static void doLayout_GmDocument_(iGmDocument *d) {
536 if (type == link_GmLineType) { 573 if (type == link_GmLineType) {
537 const size_t imgIndex = findLinkImage_GmDocument_(d, run.linkId); 574 const size_t imgIndex = findLinkImage_GmDocument_(d, run.linkId);
538 if (imgIndex != iInvalidPos) { 575 if (imgIndex != iInvalidPos) {
539 ((iGmLink *) at_PtrArray(&d->links, run.linkId - 1))->flags |= content_GmLinkFlag;
540 const iGmImage *img = constAt_PtrArray(&d->images, imgIndex); 576 const iGmImage *img = constAt_PtrArray(&d->images, imgIndex);
577 /* Mark the link as having content. */ {
578 iGmLink *link = at_PtrArray(&d->links, run.linkId - 1);
579 link->flags |= content_GmLinkFlag;
580 if (img->isPermanent) {
581 link->flags |= permanent_GmLinkFlag;
582 }
583 }
541 const int margin = 0.5f * lineHeight_Text(paragraph_FontId); 584 const int margin = 0.5f * lineHeight_Text(paragraph_FontId);
542 pos.y += margin; 585 pos.y += margin;
543 run.bounds.pos = pos; 586 run.bounds.pos = pos;
@@ -854,6 +897,8 @@ void setThemeSeed_GmDocument(iGmDocument *d, const iBlock *seed) {
854 setHsl_Color(i, color); 897 setHsl_Color(i, color);
855 } 898 }
856 } 899 }
900 set_Color(tmQuoteIcon_ColorId,
901 mix_Color(get_Color(tmQuote_ColorId), get_Color(tmBackground_ColorId), 0.55f));
857 /* Special exceptions. */ 902 /* Special exceptions. */
858 if (seed) { 903 if (seed) {
859 if (equal_CStr(cstr_Block(seed), "gemini.circumlunar.space")) { 904 if (equal_CStr(cstr_Block(seed), "gemini.circumlunar.space")) {
@@ -946,7 +991,8 @@ void setSource_GmDocument(iGmDocument *d, const iString *source, int width, int
946 setWidth_GmDocument(d, width, forceBreakWidth); /* re-do layout */ 991 setWidth_GmDocument(d, width, forceBreakWidth); /* re-do layout */
947} 992}
948 993
949void setImage_GmDocument(iGmDocument *d, iGmLinkId linkId, const iString *mime, const iBlock *data) { 994void setImage_GmDocument(iGmDocument *d, iGmLinkId linkId, const iString *mime, const iBlock *data,
995 iBool allowHide) {
950 if (!mime || !data) { 996 if (!mime || !data) {
951 iGmImage *img; 997 iGmImage *img;
952 if (take_PtrArray(&d->images, findLinkImage_GmDocument_(d, linkId), (void **) &img)) { 998 if (take_PtrArray(&d->images, findLinkImage_GmDocument_(d, linkId), (void **) &img)) {
@@ -955,9 +1001,10 @@ void setImage_GmDocument(iGmDocument *d, iGmLinkId linkId, const iString *mime,
955 } 1001 }
956 else { 1002 else {
957 /* TODO: check if we know this MIME type */ 1003 /* TODO: check if we know this MIME type */
958 /* Load the image. */ { 1004 /* Upload the image. */ {
959 iGmImage *img = new_GmImage(data); 1005 iGmImage *img = new_GmImage(data);
960 img->linkId = linkId; /* TODO: use a hash? */ 1006 img->linkId = linkId; /* TODO: use a hash? */
1007 img->isPermanent = !allowHide;
961 set_String(&img->mime, mime); 1008 set_String(&img->mime, mime);
962 if (img->texture) { 1009 if (img->texture) {
963 pushBack_PtrArray(&d->images, img); 1010 pushBack_PtrArray(&d->images, img);
@@ -1098,6 +1145,7 @@ uint16_t linkImage_GmDocument(const iGmDocument *d, iGmLinkId linkId) {
1098 1145
1099enum iColorId linkColor_GmDocument(const iGmDocument *d, iGmLinkId linkId, enum iGmLinkPart part) { 1146enum iColorId linkColor_GmDocument(const iGmDocument *d, iGmLinkId linkId, enum iGmLinkPart part) {
1100 const iGmLink *link = link_GmDocument_(d, linkId); 1147 const iGmLink *link = link_GmDocument_(d, linkId);
1148 const int www_GmLinkFlag = http_GmLinkFlag | mailto_GmLinkFlag;
1101 if (link) { 1149 if (link) {
1102 const iBool isBad = (link->flags & supportedProtocol_GmLinkFlag) == 0; 1150 const iBool isBad = (link->flags & supportedProtocol_GmLinkFlag) == 0;
1103 if (part == icon_GmLinkPart) { 1151 if (part == icon_GmLinkPart) {
@@ -1105,24 +1153,24 @@ enum iColorId linkColor_GmDocument(const iGmDocument *d, iGmLinkId linkId, enum
1105 return tmBadLink_ColorId; 1153 return tmBadLink_ColorId;
1106 } 1154 }
1107 if (link->flags & visited_GmLinkFlag) { 1155 if (link->flags & visited_GmLinkFlag) {
1108 return link->flags & http_GmLinkFlag 1156 return link->flags & www_GmLinkFlag
1109 ? tmHypertextLinkIconVisited_ColorId 1157 ? tmHypertextLinkIconVisited_ColorId
1110 : link->flags & gopher_GmLinkFlag ? tmGopherLinkIconVisited_ColorId 1158 : link->flags & gopher_GmLinkFlag ? tmGopherLinkIconVisited_ColorId
1111 : tmLinkIconVisited_ColorId; 1159 : tmLinkIconVisited_ColorId;
1112 } 1160 }
1113 return link->flags & http_GmLinkFlag 1161 return link->flags & www_GmLinkFlag
1114 ? tmHypertextLinkIcon_ColorId 1162 ? tmHypertextLinkIcon_ColorId
1115 : link->flags & gopher_GmLinkFlag ? tmGopherLinkIcon_ColorId 1163 : link->flags & gopher_GmLinkFlag ? tmGopherLinkIcon_ColorId
1116 : tmLinkIcon_ColorId; 1164 : tmLinkIcon_ColorId;
1117 } 1165 }
1118 if (part == text_GmLinkPart) { 1166 if (part == text_GmLinkPart) {
1119 return link->flags & http_GmLinkFlag 1167 return link->flags & www_GmLinkFlag
1120 ? tmHypertextLinkText_ColorId 1168 ? tmHypertextLinkText_ColorId
1121 : link->flags & gopher_GmLinkFlag ? tmGopherLinkText_ColorId 1169 : link->flags & gopher_GmLinkFlag ? tmGopherLinkText_ColorId
1122 : tmLinkText_ColorId; 1170 : tmLinkText_ColorId;
1123 } 1171 }
1124 if (part == textHover_GmLinkPart) { 1172 if (part == textHover_GmLinkPart) {
1125 return link->flags & http_GmLinkFlag 1173 return link->flags & www_GmLinkFlag
1126 ? tmHypertextLinkTextHover_ColorId 1174 ? tmHypertextLinkTextHover_ColorId
1127 : link->flags & gopher_GmLinkFlag ? tmGopherLinkTextHover_ColorId 1175 : link->flags & gopher_GmLinkFlag ? tmGopherLinkTextHover_ColorId
1128 : tmLinkTextHover_ColorId; 1176 : tmLinkTextHover_ColorId;
@@ -1131,13 +1179,13 @@ enum iColorId linkColor_GmDocument(const iGmDocument *d, iGmLinkId linkId, enum
1131 if (isBad) { 1179 if (isBad) {
1132 return tmBadLink_ColorId; 1180 return tmBadLink_ColorId;
1133 } 1181 }
1134 return link->flags & http_GmLinkFlag 1182 return link->flags & www_GmLinkFlag
1135 ? tmHypertextLinkDomain_ColorId 1183 ? tmHypertextLinkDomain_ColorId
1136 : link->flags & gopher_GmLinkFlag ? tmGopherLinkDomain_ColorId 1184 : link->flags & gopher_GmLinkFlag ? tmGopherLinkDomain_ColorId
1137 : tmLinkDomain_ColorId; 1185 : tmLinkDomain_ColorId;
1138 } 1186 }
1139 if (part == visited_GmLinkPart) { 1187 if (part == visited_GmLinkPart) {
1140 return link->flags & http_GmLinkFlag 1188 return link->flags & www_GmLinkFlag
1141 ? tmHypertextLinkLastVisitDate_ColorId 1189 ? tmHypertextLinkLastVisitDate_ColorId
1142 : link->flags & gopher_GmLinkFlag ? tmGopherLinkLastVisitDate_ColorId 1190 : link->flags & gopher_GmLinkFlag ? tmGopherLinkLastVisitDate_ColorId
1143 : tmLinkLastVisitDate_ColorId; 1191 : tmLinkLastVisitDate_ColorId;
diff --git a/src/gmdocument.h b/src/gmdocument.h
index e3c21097..b453fba9 100644
--- a/src/gmdocument.h
+++ b/src/gmdocument.h
@@ -43,13 +43,16 @@ enum iGmLinkFlags {
43 http_GmLinkFlag = iBit(3), 43 http_GmLinkFlag = iBit(3),
44 file_GmLinkFlag = iBit(4), 44 file_GmLinkFlag = iBit(4),
45 data_GmLinkFlag = iBit(5), 45 data_GmLinkFlag = iBit(5),
46 supportedProtocol_GmLinkFlag = 0x1f, 46 about_GmLinkFlag = iBit(6),
47 mailto_GmLinkFlag = iBit(7),
48 supportedProtocol_GmLinkFlag = 0xff,
47 remote_GmLinkFlag = iBit(9), 49 remote_GmLinkFlag = iBit(9),
48 userFriendly_GmLinkFlag = iBit(10), 50 humanReadable_GmLinkFlag = iBit(10), /* link has a human-readable description */
49 imageFileExtension_GmLinkFlag = iBit(11), 51 imageFileExtension_GmLinkFlag = iBit(11),
50 audioFileExtension_GmLinkFlag = iBit(12), 52 audioFileExtension_GmLinkFlag = iBit(12),
51 content_GmLinkFlag = iBit(13), /* content visible below */ 53 content_GmLinkFlag = iBit(13), /* content visible below */
52 visited_GmLinkFlag = iBit(14), /* in the history */ 54 visited_GmLinkFlag = iBit(14), /* in the history */
55 permanent_GmLinkFlag = iBit(15), /* content cannot be dismissed; media link */
53}; 56};
54 57
55struct Impl_GmImageInfo { 58struct Impl_GmImageInfo {
@@ -97,7 +100,8 @@ void setFormat_GmDocument (iGmDocument *, enum iGmDocumentFormat format);
97void setWidth_GmDocument (iGmDocument *, int width, int forceBreakWidth); 100void setWidth_GmDocument (iGmDocument *, int width, int forceBreakWidth);
98void setUrl_GmDocument (iGmDocument *, const iString *url); 101void setUrl_GmDocument (iGmDocument *, const iString *url);
99void setSource_GmDocument (iGmDocument *, const iString *source, int width, int forceBreakWidth); 102void setSource_GmDocument (iGmDocument *, const iString *source, int width, int forceBreakWidth);
100void setImage_GmDocument (iGmDocument *, iGmLinkId linkId, const iString *mime, const iBlock *data); 103void setImage_GmDocument (iGmDocument *, iGmLinkId linkId, const iString *mime, const iBlock *data,
104 iBool allowHide);
101 105
102void reset_GmDocument (iGmDocument *); /* free images */ 106void reset_GmDocument (iGmDocument *); /* free images */
103 107
diff --git a/src/gmrequest.c b/src/gmrequest.c
index 843d2f46..43cc874a 100644
--- a/src/gmrequest.c
+++ b/src/gmrequest.c
@@ -228,7 +228,6 @@ static void readIncoming_GmRequest_(iAnyObject *obj) {
228 lock_Mutex(&d->mutex); 228 lock_Mutex(&d->mutex);
229 iAssert(d->state != finished_GmRequestState); /* notifications out of order? */ 229 iAssert(d->state != finished_GmRequestState); /* notifications out of order? */
230 iBlock *data = readAll_TlsRequest(d->req); 230 iBlock *data = readAll_TlsRequest(d->req);
231 fflush(stdout);
232 if (d->state == receivingHeader_GmRequestState) { 231 if (d->state == receivingHeader_GmRequestState) {
233 appendCStrN_String(&d->resp.meta, constData_Block(data), size_Block(data)); 232 appendCStrN_String(&d->resp.meta, constData_Block(data), size_Block(data));
234 /* Check if the header line is complete. */ 233 /* Check if the header line is complete. */
@@ -305,6 +304,9 @@ static const iBlock *aboutPageSource_(iRangecc path) {
305 if (equalCase_Rangecc(path, "help")) { 304 if (equalCase_Rangecc(path, "help")) {
306 return &blobHelp_Embedded; 305 return &blobHelp_Embedded;
307 } 306 }
307 if (equalCase_Rangecc(path, "license")) {
308 return &blobLicense_Embedded;
309 }
308 if (equalCase_Rangecc(path, "version")) { 310 if (equalCase_Rangecc(path, "version")) {
309 return &blobVersion_Embedded; 311 return &blobVersion_Embedded;
310 } 312 }
diff --git a/src/gmutil.c b/src/gmutil.c
index 131734b2..f55729d1 100644
--- a/src/gmutil.c
+++ b/src/gmutil.c
@@ -132,7 +132,8 @@ const iString *absoluteUrl_String(const iString *d, const iString *urlMaybeRelat
132 iUrl rel; 132 iUrl rel;
133 init_Url(&orig, d); 133 init_Url(&orig, d);
134 init_Url(&rel, urlMaybeRelative); 134 init_Url(&rel, urlMaybeRelative);
135 if (equalCase_Rangecc(rel.scheme, "data") || equalCase_Rangecc(rel.scheme, "about")) { 135 if (equalCase_Rangecc(rel.scheme, "data") || equalCase_Rangecc(rel.scheme, "about") ||
136 equalCase_Rangecc(rel.scheme, "mailto")) {
136 /* Special case, the contents should be left unparsed. */ 137 /* Special case, the contents should be left unparsed. */
137 return urlMaybeRelative; 138 return urlMaybeRelative;
138 } 139 }
@@ -214,8 +215,7 @@ static const struct {
214 { unsupportedMimeType_GmStatusCode, 215 { unsupportedMimeType_GmStatusCode,
215 { 0x1f47d, /* alien */ 216 { 0x1f47d, /* alien */
216 "Unsupported MIME Type", 217 "Unsupported MIME Type",
217 "The received content is in an unsupported format and cannot be viewed with " 218 "The received content cannot be viewed with this application." } },
218 "this application." } },
219 { invalidHeader_GmStatusCode, 219 { invalidHeader_GmStatusCode,
220 { 0x1f4a9, /* pile of poo */ 220 { 0x1f4a9, /* pile of poo */
221 "Invalid Header", 221 "Invalid Header",
diff --git a/src/macos.m b/src/macos.m
index bbbaaa19..b000fed9 100644
--- a/src/macos.m
+++ b/src/macos.m
@@ -28,38 +28,19 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
28 28
29#import <AppKit/AppKit.h> 29#import <AppKit/AppKit.h>
30 30
31#if 0 31static NSTouchBarItemIdentifier goBack_TouchId_ = @"fi.skyjake.Lagrange.back";
32static NSTouchBarItemIdentifier play_TouchId_ = @"fi.skyjake.BitwiseHarmony.play"; 32static NSTouchBarItemIdentifier goForward_TouchId_ = @"fi.skyjake.Lagrange.forward";
33static NSTouchBarItemIdentifier restart_TouchId_ = @"fi.skyjake.BitwiseHarmony.restart"; 33static NSTouchBarItemIdentifier find_TouchId_ = @"fi.skyjake.Lagrange.find";
34 34static NSTouchBarItemIdentifier newTab_TouchId_ = @"fi.skyjake.Lagrange.tabs.new";
35static NSTouchBarItemIdentifier seqMoveUp_TouchId_ = @"fi.skyjake.BitwiseHarmony.sequence.move.up"; 35static NSTouchBarItemIdentifier sidebarMode_TouchId_ = @"fi.skyjake.Lagrange.sidebar.mode";
36static NSTouchBarItemIdentifier seqMoveDown_TouchId_ = @"fi.skyjake.BitwiseHarmony.sequence.move.down";
37
38static NSTouchBarItemIdentifier goto_TouchId_ = @"fi.skyjake.BitwiseHarmony.goto";
39static NSTouchBarItemIdentifier mute_TouchId_ = @"fi.skyjake.BitwiseHarmony.mute";
40static NSTouchBarItemIdentifier solo_TouchId_ = @"fi.skyjake.BitwiseHarmony.solo";
41static NSTouchBarItemIdentifier color_TouchId_ = @"fi.skyjake.BitwiseHarmony.color";
42static NSTouchBarItemIdentifier event_TouchId_ = @"fi.skyjake.BitwiseHarmony.event";
43
44static NSTouchBarItemIdentifier eventList_TouchId_ = @"fi.skyjake.BitwiseHarmony.eventlist";
45static NSTouchBarItemIdentifier masterGainEvent_TouchId_ = @"fi.skyjake.BitwiseHarmony.event.mastergain";
46static NSTouchBarItemIdentifier resetEvent_TouchId_ = @"fi.skyjake.BitwiseHarmony.event.reset";
47static NSTouchBarItemIdentifier voiceEvent_TouchId_ = @"fi.skyjake.BitwiseHarmony.event.voice";
48static NSTouchBarItemIdentifier panEvent_TouchId_ = @"fi.skyjake.BitwiseHarmony.event.pan";
49static NSTouchBarItemIdentifier gainEvent_TouchId_ = @"fi.skyjake.BitwiseHarmony.event.gain";
50static NSTouchBarItemIdentifier fadeEvent_TouchId_ = @"fi.skyjake.BitwiseHarmony.event.fade";
51static NSTouchBarItemIdentifier pitchSpeedEvent_TouchId_ = @"fi.skyjake.BitwiseHarmony.event.pitchspeed";
52static NSTouchBarItemIdentifier pitchBendUpEvent_TouchId_ = @"fi.skyjake.BitwiseHarmony.event.pitchbendup";
53static NSTouchBarItemIdentifier pitchBendDownEvent_TouchId_ = @"fi.skyjake.BitwiseHarmony.event.pitchbenddown";
54static NSTouchBarItemIdentifier tremoloEvent_TouchId_ = @"fi.skyjake.BitwiseHarmony.event.tremolo";
55#endif
56 36
57enum iTouchBarVariant { 37enum iTouchBarVariant {
58 default_TouchBarVariant, 38 default_TouchBarVariant,
59}; 39};
60 40
61#if 0 41/*----------------------------------------------------------------------------------------------*/
62@interface CommandButton : NSButtonTouchBarItem { 42
43@interface CommandButton : NSCustomTouchBarItem {
63 NSString *command; 44 NSString *command;
64 iWidget *widget; 45 iWidget *widget;
65} 46}
@@ -70,6 +51,10 @@ enum iTouchBarVariant {
70 title:(NSString *)title 51 title:(NSString *)title
71 widget:(iWidget *)widget 52 widget:(iWidget *)widget
72 command:(NSString *)cmd; 53 command:(NSString *)cmd;
54- (id)initWithIdentifier:(NSTouchBarItemIdentifier)identifier
55 image:(NSImage *)image
56 widget:(iWidget *)widget
57 command:(NSString *)cmd;
73- (void)dealloc; 58- (void)dealloc;
74@end 59@end
75 60
@@ -79,9 +64,17 @@ enum iTouchBarVariant {
79 title:(NSString *)title 64 title:(NSString *)title
80 command:(NSString *)cmd { 65 command:(NSString *)cmd {
81 [super initWithIdentifier:identifier]; 66 [super initWithIdentifier:identifier];
82 self.title = title; 67 self.view = [NSButton buttonWithTitle:title target:self action:@selector(buttonPressed)];
83 self.target = self; 68 command = cmd;
84 self.action = @selector(buttonPressed); 69 return self;
70}
71
72- (id)initWithIdentifier:(NSTouchBarItemIdentifier)identifier
73 image:(NSImage *)image
74 widget:(iWidget *)widget
75 command:(NSString *)cmd {
76 [super initWithIdentifier:identifier];
77 self.view = [NSButton buttonWithImage:image target:self action:@selector(buttonPressed)];
85 command = cmd; 78 command = cmd;
86 return self; 79 return self;
87} 80}
@@ -111,7 +104,8 @@ enum iTouchBarVariant {
111} 104}
112 105
113@end 106@end
114#endif 107
108/*----------------------------------------------------------------------------------------------*/
115 109
116@interface MyDelegate : NSResponder<NSApplicationDelegate, NSTouchBarDelegate> { 110@interface MyDelegate : NSResponder<NSApplicationDelegate, NSTouchBarDelegate> {
117 enum iTouchBarVariant touchBarVariant; 111 enum iTouchBarVariant touchBarVariant;
@@ -120,7 +114,7 @@ enum iTouchBarVariant {
120 NSMutableDictionary<NSString *, NSString*> *menuCommands; 114 NSMutableDictionary<NSString *, NSString*> *menuCommands;
121} 115}
122- (id)initWithSDLDelegate:(NSObject<NSApplicationDelegate> *)sdl; 116- (id)initWithSDLDelegate:(NSObject<NSApplicationDelegate> *)sdl;
123//- (NSTouchBar *)makeTouchBar; 117- (NSTouchBar *)makeTouchBar;
124- (BOOL)application:(NSApplication *)app openFile:(NSString *)filename; 118- (BOOL)application:(NSApplication *)app openFile:(NSString *)filename;
125- (void)application:(NSApplication *)app openFiles:(NSArray<NSString *> *)filenames; 119- (void)application:(NSApplication *)app openFiles:(NSArray<NSString *> *)filenames;
126- (void)application:(NSApplication *)app openURLs:(NSArray<NSURL *> *)urls; 120- (void)application:(NSApplication *)app openURLs:(NSArray<NSURL *> *)urls;
@@ -153,8 +147,6 @@ static void appearanceChanged_MacOS_(NSString *name) {
153 const iBool isDark = [name containsString:@"Dark"]; 147 const iBool isDark = [name containsString:@"Dark"];
154 const iBool isHighContrast = [name containsString:@"HighContrast"]; 148 const iBool isHighContrast = [name containsString:@"HighContrast"];
155 postCommandf_App("~os.theme.changed dark:%d contrast:%d", isDark ? 1 : 0, isHighContrast ? 1 : 0); 149 postCommandf_App("~os.theme.changed dark:%d contrast:%d", isDark ? 1 : 0, isHighContrast ? 1 : 0);
156// printf("Effective appearance changed: %s\n", [name cStringUsingEncoding:NSUTF8StringEncoding]);
157// fflush(stdout);
158} 150}
159 151
160- (void)setAppearance:(NSString *)name { 152- (void)setAppearance:(NSString *)name {
@@ -204,42 +196,19 @@ static void appearanceChanged_MacOS_(NSString *name) {
204 } 196 }
205} 197}
206 198
207#if 0 199#if 1
208- (NSTouchBar *)makeTouchBar { 200- (NSTouchBar *)makeTouchBar {
209 NSTouchBar *bar = [[NSTouchBar alloc] init]; 201 NSTouchBar *bar = [[NSTouchBar alloc] init];
210 bar.delegate = self; 202 bar.delegate = self;
211 switch (touchBarVariant) { 203 switch (touchBarVariant) {
212 case default_TouchBarVariant: 204 case default_TouchBarVariant:
213 bar.defaultItemIdentifiers = @[ play_TouchId_, restart_TouchId_, 205 bar.defaultItemIdentifiers = @[ goBack_TouchId_, goForward_TouchId_,
214 NSTouchBarItemIdentifierFixedSpaceSmall, 206 NSTouchBarItemIdentifierFixedSpaceSmall,
215 NSTouchBarItemIdentifierOtherItemsProxy ]; 207 find_TouchId_,
216 break;
217 case sequence_TouchBarVariant:
218 bar.defaultItemIdentifiers = @[ play_TouchId_, restart_TouchId_,
219 NSTouchBarItemIdentifierFlexibleSpace,
220 seqMoveUp_TouchId_, seqMoveDown_TouchId_,
221 NSTouchBarItemIdentifierFlexibleSpace,
222 NSTouchBarItemIdentifierOtherItemsProxy];
223 break;
224 case tracker_TouchBarVariant:
225 bar.defaultItemIdentifiers = @[ play_TouchId_, restart_TouchId_,
226 NSTouchBarItemIdentifierFlexibleSpace, 208 NSTouchBarItemIdentifierFlexibleSpace,
227 goto_TouchId_, 209 sidebarMode_TouchId_,
228 event_TouchId_,
229 NSTouchBarItemIdentifierFlexibleSpace,
230 solo_TouchId_, mute_TouchId_, color_TouchId_,
231 NSTouchBarItemIdentifierFlexibleSpace,
232 NSTouchBarItemIdentifierOtherItemsProxy ];
233 break;
234 case wide_TouchBarVariant:
235 bar.defaultItemIdentifiers = @[ play_TouchId_, restart_TouchId_,
236 NSTouchBarItemIdentifierFlexibleSpace,
237 event_TouchId_,
238 NSTouchBarItemIdentifierFlexibleSpace,
239 solo_TouchId_, mute_TouchId_, color_TouchId_,
240 NSTouchBarItemIdentifierFlexibleSpace,
241 seqMoveUp_TouchId_, seqMoveDown_TouchId_,
242 NSTouchBarItemIdentifierFlexibleSpace, 210 NSTouchBarItemIdentifierFlexibleSpace,
211 newTab_TouchId_,
243 NSTouchBarItemIdentifierOtherItemsProxy ]; 212 NSTouchBarItemIdentifierOtherItemsProxy ];
244 break; 213 break;
245 } 214 }
@@ -262,9 +231,48 @@ static void appearanceChanged_MacOS_(NSString *name) {
262 } 231 }
263} 232}
264 233
234- (void)sidebarModePressed:(id)sender {
235 NSSegmentedControl *seg = sender;
236 postCommandf_App("sidebar.mode arg:%d toggle:1", (int) [seg selectedSegment]);
237}
238
265- (nullable NSTouchBarItem *)touchBar:(NSTouchBar *)touchBar 239- (nullable NSTouchBarItem *)touchBar:(NSTouchBar *)touchBar
266 makeItemForIdentifier:(NSTouchBarItemIdentifier)identifier { 240 makeItemForIdentifier:(NSTouchBarItemIdentifier)identifier {
267 iUnused(touchBar); 241 iUnused(touchBar);
242 if ([identifier isEqualToString:goBack_TouchId_]) {
243 return [[CommandButton alloc] initWithIdentifier:identifier
244 image:[NSImage imageNamed:NSImageNameTouchBarGoBackTemplate]
245 widget:nil
246 command:@"navigate.back"];
247 }
248 else if ([identifier isEqualToString:goForward_TouchId_]) {
249 return [[CommandButton alloc] initWithIdentifier:identifier
250 image:[NSImage imageNamed:NSImageNameTouchBarGoForwardTemplate]
251 widget:nil
252 command:@"navigate.forward"];
253 }
254 else if ([identifier isEqualToString:find_TouchId_]) {
255 return [[CommandButton alloc] initWithIdentifier:identifier
256 image:[NSImage imageNamed:NSImageNameTouchBarSearchTemplate]
257 widget:nil
258 command:@"focus.set id:find.input"];
259 }
260 else if ([identifier isEqualToString:sidebarMode_TouchId_]) {
261 NSCustomTouchBarItem *item = [[NSCustomTouchBarItem alloc] initWithIdentifier:sidebarMode_TouchId_];
262 NSSegmentedControl *seg =
263 [NSSegmentedControl segmentedControlWithLabels:@[ @"Bookmarks", @"History", @"Identities", @"Outline"]
264 trackingMode:NSSegmentSwitchTrackingMomentary
265 target:[[NSApplication sharedApplication] delegate]
266 action:@selector(sidebarModePressed:)];
267 item.view = seg;
268 return item;
269 }
270 else if ([identifier isEqualToString:newTab_TouchId_]) {
271 return [[CommandButton alloc] initWithIdentifier:identifier
272 image:[NSImage imageNamed:NSImageNameTouchBarAddTemplate]
273 widget:nil
274 command:@"tabs.new"];
275 }
268#if 0 276#if 0
269 if ([identifier isEqualToString:play_TouchId_]) { 277 if ([identifier isEqualToString:play_TouchId_]) {
270 return [NSButtonTouchBarItem 278 return [NSButtonTouchBarItem
@@ -524,18 +532,7 @@ void handleCommand_MacOS(const char *cmd) {
524 if (equal_Command(cmd, "tabs.changed")) { 532 if (equal_Command(cmd, "tabs.changed")) {
525 MyDelegate *myDel = (MyDelegate *) [[NSApplication sharedApplication] delegate]; 533 MyDelegate *myDel = (MyDelegate *) [[NSApplication sharedApplication] delegate];
526 const char *tabId = suffixPtr_Command(cmd, "id"); 534 const char *tabId = suffixPtr_Command(cmd, "id");
527 if (equal_CStr(tabId, "tracker")) { 535 [myDel setTouchBarVariant:default_TouchBarVariant];
528 [myDel setTouchBarVariant:tracker_TouchBarVariant];
529 }
530 else if (equal_CStr(tabId, "sequence")) {
531 [myDel setTouchBarVariant:sequence_TouchBarVariant];
532 }
533 else if (equal_CStr(tabId, "trackertab")) {
534 [myDel setTouchBarVariant:wide_TouchBarVariant];
535 }
536 else {
537 [myDel setTouchBarVariant:default_TouchBarVariant];
538 }
539 } 536 }
540#endif 537#endif
541} 538}
diff --git a/src/ui/color.h b/src/ui/color.h
index 2c481d13..51d3370f 100644
--- a/src/ui/color.h
+++ b/src/ui/color.h
@@ -48,7 +48,7 @@ enum iColorId {
48 gray50_ColorId, 48 gray50_ColorId,
49 gray75_ColorId, 49 gray75_ColorId,
50 white_ColorId, 50 white_ColorId,
51 brown_ColorId, 51 brown_ColorId,
52 orange_ColorId, 52 orange_ColorId,
53 teal_ColorId, 53 teal_ColorId,
54 cyan_ColorId, 54 cyan_ColorId,
@@ -109,6 +109,7 @@ enum iColorId {
109 tmParagraph_ColorId, 109 tmParagraph_ColorId,
110 tmFirstParagraph_ColorId, 110 tmFirstParagraph_ColorId,
111 tmQuote_ColorId, 111 tmQuote_ColorId,
112 tmQuoteIcon_ColorId,
112 tmPreformatted_ColorId, 113 tmPreformatted_ColorId,
113 tmHeading1_ColorId, 114 tmHeading1_ColorId,
114 tmHeading2_ColorId, 115 tmHeading2_ColorId,
diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c
index fce548b4..70e66180 100644
--- a/src/ui/documentwidget.c
+++ b/src/ui/documentwidget.c
@@ -36,6 +36,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
36#include "../gmutil.h" 36#include "../gmutil.h"
37 37
38#include <the_Foundation/file.h> 38#include <the_Foundation/file.h>
39#include <the_Foundation/fileinfo.h>
39#include <the_Foundation/objectlist.h> 40#include <the_Foundation/objectlist.h>
40#include <the_Foundation/path.h> 41#include <the_Foundation/path.h>
41#include <the_Foundation/ptrarray.h> 42#include <the_Foundation/ptrarray.h>
@@ -46,6 +47,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
46#include <SDL_timer.h> 47#include <SDL_timer.h>
47#include <SDL_render.h> 48#include <SDL_render.h>
48#include <ctype.h> 49#include <ctype.h>
50#include <errno.h>
49 51
50iDeclareClass(MediaRequest) 52iDeclareClass(MediaRequest)
51 53
@@ -54,16 +56,12 @@ struct Impl_MediaRequest {
54 iDocumentWidget *doc; 56 iDocumentWidget *doc;
55 iGmLinkId linkId; 57 iGmLinkId linkId;
56 iGmRequest * req; 58 iGmRequest * req;
57 iAtomicInt isUpdated;
58}; 59};
59 60
60static void updated_MediaRequest_(iAnyObject *obj) { 61static void updated_MediaRequest_(iAnyObject *obj) {
61 iMediaRequest *d = obj; 62 iMediaRequest *d = obj;
62 int wasUpdated = exchange_Atomic(&d->isUpdated, iTrue);
63 if (!wasUpdated) {
64 postCommandf_App("media.updated link:%u request:%p", d->linkId, d); 63 postCommandf_App("media.updated link:%u request:%p", d->linkId, d);
65 } 64 }
66}
67 65
68static void finished_MediaRequest_(iAnyObject *obj) { 66static void finished_MediaRequest_(iAnyObject *obj) {
69 iMediaRequest *d = obj; 67 iMediaRequest *d = obj;
@@ -77,7 +75,6 @@ void init_MediaRequest(iMediaRequest *d, iDocumentWidget *doc, iGmLinkId linkId,
77 setUrl_GmRequest(d->req, url); 75 setUrl_GmRequest(d->req, url);
78 iConnect(GmRequest, d->req, updated, d, updated_MediaRequest_); 76 iConnect(GmRequest, d->req, updated, d, updated_MediaRequest_);
79 iConnect(GmRequest, d->req, finished, d, finished_MediaRequest_); 77 iConnect(GmRequest, d->req, finished, d, finished_MediaRequest_);
80 set_Atomic(&d->isUpdated, iFalse);
81 submit_GmRequest(d->req); 78 submit_GmRequest(d->req);
82} 79}
83 80
@@ -105,8 +102,8 @@ struct Impl_Model {
105}; 102};
106 103
107void init_Model(iModel *d) { 104void init_Model(iModel *d) {
108 d->history = new_History(); 105 d->history = new_History();
109 d->url = new_String(); 106 d->url = new_String();
110} 107}
111 108
112void deinit_Model(iModel *d) { 109void deinit_Model(iModel *d) {
@@ -147,6 +144,8 @@ struct Impl_DocumentWidget {
147 iGmRequest * request; 144 iGmRequest * request;
148 iAtomicInt isRequestUpdated; /* request has new content, need to parse it */ 145 iAtomicInt isRequestUpdated; /* request has new content, need to parse it */
149 iObjectList * media; 146 iObjectList * media;
147 iString sourceMime;
148 iBlock sourceContent; /* original content as received, for saving */
150 iGmDocument * doc; 149 iGmDocument * doc;
151 int certFlags; 150 int certFlags;
152 iDate certExpiry; 151 iDate certExpiry;
@@ -208,6 +207,8 @@ void init_DocumentWidget(iDocumentWidget *d) {
208 d->showLinkNumbers = iFalse; 207 d->showLinkNumbers = iFalse;
209 d->visBuf = new_VisBuf(); 208 d->visBuf = new_VisBuf();
210 d->invalidRuns = new_PtrSet(); 209 d->invalidRuns = new_PtrSet();
210 init_String(&d->sourceMime);
211 init_Block(&d->sourceContent, 0);
211 init_PtrArray(&d->visibleLinks); 212 init_PtrArray(&d->visibleLinks);
212 init_Click(&d->click, d, SDL_BUTTON_LEFT); 213 init_Click(&d->click, d, SDL_BUTTON_LEFT);
213 addChild_Widget(w, iClob(d->scroll = new_ScrollWidget())); 214 addChild_Widget(w, iClob(d->scroll = new_ScrollWidget()));
@@ -225,6 +226,8 @@ void deinit_DocumentWidget(iDocumentWidget *d) {
225 delete_PtrSet(d->invalidRuns); 226 delete_PtrSet(d->invalidRuns);
226 iRelease(d->media); 227 iRelease(d->media);
227 iRelease(d->request); 228 iRelease(d->request);
229 deinit_Block(&d->sourceContent);
230 deinit_String(&d->sourceMime);
228 iRelease(d->doc); 231 iRelease(d->doc);
229 deinit_PtrArray(&d->visibleLinks); 232 deinit_PtrArray(&d->visibleLinks);
230 delete_String(d->certSubject); 233 delete_String(d->certSubject);
@@ -269,7 +272,7 @@ static iRect documentBounds_DocumentWidget_(const iDocumentWidget *d) {
269} 272}
270 273
271static int forceBreakWidth_DocumentWidget_(const iDocumentWidget *d) { 274static int forceBreakWidth_DocumentWidget_(const iDocumentWidget *d) {
272 if (isLineWrapForced_App()) { 275 if (forceLineWrap_App()) {
273 const iRect bounds = bounds_Widget(constAs_Widget(d)); 276 const iRect bounds = bounds_Widget(constAs_Widget(d));
274 const iRect docBounds = documentBounds_DocumentWidget_(d); 277 const iRect docBounds = documentBounds_DocumentWidget_(d);
275 return right_Rect(bounds) - left_Rect(docBounds) - gap_UI * d->pageMargin; 278 return right_Rect(bounds) - left_Rect(docBounds) - gap_UI * d->pageMargin;
@@ -363,8 +366,12 @@ static void updateHover_DocumentWidget_(iDocumentWidget *d, iInt2 mouse) {
363 if (isHover_Widget(w) && !contains_Widget(constAs_Widget(d->scroll), mouse)) { 366 if (isHover_Widget(w) && !contains_Widget(constAs_Widget(d->scroll), mouse)) {
364 setCursor_Window(get_Window(), 367 setCursor_Window(get_Window(),
365 d->hoverLink ? SDL_SYSTEM_CURSOR_HAND : SDL_SYSTEM_CURSOR_IBEAM); 368 d->hoverLink ? SDL_SYSTEM_CURSOR_HAND : SDL_SYSTEM_CURSOR_IBEAM);
369 if (d->hoverLink &&
370 linkFlags_GmDocument(d->doc, d->hoverLink->linkId) & permanent_GmLinkFlag) {
371 setCursor_Window(get_Window(), SDL_SYSTEM_CURSOR_ARROW); /* not dismissable */
366 } 372 }
367} 373}
374}
368 375
369static void updateVisible_DocumentWidget_(iDocumentWidget *d) { 376static void updateVisible_DocumentWidget_(iDocumentWidget *d) {
370 const iRangei visRange = visibleRange_DocumentWidget_(d); 377 const iRangei visRange = visibleRange_DocumentWidget_(d);
@@ -507,9 +514,17 @@ static void showErrorPage_DocumentWidget_(iDocumentWidget *d, enum iGmStatusCode
507 case certificateNotValid_GmStatusCode: 514 case certificateNotValid_GmStatusCode:
508 appendFormat_String(src, "\n\n%s", cstr_String(meta)); 515 appendFormat_String(src, "\n\n%s", cstr_String(meta));
509 break; 516 break;
510 case unsupportedMimeType_GmStatusCode: 517 case unsupportedMimeType_GmStatusCode: {
511 appendFormat_String(src, "\n```\n%s\n```\n", cstr_String(meta)); 518 iString *key = collectNew_String();
519 toString_Sym(SDLK_s, KMOD_PRIMARY, key);
520 appendFormat_String(src,
521 "\n```\n%s\n```\n"
522 "You can save it as a file to your Downloads folder, though. "
523 "Press %s or select Save Page from the menu.",
524 cstr_String(meta),
525 cstr_String(key));
512 break; 526 break;
527 }
513 case slowDown_GmStatusCode: 528 case slowDown_GmStatusCode:
514 appendFormat_String(src, "\n\nWait %s seconds before your next request.", 529 appendFormat_String(src, "\n\nWait %s seconds before your next request.",
515 cstr_String(meta)); 530 cstr_String(meta));
@@ -534,6 +549,20 @@ static void updateTheme_DocumentWidget_(iDocumentWidget *d) {
534 } 549 }
535} 550}
536 551
552static void updateFetchProgress_DocumentWidget_(iDocumentWidget *d) {
553 iLabelWidget *prog = findWidget_App("document.progress");
554 const size_t dlSize = d->request ? size_Block(body_GmRequest(d->request)) : 0;
555 setFlags_Widget(as_Widget(prog), hidden_WidgetFlag, dlSize < 250000);
556 if (isVisible_Widget(prog)) {
557 updateText_LabelWidget(prog,
558 collectNewFormat_String("%s%.3f MB",
559 isFinished_GmRequest(d->request)
560 ? uiHeading_ColorEscape
561 : uiTextCaution_ColorEscape,
562 dlSize / 1.0e6f));
563 }
564}
565
537static void updateDocument_DocumentWidget_(iDocumentWidget *d, const iGmResponse *response) { 566static void updateDocument_DocumentWidget_(iDocumentWidget *d, const iGmResponse *response) {
538 if (d->state == ready_RequestState) { 567 if (d->state == ready_RequestState) {
539 return; 568 return;
@@ -545,12 +574,15 @@ static void updateDocument_DocumentWidget_(iDocumentWidget *d, const iGmResponse
545 iString str; 574 iString str;
546 invalidate_DocumentWidget_(d); 575 invalidate_DocumentWidget_(d);
547 updateTheme_DocumentWidget_(d); 576 updateTheme_DocumentWidget_(d);
577 clear_String(&d->sourceMime);
578// set_Block(&d->sourceContent, &response->body);
548 initBlock_String(&str, &response->body); 579 initBlock_String(&str, &response->body);
549 if (category_GmStatusCode(statusCode) == categorySuccess_GmStatusCode) { 580 if (category_GmStatusCode(statusCode) == categorySuccess_GmStatusCode) {
550 /* Check the MIME type. */ 581 /* Check the MIME type. */
551 iRangecc charset = range_CStr("utf-8"); 582 iRangecc charset = range_CStr("utf-8");
552 enum iGmDocumentFormat docFormat = undefined_GmDocumentFormat; 583 enum iGmDocumentFormat docFormat = undefined_GmDocumentFormat;
553 const iString *mimeStr = collect_String(lower_String(&response->meta)); /* for convenience */ 584 const iString *mimeStr = collect_String(lower_String(&response->meta)); /* for convenience */
585 set_String(&d->sourceMime, mimeStr);
554 iRangecc mime = range_String(mimeStr); 586 iRangecc mime = range_String(mimeStr);
555 iRangecc seg = iNullRange; 587 iRangecc seg = iNullRange;
556 while (nextSplit_Rangecc(mime, ";", &seg)) { 588 while (nextSplit_Rangecc(mime, ";", &seg)) {
@@ -558,12 +590,15 @@ static void updateDocument_DocumentWidget_(iDocumentWidget *d, const iGmResponse
558 trim_Rangecc(&param); 590 trim_Rangecc(&param);
559 if (equal_Rangecc(param, "text/plain")) { 591 if (equal_Rangecc(param, "text/plain")) {
560 docFormat = plainText_GmDocumentFormat; 592 docFormat = plainText_GmDocumentFormat;
593 setRange_String(&d->sourceMime, param);
561 } 594 }
562 else if (equal_Rangecc(param, "text/gemini")) { 595 else if (equal_Rangecc(param, "text/gemini")) {
563 docFormat = gemini_GmDocumentFormat; 596 docFormat = gemini_GmDocumentFormat;
597 setRange_String(&d->sourceMime, param);
564 } 598 }
565 else if (startsWith_Rangecc(param, "image/")) { 599 else if (startsWith_Rangecc(param, "image/")) {
566 docFormat = gemini_GmDocumentFormat; 600 docFormat = gemini_GmDocumentFormat;
601 setRange_String(&d->sourceMime, param);
567 if (!d->request || isFinished_GmRequest(d->request)) { 602 if (!d->request || isFinished_GmRequest(d->request)) {
568 /* Make a simple document with an image. */ 603 /* Make a simple document with an image. */
569 const char *imageTitle = "Image"; 604 const char *imageTitle = "Image";
@@ -573,9 +608,9 @@ static void updateDocument_DocumentWidget_(iDocumentWidget *d, const iGmResponse
573 imageTitle = 608 imageTitle =
574 baseName_Path(collect_String(newRange_String(parts.path))).start; 609 baseName_Path(collect_String(newRange_String(parts.path))).start;
575 } 610 }
576 format_String( 611 format_String(&str, "=> %s %s\n", cstr_String(d->mod.url), imageTitle);
577 &str, "=> %s %s\n", cstr_String(d->mod.url), imageTitle); 612 setImage_GmDocument(
578 setImage_GmDocument(d->doc, 1, mimeStr, &response->body); 613 d->doc, 1, mimeStr, &response->body, iFalse /* it's fixed */);
579 } 614 }
580 else { 615 else {
581 clear_String(&str); 616 clear_String(&str);
@@ -747,7 +782,9 @@ void setRedirectCount_DocumentWidget(iDocumentWidget *d, int count) {
747} 782}
748 783
749iBool isRequestOngoing_DocumentWidget(const iDocumentWidget *d) { 784iBool isRequestOngoing_DocumentWidget(const iDocumentWidget *d) {
750 return d->state == fetching_RequestState || d->state == receivedPartialResponse_RequestState; 785 /*return d->state == fetching_RequestState ||
786 d->state == receivedPartialResponse_RequestState;*/
787 return d->request != NULL;
751} 788}
752 789
753static void scroll_DocumentWidget_(iDocumentWidget *d, int offset) { 790static void scroll_DocumentWidget_(iDocumentWidget *d, int offset) {
@@ -961,7 +998,9 @@ static iBool handleMediaCommand_DocumentWidget_(iDocumentWidget *d, const char *
961 return iFalse; /* not our request */ 998 return iFalse; /* not our request */
962 } 999 }
963 if (equal_Command(cmd, "media.updated")) { 1000 if (equal_Command(cmd, "media.updated")) {
964 /* TODO: Show a progress indicator */ 1001 /* Update the link's progress. */
1002 invalidateLink_DocumentWidget_(d, req->linkId);
1003 refresh_Widget(d);
965 return iTrue; 1004 return iTrue;
966 } 1005 }
967 else if (equal_Command(cmd, "media.finished")) { 1006 else if (equal_Command(cmd, "media.finished")) {
@@ -974,7 +1013,7 @@ static iBool handleMediaCommand_DocumentWidget_(iDocumentWidget *d, const char *
974// cstr_String(meta_GmRequest(req->req))); 1013// cstr_String(meta_GmRequest(req->req)));
975 if (startsWith_String(meta_GmRequest(req->req), "image/")) { 1014 if (startsWith_String(meta_GmRequest(req->req), "image/")) {
976 setImage_GmDocument(d->doc, req->linkId, meta_GmRequest(req->req), 1015 setImage_GmDocument(d->doc, req->linkId, meta_GmRequest(req->req),
977 body_GmRequest(req->req)); 1016 body_GmRequest(req->req), iTrue);
978 updateVisible_DocumentWidget_(d); 1017 updateVisible_DocumentWidget_(d);
979 invalidate_DocumentWidget_(d); 1018 invalidate_DocumentWidget_(d);
980 refresh_Widget(as_Widget(d)); 1019 refresh_Widget(as_Widget(d));
@@ -999,20 +1038,6 @@ static void allocVisBuffer_DocumentWidget_(const iDocumentWidget *d) {
999 } 1038 }
1000 else { 1039 else {
1001 dealloc_VisBuf(d->visBuf); 1040 dealloc_VisBuf(d->visBuf);
1002#if 0
1003 iZap(d->visBuffer->validRange);
1004 d->visBuffer->size = size;
1005 iAssert(size.x > 0);
1006 iForIndices(i, d->visBuffer->texture) {
1007 d->visBuffer->texture[i] =
1008 SDL_CreateTexture(renderer_Window(get_Window()),
1009 SDL_PIXELFORMAT_RGBA8888,
1010 SDL_TEXTUREACCESS_STATIC | SDL_TEXTUREACCESS_TARGET,
1011 size.x,
1012 size.y);
1013 SDL_SetTextureBlendMode(d->visBuffer->texture[i], SDL_BLENDMODE_NONE);
1014 }
1015#endif
1016 } 1041 }
1017} 1042}
1018 1043
@@ -1054,6 +1079,7 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
1054 updateTheme_DocumentWidget_(d); 1079 updateTheme_DocumentWidget_(d);
1055 updateTrust_DocumentWidget_(d, NULL); 1080 updateTrust_DocumentWidget_(d, NULL);
1056 updateSize_DocumentWidget(d); 1081 updateSize_DocumentWidget(d);
1082 updateFetchProgress_DocumentWidget_(d);
1057 } 1083 }
1058 updateWindowTitle_DocumentWidget_(d); 1084 updateWindowTitle_DocumentWidget_(d);
1059 allocVisBuffer_DocumentWidget_(d); 1085 allocVisBuffer_DocumentWidget_(d);
@@ -1135,17 +1161,25 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
1135 } 1161 }
1136 else if (equalWidget_Command(cmd, w, "document.request.updated") && 1162 else if (equalWidget_Command(cmd, w, "document.request.updated") &&
1137 pointerLabel_Command(cmd, "request") == d->request) { 1163 pointerLabel_Command(cmd, "request") == d->request) {
1164 set_Block(&d->sourceContent, body_GmRequest(d->request));
1165 if (document_App() == d) {
1166 updateFetchProgress_DocumentWidget_(d);
1167 }
1138 checkResponse_DocumentWidget_(d); 1168 checkResponse_DocumentWidget_(d);
1169 set_Atomic(&d->isRequestUpdated, iFalse); /* ready to be notified again */
1139 return iFalse; 1170 return iFalse;
1140 } 1171 }
1141 else if (equalWidget_Command(cmd, w, "document.request.finished") && 1172 else if (equalWidget_Command(cmd, w, "document.request.finished") &&
1142 pointerLabel_Command(cmd, "request") == d->request) { 1173 pointerLabel_Command(cmd, "request") == d->request) {
1174 set_Block(&d->sourceContent, body_GmRequest(d->request));
1175 updateFetchProgress_DocumentWidget_(d);
1143 checkResponse_DocumentWidget_(d); 1176 checkResponse_DocumentWidget_(d);
1144 resetSmoothScroll_DocumentWidget_(d); 1177 resetSmoothScroll_DocumentWidget_(d);
1145 d->scrollY = d->initNormScrollY * size_GmDocument(d->doc).y; 1178 d->scrollY = d->initNormScrollY * size_GmDocument(d->doc).y;
1146 d->state = ready_RequestState; 1179 d->state = ready_RequestState;
1147 /* The response may be cached. */ { 1180 /* The response may be cached. */ {
1148 if (!equal_Rangecc(urlScheme_String(d->mod.url), "about")) { 1181 if (!equal_Rangecc(urlScheme_String(d->mod.url), "about") &&
1182 startsWithCase_String(meta_GmRequest(d->request), "text/")) {
1149 setCachedResponse_History(d->mod.history, response_GmRequest(d->request)); 1183 setCachedResponse_History(d->mod.history, response_GmRequest(d->request));
1150 } 1184 }
1151 } 1185 }
@@ -1159,20 +1193,102 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
1159 cancel_GmRequest(d->request); 1193 cancel_GmRequest(d->request);
1160 return iFalse; 1194 return iFalse;
1161 } 1195 }
1196 /*
1162 else if (equal_Command(cmd, "document.request.cancelled") && document_Command(cmd) == d) { 1197 else if (equal_Command(cmd, "document.request.cancelled") && document_Command(cmd) == d) {
1163 postCommand_App("navigate.back"); 1198 postCommand_App("navigate.back");
1164 return iFalse; 1199 return iFalse;
1165 } 1200 }
1201 */
1202 else if (equal_Command(cmd, "media.updated") || equal_Command(cmd, "media.finished")) {
1203 return handleMediaCommand_DocumentWidget_(d, cmd);
1204 }
1166 else if (equal_Command(cmd, "document.stop") && document_App() == d) { 1205 else if (equal_Command(cmd, "document.stop") && document_App() == d) {
1167 if (d->request) { 1206 if (d->request) {
1168 postCommandf_App("document.request.cancelled doc:%p url:%s", d, cstr_String(d->mod.url)); 1207 postCommandf_App(
1208 "document.request.cancelled doc:%p url:%s", d, cstr_String(d->mod.url));
1169 iReleasePtr(&d->request); 1209 iReleasePtr(&d->request);
1210 if (d->state != ready_RequestState) {
1170 d->state = ready_RequestState; 1211 d->state = ready_RequestState;
1212 postCommand_App("navigate.back");
1213 }
1214 updateFetchProgress_DocumentWidget_(d);
1171 return iTrue; 1215 return iTrue;
1172 } 1216 }
1173 } 1217 }
1174 else if (equal_Command(cmd, "media.updated") || equal_Command(cmd, "media.finished")) { 1218 else if (equal_Command(cmd, "document.save") && document_App() == d) {
1175 return handleMediaCommand_DocumentWidget_(d, cmd); 1219 if (d->request) {
1220 makeMessage_Widget(uiTextCaution_ColorEscape "PAGE INCOMPLETE",
1221 "The page contents are still being downloaded.");
1222 }
1223 else if (!isEmpty_Block(&d->sourceContent)) {
1224 /* Figure out a file name from the URL. */
1225 /* TODO: Make this a utility function. */
1226 iUrl parts;
1227 init_Url(&parts, d->mod.url);
1228 while (startsWith_Rangecc(parts.path, "/")) {
1229 parts.path.start++;
1230 }
1231 while (endsWith_Rangecc(parts.path, "/")) {
1232 parts.path.end--;
1233 }
1234 iString *name = collectNewCStr_String("pagecontent");
1235 if (isEmpty_Range(&parts.path)) {
1236 if (!isEmpty_Range(&parts.host)) {
1237 setRange_String(name, parts.host);
1238 replace_Block(&name->chars, '.', '_');
1239 }
1240 }
1241 else {
1242 iRangecc fn = { parts.path.start + lastIndexOfCStr_Rangecc(parts.path, "/") + 1,
1243 parts.path.end };
1244 if (!isEmpty_Range(&fn)) {
1245 setRange_String(name, fn);
1246 }
1247 }
1248 iString *savePath = concat_Path(downloadDir_App(), name);
1249 if (lastIndexOfCStr_String(savePath, ".") == iInvalidPos) {
1250 /* No extension specified in URL. */
1251 if (startsWith_String(&d->sourceMime, "text/gemini")) {
1252 appendCStr_String(savePath, ".gmi");
1253 }
1254 else if (startsWith_String(&d->sourceMime, "text/")) {
1255 appendCStr_String(savePath, ".txt");
1256 }
1257 else if (startsWith_String(&d->sourceMime, "image/")) {
1258 appendCStr_String(savePath, cstr_String(&d->sourceMime) + 6);
1259 if (fileExists_FileInfo(savePath)) {
1260 }
1261 }
1262 /* Make it unique. */
1263 iDate now;
1264 initCurrent_Date(&now);
1265 size_t insPos = lastIndexOfCStr_String(savePath, ".");
1266 if (insPos == iInvalidPos) {
1267 insPos = size_String(savePath);
1268 }
1269 const iString *date = collect_String(format_Date(&now, "_%Y-%m-%d_%H%M%S"));
1270 insertData_Block(&savePath->chars, insPos, cstr_String(date), size_String(date));
1271 }
1272 /* Write the file. */ {
1273 iFile *f = new_File(savePath);
1274 if (open_File(f, writeOnly_FileMode)) {
1275 write_File(f, &d->sourceContent);
1276 const size_t size = size_Block(&d->sourceContent);
1277 const iBool isMega = size >= 1000000;
1278 makeMessage_Widget(uiHeading_ColorEscape "PAGE SAVED",
1279 format_CStr("%s\nSize: %.3f %s", cstr_String(path_File(f)),
1280 isMega ? size / 1.0e6f : (size / 1.0e3f),
1281 isMega ? "MB" : "KB"));
1282 }
1283 else {
1284 makeMessage_Widget(uiTextCaution_ColorEscape "ERROR SAVING PAGE",
1285 strerror(errno));
1286 }
1287 iRelease(f);
1288 }
1289 delete_String(savePath);
1290 }
1291 return iTrue;
1176 } 1292 }
1177 else if (equal_Command(cmd, "document.reload") && document_App() == d) { 1293 else if (equal_Command(cmd, "document.reload") && document_App() == d) {
1178 d->initNormScrollY = normScrollPos_DocumentWidget_(d); 1294 d->initNormScrollY = normScrollPos_DocumentWidget_(d);
@@ -1368,7 +1484,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
1368 case '`': { 1484 case '`': {
1369 iBlock *seed = new_Block(64); 1485 iBlock *seed = new_Block(64);
1370 for (size_t i = 0; i < 64; ++i) { 1486 for (size_t i = 0; i < 64; ++i) {
1371 setByte_Block(seed, i, iRandom(0, 255)); 1487 setByte_Block(seed, i, iRandom(0, 256));
1372 } 1488 }
1373 setThemeSeed_GmDocument(d->doc, seed); 1489 setThemeSeed_GmDocument(d->doc, seed);
1374 delete_Block(seed); 1490 delete_Block(seed);
@@ -1400,8 +1516,8 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
1400 } 1516 }
1401 smoothScroll_DocumentWidget_( 1517 smoothScroll_DocumentWidget_(
1402 d, 1518 d,
1403 -3 * ev->wheel.y * lineHeight_Text(default_FontId), 1519 -3 * ev->wheel.y * lineHeight_Text(paragraph_FontId),
1404 gap_UI * smoothSpeed_DocumentWidget_ + 1520 gap_Text * smoothSpeed_DocumentWidget_ +
1405 (isSmoothScrolling_DocumentWidget_(d) ? d->smoothSpeed : 0)); 1521 (isSmoothScrolling_DocumentWidget_(d) ? d->smoothSpeed : 0));
1406#endif 1522#endif
1407 d->noHoverWhileScrolling = iTrue; 1523 d->noHoverWhileScrolling = iTrue;
@@ -1433,23 +1549,48 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
1433 } 1549 }
1434 iArray items; 1550 iArray items;
1435 init_Array(&items, sizeof(iMenuItem)); 1551 init_Array(&items, sizeof(iMenuItem));
1552 if (d->contextLink) {
1553 pushBackN_Array(
1554 &items,
1555 (iMenuItem[]){ { "Open Link in New Tab",
1556 0,
1557 0,
1558 format_CStr("!open newtab:1 url:%s",
1559 cstr_String(linkUrl_GmDocument(
1560 d->doc, d->contextLink->linkId))) },
1561 { "---", 0, 0, NULL },
1562 { "Copy Link",
1563 0,
1564 0,
1565 "document.copylink" }},
1566 3);
1567 }
1568 else {
1569 if (!isEmpty_Range(&d->selectMark)) {
1436 pushBackN_Array( 1570 pushBackN_Array(
1437 &items, 1571 &items,
1572 (iMenuItem[]){ { "Copy", 0, 0, "copy" }, { "---", 0, 0, NULL } },
1573 2);
1574 }
1575 pushBackN_Array(
1576 &items,
1438 (iMenuItem[]){ 1577 (iMenuItem[]){
1439 { "Go Back", navigateBack_KeyShortcut, "navigate.back" }, 1578 { "Go Back", navigateBack_KeyShortcut, "navigate.back" },
1440 { "Go Forward", navigateForward_KeyShortcut, "navigate.forward" }, 1579 { "Go Forward", navigateForward_KeyShortcut, "navigate.forward" },
1441 { "Reload Page", reload_KeyShortcut, "navigate.reload" }, 1580 { "Reload Page", reload_KeyShortcut, "navigate.reload" },
1442 { "---", 0, 0, NULL }, 1581 { "---", 0, 0, NULL },
1443 { d->contextLink ? "Copy Link URL" : "Copy Page URL", 1582 { "Copy Page URL", 0, 0, "document.copylink" },
1444 0, 1583 { "---", 0, 0, NULL } },
1445 0,
1446 "document.copylink" },
1447 { isEmpty_Range(&d->selectMark) ? "Copy Full Source" : "Copy Selected",
1448 'c',
1449 KMOD_PRIMARY,
1450 "copy" },
1451 },
1452 6); 1584 6);
1585 if (isEmpty_Range(&d->selectMark)) {
1586 pushBackN_Array(
1587 &items,
1588 (iMenuItem[]){
1589 { "Copy Page Source", 'c', KMOD_PRIMARY, "copy" },
1590 { "Save to Downloads", SDLK_s, KMOD_PRIMARY, "document.save" } },
1591 2);
1592 }
1593 }
1453 d->menu = makeMenu_Widget(w, data_Array(&items), size_Array(&items)); 1594 d->menu = makeMenu_Widget(w, data_Array(&items), size_Array(&items));
1454 deinit_Array(&items); 1595 deinit_Array(&items);
1455 } 1596 }
@@ -1489,10 +1630,16 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
1489 iAssert(linkId); 1630 iAssert(linkId);
1490 /* Media links are opened inline by default. */ 1631 /* Media links are opened inline by default. */
1491 if (isMediaLink_GmDocument(d->doc, linkId)) { 1632 if (isMediaLink_GmDocument(d->doc, linkId)) {
1633 const int linkFlags = linkFlags_GmDocument(d->doc, linkId);
1634 if (linkFlags & content_GmLinkFlag && linkFlags & permanent_GmLinkFlag) {
1635 /* We have the image and it cannot be dismissed, so nothing
1636 further to do. */
1637 return iTrue;
1638 }
1492 if (!requestMedia_DocumentWidget_(d, linkId)) { 1639 if (!requestMedia_DocumentWidget_(d, linkId)) {
1493 if (linkFlags_GmDocument(d->doc, linkId) & content_GmLinkFlag) { 1640 if (linkFlags & content_GmLinkFlag) {
1494 /* Dismiss shown content on click. */ 1641 /* Dismiss shown content on click. */
1495 setImage_GmDocument(d->doc, linkId, NULL, NULL); 1642 setImage_GmDocument(d->doc, linkId, NULL, NULL, iTrue);
1496 d->hoverLink = NULL; 1643 d->hoverLink = NULL;
1497 scroll_DocumentWidget_(d, 0); 1644 scroll_DocumentWidget_(d, 0);
1498 updateVisible_DocumentWidget_(d); 1645 updateVisible_DocumentWidget_(d);
@@ -1505,7 +1652,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
1505 iMediaRequest *req = findMediaRequest_DocumentWidget_(d, linkId); 1652 iMediaRequest *req = findMediaRequest_DocumentWidget_(d, linkId);
1506 if (req) { 1653 if (req) {
1507 setImage_GmDocument(d->doc, linkId, meta_GmRequest(req->req), 1654 setImage_GmDocument(d->doc, linkId, meta_GmRequest(req->req),
1508 body_GmRequest(req->req)); 1655 body_GmRequest(req->req), iTrue);
1509 updateVisible_DocumentWidget_(d); 1656 updateVisible_DocumentWidget_(d);
1510 invalidate_DocumentWidget_(d); 1657 invalidate_DocumentWidget_(d);
1511 refresh_Widget(w); 1658 refresh_Widget(w);
@@ -1671,8 +1818,8 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) {
1671 const int flags = linkFlags_GmDocument(doc, run->linkId); 1818 const int flags = linkFlags_GmDocument(doc, run->linkId);
1672 const iRect linkRect = moved_Rect(run->visBounds, origin); 1819 const iRect linkRect = moved_Rect(run->visBounds, origin);
1673 iMediaRequest *mr = NULL; 1820 iMediaRequest *mr = NULL;
1674 /* Show inline content. */ 1821 /* Show metadata about inline content. */
1675 if (flags & content_GmLinkFlag) { 1822 if (flags & content_GmLinkFlag && run->flags & endOfLine_GmRunFlag) {
1676 fg = linkColor_GmDocument(doc, run->linkId, textHover_GmLinkPart); 1823 fg = linkColor_GmDocument(doc, run->linkId, textHover_GmLinkPart);
1677 iAssert(!isEmpty_Rect(run->bounds)); 1824 iAssert(!isEmpty_Rect(run->bounds));
1678 iGmImageInfo info; 1825 iGmImageInfo info;
@@ -1704,7 +1851,8 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) {
1704 draw_Text(metaFont, 1851 draw_Text(metaFont,
1705 topRight_Rect(linkRect), 1852 topRight_Rect(linkRect),
1706 tmInlineContentMetadata_ColorId, 1853 tmInlineContentMetadata_ColorId,
1707 " \u2014 Fetching\u2026"); 1854 " \u2014 Fetching\u2026 (%.1f MB)",
1855 (float) size_Block(body_GmRequest(mr->req)) / 1.0e6f);
1708 } 1856 }
1709 } 1857 }
1710 else if (isHover) { 1858 else if (isHover) {
@@ -1713,20 +1861,23 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) {
1713 const int flags = linkFlags_GmDocument(doc, linkId); 1861 const int flags = linkFlags_GmDocument(doc, linkId);
1714 iUrl parts; 1862 iUrl parts;
1715 init_Url(&parts, url); 1863 init_Url(&parts, url);
1716 fg = linkColor_GmDocument(doc, linkId, textHover_GmLinkPart); 1864 fg = linkColor_GmDocument(doc, linkId, textHover_GmLinkPart);
1717 const iBool showHost = (!isEmpty_Range(&parts.host) && flags & userFriendly_GmLinkFlag); 1865 const iBool showHost = (flags & humanReadable_GmLinkFlag &&
1866 (!isEmpty_Range(&parts.host) || flags & mailto_GmLinkFlag));
1718 const iBool showImage = (flags & imageFileExtension_GmLinkFlag) != 0; 1867 const iBool showImage = (flags & imageFileExtension_GmLinkFlag) != 0;
1719 const iBool showAudio = (flags & audioFileExtension_GmLinkFlag) != 0; 1868 const iBool showAudio = (flags & audioFileExtension_GmLinkFlag) != 0;
1720 iString str; 1869 iString str;
1721 init_String(&str); 1870 init_String(&str);
1871 /* Show scheme and host. */
1722 if (run->flags & endOfLine_GmRunFlag && 1872 if (run->flags & endOfLine_GmRunFlag &&
1723 (flags & (imageFileExtension_GmLinkFlag | audioFileExtension_GmLinkFlag) || 1873 (flags & (imageFileExtension_GmLinkFlag | audioFileExtension_GmLinkFlag) ||
1724 showHost)) { 1874 showHost)) {
1725 format_String( 1875 format_String(&str,
1726 &str,
1727 " \u2014%s%s%s\r%c%s", 1876 " \u2014%s%s%s\r%c%s",
1728 showHost ? " " : "", 1877 showHost ? " " : "",
1729 showHost ? (!equalCase_Rangecc(parts.scheme, "gemini") 1878 showHost ? (flags & mailto_GmLinkFlag
1879 ? cstr_String(url)
1880 : ~flags & gemini_GmLinkFlag
1730 ? format_CStr("%s://%s", 1881 ? format_CStr("%s://%s",
1731 cstr_Rangecc(parts.scheme), 1882 cstr_Rangecc(parts.scheme),
1732 cstr_Rangecc(parts.host)) 1883 cstr_Rangecc(parts.host))
@@ -1735,7 +1886,8 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) {
1735 showHost && (showImage || showAudio) ? " \u2014" : "", 1886 showHost && (showImage || showAudio) ? " \u2014" : "",
1736 showImage || showAudio 1887 showImage || showAudio
1737 ? asciiBase_ColorEscape + fg 1888 ? asciiBase_ColorEscape + fg
1738 : (asciiBase_ColorEscape + linkColor_GmDocument(doc, run->linkId, domain_GmLinkPart)), 1889 : (asciiBase_ColorEscape +
1890 linkColor_GmDocument(doc, run->linkId, domain_GmLinkPart)),
1739 showImage ? " View Image \U0001f5bc" 1891 showImage ? " View Image \U0001f5bc"
1740 : showAudio ? " Play Audio \U0001f3b5" : ""); 1892 : showAudio ? " Play Audio \U0001f3b5" : "");
1741 } 1893 }
diff --git a/src/ui/inputwidget.c b/src/ui/inputwidget.c
index 11098c80..2d6d84dd 100644
--- a/src/ui/inputwidget.c
+++ b/src/ui/inputwidget.c
@@ -182,6 +182,7 @@ void setText_InputWidget(iInputWidget *d, const iString *text) {
182 pushBack_Array(&d->text, &i.value); 182 pushBack_Array(&d->text, &i.value);
183 } 183 }
184 iZap(d->mark); 184 iZap(d->mark);
185 d->cursor = iMin(d->cursor, size_Array(&d->text));
185 refresh_Widget(as_Widget(d)); 186 refresh_Widget(as_Widget(d));
186} 187}
187 188
@@ -733,6 +734,7 @@ static void draw_InputWidget_(const iInputWidget *d) {
733 deinit_String(&cur); 734 deinit_String(&cur);
734 } 735 }
735 delete_String(text); 736 delete_String(text);
737 drawChildren_Widget(w);
736} 738}
737 739
738iBeginDefineSubclass(InputWidget, Widget) 740iBeginDefineSubclass(InputWidget, Widget)
diff --git a/src/ui/labelwidget.c b/src/ui/labelwidget.c
index 8b2506e7..28f43173 100644
--- a/src/ui/labelwidget.c
+++ b/src/ui/labelwidget.c
@@ -94,54 +94,7 @@ static iBool processEvent_LabelWidget_(iLabelWidget *d, const SDL_Event *ev) {
94} 94}
95 95
96static void keyStr_LabelWidget_(const iLabelWidget *d, iString *str) { 96static void keyStr_LabelWidget_(const iLabelWidget *d, iString *str) {
97#if defined (iPlatformApple) 97 toString_Sym(d->key, d->kmods, str);
98 if (d->kmods & KMOD_CTRL) {
99 appendChar_String(str, 0x2303);
100 }
101 if (d->kmods & KMOD_ALT) {
102 appendChar_String(str, 0x2325);
103 }
104 if (d->kmods & KMOD_SHIFT) {
105 appendChar_String(str, 0x21e7);
106 }
107 if (d->kmods & KMOD_GUI) {
108 appendChar_String(str, 0x2318);
109 }
110#else
111 if (d->kmods & KMOD_CTRL) {
112 appendCStr_String(str, "Ctrl+");
113 }
114 if (d->kmods & KMOD_ALT) {
115 appendCStr_String(str, "Alt+");
116 }
117 if (d->kmods & KMOD_SHIFT) {
118 appendCStr_String(str, "Shift+");
119 }
120 if (d->kmods & KMOD_GUI) {
121 appendCStr_String(str, "Meta+");
122 }
123#endif
124 if (d->key == 0x20) {
125 appendCStr_String(str, "Space");
126 }
127 else if (d->key == SDLK_LEFT) {
128 appendChar_String(str, 0x2190);
129 }
130 else if (d->key == SDLK_RIGHT) {
131 appendChar_String(str, 0x2192);
132 }
133 else if (d->key < 128 && (isalnum(d->key) || ispunct(d->key))) {
134 appendChar_String(str, upper_Char(d->key));
135 }
136 else if (d->key == SDLK_BACKSPACE) {
137 appendChar_String(str, 0x232b); /* Erase to the Left */
138 }
139 else if (d->key == SDLK_DELETE) {
140 appendChar_String(str, 0x2326); /* Erase to the Right */
141 }
142 else {
143 appendCStr_String(str, SDL_GetKeyName(d->key));
144 }
145} 98}
146 99
147static void getColors_LabelWidget_(const iLabelWidget *d, int *bg, int *fg, int *frame1, int *frame2) { 100static void getColors_LabelWidget_(const iLabelWidget *d, int *bg, int *fg, int *frame1, int *frame2) {
diff --git a/src/ui/lookupwidget.c b/src/ui/lookupwidget.c
index b7de5872..dcde7d79 100644
--- a/src/ui/lookupwidget.c
+++ b/src/ui/lookupwidget.c
@@ -265,7 +265,7 @@ static void searchIdentities_LookupJob_(iLookupJob *d) {
265 265
266static iThreadResult worker_LookupWidget_(iThread *thread) { 266static iThreadResult worker_LookupWidget_(iThread *thread) {
267 iLookupWidget *d = userData_Thread(thread); 267 iLookupWidget *d = userData_Thread(thread);
268 printf("[LookupWidget] worker is running\n"); fflush(stdout); 268// printf("[LookupWidget] worker is running\n"); fflush(stdout);
269 lock_Mutex(d->mtx); 269 lock_Mutex(d->mtx);
270 for (;;) { 270 for (;;) {
271 wait_Condition(&d->jobAvailable, d->mtx); 271 wait_Condition(&d->jobAvailable, d->mtx);
@@ -312,13 +312,13 @@ static iThreadResult worker_LookupWidget_(iThread *thread) {
312 /* Previous results haven't been taken yet. */ 312 /* Previous results haven't been taken yet. */
313 delete_LookupJob(d->finishedJob); 313 delete_LookupJob(d->finishedJob);
314 } 314 }
315 printf("[LookupWidget] worker has %zu results\n", size_PtrArray(&job->results)); 315// printf("[LookupWidget] worker has %zu results\n", size_PtrArray(&job->results));
316 fflush(stdout); 316 fflush(stdout);
317 d->finishedJob = job; 317 d->finishedJob = job;
318 postCommand_Widget(as_Widget(d), "lookup.ready"); 318 postCommand_Widget(as_Widget(d), "lookup.ready");
319 } 319 }
320 unlock_Mutex(d->mtx); 320 unlock_Mutex(d->mtx);
321 printf("[LookupWidget] worker has quit\n"); fflush(stdout); 321// printf("[LookupWidget] worker has quit\n"); fflush(stdout);
322 return 0; 322 return 0;
323} 323}
324 324
diff --git a/src/ui/util.c b/src/ui/util.c
index 0af33138..ff6f8822 100644
--- a/src/ui/util.c
+++ b/src/ui/util.c
@@ -52,6 +52,57 @@ const char *command_UserEvent(const SDL_Event *d) {
52 return ""; 52 return "";
53} 53}
54 54
55void toString_Sym(int key, int kmods, iString *str) {
56#if defined (iPlatformApple)
57 if (kmods & KMOD_CTRL) {
58 appendChar_String(str, 0x2303);
59 }
60 if (kmods & KMOD_ALT) {
61 appendChar_String(str, 0x2325);
62 }
63 if (kmods & KMOD_SHIFT) {
64 appendChar_String(str, 0x21e7);
65 }
66 if (kmods & KMOD_GUI) {
67 appendChar_String(str, 0x2318);
68 }
69#else
70 if (kmods & KMOD_CTRL) {
71 appendCStr_String(str, "Ctrl+");
72 }
73 if (kmods & KMOD_ALT) {
74 appendCStr_String(str, "Alt+");
75 }
76 if (kmods & KMOD_SHIFT) {
77 appendCStr_String(str, "Shift+");
78 }
79 if (kmods & KMOD_GUI) {
80 appendCStr_String(str, "Meta+");
81 }
82#endif
83 if (key == 0x20) {
84 appendCStr_String(str, "Space");
85 }
86 else if (key == SDLK_LEFT) {
87 appendChar_String(str, 0x2190);
88 }
89 else if (key == SDLK_RIGHT) {
90 appendChar_String(str, 0x2192);
91 }
92 else if (key < 128 && (isalnum(key) || ispunct(key))) {
93 appendChar_String(str, upper_Char(key));
94 }
95 else if (key == SDLK_BACKSPACE) {
96 appendChar_String(str, 0x232b); /* Erase to the Left */
97 }
98 else if (key == SDLK_DELETE) {
99 appendChar_String(str, 0x2326); /* Erase to the Right */
100 }
101 else {
102 appendCStr_String(str, SDL_GetKeyName(key));
103 }
104}
105
55int keyMods_Sym(int kmods) { 106int keyMods_Sym(int kmods) {
56 kmods &= (KMOD_SHIFT | KMOD_ALT | KMOD_CTRL | KMOD_GUI); 107 kmods &= (KMOD_SHIFT | KMOD_ALT | KMOD_CTRL | KMOD_GUI);
57 /* Don't treat left/right modifiers differently. */ 108 /* Don't treat left/right modifiers differently. */
@@ -192,6 +243,12 @@ iWidget *addAction_Widget(iWidget *parent, int key, int kmods, const char *comma
192 243
193/*-----------------------------------------------------------------------------------------------*/ 244/*-----------------------------------------------------------------------------------------------*/
194 245
246static iBool isCommandIgnoredByMenus_(const char *cmd) {
247 return equal_Command(cmd, "media.updated") || equal_Command(cmd, "document.request.updated") ||
248 equal_Command(cmd, "window.resized") ||
249 (equal_Command(cmd, "mouse.clicked") && !arg_Command(cmd)); /* button released */
250}
251
195static iBool menuHandler_(iWidget *menu, const char *cmd) { 252static iBool menuHandler_(iWidget *menu, const char *cmd) {
196 if (isVisible_Widget(menu)) { 253 if (isVisible_Widget(menu)) {
197 if (equalWidget_Command(cmd, menu, "menu.opened")) { 254 if (equalWidget_Command(cmd, menu, "menu.opened")) {
@@ -201,13 +258,13 @@ static iBool menuHandler_(iWidget *menu, const char *cmd) {
201 /* Don't reopen self; instead, root will close the menu. */ 258 /* Don't reopen self; instead, root will close the menu. */
202 return iFalse; 259 return iFalse;
203 } 260 }
204 if (equal_Command(cmd, "mouse.clicked") && arg_Command(cmd)) { 261 if ((equal_Command(cmd, "mouse.clicked") || equal_Command(cmd, "mouse.missed")) &&
262 arg_Command(cmd)) {
205 /* Dismiss open menus when clicking outside them. */ 263 /* Dismiss open menus when clicking outside them. */
206 closeMenu_Widget(menu); 264 closeMenu_Widget(menu);
207 return iTrue; 265 return iTrue;
208 } 266 }
209 if (!equal_Command(cmd, "window.resized") && 267 if (!isCommandIgnoredByMenus_(cmd)) {
210 !(equal_Command(cmd, "mouse.clicked") && !arg_Command(cmd)) /* ignore button release */) {
211 closeMenu_Widget(menu); 268 closeMenu_Widget(menu);
212 } 269 }
213 } 270 }
@@ -252,6 +309,7 @@ void openMenu_Widget(iWidget *d, iInt2 coord) {
252 postCommand_App("cancel"); /* dismiss any other menus */ 309 postCommand_App("cancel"); /* dismiss any other menus */
253 processEvents_App(postedEventsOnly_AppEventMode); 310 processEvents_App(postedEventsOnly_AppEventMode);
254 setFlags_Widget(d, hidden_WidgetFlag, iFalse); 311 setFlags_Widget(d, hidden_WidgetFlag, iFalse);
312 setFlags_Widget(d, commandOnMouseMiss_WidgetFlag, iTrue);
255 setFlags_Widget(findChild_Widget(d, "menu.cancel"), disabled_WidgetFlag, iFalse); 313 setFlags_Widget(findChild_Widget(d, "menu.cancel"), disabled_WidgetFlag, iFalse);
256 arrange_Widget(d); 314 arrange_Widget(d);
257 d->rect.pos = coord; 315 d->rect.pos = coord;
@@ -681,10 +739,9 @@ void updateValueInput_Widget(iWidget *d, const char *title, const char *prompt)
681 739
682static iBool messageHandler_(iWidget *msg, const char *cmd) { 740static iBool messageHandler_(iWidget *msg, const char *cmd) {
683 /* Almost any command dismisses the sheet. */ 741 /* Almost any command dismisses the sheet. */
684// if (equal_Command(cmd, "menu.closed")) { 742 if (!(equal_Command(cmd, "media.updated") || equal_Command(cmd, "document.request.updated"))) {
685// return iFalse; 743 destroy_Widget(msg);
686// } 744 }
687 destroy_Widget(msg);
688 return iFalse; 745 return iFalse;
689} 746}
690 747
@@ -755,14 +812,16 @@ iWidget *makePreferences_Widget(void) {
755 addChild_Widget(dlg, iClob(page)); 812 addChild_Widget(dlg, iClob(page));
756 setFlags_Widget(page, arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag, iTrue); 813 setFlags_Widget(page, arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag, iTrue);
757 iWidget *headings = addChildFlags_Widget( 814 iWidget *headings = addChildFlags_Widget(
758 page, iClob(new_Widget()), arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag); 815 page, iClob(new_Widget()), arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag);
759 iWidget *values = addChildFlags_Widget( 816 iWidget *values = addChildFlags_Widget(
760 page, iClob(new_Widget()), arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag); 817 page, iClob(new_Widget()), arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag);
818 addChild_Widget(headings, iClob(makeHeading_Widget("Downloads folder:")));
819 setId_Widget(addChild_Widget(values, iClob(new_InputWidget(0))), "prefs.downloads");
761#if defined (iPlatformApple) || defined (iPlatformMSys) 820#if defined (iPlatformApple) || defined (iPlatformMSys)
762 addChild_Widget(headings, iClob(makeHeading_Widget("Use system theme:"))); 821 addChild_Widget(headings, iClob(makeHeading_Widget("Use system theme:")));
763 addChild_Widget(values, iClob(makeToggle_Widget("prefs.ostheme"))); 822 addChild_Widget(values, iClob(makeToggle_Widget("prefs.ostheme")));
764#endif 823#endif
765 addChild_Widget(headings, iClob(makeHeading_Widget("Theme:"))); 824 addChild_Widget(headings, iClob(makeHeading_Widget("Theme:")));
766 iWidget *themes = new_Widget(); 825 iWidget *themes = new_Widget();
767 /* Themes. */ { 826 /* Themes. */ {
768 setId_Widget(addChild_Widget(themes, iClob(new_LabelWidget("Pure Black", 0, 0, "theme.set arg:0"))), "prefs.theme.0"); 827 setId_Widget(addChild_Widget(themes, iClob(new_LabelWidget("Pure Black", 0, 0, "theme.set arg:0"))), "prefs.theme.0");
@@ -782,8 +841,9 @@ iWidget *makePreferences_Widget(void) {
782 addChild_Widget(headings, iClob(makeHeading_Widget("HTTP proxy:"))); 841 addChild_Widget(headings, iClob(makeHeading_Widget("HTTP proxy:")));
783 setId_Widget(addChild_Widget(values, iClob(new_InputWidget(0))), "prefs.proxy.http"); 842 setId_Widget(addChild_Widget(values, iClob(new_InputWidget(0))), "prefs.proxy.http");
784 arrange_Widget(dlg); 843 arrange_Widget(dlg);
785 /* Text input widths. */ { 844 /* Set text input widths. */ {
786 const int inputWidth = width_Rect(page->rect) - width_Rect(headings->rect); 845 const int inputWidth = width_Rect(page->rect) - width_Rect(headings->rect);
846 as_Widget(findChild_Widget(values, "prefs.downloads"))->rect.size.x = inputWidth;
787 as_Widget(findChild_Widget(values, "prefs.proxy.http"))->rect.size.x = inputWidth; 847 as_Widget(findChild_Widget(values, "prefs.proxy.http"))->rect.size.x = inputWidth;
788 as_Widget(findChild_Widget(values, "prefs.proxy.gopher"))->rect.size.x = inputWidth; 848 as_Widget(findChild_Widget(values, "prefs.proxy.gopher"))->rect.size.x = inputWidth;
789 } 849 }
diff --git a/src/ui/util.h b/src/ui/util.h
index 8ca9dd53..5590d008 100644
--- a/src/ui/util.h
+++ b/src/ui/util.h
@@ -49,6 +49,7 @@ iLocalDef iBool isResize_UserEvent(const SDL_Event *d) {
49#endif 49#endif
50 50
51int keyMods_Sym (int kmods); /* shift, alt, control, or gui */ 51int keyMods_Sym (int kmods); /* shift, alt, control, or gui */
52void toString_Sym (int key, int kmods, iString *str);
52 53
53iRangei intersect_Rangei (iRangei a, iRangei b); 54iRangei intersect_Rangei (iRangei a, iRangei b);
54iRangei union_Rangei (iRangei a, iRangei b); 55iRangei union_Rangei (iRangei a, iRangei b);
diff --git a/src/ui/visbuf.c b/src/ui/visbuf.c
index 64d861c6..8a66c300 100644
--- a/src/ui/visbuf.c
+++ b/src/ui/visbuf.c
@@ -37,6 +37,7 @@ void deinit_VisBuf(iVisBuf *d) {
37 37
38void invalidate_VisBuf(iVisBuf *d) { 38void invalidate_VisBuf(iVisBuf *d) {
39 iForIndices(i, d->buffers) { 39 iForIndices(i, d->buffers) {
40 d->buffers[i].origin = i * d->texSize.y;
40 iZap(d->buffers[i].validRange); 41 iZap(d->buffers[i].validRange);
41 } 42 }
42} 43}
diff --git a/src/ui/widget.c b/src/ui/widget.c
index d3f28b08..05bb62cc 100644
--- a/src/ui/widget.c
+++ b/src/ui/widget.c
@@ -542,6 +542,15 @@ iBool processEvent_Widget(iWidget *d, const SDL_Event *ev) {
542 break; 542 break;
543 } 543 }
544 } 544 }
545 if (d->flags & commandOnMouseMiss_WidgetFlag && ev->type == SDL_MOUSEBUTTONDOWN &&
546 !contains_Widget(d, init_I2(ev->button.x, ev->button.y))) {
547 postCommand_Widget(d,
548 "mouse.missed arg:%d button:%d coord:%d %d",
549 ev->type == SDL_MOUSEBUTTONDOWN ? 1 : 0,
550 ev->button.button,
551 ev->button.x,
552 ev->button.y);
553 }
545 if (d->flags & mouseModal_WidgetFlag && isMouseEvent_(ev)) { 554 if (d->flags & mouseModal_WidgetFlag && isMouseEvent_(ev)) {
546 setCursor_Window(get_Window(), SDL_SYSTEM_CURSOR_ARROW); 555 setCursor_Window(get_Window(), SDL_SYSTEM_CURSOR_ARROW);
547 return iTrue; 556 return iTrue;
diff --git a/src/ui/widget.h b/src/ui/widget.h
index 25208c30..fa4fbe0f 100644
--- a/src/ui/widget.h
+++ b/src/ui/widget.h
@@ -58,6 +58,7 @@ enum iWidgetFlag {
58 tight_WidgetFlag = iBit(12), /* smaller padding */ 58 tight_WidgetFlag = iBit(12), /* smaller padding */
59 keepOnTop_WidgetFlag = iBit(13), /* gets events first; drawn last */ 59 keepOnTop_WidgetFlag = iBit(13), /* gets events first; drawn last */
60 mouseModal_WidgetFlag = iBit(14), /* eats all unprocessed mouse events */ 60 mouseModal_WidgetFlag = iBit(14), /* eats all unprocessed mouse events */
61 commandOnMouseMiss_WidgetFlag = iBit(15),
61 /* arrange behavior */ 62 /* arrange behavior */
62 fixedPosition_WidgetFlag = iBit(16), 63 fixedPosition_WidgetFlag = iBit(16),
63 arrangeHorizontal_WidgetFlag = iBit(17), /* arrange children horizontally */ 64 arrangeHorizontal_WidgetFlag = iBit(17), /* arrange children horizontally */
diff --git a/src/ui/window.c b/src/ui/window.c
index 0a63a941..8ebb67a8 100644
--- a/src/ui/window.c
+++ b/src/ui/window.c
@@ -91,10 +91,13 @@ static iBool handleRootCommands_(iWidget *root, const char *cmd) {
91#endif 91#endif
92 92
93#if !defined (iHaveNativeMenus) 93#if !defined (iHaveNativeMenus)
94/* TODO: Submenus wouldn't hurt here. */
94static const iMenuItem navMenuItems[] = { 95static const iMenuItem navMenuItems[] = {
95 { "New Tab", 't', KMOD_PRIMARY, "tabs.new" }, 96 { "New Tab", 't', KMOD_PRIMARY, "tabs.new" },
96 { "Open Location...", SDLK_l, KMOD_PRIMARY, "focus.set id:url" }, 97 { "Open Location...", SDLK_l, KMOD_PRIMARY, "focus.set id:url" },
97 { "---", 0, 0, NULL }, 98 { "---", 0, 0, NULL },
99 { "Save to Downloads", SDLK_s, KMOD_PRIMARY, "document.save" },
100 { "---", 0, 0, NULL },
98 { "Copy Source Text", SDLK_c, KMOD_PRIMARY, "copy" }, 101 { "Copy Source Text", SDLK_c, KMOD_PRIMARY, "copy" },
99 { "Bookmark This Page", SDLK_d, KMOD_PRIMARY, "bookmark.add" }, 102 { "Bookmark This Page", SDLK_d, KMOD_PRIMARY, "bookmark.add" },
100 { "---", 0, 0, NULL }, 103 { "---", 0, 0, NULL },
@@ -104,7 +107,7 @@ static const iMenuItem navMenuItems[] = {
104 { "Reset Zoom", SDLK_0, KMOD_PRIMARY, "zoom.set arg:100" }, 107 { "Reset Zoom", SDLK_0, KMOD_PRIMARY, "zoom.set arg:100" },
105 { "---", 0, 0, NULL }, 108 { "---", 0, 0, NULL },
106 { "Preferences...", SDLK_COMMA, KMOD_PRIMARY, "preferences" }, 109 { "Preferences...", SDLK_COMMA, KMOD_PRIMARY, "preferences" },
107 { "Help", 0, 0, "!open url:about:help" }, 110 { "Help", SDLK_F1, 0, "!open url:about:help" },
108 { "Release Notes", 0, 0, "!open url:about:version" }, 111 { "Release Notes", 0, 0, "!open url:about:version" },
109 { "---", 0, 0, NULL }, 112 { "---", 0, 0, NULL },
110 { "Quit Lagrange", 'q', KMOD_PRIMARY, "quit" } 113 { "Quit Lagrange", 'q', KMOD_PRIMARY, "quit" }
@@ -116,6 +119,8 @@ static const iMenuItem navMenuItems[] = {
116static const iMenuItem fileMenuItems[] = { 119static const iMenuItem fileMenuItems[] = {
117 { "New Tab", SDLK_t, KMOD_PRIMARY, "tabs.new" }, 120 { "New Tab", SDLK_t, KMOD_PRIMARY, "tabs.new" },
118 { "Open Location...", SDLK_l, KMOD_PRIMARY, "focus.set id:url" }, 121 { "Open Location...", SDLK_l, KMOD_PRIMARY, "focus.set id:url" },
122 { "---", 0, 0, NULL },
123 { "Save to Downloads", SDLK_s, KMOD_PRIMARY, "document.save" },
119}; 124};
120 125
121static const iMenuItem editMenuItems[] = { 126static const iMenuItem editMenuItems[] = {
@@ -362,12 +367,24 @@ static void setupUserInterface_Window(iWindow *d) {
362 setId_Widget(as_Widget(lock), "navbar.lock"); 367 setId_Widget(as_Widget(lock), "navbar.lock");
363 setFont_LabelWidget(lock, defaultSymbols_FontId); 368 setFont_LabelWidget(lock, defaultSymbols_FontId);
364 updateTextCStr_LabelWidget(lock, "\U0001f512"); 369 updateTextCStr_LabelWidget(lock, "\U0001f512");
365 iInputWidget *url = new_InputWidget(0); 370 /* URL input field. */ {
366 setSelectAllOnFocus_InputWidget(url, iTrue); 371 iInputWidget *url = new_InputWidget(0);
367 setId_Widget(as_Widget(url), "url"); 372 setSelectAllOnFocus_InputWidget(url, iTrue);
368 setNotifyEdits_InputWidget(url, iTrue); 373 setId_Widget(as_Widget(url), "url");
369 setTextCStr_InputWidget(url, "gemini://"); 374 setNotifyEdits_InputWidget(url, iTrue);
370 addChildFlags_Widget(navBar, iClob(url), expand_WidgetFlag); 375 setTextCStr_InputWidget(url, "gemini://");
376 addChildFlags_Widget(navBar, iClob(url), expand_WidgetFlag);
377 /* Download progress indicator is inside the input field, but hidden normally. */
378 setPadding_Widget(as_Widget(url),0, 0, gap_UI * 1, 0);
379 iLabelWidget *progress = new_LabelWidget(uiTextCaution_ColorEscape "00.000 MB", 0, 0, NULL);
380 setId_Widget(as_Widget(progress), "document.progress");
381 setAlignVisually_LabelWidget(progress, iTrue);
382 shrink_Rect(&as_Widget(progress)->rect, init_I2(0, gap_UI));
383 addChildFlags_Widget(as_Widget(url),
384 iClob(progress),
385 moveToParentRightEdge_WidgetFlag);
386 setBackgroundColor_Widget(as_Widget(progress), uiBackground_ColorId);
387 }
371 setId_Widget(addChild_Widget( 388 setId_Widget(addChild_Widget(
372 navBar, iClob(newIcon_LabelWidget(reloadCStr_, 0, 0, "navigate.reload"))), 389 navBar, iClob(newIcon_LabelWidget(reloadCStr_, 0, 0, "navigate.reload"))),
373 "reload"); 390 "reload");
@@ -477,26 +494,36 @@ static void drawBlank_Window_(iWindow *d) {
477 SDL_RenderPresent(d->render); 494 SDL_RenderPresent(d->render);
478} 495}
479 496
480// #define ENABLE_SWRENDER 497iBool create_Window_(iWindow *d, iRect rect, uint32_t flags) {
498 flags |= SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI;
499 if (SDL_CreateWindowAndRenderer(
500 width_Rect(rect), height_Rect(rect), flags, &d->win, &d->render)) {
501 return iFalse;
502 }
503 return iTrue;
504}
481 505
482void init_Window(iWindow *d, iRect rect) { 506void init_Window(iWindow *d, iRect rect) {
483 theWindow_ = d; 507 theWindow_ = d;
484 iZap(d->cursors); 508 iZap(d->cursors);
509 d->initialPos = rect.pos;
485 d->pendingCursor = NULL; 510 d->pendingCursor = NULL;
486 d->isDrawFrozen = iTrue; 511 d->isDrawFrozen = iTrue;
487 uint32_t flags = SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI; 512 uint32_t flags = 0;
488#if defined (ENABLE_SWRENDER) 513#if defined (iPlatformApple)
489 SDL_SetHint(SDL_HINT_RENDER_DRIVER, "software");
490#elif defined (iPlatformApple)
491 SDL_SetHint(SDL_HINT_RENDER_DRIVER, "metal"); 514 SDL_SetHint(SDL_HINT_RENDER_DRIVER, "metal");
492#else 515#else
493 flags |= SDL_WINDOW_OPENGL; 516 flags |= SDL_WINDOW_OPENGL;
494#endif 517#endif
495 SDL_SetHint(SDL_HINT_RENDER_VSYNC, "1"); 518 SDL_SetHint(SDL_HINT_RENDER_VSYNC, "1");
496 if (SDL_CreateWindowAndRenderer( 519 /* First try SDL's default renderer that should be the best option. */
497 width_Rect(rect), height_Rect(rect), flags, &d->win, &d->render)) { 520 if (forceSoftwareRender_App() || !create_Window_(d, rect, flags)) {
498 fprintf(stderr, "Error when creating window: %s\n", SDL_GetError()); 521 /* No luck, maybe software only? This should always work as long as there is a display. */
499 exit(-2); 522 SDL_SetHint(SDL_HINT_RENDER_DRIVER, "software");
523 if (!create_Window_(d, rect, 0)) {
524 fprintf(stderr, "Error when creating window: %s\n", SDL_GetError());
525 exit(-2);
526 }
500 } 527 }
501 if (left_Rect(rect) >= 0) { 528 if (left_Rect(rect) >= 0) {
502 SDL_SetWindowPosition(d->win, left_Rect(rect), top_Rect(rect)); 529 SDL_SetWindowPosition(d->win, left_Rect(rect), top_Rect(rect));
@@ -571,6 +598,16 @@ SDL_Renderer *renderer_Window(const iWindow *d) {
571 598
572static iBool handleWindowEvent_Window_(iWindow *d, const SDL_WindowEvent *ev) { 599static iBool handleWindowEvent_Window_(iWindow *d, const SDL_WindowEvent *ev) {
573 switch (ev->event) { 600 switch (ev->event) {
601#if defined (LAGRANGE_ENABLE_WINDOWPOS_FIX)
602 case SDL_WINDOWEVENT_EXPOSED:
603 if (d->initialPos.x >= 0) {
604 int bx, by;
605 SDL_GetWindowBordersSize(d->win, &by, &bx, NULL, NULL);
606 SDL_SetWindowPosition(d->win, d->initialPos.x + bx, d->initialPos.y + by);
607 d->initialPos = init1_I2(-1);
608 }
609 return iFalse;
610#endif
574 case SDL_WINDOWEVENT_MOVED: 611 case SDL_WINDOWEVENT_MOVED:
575 /* No need to do anything. */ 612 /* No need to do anything. */
576 return iTrue; 613 return iTrue;
diff --git a/src/ui/window.h b/src/ui/window.h
index 4aec2fa7..b067d30e 100644
--- a/src/ui/window.h
+++ b/src/ui/window.h
@@ -34,6 +34,7 @@ iDeclareTypeConstructionArgs(Window, iRect rect)
34 34
35struct Impl_Window { 35struct Impl_Window {
36 SDL_Window * win; 36 SDL_Window * win;
37 iInt2 initialPos;
37 iBool isDrawFrozen; /* avoids premature draws while restoring window state */ 38 iBool isDrawFrozen; /* avoids premature draws while restoring window state */
38 SDL_Renderer *render; 39 SDL_Renderer *render;
39 iWidget * root; 40 iWidget * root;