diff options
59 files changed, 3990 insertions, 659 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index ab57900e..a5b41da3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt | |||
@@ -7,6 +7,10 @@ project (Lagrange | |||
7 | ) | 7 | ) |
8 | set (COPYRIGHT_YEAR 2020) | 8 | set (COPYRIGHT_YEAR 2020) |
9 | 9 | ||
10 | # Build configuration. | ||
11 | option (ENABLE_KERNING "Enable kerning in font renderer (slower)" ON) | ||
12 | option (ENABLE_RESOURCE_EMBED "Embed resources inside the executable" OFF) | ||
13 | |||
10 | include (Embed.cmake) | 14 | include (Embed.cmake) |
11 | find_package (the_Foundation REQUIRED) | 15 | find_package (the_Foundation REQUIRED) |
12 | find_package (PkgConfig REQUIRED) | 16 | find_package (PkgConfig REQUIRED) |
@@ -15,11 +19,10 @@ pkg_check_modules (SDL2 REQUIRED sdl2) | |||
15 | # Embedded resources are written to a generated source file. | 19 | # Embedded resources are written to a generated source file. |
16 | message (STATUS "Preparing embedded resources...") | 20 | message (STATUS "Preparing embedded resources...") |
17 | # Fonts are too large to comfortably embed as a C source. | 21 | # Fonts are too large to comfortably embed as a C source. |
18 | set (EMBED_IN_EXECUTABLE OFF CACHE BOOL "Embed resources inside the executable") | ||
19 | set (EMBED_RESOURCES | 22 | set (EMBED_RESOURCES |
20 | res/about.gmi | 23 | res/about/help.gmi |
21 | res/help.gmi | 24 | res/about/lagrange.gmi |
22 | res/version.gmi | 25 | res/about/version.gmi |
23 | res/SourceSansPro-Regular.ttf | 26 | res/SourceSansPro-Regular.ttf |
24 | res/FiraSans-Regular.ttf | 27 | res/FiraSans-Regular.ttf |
25 | res/FiraSans-Bold.ttf | 28 | res/FiraSans-Bold.ttf |
@@ -34,12 +37,12 @@ set (EMBED_RESOURCES | |||
34 | endif () | 37 | endif () |
35 | embed_make (${EMBED_RESOURCES}) | 38 | embed_make (${EMBED_RESOURCES}) |
36 | 39 | ||
37 | set (EMB_BIN ${CMAKE_CURRENT_BINARY_DIR}/resources.bin) | 40 | set (EMB_BIN ${CMAKE_CURRENT_BINARY_DIR}/resources.binary) |
38 | set_source_files_properties (${EMB_BIN} PROPERTIES MACOSX_PACKAGE_LOCATION Resources) | 41 | set_source_files_properties (${EMB_BIN} PROPERTIES MACOSX_PACKAGE_LOCATION Resources) |
39 | 42 | ||
40 | # Source files. | 43 | # Source files. |
41 | set (SOURCES | 44 | set (SOURCES |
42 | ${CMAKE_CURRENT_BINARY_DIR}/resources.bin | 45 | ${CMAKE_CURRENT_BINARY_DIR}/resources.binary |
43 | ${CMAKE_CURRENT_BINARY_DIR}/embedded.c | 46 | ${CMAKE_CURRENT_BINARY_DIR}/embedded.c |
44 | ${CMAKE_CURRENT_BINARY_DIR}/embedded.h | 47 | ${CMAKE_CURRENT_BINARY_DIR}/embedded.h |
45 | src/main.c | 48 | src/main.c |
@@ -92,7 +95,7 @@ set (SOURCES | |||
92 | ) | 95 | ) |
93 | if (IOS) | 96 | if (IOS) |
94 | elseif (APPLE) | 97 | elseif (APPLE) |
95 | list (APPEND SOURCES src/ui/macos.m src/ui/macos.h) | 98 | list (APPEND SOURCES src/macos.m src/macos.h) |
96 | list (APPEND RESOURCES "res/Lagrange.icns") | 99 | list (APPEND RESOURCES "res/Lagrange.icns") |
97 | endif () | 100 | endif () |
98 | if (MSYS) | 101 | if (MSYS) |
@@ -113,9 +116,13 @@ target_include_directories (app PUBLIC | |||
113 | ) | 116 | ) |
114 | target_compile_options (app PUBLIC | 117 | target_compile_options (app PUBLIC |
115 | -Werror=implicit-function-declaration | 118 | -Werror=implicit-function-declaration |
119 | -Werror=incompatible-pointer-types | ||
116 | ${SDL2_CFLAGS} | 120 | ${SDL2_CFLAGS} |
117 | ) | 121 | ) |
118 | target_compile_definitions (app PUBLIC LAGRANGE_APP_VERSION="${PROJECT_VERSION}") | 122 | target_compile_definitions (app PUBLIC LAGRANGE_APP_VERSION="${PROJECT_VERSION}") |
123 | if (ENABLE_KERNING) | ||
124 | target_compile_definitions (app PUBLIC LAGRANGE_ENABLE_KERNING=1) | ||
125 | endif () | ||
119 | target_link_libraries (app PUBLIC the_Foundation::the_Foundation) | 126 | target_link_libraries (app PUBLIC the_Foundation::the_Foundation) |
120 | target_link_libraries (app PUBLIC ${SDL2_LDFLAGS}) | 127 | target_link_libraries (app PUBLIC ${SDL2_LDFLAGS}) |
121 | if (APPLE) | 128 | if (APPLE) |
@@ -145,7 +152,7 @@ endif () | |||
145 | # Deployment. | 152 | # Deployment. |
146 | if (MSYS) | 153 | if (MSYS) |
147 | install (TARGETS app DESTINATION .) | 154 | install (TARGETS app DESTINATION .) |
148 | if (NOT EMBED_IN_EXECUTABLE) | 155 | if (NOT ENABLE_RESOURCE_EMBED) |
149 | install (FILES ${EMB_BIN} DESTINATION .) | 156 | install (FILES ${EMB_BIN} DESTINATION .) |
150 | endif () | 157 | endif () |
151 | install (PROGRAMS | 158 | install (PROGRAMS |
@@ -172,7 +179,7 @@ Icon=fi.skyjake.lagrange") | |||
172 | DESTINATION share/icons/hicolor/256x256/apps | 179 | DESTINATION share/icons/hicolor/256x256/apps |
173 | RENAME fi.skyjake.lagrange.png | 180 | RENAME fi.skyjake.lagrange.png |
174 | ) | 181 | ) |
175 | if (NOT EMBED_IN_EXECUTABLE) | 182 | if (NOT ENABLE_RESOURCE_EMBED) |
176 | install (FILES ${EMB_BIN} DESTINATION share/lagrange) | 183 | install (FILES ${EMB_BIN} DESTINATION share/lagrange) |
177 | endif () | 184 | endif () |
178 | endif () | 185 | endif () |
diff --git a/Embed.cmake b/Embed.cmake index 4d51f418..c75f19e4 100644 --- a/Embed.cmake +++ b/Embed.cmake | |||
@@ -2,9 +2,9 @@ | |||
2 | # Copyright: 2020 Jaakko Keränen <jaakko.keranen@iki.fi> | 2 | # Copyright: 2020 Jaakko Keränen <jaakko.keranen@iki.fi> |
3 | # License: BSD 2-Clause | 3 | # License: BSD 2-Clause |
4 | 4 | ||
5 | option (EMBED_IN_EXECUTABLE "Embed resources inside the executable" OFF) | 5 | option (ENABLE_RESOURCE_EMBED "Embed resources inside the executable" OFF) |
6 | # Note: If disabled, the Unix "cat" tool is required for concatenating | 6 | # Note: If disabled, the Unix "cat" tool is required for concatenating |
7 | # the resources into a single "resources.bin" file. | 7 | # the resources into a single "resources.binary" file. |
8 | 8 | ||
9 | function (embed_getname output fn) | 9 | function (embed_getname output fn) |
10 | get_filename_component (name ${fn} NAME_WE) | 10 | get_filename_component (name ${fn} NAME_WE) |
@@ -57,7 +57,7 @@ endfunction (embed_filesize) | |||
57 | function (embed_make) | 57 | function (embed_make) |
58 | set (EMB_H ${CMAKE_CURRENT_BINARY_DIR}/embedded.h) | 58 | set (EMB_H ${CMAKE_CURRENT_BINARY_DIR}/embedded.h) |
59 | set (EMB_C ${CMAKE_CURRENT_BINARY_DIR}/embedded.c) | 59 | set (EMB_C ${CMAKE_CURRENT_BINARY_DIR}/embedded.c) |
60 | if (EMBED_IN_EXECUTABLE) | 60 | if (ENABLE_RESOURCE_EMBED) |
61 | set (needGen NO) | 61 | set (needGen NO) |
62 | if (NOT EXISTS ${EMB_H} OR NOT EXISTS ${EMB_C}) | 62 | if (NOT EXISTS ${EMB_H} OR NOT EXISTS ${EMB_C}) |
63 | set (needGen YES) | 63 | set (needGen YES) |
@@ -75,7 +75,7 @@ function (embed_make) | |||
75 | set (needGen YES) | 75 | set (needGen YES) |
76 | endif () | 76 | endif () |
77 | if (needGen) | 77 | if (needGen) |
78 | if (EMBED_IN_EXECUTABLE) | 78 | if (ENABLE_RESOURCE_EMBED) |
79 | # Compose a source file with the resource data in an array. | 79 | # Compose a source file with the resource data in an array. |
80 | file (WRITE ${EMB_H} "#include <the_Foundation/block.h>\n") | 80 | file (WRITE ${EMB_H} "#include <the_Foundation/block.h>\n") |
81 | file (WRITE ${EMB_C} "#include \"embedded.h\"\n") | 81 | file (WRITE ${EMB_C} "#include \"embedded.h\"\n") |
@@ -85,7 +85,7 @@ function (embed_make) | |||
85 | endforeach (fn) | 85 | endforeach (fn) |
86 | else () | 86 | else () |
87 | # Collect resources in a single binary file. | 87 | # Collect resources in a single binary file. |
88 | set (EMB_BIN ${CMAKE_CURRENT_BINARY_DIR}/resources.bin) | 88 | set (EMB_BIN ${CMAKE_CURRENT_BINARY_DIR}/resources.binary) |
89 | file (REMOVE ${EMB_BIN}) | 89 | file (REMOVE ${EMB_BIN}) |
90 | execute_process (COMMAND cat ${ARGV} OUTPUT_FILE ${EMB_BIN} | 90 | execute_process (COMMAND cat ${ARGV} OUTPUT_FILE ${EMB_BIN} |
91 | WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}) | 91 | WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}) |
diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 00000000..4de431cf --- /dev/null +++ b/LICENSE.md | |||
@@ -0,0 +1,13 @@ | |||
1 | # BSD 2-Clause License | ||
2 | |||
3 | **Copyright 2020 Jaakko Keränen (jaakko.keranen@iki.fi)** | ||
4 | |||
5 | All rights reserved. | ||
6 | |||
7 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: | ||
8 | |||
9 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. | ||
10 | |||
11 | * 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. | ||
12 | |||
13 | 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. | ||
diff --git a/res/MacOSXBundleInfo.plist.in b/res/MacOSXBundleInfo.plist.in index 74b1662e..07a596aa 100644 --- a/res/MacOSXBundleInfo.plist.in +++ b/res/MacOSXBundleInfo.plist.in | |||
@@ -38,7 +38,8 @@ | |||
38 | <key>CFBundleTypeExtensions</key> | 38 | <key>CFBundleTypeExtensions</key> |
39 | <array> | 39 | <array> |
40 | <string>gmi</string> | 40 | <string>gmi</string> |
41 | </array> | 41 | <string>gemini</string> |
42 | </array> | ||
42 | <key>CFBundleTypeIconFile</key> | 43 | <key>CFBundleTypeIconFile</key> |
43 | <string>text-gemini.icns</string> | 44 | <string>text-gemini.icns</string> |
44 | <key>CFBundleTypeName</key> | 45 | <key>CFBundleTypeName</key> |
diff --git a/res/Symbola.ttf b/res/Symbola.ttf index 59e428fe..f4bb1b7b 100644 --- a/res/Symbola.ttf +++ b/res/Symbola.ttf | |||
Binary files differ | |||
diff --git a/res/about/help.gmi b/res/about/help.gmi new file mode 100644 index 00000000..11c76931 --- /dev/null +++ b/res/about/help.gmi | |||
@@ -0,0 +1,147 @@ | |||
1 | ``` | ||
2 | __ __ __ ___ | ||
3 | | /\ / _` |__) /\ |\ | / _` |__ | ||
4 | |___ /~~\ \__> | \ /~~\ | \| \__> |___ | ||
5 | |||
6 | ``` | ||
7 | # Introduction | ||
8 | |||
9 | ## What is Lagrange | ||
10 | |||
11 | Lagrange is a GUI client for browsing Geminispace. It offers modern conveniences familiar from web browsers, such as smooth scrolling, inline image viewing, multiple tabs, visual themes, Unicode fonts, bookmarks, history, and page outlines. | ||
12 | |||
13 | Lagrange relies on a minimal set of dependencies. It is written in C and uses SDL for window management and hardware-accelerated graphics. OpenSSL is used for secure communications. | ||
14 | |||
15 | => https://www.libsdl.org SDL: Simple DirectMedia Layer | ||
16 | => https://www.openssl.org OpenSSL: Cryptography and SSL/TLS Toolkit | ||
17 | |||
18 | ## What is Gemini | ||
19 | |||
20 | Gemini is a simple protocol for serving content over the internet. It specifies a Markdown inspired format allowing basic plain text document markup. Compared to HTTP and HTML, Gemini is vastly simpler and easier to work with. | ||
21 | |||
22 | => gemini://gemini.circumlunar.space/docs/faq.gmi Project Gemini FAQ | ||
23 | => gemini://gemini.circumlunar.space/docs/specification.gmi Protocol and text/gemini specification | ||
24 | |||
25 | ## Why not just use a web browser | ||
26 | |||
27 | Modern web browsers are complex beasts. In fact, they are so complex that one can create a fully functional virtual machine inside one and run another operating system! | ||
28 | |||
29 | => https://win95.ajf.me Windows 95 on DOSBox (using Emscripten) | ||
30 | |||
31 | If one seeks to just read text and view images, this is absurd overkill. While having a universal platform that runs on everything and everywhere is clearly a valuable notion, it comes with a hefty price tag. The software stack towers ever higher, and similarly hardware needs to be ever more powerful and complicated to run it well. But everything happening over the internet doesn't have to be on this behemoth. | ||
32 | |||
33 | One way to browse Gemini content is via web browser extensions or proxies that translate the content for the web. This may be a sufficient and easy solution for you. However, native clients such as Lagrange also benefit from the simpleness of the protocol and the content. The experience can be optimized, and the software runs well even on simple hardware like the Raspberry Pi. | ||
34 | |||
35 | # User interface | ||
36 | |||
37 | ## Navigation | ||
38 | |||
39 | When navigating via keyboard, hold down ${ALT} to see link shortcut keys. Try doing so now and see how the link icon below is replaced with a number. | ||
40 | |||
41 | => gemini://gemini.circumlunar.space/ Project Gemini | ||
42 | |||
43 | Each 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. | ||
44 | |||
45 | ### SEomting | ||
46 | |||
47 | ## Bookmarking | ||
48 | |||
49 | ## Managing and using identities | ||
50 | |||
51 | TLS client certificates used to identify you. | ||
52 | |||
53 | # Platform-specific instructions | ||
54 | |||
55 | # Compiling from source | ||
56 | |||
57 | # Open source licenses | ||
58 | |||
59 | Lagrange itself is distributed under the BSD 2-clause license: | ||
60 | => https://opensource.org/licenses/BSD-2-Clause The 2-Clause BSD License | ||
61 | => https://git.skyjake.fi/skyjake/lagrange.git Lagrange Git Repository | ||
62 | |||
63 | > Copyright 2020 Jaakko Keränen | ||
64 | > | ||
65 | > Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: | ||
66 | > | ||
67 | > 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. | ||
68 | > | ||
69 | > 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. | ||
70 | > | ||
71 | > 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. | ||
72 | |||
73 | ## SDL | ||
74 | |||
75 | SDL 2.0 and newer are available under the zlib license: | ||
76 | => https://www.zlib.net/zlib_license.html ZLIB License | ||
77 | |||
78 | > 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. | ||
79 | > | ||
80 | > 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: | ||
81 | > | ||
82 | > 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. | ||
83 | > | ||
84 | > 2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. | ||
85 | > | ||
86 | > 3. This notice may not be removed or altered from any source distribution. | ||
87 | |||
88 | ## the_Foundation | ||
89 | |||
90 | the_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. | ||
91 | => https://opensource.org/licenses/BSD-2-Clause The 2-Clause BSD License | ||
92 | => https://git.skyjake.fi/skyjake/the_Foundation.git the_Foundation Git Repository | ||
93 | |||
94 | ## OpenSSL | ||
95 | |||
96 | OpenSSL 1.1.1 is under a double license, which both apply to the library. | ||
97 | => https://www.openssl.org/source/license-openssl-ssleay.txt OpenSSL and SSLeay Licenses | ||
98 | |||
99 | > Copyright (c) 1998-2019 The OpenSSL Project. All rights reserved. | ||
100 | > | ||
101 | > Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: | ||
102 | > | ||
103 | > 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. | ||
104 | > | ||
105 | > 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. | ||
106 | > | ||
107 | > 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/)" | ||
108 | > | ||
109 | > 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. | ||
110 | > | ||
111 | > 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. | ||
112 | > | ||
113 | > 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/)" | ||
114 | > | ||
115 | > 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. | ||
116 | > | ||
117 | > This product includes cryptographic software written by Eric Young (eay@cryptsoft.com). This product includes software written by Tim Hudson (tjh@cryptsoft.com). | ||
118 | |||
119 | SSLeay license: | ||
120 | |||
121 | > Copyright (C) 1995-1998 Eric Young (eay@cryptsoft.com) | ||
122 | > All rights reserved. | ||
123 | > | ||
124 | > This package is an SSL implementation written by Eric Young (eay@cryptsoft.com). The implementation was written so as to conform with Netscapes SSL. | ||
125 | > | ||
126 | > 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). | ||
127 | > | ||
128 | > 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. | ||
129 | > | ||
130 | > Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: | ||
131 | > | ||
132 | > 1. Redistributions of source code must retain the copyright notice, this list of conditions and the following disclaimer. | ||
133 | > | ||
134 | > 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. | ||
135 | > | ||
136 | > 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 :-). | ||
137 | > | ||
138 | > 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)" | ||
139 | > | ||
140 | > 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. | ||
141 | > | ||
142 | > 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.] | ||
143 | |||
144 | ## GNU libunistring | ||
145 | |||
146 | The libunistring library is covered by the GNU Lesser General Public License (LGPL): | ||
147 | => https://www.gnu.org/software/libunistring/manual/libunistring.html#GNU-LGPL GNU LGPL License | ||
diff --git a/res/about.gmi b/res/about/lagrange.gmi index 8e78fa60..9c19a465 100644 --- a/res/about.gmi +++ b/res/about/lagrange.gmi | |||
@@ -12,4 +12,4 @@ o888ooooood8 `Y888""8o `8oooooo. d888b `Y888""8o o888o o888o `8oooooo. `Y8b | |||
12 | # A Beautiful Gemini Client | 12 | # A Beautiful Gemini Client |
13 | ## Version ${APP_VERSION} | 13 | ## Version ${APP_VERSION} |
14 | => https://skyjake.fi/@jk by Jaakko Keränen <code@iki.fi> | 14 | => https://skyjake.fi/@jk by Jaakko Keränen <code@iki.fi> |
15 | Crafted with ☕️ in Finland | 15 | Crafted with ☕️ in Finland \ No newline at end of file |
diff --git a/res/version.gmi b/res/about/version.gmi index 7f303297..7f303297 100644 --- a/res/version.gmi +++ b/res/about/version.gmi | |||
diff --git a/res/help.gmi b/res/help.gmi deleted file mode 100644 index 1652f392..00000000 --- a/res/help.gmi +++ /dev/null | |||
@@ -1,31 +0,0 @@ | |||
1 | ``` | ||
2 | __ __ __ ___ | ||
3 | | /\ / _` |__) /\ |\ | / _` |__ | ||
4 | |___ /~~\ \__> | \ /~~\ | \| \__> |___ | ||
5 | |||
6 | ``` | ||
7 | # Help | ||
8 | |||
9 | ## What is Lagrange | ||
10 | |||
11 | ## What is the Gemini protocol | ||
12 | |||
13 | ## Why not just use a web browser | ||
14 | |||
15 | # User interface | ||
16 | |||
17 | ## Navigation | ||
18 | |||
19 | Hold down Alt/Option to see link shortcut keys. Each 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 Control/Command to open the link in a new tab. | ||
20 | |||
21 | ### SEomting | ||
22 | |||
23 | ## Bookmarking | ||
24 | |||
25 | ## Managing and using identities | ||
26 | |||
27 | TLS client certificates used to identify you. | ||
28 | |||
29 | # Platform-specific instructions | ||
30 | |||
31 | # Compiling from source | ||
@@ -1,3 +1,25 @@ | |||
1 | /* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi> | ||
2 | |||
3 | Redistribution and use in source and binary forms, with or without | ||
4 | modification, are permitted provided that the following conditions are met: | ||
5 | |||
6 | 1. Redistributions of source code must retain the above copyright notice, this | ||
7 | list of conditions and the following disclaimer. | ||
8 | 2. Redistributions in binary form must reproduce the above copyright notice, | ||
9 | this list of conditions and the following disclaimer in the documentation | ||
10 | and/or other materials provided with the distribution. | ||
11 | |||
12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | ||
13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | ||
14 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||
15 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR | ||
16 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | ||
17 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | ||
18 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON | ||
19 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||
20 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | ||
21 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | ||
22 | |||
1 | #include "app.h" | 23 | #include "app.h" |
2 | #include "bookmarks.h" | 24 | #include "bookmarks.h" |
3 | #include "embedded.h" | 25 | #include "embedded.h" |
@@ -25,6 +47,7 @@ | |||
25 | #include <the_Foundation/time.h> | 47 | #include <the_Foundation/time.h> |
26 | #include <SDL_events.h> | 48 | #include <SDL_events.h> |
27 | #include <SDL_render.h> | 49 | #include <SDL_render.h> |
50 | #include <SDL_timer.h> | ||
28 | #include <SDL_video.h> | 51 | #include <SDL_video.h> |
29 | 52 | ||
30 | #include <stdio.h> | 53 | #include <stdio.h> |
@@ -32,26 +55,26 @@ | |||
32 | #include <errno.h> | 55 | #include <errno.h> |
33 | 56 | ||
34 | #if defined (iPlatformApple) && !defined (iPlatformIOS) | 57 | #if defined (iPlatformApple) && !defined (iPlatformIOS) |
35 | # include "ui/macos.h" | 58 | # include "macos.h" |
36 | #endif | 59 | #endif |
37 | 60 | ||
38 | iDeclareType(App) | 61 | iDeclareType(App) |
39 | 62 | ||
40 | #if defined (iPlatformApple) | 63 | #if defined (iPlatformApple) |
41 | #define EMB_BIN "../../Resources/resources.bin" | 64 | #define EMB_BIN "../../Resources/resources.binary" |
42 | static const char *dataDir_App_ = "~/Library/Application Support/fi.skyjake.Lagrange"; | 65 | static const char *dataDir_App_ = "~/Library/Application Support/fi.skyjake.Lagrange"; |
43 | #endif | 66 | #endif |
44 | #if defined (iPlatformMsys) | 67 | #if defined (iPlatformMsys) |
45 | #define EMB_BIN "../resources.bin" | 68 | #define EMB_BIN "../resources.binary" |
46 | static const char *dataDir_App_ = "~/AppData/Roaming/fi.skyjake.Lagrange"; | 69 | static const char *dataDir_App_ = "~/AppData/Roaming/fi.skyjake.Lagrange"; |
47 | #endif | 70 | #endif |
48 | #if defined (iPlatformLinux) | 71 | #if defined (iPlatformLinux) |
49 | #define EMB_BIN "../../share/lagrange/resources.bin" | 72 | #define EMB_BIN "../../share/lagrange/resources.binary" |
50 | #define EMB_BIN2 "../resources.bin" /* try from build dir as well */ | ||
51 | static const char *dataDir_App_ = "~/.config/lagrange"; | 73 | static const char *dataDir_App_ = "~/.config/lagrange"; |
52 | #endif | 74 | #endif |
75 | #define EMB_BIN2 "../resources.binary" /* fallback from build/executable dir */ | ||
53 | static const char *prefsFileName_App_ = "prefs.cfg"; | 76 | static const char *prefsFileName_App_ = "prefs.cfg"; |
54 | static const char *stateFileName_App_ = "state.bin"; | 77 | static const char *stateFileName_App_ = "state.binary"; |
55 | 78 | ||
56 | struct Impl_App { | 79 | struct Impl_App { |
57 | iCommandLine args; | 80 | iCommandLine args; |
@@ -60,6 +83,8 @@ struct Impl_App { | |||
60 | iBookmarks * bookmarks; | 83 | iBookmarks * bookmarks; |
61 | iWindow * window; | 84 | iWindow * window; |
62 | iSortedArray tickers; | 85 | iSortedArray tickers; |
86 | uint32_t lastTickerTime; | ||
87 | uint32_t elapsedSinceLastTicker; | ||
63 | iBool running; | 88 | iBool running; |
64 | iBool pendingRefresh; | 89 | iBool pendingRefresh; |
65 | int tabEnum; | 90 | int tabEnum; |
@@ -70,6 +95,9 @@ struct Impl_App { | |||
70 | float uiScale; | 95 | float uiScale; |
71 | int zoomPercent; | 96 | int zoomPercent; |
72 | enum iColorTheme theme; | 97 | enum iColorTheme theme; |
98 | iBool useSystemTheme; | ||
99 | iString gopherProxy; | ||
100 | iString httpProxy; | ||
73 | }; | 101 | }; |
74 | 102 | ||
75 | static iApp app_; | 103 | static iApp app_; |
@@ -99,21 +127,32 @@ const iString *dateStr_(const iDate *date) { | |||
99 | static iString *serializePrefs_App_(const iApp *d) { | 127 | static iString *serializePrefs_App_(const iApp *d) { |
100 | iString *str = new_String(); | 128 | iString *str = new_String(); |
101 | const iSidebarWidget *sidebar = findWidget_App("sidebar"); | 129 | const iSidebarWidget *sidebar = findWidget_App("sidebar"); |
130 | appendFormat_String(str, "window.retain arg:%d\n", d->retainWindowSize); | ||
102 | if (d->retainWindowSize) { | 131 | if (d->retainWindowSize) { |
103 | int w, h, x, y; | 132 | int w, h, x, y; |
104 | SDL_GetWindowSize(d->window->win, &w, &h); | 133 | SDL_GetWindowSize(d->window->win, &w, &h); |
105 | SDL_GetWindowPosition(d->window->win, &x, &y); | 134 | SDL_GetWindowPosition(d->window->win, &x, &y); |
135 | #if defined (iPlatformLinux) | ||
136 | /* Workaround for window position being unaffected by decorations on creation. */ { | ||
137 | int bl, bt; | ||
138 | SDL_GetWindowBordersSize(d->window->win, &bt, &bl, NULL, NULL); | ||
139 | x -= bl; | ||
140 | y -= bt; | ||
141 | } | ||
142 | #endif | ||
106 | appendFormat_String(str, "window.setrect width:%d height:%d coord:%d %d\n", w, h, x, y); | 143 | appendFormat_String(str, "window.setrect width:%d height:%d coord:%d %d\n", w, h, x, y); |
107 | appendFormat_String(str, "sidebar.width arg:%d\n", width_SidebarWidget(sidebar)); | 144 | appendFormat_String(str, "sidebar.width arg:%d\n", width_SidebarWidget(sidebar)); |
108 | } | 145 | } |
109 | appendFormat_String(str, "retainwindow arg:%d\n", d->retainWindowSize); | ||
110 | if (isVisible_Widget(constAs_Widget(sidebar))) { | 146 | if (isVisible_Widget(constAs_Widget(sidebar))) { |
111 | appendCStr_String(str, "sidebar.toggle\n"); | 147 | appendCStr_String(str, "sidebar.toggle\n"); |
112 | } | 148 | } |
113 | appendFormat_String(str, "sidebar.mode arg:%d\n", mode_SidebarWidget(sidebar)); | 149 | appendFormat_String(str, "sidebar.mode arg:%d\n", mode_SidebarWidget(sidebar)); |
114 | appendFormat_String(str, "uiscale arg:%f\n", uiScale_Window(d->window)); | 150 | appendFormat_String(str, "uiscale arg:%f\n", uiScale_Window(d->window)); |
115 | appendFormat_String(str, "zoom.set arg:%d\n", d->zoomPercent); | 151 | appendFormat_String(str, "zoom.set arg:%d\n", d->zoomPercent); |
116 | appendFormat_String(str, "theme.set arg:%d\n", d->theme); | 152 | appendFormat_String(str, "theme.set arg:%d auto:1\n", d->theme); |
153 | appendFormat_String(str, "ostheme arg:%d\n", d->useSystemTheme); | ||
154 | appendFormat_String(str, "proxy.gopher address:%s\n", cstr_String(&d->gopherProxy)); | ||
155 | appendFormat_String(str, "proxy.http address:%s\n", cstr_String(&d->httpProxy)); | ||
117 | return str; | 156 | return str; |
118 | } | 157 | } |
119 | 158 | ||
@@ -130,7 +169,7 @@ static void loadPrefs_App_(iApp *d) { | |||
130 | iString *str = readString_File(f); | 169 | iString *str = readString_File(f); |
131 | const iRangecc src = range_String(str); | 170 | const iRangecc src = range_String(str); |
132 | iRangecc line = iNullRange; | 171 | iRangecc line = iNullRange; |
133 | while (nextSplit_Rangecc(&src, "\n", &line)) { | 172 | while (nextSplit_Rangecc(src, "\n", &line)) { |
134 | iString cmdStr; | 173 | iString cmdStr; |
135 | initRange_String(&cmdStr, line); | 174 | initRange_String(&cmdStr, line); |
136 | const char *cmd = cstr_String(&cmdStr); | 175 | const char *cmd = cstr_String(&cmdStr); |
@@ -231,9 +270,12 @@ static void saveState_App_(const iApp *d) { | |||
231 | static void init_App_(iApp *d, int argc, char **argv) { | 270 | static void init_App_(iApp *d, int argc, char **argv) { |
232 | init_CommandLine(&d->args, argc, argv); | 271 | init_CommandLine(&d->args, argc, argv); |
233 | init_SortedArray(&d->tickers, sizeof(iTicker), cmp_Ticker_); | 272 | init_SortedArray(&d->tickers, sizeof(iTicker), cmp_Ticker_); |
273 | d->lastTickerTime = SDL_GetTicks(); | ||
274 | d->elapsedSinceLastTicker = 0; | ||
234 | d->commandEcho = checkArgument_CommandLine(&d->args, "echo") != NULL; | 275 | d->commandEcho = checkArgument_CommandLine(&d->args, "echo") != NULL; |
235 | d->initialWindowRect = init_Rect(-1, -1, 800, 500); | 276 | d->initialWindowRect = init_Rect(-1, -1, 800, 500); |
236 | d->theme = dark_ColorTheme; | 277 | d->theme = dark_ColorTheme; |
278 | d->useSystemTheme = iTrue; | ||
237 | d->running = iFalse; | 279 | d->running = iFalse; |
238 | d->window = NULL; | 280 | d->window = NULL; |
239 | d->retainWindowSize = iTrue; | 281 | d->retainWindowSize = iTrue; |
@@ -243,15 +285,17 @@ static void init_App_(iApp *d, int argc, char **argv) { | |||
243 | d->visited = new_Visited(); | 285 | d->visited = new_Visited(); |
244 | d->bookmarks = new_Bookmarks(); | 286 | d->bookmarks = new_Bookmarks(); |
245 | d->tabEnum = 0; /* generates unique IDs for tab pages */ | 287 | d->tabEnum = 0; /* generates unique IDs for tab pages */ |
288 | init_String(&d->gopherProxy); | ||
289 | init_String(&d->httpProxy); | ||
246 | setThemePalette_Color(d->theme); | 290 | setThemePalette_Color(d->theme); |
247 | loadPrefs_App_(d); | 291 | loadPrefs_App_(d); |
248 | load_Visited(d->visited, dataDir_App_); | 292 | load_Visited(d->visited, dataDir_App_); |
249 | load_Bookmarks(d->bookmarks, dataDir_App_); | 293 | load_Bookmarks(d->bookmarks, dataDir_App_); |
250 | #if defined (iHaveLoadEmbed) | 294 | #if defined (iHaveLoadEmbed) |
251 | /* Load the resources from a file. */ { | 295 | /* Load the resources from a file. */ { |
252 | if (!load_Embed(concatPath_CStr(cstr_String(execPath_App()), "../resources.bin"))) { | 296 | if (!load_Embed(concatPath_CStr(cstr_String(execPath_App()), EMB_BIN))) { |
253 | if (!load_Embed(concatPath_CStr(cstr_String(execPath_App()), EMB_BIN))) { | 297 | if (!load_Embed(concatPath_CStr(cstr_String(execPath_App()), EMB_BIN2))) { |
254 | fprintf(stderr, "failed to load resources.bin: %s\n", strerror(errno)); | 298 | fprintf(stderr, "failed to load resources: %s\n", strerror(errno)); |
255 | exit(-1); | 299 | exit(-1); |
256 | } | 300 | } |
257 | } | 301 | } |
@@ -269,6 +313,8 @@ static void init_App_(iApp *d, int argc, char **argv) { | |||
269 | static void deinit_App(iApp *d) { | 313 | static void deinit_App(iApp *d) { |
270 | saveState_App_(d); | 314 | saveState_App_(d); |
271 | savePrefs_App_(d); | 315 | savePrefs_App_(d); |
316 | deinit_String(&d->httpProxy); | ||
317 | deinit_String(&d->gopherProxy); | ||
272 | save_Bookmarks(d->bookmarks, dataDir_App_); | 318 | save_Bookmarks(d->bookmarks, dataDir_App_); |
273 | delete_Bookmarks(d->bookmarks); | 319 | delete_Bookmarks(d->bookmarks); |
274 | save_Visited(d->visited, dataDir_App_); | 320 | save_Visited(d->visited, dataDir_App_); |
@@ -288,12 +334,17 @@ const iString *dataDir_App(void) { | |||
288 | return collect_String(cleanedCStr_Path(dataDir_App_)); | 334 | return collect_String(cleanedCStr_Path(dataDir_App_)); |
289 | } | 335 | } |
290 | 336 | ||
337 | iLocalDef iBool isWaitingAllowed_App_(const iApp *d) { | ||
338 | return !d->pendingRefresh && isEmpty_SortedArray(&d->tickers); | ||
339 | } | ||
340 | |||
291 | void processEvents_App(enum iAppEventMode eventMode) { | 341 | void processEvents_App(enum iAppEventMode eventMode) { |
292 | iApp *d = &app_; | 342 | iApp *d = &app_; |
293 | SDL_Event ev; | 343 | SDL_Event ev; |
294 | while ( | 344 | while ((isWaitingAllowed_App_(d) && eventMode == waitForNewEvents_AppEventMode && |
295 | (!d->pendingRefresh && eventMode == waitForNewEvents_AppEventMode && SDL_WaitEvent(&ev)) || | 345 | SDL_WaitEvent(&ev)) || |
296 | ((d->pendingRefresh || eventMode == postedEventsOnly_AppEventMode) && SDL_PollEvent(&ev))) { | 346 | ((!isWaitingAllowed_App_(d) || eventMode == postedEventsOnly_AppEventMode) && |
347 | SDL_PollEvent(&ev))) { | ||
297 | switch (ev.type) { | 348 | switch (ev.type) { |
298 | case SDL_QUIT: | 349 | case SDL_QUIT: |
299 | d->running = iFalse; | 350 | d->running = iFalse; |
@@ -325,12 +376,17 @@ backToMainLoop:; | |||
325 | } | 376 | } |
326 | 377 | ||
327 | static void runTickers_App_(iApp *d) { | 378 | static void runTickers_App_(iApp *d) { |
379 | const uint32_t now = SDL_GetTicks(); | ||
380 | d->elapsedSinceLastTicker = (d->lastTickerTime ? now - d->lastTickerTime : 0); | ||
381 | d->lastTickerTime = now; | ||
382 | if (isEmpty_SortedArray(&d->tickers)) { | ||
383 | d->lastTickerTime = 0; | ||
384 | return; | ||
385 | } | ||
328 | /* Tickers may add themselves again, so we'll run off a copy. */ | 386 | /* Tickers may add themselves again, so we'll run off a copy. */ |
329 | iSortedArray *pending = copy_SortedArray(&d->tickers); | 387 | iSortedArray *pending = copy_SortedArray(&d->tickers); |
330 | clear_SortedArray(&d->tickers); | 388 | clear_SortedArray(&d->tickers); |
331 | if (!isEmpty_SortedArray(pending)) { | 389 | postRefresh_App(); |
332 | postRefresh_App(); | ||
333 | } | ||
334 | iConstForEach(Array, i, &pending->values) { | 390 | iConstForEach(Array, i, &pending->values) { |
335 | const iTicker *ticker = i.value; | 391 | const iTicker *ticker = i.value; |
336 | if (ticker->callback) { | 392 | if (ticker->callback) { |
@@ -338,6 +394,9 @@ static void runTickers_App_(iApp *d) { | |||
338 | } | 394 | } |
339 | } | 395 | } |
340 | delete_SortedArray(pending); | 396 | delete_SortedArray(pending); |
397 | if (isEmpty_SortedArray(&d->tickers)) { | ||
398 | d->lastTickerTime = 0; | ||
399 | } | ||
341 | } | 400 | } |
342 | 401 | ||
343 | static int run_App_(iApp *d) { | 402 | static int run_App_(iApp *d) { |
@@ -345,8 +404,8 @@ static int run_App_(iApp *d) { | |||
345 | d->running = iTrue; | 404 | d->running = iTrue; |
346 | SDL_EventState(SDL_DROPFILE, SDL_ENABLE); /* open files via drag'n'drop */ | 405 | SDL_EventState(SDL_DROPFILE, SDL_ENABLE); /* open files via drag'n'drop */ |
347 | while (d->running) { | 406 | while (d->running) { |
348 | runTickers_App_(d); | ||
349 | processEvents_App(waitForNewEvents_AppEventMode); | 407 | processEvents_App(waitForNewEvents_AppEventMode); |
408 | runTickers_App_(d); | ||
350 | refresh_App(); | 409 | refresh_App(); |
351 | recycle_Garbage(); | 410 | recycle_Garbage(); |
352 | } | 411 | } |
@@ -364,6 +423,10 @@ iBool isRefreshPending_App(void) { | |||
364 | return app_.pendingRefresh; | 423 | return app_.pendingRefresh; |
365 | } | 424 | } |
366 | 425 | ||
426 | uint32_t elapsedSinceLastTicker_App(void) { | ||
427 | return app_.elapsedSinceLastTicker; | ||
428 | } | ||
429 | |||
367 | int zoom_App(void) { | 430 | int zoom_App(void) { |
368 | return app_.zoomPercent; | 431 | return app_.zoomPercent; |
369 | } | 432 | } |
@@ -372,6 +435,17 @@ enum iColorTheme colorTheme_App(void) { | |||
372 | return app_.theme; | 435 | return app_.theme; |
373 | } | 436 | } |
374 | 437 | ||
438 | const iString *schemeProxy_App(iRangecc scheme) { | ||
439 | iApp *d = &app_; | ||
440 | if (equalCase_Rangecc(scheme, "gopher")) { | ||
441 | return &d->gopherProxy; | ||
442 | } | ||
443 | if (equalCase_Rangecc(scheme, "http") || equalCase_Rangecc(scheme, "https")) { | ||
444 | return &d->httpProxy; | ||
445 | } | ||
446 | return NULL; | ||
447 | } | ||
448 | |||
375 | int run_App(int argc, char **argv) { | 449 | int run_App(int argc, char **argv) { |
376 | init_App_(&app_, argc, argv); | 450 | init_App_(&app_, argc, argv); |
377 | const int rc = run_App_(&app_); | 451 | const int rc = run_App_(&app_); |
@@ -394,7 +468,12 @@ void postRefresh_App(void) { | |||
394 | } | 468 | } |
395 | 469 | ||
396 | void postCommand_App(const char *command) { | 470 | void postCommand_App(const char *command) { |
471 | iAssert(command); | ||
397 | SDL_Event ev; | 472 | SDL_Event ev; |
473 | if (*command == '!') { | ||
474 | /* Global command; this is global context so just ignore. */ | ||
475 | command++; | ||
476 | } | ||
398 | ev.user.type = SDL_USEREVENT; | 477 | ev.user.type = SDL_USEREVENT; |
399 | ev.user.code = command_UserEventCode; | 478 | ev.user.code = command_UserEventCode; |
400 | ev.user.windowID = get_Window() ? SDL_GetWindowID(get_Window()->win) : 0; | 479 | ev.user.windowID = get_Window() ? SDL_GetWindowID(get_Window()->win) : 0; |
@@ -424,6 +503,7 @@ iAny *findWidget_App(const char *id) { | |||
424 | void addTicker_App(void (*ticker)(iAny *), iAny *context) { | 503 | void addTicker_App(void (*ticker)(iAny *), iAny *context) { |
425 | iApp *d = &app_; | 504 | iApp *d = &app_; |
426 | insert_SortedArray(&d->tickers, &(iTicker){ context, ticker }); | 505 | insert_SortedArray(&d->tickers, &(iTicker){ context, ticker }); |
506 | postRefresh_App(); | ||
427 | } | 507 | } |
428 | 508 | ||
429 | iGmCerts *certs_App(void) { | 509 | iGmCerts *certs_App(void) { |
@@ -450,13 +530,25 @@ static iBool handlePrefsCommands_(iWidget *d, const char *cmd) { | |||
450 | if (equal_Command(cmd, "prefs.dismiss") || equal_Command(cmd, "preferences")) { | 530 | if (equal_Command(cmd, "prefs.dismiss") || equal_Command(cmd, "preferences")) { |
451 | setUiScale_Window(get_Window(), | 531 | setUiScale_Window(get_Window(), |
452 | toFloat_String(text_InputWidget(findChild_Widget(d, "prefs.uiscale")))); | 532 | toFloat_String(text_InputWidget(findChild_Widget(d, "prefs.uiscale")))); |
453 | postCommandf_App("retainwindow arg:%d", | 533 | postCommandf_App("window.retain arg:%d", |
454 | isSelected_Widget(findChild_Widget(d, "prefs.retainwindow"))); | 534 | isSelected_Widget(findChild_Widget(d, "prefs.retainwindow"))); |
535 | postCommandf_App("ostheme arg:%d", | ||
536 | isSelected_Widget(findChild_Widget(d, "prefs.ostheme"))); | ||
537 | postCommandf_App("proxy.http address:%s", | ||
538 | cstr_String(text_InputWidget(findChild_Widget(d, "prefs.proxy.http")))); | ||
539 | postCommandf_App("proxy.gopher address:%s", | ||
540 | cstr_String(text_InputWidget(findChild_Widget(d, "prefs.proxy.gopher")))); | ||
455 | destroy_Widget(d); | 541 | destroy_Widget(d); |
456 | return iTrue; | 542 | return iTrue; |
457 | } | 543 | } |
458 | else if (equal_Command(cmd, "theme.changed")) { | 544 | else if (equal_Command(cmd, "prefs.ostheme.changed")) { |
545 | postCommandf_App("ostheme arg:%d", arg_Command(cmd)); | ||
546 | } | ||
547 | else if (equal_Command(cmd, "theme.changed")) { | ||
459 | updatePrefsThemeButtons_(d); | 548 | updatePrefsThemeButtons_(d); |
549 | if (!argLabel_Command(cmd, "auto")) { | ||
550 | setToggle_Widget(findChild_Widget(d, "prefs.ostheme"), iFalse); | ||
551 | } | ||
460 | } | 552 | } |
461 | return iFalse; | 553 | return iFalse; |
462 | } | 554 | } |
@@ -502,18 +594,81 @@ iDocumentWidget *newTab_App(const iDocumentWidget *duplicateOf) { | |||
502 | return doc; | 594 | return doc; |
503 | } | 595 | } |
504 | 596 | ||
597 | static iBool handleIdentityCreationCommands_(iWidget *dlg, const char *cmd) { | ||
598 | iApp *d = &app_; | ||
599 | if (equal_Command(cmd, "ident.accept") || equal_Command(cmd, "cancel")) { | ||
600 | if (equal_Command(cmd, "ident.accept")) { | ||
601 | const iString *commonName = text_InputWidget (findChild_Widget(dlg, "ident.common")); | ||
602 | const iString *email = text_InputWidget (findChild_Widget(dlg, "ident.email")); | ||
603 | const iString *userId = text_InputWidget (findChild_Widget(dlg, "ident.userid")); | ||
604 | const iString *domain = text_InputWidget (findChild_Widget(dlg, "ident.domain")); | ||
605 | const iString *organization = text_InputWidget (findChild_Widget(dlg, "ident.org")); | ||
606 | const iString *country = text_InputWidget (findChild_Widget(dlg, "ident.country")); | ||
607 | const iBool isTemp = isSelected_Widget(findChild_Widget(dlg, "ident.temp")); | ||
608 | if (isEmpty_String(commonName)) { | ||
609 | makeMessage_Widget(orange_ColorEscape "MISSING INFO", | ||
610 | "A \"Common name\" must be specified."); | ||
611 | return iTrue; | ||
612 | } | ||
613 | iDate until; | ||
614 | /* Validate the date. */ { | ||
615 | iZap(until); | ||
616 | unsigned int val[6]; | ||
617 | iDate today; | ||
618 | initCurrent_Date(&today); | ||
619 | const int n = | ||
620 | sscanf(cstr_String(text_InputWidget(findChild_Widget(dlg, "ident.until"))), | ||
621 | "%04u-%u-%u %u:%u:%u", | ||
622 | &val[0], &val[1], &val[2], &val[3], &val[4], &val[5]); | ||
623 | if (n <= 0 || val[0] < (unsigned) today.year) { | ||
624 | makeMessage_Widget(orange_ColorEscape "INVALID DATE", | ||
625 | "Please check the \"Valid until\" date. Examples:\n" | ||
626 | "\u2022 2030\n" | ||
627 | "\u2022 2025-06-30\n" | ||
628 | "\u2022 2021-12-31 23:59:59"); | ||
629 | return iTrue; | ||
630 | } | ||
631 | until.year = val[0]; | ||
632 | until.month = n >= 2 ? val[1] : 1; | ||
633 | until.day = n >= 3 ? val[2] : 1; | ||
634 | until.hour = n >= 4 ? val[3] : 0; | ||
635 | until.minute = n >= 5 ? val[4] : 0; | ||
636 | until.second = n == 6 ? val[5] : 0; | ||
637 | /* In the past? */ { | ||
638 | iTime now, t; | ||
639 | initCurrent_Time(&now); | ||
640 | init_Time(&t, &until); | ||
641 | if (cmp_Time(&t, &now) <= 0) { | ||
642 | makeMessage_Widget(orange_ColorEscape "INVALID DATE", | ||
643 | "Expiration date must be in the future."); | ||
644 | return iTrue; | ||
645 | } | ||
646 | } | ||
647 | } | ||
648 | /* The input seems fine. */ | ||
649 | newIdentity_GmCerts(d->certs, isTemp ? temporary_GmIdentityFlag : 0, | ||
650 | until, commonName, email, userId, domain, organization, country); | ||
651 | postCommandf_App("sidebar.mode arg:%d show:1", identities_SidebarMode); | ||
652 | postCommand_App("idents.changed"); | ||
653 | } | ||
654 | destroy_Widget(dlg); | ||
655 | return iTrue; | ||
656 | } | ||
657 | return iFalse; | ||
658 | } | ||
659 | |||
505 | iBool handleCommand_App(const char *cmd) { | 660 | iBool handleCommand_App(const char *cmd) { |
506 | iApp *d = &app_; | 661 | iApp *d = &app_; |
507 | if (equal_Command(cmd, "retainwindow")) { | 662 | if (equal_Command(cmd, "window.retain")) { |
508 | d->retainWindowSize = arg_Command(cmd); | 663 | d->retainWindowSize = arg_Command(cmd); |
509 | return iTrue; | 664 | return iTrue; |
510 | } | 665 | } |
511 | else if (equal_Command(cmd, "open")) { | 666 | else if (equal_Command(cmd, "open")) { |
512 | const iString *url = collect_String(newCStr_String(suffixPtr_Command(cmd, "url"))); | 667 | const iString *url = collectNewCStr_String(suffixPtr_Command(cmd, "url")); |
513 | iUrl parts; | 668 | iUrl parts; |
514 | init_Url(&parts, url); | 669 | init_Url(&parts, url); |
515 | if (equalCase_Rangecc(&parts.protocol, "http") || | 670 | if (isEmpty_String(&d->httpProxy) && |
516 | equalCase_Rangecc(&parts.protocol, "https")) { | 671 | (equalCase_Rangecc(parts.scheme, "http") || equalCase_Rangecc(parts.scheme, "https"))) { |
517 | openInDefaultBrowser_App(url); | 672 | openInDefaultBrowser_App(url); |
518 | return iTrue; | 673 | return iTrue; |
519 | } | 674 | } |
@@ -523,8 +678,9 @@ iBool handleCommand_App(const char *cmd) { | |||
523 | } | 678 | } |
524 | iHistory *history = history_DocumentWidget(doc); | 679 | iHistory *history = history_DocumentWidget(doc); |
525 | const iBool isHistory = argLabel_Command(cmd, "history") != 0; | 680 | const iBool isHistory = argLabel_Command(cmd, "history") != 0; |
681 | int redirectCount = argLabel_Command(cmd, "redirect"); | ||
526 | if (!isHistory) { | 682 | if (!isHistory) { |
527 | if (argLabel_Command(cmd, "redirect")) { | 683 | if (redirectCount) { |
528 | replace_History(history, url); | 684 | replace_History(history, url); |
529 | } | 685 | } |
530 | else { | 686 | else { |
@@ -533,6 +689,7 @@ iBool handleCommand_App(const char *cmd) { | |||
533 | } | 689 | } |
534 | visitUrl_Visited(d->visited, url); | 690 | visitUrl_Visited(d->visited, url); |
535 | setInitialScroll_DocumentWidget(doc, argfLabel_Command(cmd, "scroll")); | 691 | setInitialScroll_DocumentWidget(doc, argfLabel_Command(cmd, "scroll")); |
692 | setRedirectCount_DocumentWidget(doc, redirectCount); | ||
536 | setUrlFromCache_DocumentWidget(doc, url, isHistory); | 693 | setUrlFromCache_DocumentWidget(doc, url, isHistory); |
537 | } | 694 | } |
538 | else if (equal_Command(cmd, "document.request.cancelled")) { | 695 | else if (equal_Command(cmd, "document.request.cancelled")) { |
@@ -601,9 +758,14 @@ iBool handleCommand_App(const char *cmd) { | |||
601 | else if (equal_Command(cmd, "preferences")) { | 758 | else if (equal_Command(cmd, "preferences")) { |
602 | iWidget *dlg = makePreferences_Widget(); | 759 | iWidget *dlg = makePreferences_Widget(); |
603 | updatePrefsThemeButtons_(dlg); | 760 | updatePrefsThemeButtons_(dlg); |
761 | setToggle_Widget(findChild_Widget(dlg, "prefs.ostheme"), d->useSystemTheme); | ||
604 | setToggle_Widget(findChild_Widget(dlg, "prefs.retainwindow"), d->retainWindowSize); | 762 | setToggle_Widget(findChild_Widget(dlg, "prefs.retainwindow"), d->retainWindowSize); |
605 | setText_InputWidget(findChild_Widget(dlg, "prefs.uiscale"), | 763 | setText_InputWidget(findChild_Widget(dlg, "prefs.uiscale"), |
606 | collectNewFormat_String("%g", uiScale_Window(d->window))); | 764 | collectNewFormat_String("%g", uiScale_Window(d->window))); |
765 | setText_InputWidget(findChild_Widget(dlg, "prefs.proxy.http"), | ||
766 | schemeProxy_App(range_CStr("http"))); | ||
767 | setText_InputWidget(findChild_Widget(dlg, "prefs.proxy.gopher"), | ||
768 | schemeProxy_App(range_CStr("gopher"))); | ||
607 | setCommandHandler_Widget(dlg, handlePrefsCommands_); | 769 | setCommandHandler_Widget(dlg, handlePrefsCommands_); |
608 | } | 770 | } |
609 | else if (equal_Command(cmd, "navigate.home")) { | 771 | else if (equal_Command(cmd, "navigate.home")) { |
@@ -633,17 +795,46 @@ iBool handleCommand_App(const char *cmd) { | |||
633 | } | 795 | } |
634 | else if (equal_Command(cmd, "bookmark.add")) { | 796 | else if (equal_Command(cmd, "bookmark.add")) { |
635 | iDocumentWidget *doc = document_App(); | 797 | iDocumentWidget *doc = document_App(); |
636 | add_Bookmarks(d->bookmarks, url_DocumentWidget(doc), | 798 | makeBookmarkCreation_Widget(url_DocumentWidget(doc), |
637 | bookmarkTitle_DocumentWidget(doc), | 799 | bookmarkTitle_DocumentWidget(doc), |
638 | collectNew_String(), | 800 | siteIcon_GmDocument(document_DocumentWidget(doc))); |
639 | siteIcon_GmDocument(document_DocumentWidget(doc))); | 801 | return iTrue; |
640 | postCommand_App("bookmarks.changed"); | 802 | } |
803 | else if (equal_Command(cmd, "ident.new")) { | ||
804 | iWidget *dlg = makeIdentityCreation_Widget(); | ||
805 | setCommandHandler_Widget(dlg, handleIdentityCreationCommands_); | ||
641 | return iTrue; | 806 | return iTrue; |
642 | } | 807 | } |
643 | else if (equal_Command(cmd, "theme.set")) { | 808 | else if (equal_Command(cmd, "theme.set")) { |
809 | const int isAuto = argLabel_Command(cmd, "auto"); | ||
644 | d->theme = arg_Command(cmd); | 810 | d->theme = arg_Command(cmd); |
811 | if (!isAuto) { | ||
812 | postCommand_App("ostheme arg:0"); | ||
813 | } | ||
645 | setThemePalette_Color(d->theme); | 814 | setThemePalette_Color(d->theme); |
646 | postCommand_App("theme.changed"); | 815 | postCommandf_App("theme.changed auto:%d", isAuto); |
816 | return iTrue; | ||
817 | } | ||
818 | else if (equal_Command(cmd, "ostheme")) { | ||
819 | d->useSystemTheme = arg_Command(cmd); | ||
820 | return iTrue; | ||
821 | } | ||
822 | else if (equal_Command(cmd, "os.theme.changed")) { | ||
823 | if (d->useSystemTheme) { | ||
824 | const int dark = argLabel_Command(cmd, "dark"); | ||
825 | const int contrast = argLabel_Command(cmd, "contrast"); | ||
826 | postCommandf_App("theme.set arg:%d auto:1", | ||
827 | dark ? (contrast ? pureBlack_ColorTheme : dark_ColorTheme) | ||
828 | : (contrast ? pureWhite_ColorTheme : light_ColorTheme)); | ||
829 | } | ||
830 | return iFalse; | ||
831 | } | ||
832 | else if (equal_Command(cmd, "proxy.gopher")) { | ||
833 | setCStr_String(&d->gopherProxy, suffixPtr_Command(cmd, "address")); | ||
834 | return iTrue; | ||
835 | } | ||
836 | else if (equal_Command(cmd, "proxy.http")) { | ||
837 | setCStr_String(&d->httpProxy, suffixPtr_Command(cmd, "address")); | ||
647 | return iTrue; | 838 | return iTrue; |
648 | } | 839 | } |
649 | else { | 840 | else { |
@@ -666,3 +857,44 @@ void openInDefaultBrowser_App(const iString *url) { | |||
666 | start_Process(proc); | 857 | start_Process(proc); |
667 | iRelease(proc); | 858 | iRelease(proc); |
668 | } | 859 | } |
860 | |||
861 | void revealPath_App(const iString *path) { | ||
862 | #if defined (iPlatformApple) | ||
863 | const char *scriptPath = concatPath_CStr(dataDir_App_, "revealfile.scpt"); | ||
864 | iFile *f = newCStr_File(scriptPath); | ||
865 | if (open_File(f, writeOnly_FileMode | text_FileMode)) { | ||
866 | /* AppleScript to select a specific file. */ | ||
867 | write_File(f, collect_Block(newCStr_Block("on run argv\n" | ||
868 | " tell application \"Finder\"\n" | ||
869 | " activate\n" | ||
870 | " reveal POSIX file (item 1 of argv) as text\n" | ||
871 | " end tell\n" | ||
872 | "end run\n"))); | ||
873 | close_File(f); | ||
874 | iProcess *proc = new_Process(); | ||
875 | setArguments_Process( | ||
876 | proc, | ||
877 | iClob(newStringsCStr_StringList( | ||
878 | "/usr/bin/osascript", scriptPath, cstr_String(path), NULL))); | ||
879 | start_Process(proc); | ||
880 | iRelease(proc); | ||
881 | } | ||
882 | iRelease(f); | ||
883 | #elif defined (iPlatformLinux) | ||
884 | iFileInfo *inf = iClob(new_FileInfo(path)); | ||
885 | iRangecc target; | ||
886 | if (isDirectory_FileInfo(inf)) { | ||
887 | target = range_String(path); | ||
888 | } | ||
889 | else { | ||
890 | target = dirName_Path(path); | ||
891 | } | ||
892 | iProcess *proc = new_Process(); | ||
893 | setArguments_Process( | ||
894 | proc, iClob(newStringsCStr_StringList("/usr/bin/xdg-open", cstr_Rangecc(target), NULL))); | ||
895 | start_Process(proc); | ||
896 | iRelease(proc); | ||
897 | #else | ||
898 | iAssert(0 /* File revealing not implemented on this platform */); | ||
899 | #endif | ||
900 | } | ||
@@ -1,3 +1,25 @@ | |||
1 | /* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi> | ||
2 | |||
3 | Redistribution and use in source and binary forms, with or without | ||
4 | modification, are permitted provided that the following conditions are met: | ||
5 | |||
6 | 1. Redistributions of source code must retain the above copyright notice, this | ||
7 | list of conditions and the following disclaimer. | ||
8 | 2. Redistributions in binary form must reproduce the above copyright notice, | ||
9 | this list of conditions and the following disclaimer in the documentation | ||
10 | and/or other materials provided with the distribution. | ||
11 | |||
12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | ||
13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | ||
14 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||
15 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR | ||
16 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | ||
17 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | ||
18 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON | ||
19 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||
20 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | ||
21 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | ||
22 | |||
1 | #pragma once | 23 | #pragma once |
2 | 24 | ||
3 | /* Application core: event loop, base event processing, audio synth. */ | 25 | /* Application core: event loop, base event processing, audio synth. */ |
@@ -26,14 +48,16 @@ enum iUserEventCode { | |||
26 | const iString *execPath_App (void); | 48 | const iString *execPath_App (void); |
27 | const iString *dataDir_App (void); | 49 | const iString *dataDir_App (void); |
28 | 50 | ||
29 | int run_App (int argc, char **argv); | 51 | int run_App (int argc, char **argv); |
30 | void processEvents_App (enum iAppEventMode mode); | 52 | void processEvents_App (enum iAppEventMode mode); |
31 | iBool handleCommand_App (const char *cmd); | 53 | iBool handleCommand_App (const char *cmd); |
32 | void refresh_App (void); | 54 | void refresh_App (void); |
33 | iBool isRefreshPending_App(void); | 55 | iBool isRefreshPending_App (void); |
56 | uint32_t elapsedSinceLastTicker_App (void); /* milliseconds */ | ||
34 | 57 | ||
35 | int zoom_App (void); | 58 | int zoom_App (void); |
36 | enum iColorTheme colorTheme_App (void); | 59 | enum iColorTheme colorTheme_App (void); |
60 | const iString * schemeProxy_App (iRangecc scheme); | ||
37 | 61 | ||
38 | iGmCerts * certs_App (void); | 62 | iGmCerts * certs_App (void); |
39 | iVisited * visited_App (void); | 63 | iVisited * visited_App (void); |
@@ -53,3 +77,4 @@ iLocalDef void postCommandString_App(const iString *command) { | |||
53 | } | 77 | } |
54 | 78 | ||
55 | void openInDefaultBrowser_App (const iString *url); | 79 | void openInDefaultBrowser_App (const iString *url); |
80 | void revealPath_App (const iString *path); | ||
diff --git a/src/bookmarks.c b/src/bookmarks.c index 795e0d44..eff0146d 100644 --- a/src/bookmarks.c +++ b/src/bookmarks.c | |||
@@ -1,3 +1,25 @@ | |||
1 | /* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi> | ||
2 | |||
3 | Redistribution and use in source and binary forms, with or without | ||
4 | modification, are permitted provided that the following conditions are met: | ||
5 | |||
6 | 1. Redistributions of source code must retain the above copyright notice, this | ||
7 | list of conditions and the following disclaimer. | ||
8 | 2. Redistributions in binary form must reproduce the above copyright notice, | ||
9 | this list of conditions and the following disclaimer in the documentation | ||
10 | and/or other materials provided with the distribution. | ||
11 | |||
12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | ||
13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | ||
14 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||
15 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR | ||
16 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | ||
17 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | ||
18 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON | ||
19 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||
20 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | ||
21 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | ||
22 | |||
1 | #include "bookmarks.h" | 23 | #include "bookmarks.h" |
2 | 24 | ||
3 | #include <the_Foundation/file.h> | 25 | #include <the_Foundation/file.h> |
@@ -63,7 +85,7 @@ void load_Bookmarks(iBookmarks *d, const char *dirPath) { | |||
63 | if (open_File(f, readOnly_FileMode | text_FileMode)) { | 85 | if (open_File(f, readOnly_FileMode | text_FileMode)) { |
64 | const iRangecc src = range_Block(collect_Block(readAll_File(f))); | 86 | const iRangecc src = range_Block(collect_Block(readAll_File(f))); |
65 | iRangecc line = iNullRange; | 87 | iRangecc line = iNullRange; |
66 | while (nextSplit_Rangecc(&src, "\n", &line)) { | 88 | while (nextSplit_Rangecc(src, "\n", &line)) { |
67 | /* Skip empty lines. */ { | 89 | /* Skip empty lines. */ { |
68 | iRangecc ln = line; | 90 | iRangecc ln = line; |
69 | trim_Rangecc(&ln); | 91 | trim_Rangecc(&ln); |
@@ -78,9 +100,9 @@ void load_Bookmarks(iBookmarks *d, const char *dirPath) { | |||
78 | initSeconds_Time(&bm->when, strtod(line.start, &endPos)); | 100 | initSeconds_Time(&bm->when, strtod(line.start, &endPos)); |
79 | line.start = skipSpace_CStr(endPos); | 101 | line.start = skipSpace_CStr(endPos); |
80 | setRange_String(&bm->url, line); | 102 | setRange_String(&bm->url, line); |
81 | nextSplit_Rangecc(&src, "\n", &line); | 103 | nextSplit_Rangecc(src, "\n", &line); |
82 | setRange_String(&bm->title, line); | 104 | setRange_String(&bm->title, line); |
83 | nextSplit_Rangecc(&src, "\n", &line); | 105 | nextSplit_Rangecc(src, "\n", &line); |
84 | setRange_String(&bm->tags, line); | 106 | setRange_String(&bm->tags, line); |
85 | insert_Bookmarks_(d, bm); | 107 | insert_Bookmarks_(d, bm); |
86 | } | 108 | } |
diff --git a/src/bookmarks.h b/src/bookmarks.h index e05aefd3..a3269a59 100644 --- a/src/bookmarks.h +++ b/src/bookmarks.h | |||
@@ -1,3 +1,25 @@ | |||
1 | /* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi> | ||
2 | |||
3 | Redistribution and use in source and binary forms, with or without | ||
4 | modification, are permitted provided that the following conditions are met: | ||
5 | |||
6 | 1. Redistributions of source code must retain the above copyright notice, this | ||
7 | list of conditions and the following disclaimer. | ||
8 | 2. Redistributions in binary form must reproduce the above copyright notice, | ||
9 | this list of conditions and the following disclaimer in the documentation | ||
10 | and/or other materials provided with the distribution. | ||
11 | |||
12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | ||
13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | ||
14 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||
15 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR | ||
16 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | ||
17 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | ||
18 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON | ||
19 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||
20 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | ||
21 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | ||
22 | |||
1 | #pragma once | 23 | #pragma once |
2 | 24 | ||
3 | #include <the_Foundation/hash.h> | 25 | #include <the_Foundation/hash.h> |
@@ -42,7 +64,7 @@ typedef int (*iBookmarksCompareFunc)(const iBookmark **, const iBookmark **); | |||
42 | * returned list is sorted by descending creation time. | 64 | * returned list is sorted by descending creation time. |
43 | * | 65 | * |
44 | * @return Collected array of bookmarks. Caller does not get ownership of the | 66 | * @return Collected array of bookmarks. Caller does not get ownership of the |
45 | * list or the bookmarks. | 67 | * listed bookmarks. |
46 | */ | 68 | */ |
47 | const iPtrArray *list_Bookmarks(const iBookmarks *, iBookmarksFilterFunc filter, | 69 | const iPtrArray *list_Bookmarks(const iBookmarks *, iBookmarksFilterFunc filter, |
48 | iBookmarksCompareFunc cmp); | 70 | iBookmarksCompareFunc cmp); |
diff --git a/src/gmcerts.c b/src/gmcerts.c index 11e9ce2c..b24a6d9c 100644 --- a/src/gmcerts.c +++ b/src/gmcerts.c | |||
@@ -1,14 +1,41 @@ | |||
1 | /* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi> | ||
2 | |||
3 | Redistribution and use in source and binary forms, with or without | ||
4 | modification, are permitted provided that the following conditions are met: | ||
5 | |||
6 | 1. Redistributions of source code must retain the above copyright notice, this | ||
7 | list of conditions and the following disclaimer. | ||
8 | 2. Redistributions in binary form must reproduce the above copyright notice, | ||
9 | this list of conditions and the following disclaimer in the documentation | ||
10 | and/or other materials provided with the distribution. | ||
11 | |||
12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | ||
13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | ||
14 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||
15 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR | ||
16 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | ||
17 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | ||
18 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON | ||
19 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||
20 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | ||
21 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | ||
22 | |||
1 | #include "gmcerts.h" | 23 | #include "gmcerts.h" |
2 | 24 | ||
3 | #include <the_Foundation/file.h> | 25 | #include <the_Foundation/file.h> |
26 | #include <the_Foundation/fileinfo.h> | ||
4 | #include <the_Foundation/mutex.h> | 27 | #include <the_Foundation/mutex.h> |
5 | #include <the_Foundation/path.h> | 28 | #include <the_Foundation/path.h> |
6 | #include <the_Foundation/regexp.h> | 29 | #include <the_Foundation/regexp.h> |
30 | #include <the_Foundation/stringarray.h> | ||
7 | #include <the_Foundation/stringhash.h> | 31 | #include <the_Foundation/stringhash.h> |
32 | #include <the_Foundation/stringlist.h> | ||
8 | #include <the_Foundation/time.h> | 33 | #include <the_Foundation/time.h> |
9 | #include <ctype.h> | 34 | #include <ctype.h> |
10 | 35 | ||
11 | static const char *filename_GmCerts_ = "trusted.txt"; | 36 | static const char *filename_GmCerts_ = "trusted.txt"; |
37 | static const char *identsDir_GmCerts_ = "idents"; | ||
38 | static const char *identsFilename_GmCerts_ = "idents.binary"; | ||
12 | 39 | ||
13 | iDeclareClass(TrustEntry) | 40 | iDeclareClass(TrustEntry) |
14 | 41 | ||
@@ -27,19 +54,158 @@ void deinit_TrustEntry(iTrustEntry *d) { | |||
27 | deinit_Block(&d->fingerprint); | 54 | deinit_Block(&d->fingerprint); |
28 | } | 55 | } |
29 | 56 | ||
30 | iDefineObjectConstructionArgs(TrustEntry, (const iBlock *fingerprint, const iDate *until), fingerprint, until) | 57 | iDefineObjectConstructionArgs(TrustEntry, |
58 | (const iBlock *fingerprint, const iDate *until), | ||
59 | fingerprint, until) | ||
31 | iDefineClass(TrustEntry) | 60 | iDefineClass(TrustEntry) |
32 | 61 | ||
62 | /*----------------------------------------------------------------------------------------------*/ | ||
63 | |||
64 | static int cmpUrl_GmIdentity_(const iString *a, const iString *b) { | ||
65 | return cmpStringCase_String(a, b); | ||
66 | } | ||
67 | |||
68 | void init_GmIdentity(iGmIdentity *d) { | ||
69 | d->icon = 0x1f511; /* key */ | ||
70 | d->flags = 0; | ||
71 | d->cert = new_TlsCertificate(); | ||
72 | init_Block(&d->fingerprint, 0); | ||
73 | d->useUrls = newCmp_StringSet(cmpUrl_GmIdentity_); | ||
74 | init_String(&d->notes); | ||
75 | } | ||
76 | |||
77 | void deinit_GmIdentity(iGmIdentity *d) { | ||
78 | iRelease(d->useUrls); | ||
79 | deinit_String(&d->notes); | ||
80 | delete_TlsCertificate(d->cert); | ||
81 | deinit_Block(&d->fingerprint); | ||
82 | } | ||
83 | |||
84 | void serialize_GmIdentity(const iGmIdentity *d, iStream *outs) { | ||
85 | serialize_Block(&d->fingerprint, outs); | ||
86 | writeU32_Stream(outs, d->icon); | ||
87 | serialize_String(&d->notes, outs); | ||
88 | write32_Stream(outs, d->flags); | ||
89 | writeU32_Stream(outs, size_StringSet(d->useUrls)); | ||
90 | iConstForEach(StringSet, i, d->useUrls) { | ||
91 | serialize_String(i.value, outs); | ||
92 | } | ||
93 | } | ||
94 | |||
95 | void deserialize_GmIdentity(iGmIdentity *d, iStream *ins) { | ||
96 | deserialize_Block(&d->fingerprint, ins); | ||
97 | d->icon = readU32_Stream(ins); | ||
98 | deserialize_String(&d->notes, ins); | ||
99 | d->flags = read32_Stream(ins); | ||
100 | size_t n = readU32_Stream(ins); | ||
101 | while (n-- && !atEnd_Stream(ins)) { | ||
102 | iString url; | ||
103 | init_String(&url); | ||
104 | deserialize_String(&url, ins); | ||
105 | insert_StringSet(d->useUrls, &url); | ||
106 | deinit_String(&url); | ||
107 | } | ||
108 | } | ||
109 | |||
110 | static iBool isValid_GmIdentity_(const iGmIdentity *d) { | ||
111 | return !isEmpty_TlsCertificate(d->cert); | ||
112 | } | ||
113 | |||
114 | static void setCertificate_GmIdentity_(iGmIdentity *d, iTlsCertificate *cert) { | ||
115 | delete_TlsCertificate(d->cert); | ||
116 | d->cert = cert; | ||
117 | set_Block(&d->fingerprint, collect_Block(fingerprint_TlsCertificate(cert))); | ||
118 | } | ||
119 | |||
120 | static const iString *readFile_(const iString *path) { | ||
121 | iString *str = NULL; | ||
122 | iFile *f = new_File(path); | ||
123 | if (open_File(f, readOnly_FileMode | text_FileMode)) { | ||
124 | str = readString_File(f); | ||
125 | } | ||
126 | iRelease(f); | ||
127 | return str ? collect_String(str) : collectNew_String(); | ||
128 | } | ||
129 | |||
130 | static iBool writeTextFile_(const iString *path, const iString *content) { | ||
131 | iFile *f = iClob(new_File(path)); | ||
132 | if (open_File(f, writeOnly_FileMode | text_FileMode)) { | ||
133 | write_File(f, &content->chars); | ||
134 | close_File(f); | ||
135 | return iTrue; | ||
136 | } | ||
137 | return iFalse; | ||
138 | } | ||
139 | |||
140 | iBool isUsed_GmIdentity(const iGmIdentity *d) { | ||
141 | return d && !isEmpty_StringSet(d->useUrls); | ||
142 | } | ||
143 | |||
144 | iBool isUsedOn_GmIdentity(const iGmIdentity *d, const iString *url) { | ||
145 | size_t pos = iInvalidPos; | ||
146 | locate_StringSet(d->useUrls, url, &pos); | ||
147 | if (pos < size_StringSet(d->useUrls)) { | ||
148 | return startsWithCase_String(url, cstr_String(constAt_StringSet(d->useUrls, pos))); | ||
149 | } | ||
150 | return iFalse; | ||
151 | } | ||
152 | |||
153 | void setUse_GmIdentity(iGmIdentity *d, const iString *url, iBool use) { | ||
154 | if (use && isUsedOn_GmIdentity(d, url)) { | ||
155 | return; /* Redudant. */ | ||
156 | } | ||
157 | if (use) { | ||
158 | #if !defined (NDEBUG) | ||
159 | const iBool wasInserted = | ||
160 | #endif | ||
161 | insert_StringSet(d->useUrls, url); | ||
162 | iAssert(wasInserted); | ||
163 | } | ||
164 | else { | ||
165 | remove_StringSet(d->useUrls, url); | ||
166 | } | ||
167 | } | ||
168 | |||
169 | void clearUse_GmIdentity(iGmIdentity *d) { | ||
170 | clear_StringSet(d->useUrls); | ||
171 | } | ||
172 | |||
173 | const iString *name_GmIdentity(const iGmIdentity *d) { | ||
174 | return collect_String(subject_TlsCertificate(d->cert)); | ||
175 | } | ||
176 | |||
177 | iDefineTypeConstruction(GmIdentity) | ||
178 | |||
33 | /*-----------------------------------------------------------------------------------------------*/ | 179 | /*-----------------------------------------------------------------------------------------------*/ |
34 | 180 | ||
35 | struct Impl_GmCerts { | 181 | struct Impl_GmCerts { |
36 | iMutex mtx; | 182 | iMutex mtx; |
37 | iString saveDir; | 183 | iString saveDir; |
38 | iStringHash *trusted; | 184 | iStringHash *trusted; |
185 | iPtrArray idents; | ||
39 | }; | 186 | }; |
40 | 187 | ||
188 | static const char *magicIdMeta_GmCerts_ = "lgL2"; | ||
189 | static const char *magicIdentity_GmCerts_ = "iden"; | ||
190 | |||
41 | iDefineTypeConstructionArgs(GmCerts, (const char *saveDir), saveDir) | 191 | iDefineTypeConstructionArgs(GmCerts, (const char *saveDir), saveDir) |
42 | 192 | ||
193 | static void saveIdentities_GmCerts_(const iGmCerts *d) { | ||
194 | iFile *f = new_File(collect_String(concatCStr_Path(&d->saveDir, identsFilename_GmCerts_))); | ||
195 | if (open_File(f, writeOnly_FileMode)) { | ||
196 | writeData_File(f, magicIdMeta_GmCerts_, 4); | ||
197 | writeU32_File(f, 0); /* version */ | ||
198 | iConstForEach(PtrArray, i, &d->idents) { | ||
199 | const iGmIdentity *ident = i.ptr; | ||
200 | if (~ident->flags & temporary_GmIdentityFlag) { | ||
201 | writeData_File(f, magicIdentity_GmCerts_, 4); | ||
202 | serialize_GmIdentity(ident, stream_File(f)); | ||
203 | } | ||
204 | } | ||
205 | } | ||
206 | iRelease(f); | ||
207 | } | ||
208 | |||
43 | static void save_GmCerts_(const iGmCerts *d) { | 209 | static void save_GmCerts_(const iGmCerts *d) { |
44 | iBeginCollect(); | 210 | iBeginCollect(); |
45 | iFile *f = new_File(collect_String(concatCStr_Path(&d->saveDir, filename_GmCerts_))); | 211 | iFile *f = new_File(collect_String(concatCStr_Path(&d->saveDir, filename_GmCerts_))); |
@@ -61,14 +227,75 @@ static void save_GmCerts_(const iGmCerts *d) { | |||
61 | iEndCollect(); | 227 | iEndCollect(); |
62 | } | 228 | } |
63 | 229 | ||
230 | static void loadIdentities_GmCerts_(iGmCerts *d) { | ||
231 | iFile *f = | ||
232 | iClob(new_File(collect_String(concatCStr_Path(&d->saveDir, identsFilename_GmCerts_)))); | ||
233 | if (open_File(f, readOnly_FileMode)) { | ||
234 | char magic[4]; | ||
235 | readData_File(f, sizeof(magic), magic); | ||
236 | if (memcmp(magic, magicIdMeta_GmCerts_, sizeof(magic))) { | ||
237 | printf("%s: format not recognized\n", cstr_String(path_File(f))); | ||
238 | return; | ||
239 | } | ||
240 | setVersion_Stream(stream_File(f), readU32_File(f)); | ||
241 | while (!atEnd_File(f)) { | ||
242 | readData_File(f, sizeof(magic), magic); | ||
243 | if (!memcmp(magic, magicIdentity_GmCerts_, sizeof(magic))) { | ||
244 | iGmIdentity *id = new_GmIdentity(); | ||
245 | deserialize_GmIdentity(id, stream_File(f)); | ||
246 | pushBack_PtrArray(&d->idents, id); | ||
247 | } | ||
248 | else { | ||
249 | printf("%s: invalid file contents\n", cstr_String(path_File(f))); | ||
250 | break; | ||
251 | } | ||
252 | } | ||
253 | } | ||
254 | } | ||
255 | |||
256 | static iGmIdentity *findIdentity_GmCerts_(iGmCerts *d, const iBlock *fingerprint) { | ||
257 | iForEach(PtrArray, i, &d->idents) { | ||
258 | iGmIdentity *ident = i.ptr; | ||
259 | if (cmp_Block(fingerprint, &ident->fingerprint) == 0) { /* TODO: could use a hash */ | ||
260 | return ident; | ||
261 | } | ||
262 | } | ||
263 | return NULL; | ||
264 | } | ||
265 | |||
266 | static void loadIdentityFromCertificate_GmCerts_(iGmCerts *d, const iString *crtPath) { | ||
267 | iAssert(fileExists_FileInfo(crtPath)); | ||
268 | iString *keyPath = collect_String(copy_String(crtPath)); | ||
269 | truncate_Block(&keyPath->chars, size_String(keyPath) - 3); | ||
270 | appendCStr_String(keyPath, "key"); | ||
271 | if (!fileExists_FileInfo(keyPath)) { | ||
272 | return; | ||
273 | } | ||
274 | iTlsCertificate *cert = newPemKey_TlsCertificate(readFile_(crtPath), readFile_(keyPath)); | ||
275 | iBlock *finger = fingerprint_TlsCertificate(cert); | ||
276 | iGmIdentity *ident = findIdentity_GmCerts_(d, finger); | ||
277 | if (!ident) { | ||
278 | /* User-provided certificate. */ | ||
279 | ident = new_GmIdentity(); | ||
280 | ident->flags |= imported_GmIdentityFlag; | ||
281 | iDate today; | ||
282 | initCurrent_Date(&today); | ||
283 | set_String(&ident->notes, collect_String(format_Date(&today, "Imported on %b %d, %Y"))); | ||
284 | pushBack_PtrArray(&d->idents, ident); | ||
285 | } | ||
286 | setCertificate_GmIdentity_(ident, cert); | ||
287 | delete_Block(finger); | ||
288 | } | ||
289 | |||
64 | static void load_GmCerts_(iGmCerts *d) { | 290 | static void load_GmCerts_(iGmCerts *d) { |
65 | iFile *f = new_File(collect_String(concatCStr_Path(&d->saveDir, filename_GmCerts_))); | 291 | iFile *f = new_File(collect_String(concatCStr_Path(&d->saveDir, filename_GmCerts_))); |
66 | if (open_File(f, readOnly_FileMode | text_FileMode)) { | 292 | if (open_File(f, readOnly_FileMode | text_FileMode)) { |
67 | iRegExp * pattern = new_RegExp("([^\\s]+) ([0-9]+) ([a-z0-9]+)", 0); | 293 | iRegExp * pattern = new_RegExp("([^\\s]+) ([0-9]+) ([a-z0-9]+)", 0); |
68 | const iRangecc src = range_Block(collect_Block(readAll_File(f))); | 294 | const iRangecc src = range_Block(collect_Block(readAll_File(f))); |
69 | iRangecc line = iNullRange; | 295 | iRangecc line = iNullRange; |
70 | while (nextSplit_Rangecc(&src, "\n", &line)) { | 296 | while (nextSplit_Rangecc(src, "\n", &line)) { |
71 | iRegExpMatch m; | 297 | iRegExpMatch m; |
298 | init_RegExpMatch(&m); | ||
72 | if (matchRange_RegExp(pattern, line, &m)) { | 299 | if (matchRange_RegExp(pattern, line, &m)) { |
73 | const iRangecc domain = capturedRange_RegExpMatch(&m, 1); | 300 | const iRangecc domain = capturedRange_RegExpMatch(&m, 1); |
74 | const iRangecc until = capturedRange_RegExpMatch(&m, 2); | 301 | const iRangecc until = capturedRange_RegExpMatch(&m, 2); |
@@ -86,17 +313,44 @@ static void load_GmCerts_(iGmCerts *d) { | |||
86 | iRelease(pattern); | 313 | iRelease(pattern); |
87 | } | 314 | } |
88 | iRelease(f); | 315 | iRelease(f); |
316 | /* Load all identity certificates. */ { | ||
317 | loadIdentities_GmCerts_(d); | ||
318 | const iString *idDir = collect_String(concatCStr_Path(&d->saveDir, identsDir_GmCerts_)); | ||
319 | if (!fileExists_FileInfo(idDir)) { | ||
320 | mkdir_Path(idDir); | ||
321 | } | ||
322 | iForEach(DirFileInfo, i, iClob(directoryContents_FileInfo(iClob(new_FileInfo(idDir))))) { | ||
323 | const iFileInfo *entry = i.value; | ||
324 | if (endsWithCase_String(path_FileInfo(entry), ".crt")) { | ||
325 | loadIdentityFromCertificate_GmCerts_(d, path_FileInfo(entry)); | ||
326 | } | ||
327 | } | ||
328 | /* Remove certificates whose crt/key files were missing. */ | ||
329 | iForEach(PtrArray, j, &d->idents) { | ||
330 | iGmIdentity *ident = j.ptr; | ||
331 | if (!isValid_GmIdentity_(ident)) { | ||
332 | delete_GmIdentity(ident); | ||
333 | remove_PtrArrayIterator(&j); | ||
334 | } | ||
335 | } | ||
336 | } | ||
89 | } | 337 | } |
90 | 338 | ||
91 | void init_GmCerts(iGmCerts *d, const char *saveDir) { | 339 | void init_GmCerts(iGmCerts *d, const char *saveDir) { |
92 | init_Mutex(&d->mtx); | 340 | init_Mutex(&d->mtx); |
93 | initCStr_String(&d->saveDir, saveDir); | 341 | initCStr_String(&d->saveDir, saveDir); |
94 | d->trusted = new_StringHash(); | 342 | d->trusted = new_StringHash(); |
343 | init_PtrArray(&d->idents); | ||
95 | load_GmCerts_(d); | 344 | load_GmCerts_(d); |
96 | } | 345 | } |
97 | 346 | ||
98 | void deinit_GmCerts(iGmCerts *d) { | 347 | void deinit_GmCerts(iGmCerts *d) { |
99 | iGuardMutex(&d->mtx, { | 348 | iGuardMutex(&d->mtx, { |
349 | saveIdentities_GmCerts_(d); | ||
350 | iForEach(PtrArray, i, &d->idents) { | ||
351 | delete_GmIdentity(i.ptr); | ||
352 | } | ||
353 | deinit_PtrArray(&d->idents); | ||
100 | iRelease(d->trusted); | 354 | iRelease(d->trusted); |
101 | deinit_String(&d->saveDir); | 355 | deinit_String(&d->saveDir); |
102 | }); | 356 | }); |
@@ -142,3 +396,102 @@ iBool checkTrust_GmCerts(iGmCerts *d, iRangecc domain, const iTlsCertificate *ce | |||
142 | unlock_Mutex(&d->mtx); | 396 | unlock_Mutex(&d->mtx); |
143 | return iTrue; | 397 | return iTrue; |
144 | } | 398 | } |
399 | |||
400 | iGmIdentity *identity_GmCerts(iGmCerts *d, unsigned int id) { | ||
401 | return at_PtrArray(&d->idents, id); | ||
402 | } | ||
403 | |||
404 | const iGmIdentity *constIdentity_GmCerts(const iGmCerts *d, unsigned int id) { | ||
405 | return constAt_PtrArray(&d->idents, id); | ||
406 | } | ||
407 | |||
408 | const iGmIdentity *identityForUrl_GmCerts(const iGmCerts *d, const iString *url) { | ||
409 | iConstForEach(PtrArray, i, &d->idents) { | ||
410 | const iGmIdentity *ident = i.ptr; | ||
411 | iConstForEach(StringSet, j, ident->useUrls) { | ||
412 | const iString *used = j.value; | ||
413 | if (startsWithCase_String(url, cstr_String(used))) { | ||
414 | return ident; | ||
415 | } | ||
416 | } | ||
417 | } | ||
418 | return NULL; | ||
419 | } | ||
420 | |||
421 | iGmIdentity *newIdentity_GmCerts(iGmCerts *d, int flags, iDate validUntil, const iString *commonName, | ||
422 | const iString *email, const iString *userId, const iString *domain, | ||
423 | const iString *org, const iString *country) { | ||
424 | const iTlsCertificateName names[] = { | ||
425 | { issuerCommonName_TlsCertificateNameType, collectNewCStr_String("Lagrange v" LAGRANGE_APP_VERSION) }, | ||
426 | { issuerDomain_TlsCertificateNameType, collectNewCStr_String("lagrange.skyjake.fi") }, | ||
427 | { subjectCommonName_TlsCertificateNameType, commonName }, | ||
428 | { subjectEmailAddress_TlsCertificateNameType, !isEmpty_String(email) ? email : NULL }, | ||
429 | { subjectUserId_TlsCertificateNameType, !isEmpty_String(userId) ? userId : NULL }, | ||
430 | { subjectDomain_TlsCertificateNameType, !isEmpty_String(domain) ? domain : NULL }, | ||
431 | { subjectOrganization_TlsCertificateNameType, !isEmpty_String(org) ? org : NULL }, | ||
432 | { subjectCountry_TlsCertificateNameType, !isEmpty_String(country) ? country : NULL }, | ||
433 | { 0, NULL } | ||
434 | }; | ||
435 | iGmIdentity *id = new_GmIdentity(); | ||
436 | setCertificate_GmIdentity_(id, newSelfSignedRSA_TlsCertificate(2048, validUntil, names)); | ||
437 | /* Save the certificate and private key as PEM files. */ | ||
438 | if (~flags & temporary_GmIdentityFlag) { | ||
439 | const char *finger = cstrCollect_String(hexEncode_Block(&id->fingerprint)); | ||
440 | if (!writeTextFile_( | ||
441 | collect_String(concatCStr_Path(&d->saveDir, format_CStr("idents/%s.crt", finger))), | ||
442 | collect_String(pem_TlsCertificate(id->cert)))) { | ||
443 | delete_GmIdentity(id); | ||
444 | return NULL; | ||
445 | } | ||
446 | if (!writeTextFile_( | ||
447 | collect_String(concatCStr_Path(&d->saveDir, format_CStr("idents/%s.key", finger))), | ||
448 | collect_String(privateKeyPem_TlsCertificate(id->cert)))) { | ||
449 | delete_GmIdentity(id); | ||
450 | return NULL; | ||
451 | } | ||
452 | } | ||
453 | pushBack_PtrArray(&d->idents, id); | ||
454 | return id; | ||
455 | } | ||
456 | |||
457 | static const char *certPath_GmCerts_(const iGmCerts *d, const iGmIdentity *identity) { | ||
458 | if (!(identity->flags & (temporary_GmIdentityFlag | imported_GmIdentityFlag))) { | ||
459 | const char *finger = cstrCollect_String(hexEncode_Block(&identity->fingerprint)); | ||
460 | return concatPath_CStr(cstr_String(&d->saveDir), format_CStr("idents/%s", finger)); | ||
461 | } | ||
462 | return NULL; | ||
463 | } | ||
464 | |||
465 | void deleteIdentity_GmCerts(iGmCerts *d, iGmIdentity *identity) { | ||
466 | /* Only delete the files if we created them. */ | ||
467 | const char *filename = certPath_GmCerts_(d, identity); | ||
468 | if (filename) { | ||
469 | remove(format_CStr("%s.crt", filename)); | ||
470 | remove(format_CStr("%s.key", filename)); | ||
471 | } | ||
472 | removeOne_PtrArray(&d->idents, identity); | ||
473 | collect_GmIdentity(identity); | ||
474 | } | ||
475 | |||
476 | const iString *certificatePath_GmCerts(const iGmCerts *d, const iGmIdentity *identity) { | ||
477 | const char *filename = certPath_GmCerts_(d, identity); | ||
478 | if (filename) { | ||
479 | return collectNewFormat_String("%s.crt", filename); | ||
480 | } | ||
481 | return NULL; | ||
482 | } | ||
483 | |||
484 | const iPtrArray *identities_GmCerts(const iGmCerts *d) { | ||
485 | return &d->idents; | ||
486 | } | ||
487 | |||
488 | void signIn_GmCerts(iGmCerts *d, iGmIdentity *identity, const iString *url) { | ||
489 | signOut_GmCerts(d, url); | ||
490 | setUse_GmIdentity(identity, url, iTrue); | ||
491 | } | ||
492 | |||
493 | void signOut_GmCerts(iGmCerts *d, const iString *url) { | ||
494 | iForEach(PtrArray, i, &d->idents) { | ||
495 | setUse_GmIdentity(i.ptr, url, iFalse); | ||
496 | } | ||
497 | } | ||
diff --git a/src/gmcerts.h b/src/gmcerts.h index a3df8f33..92a12a6a 100644 --- a/src/gmcerts.h +++ b/src/gmcerts.h | |||
@@ -1,8 +1,88 @@ | |||
1 | /* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi> | ||
2 | |||
3 | Redistribution and use in source and binary forms, with or without | ||
4 | modification, are permitted provided that the following conditions are met: | ||
5 | |||
6 | 1. Redistributions of source code must retain the above copyright notice, this | ||
7 | list of conditions and the following disclaimer. | ||
8 | 2. Redistributions in binary form must reproduce the above copyright notice, | ||
9 | this list of conditions and the following disclaimer in the documentation | ||
10 | and/or other materials provided with the distribution. | ||
11 | |||
12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | ||
13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | ||
14 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||
15 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR | ||
16 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | ||
17 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | ||
18 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON | ||
19 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||
20 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | ||
21 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | ||
22 | |||
1 | #pragma once | 23 | #pragma once |
2 | 24 | ||
25 | #include <the_Foundation/ptrarray.h> | ||
26 | #include <the_Foundation/stringset.h> | ||
3 | #include <the_Foundation/tlsrequest.h> | 27 | #include <the_Foundation/tlsrequest.h> |
4 | 28 | ||
29 | iDeclareType(GmIdentity) | ||
30 | iDeclareTypeConstruction(GmIdentity) | ||
31 | iDeclareTypeSerialization(GmIdentity) | ||
32 | |||
33 | enum iGmIdentityFlags { | ||
34 | temporary_GmIdentityFlag = 0x1, /* not saved persistently */ | ||
35 | imported_GmIdentityFlag = 0x2, /* user-provided files */ | ||
36 | }; | ||
37 | |||
38 | struct Impl_GmIdentity { | ||
39 | iBlock fingerprint; | ||
40 | iTlsCertificate *cert; | ||
41 | iStringSet *useUrls; | ||
42 | iChar icon; | ||
43 | iString notes; /* private, local usage notes */ | ||
44 | int flags; | ||
45 | }; | ||
46 | |||
47 | iBool isUsed_GmIdentity (const iGmIdentity *); | ||
48 | iBool isUsedOn_GmIdentity (const iGmIdentity *, const iString *url); | ||
49 | |||
50 | void setUse_GmIdentity (iGmIdentity *, const iString *url, iBool use); | ||
51 | void clearUse_GmIdentity (iGmIdentity *); | ||
52 | |||
53 | const iString *name_GmIdentity(const iGmIdentity *); | ||
54 | |||
55 | /*----------------------------------------------------------------------------------------------*/ | ||
56 | |||
5 | iDeclareType(GmCerts) | 57 | iDeclareType(GmCerts) |
6 | iDeclareTypeConstructionArgs(GmCerts, const char *saveDir) | 58 | iDeclareTypeConstructionArgs(GmCerts, const char *saveDir) |
7 | 59 | ||
8 | iBool checkTrust_GmCerts (iGmCerts *, iRangecc domain, const iTlsCertificate *cert); | 60 | iBool checkTrust_GmCerts (iGmCerts *, iRangecc domain, const iTlsCertificate *cert); |
61 | |||
62 | /** | ||
63 | * Create a new self-signed TLS client certificate for identifying the user. | ||
64 | * @a commonName and the other name parameters are inserted in the subject field | ||
65 | * of the certificate. | ||
66 | * | ||
67 | * @param flags Identity flags. A temporary identity is not saved persistently and | ||
68 | * will be erased when the application is shut down. | ||
69 | * @param validUntil Expiration date. Must be in the future. | ||
70 | * | ||
71 | * @returns Created identity. GmCerts retains ownership of returned object. | ||
72 | */ | ||
73 | iGmIdentity * newIdentity_GmCerts (iGmCerts *, int flags, iDate validUntil, | ||
74 | const iString *commonName, const iString *email, | ||
75 | const iString *userId, const iString *domain, | ||
76 | const iString *org, const iString *country); | ||
77 | |||
78 | void deleteIdentity_GmCerts (iGmCerts *, iGmIdentity *identity); | ||
79 | |||
80 | const iString * certificatePath_GmCerts (const iGmCerts *, const iGmIdentity *identity); | ||
81 | |||
82 | iGmIdentity * identity_GmCerts (iGmCerts *, unsigned int id); | ||
83 | const iGmIdentity * constIdentity_GmCerts (const iGmCerts *, unsigned int id); | ||
84 | const iGmIdentity * identityForUrl_GmCerts (const iGmCerts *, const iString *url); | ||
85 | const iPtrArray * identities_GmCerts (const iGmCerts *); | ||
86 | |||
87 | void signIn_GmCerts (iGmCerts *, iGmIdentity *identity, const iString *url); | ||
88 | void signOut_GmCerts (iGmCerts *, const iString *url); | ||
diff --git a/src/gmdocument.c b/src/gmdocument.c index a6fc39c5..fc49dac4 100644 --- a/src/gmdocument.c +++ b/src/gmdocument.c | |||
@@ -1,3 +1,25 @@ | |||
1 | /* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi> | ||
2 | |||
3 | Redistribution and use in source and binary forms, with or without | ||
4 | modification, are permitted provided that the following conditions are met: | ||
5 | |||
6 | 1. Redistributions of source code must retain the above copyright notice, this | ||
7 | list of conditions and the following disclaimer. | ||
8 | 2. Redistributions in binary form must reproduce the above copyright notice, | ||
9 | this list of conditions and the following disclaimer in the documentation | ||
10 | and/or other materials provided with the distribution. | ||
11 | |||
12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | ||
13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | ||
14 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||
15 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR | ||
16 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | ||
17 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | ||
18 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON | ||
19 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||
20 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | ||
21 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | ||
22 | |||
1 | #include "gmdocument.h" | 23 | #include "gmdocument.h" |
2 | #include "gmutil.h" | 24 | #include "gmutil.h" |
3 | #include "ui/color.h" | 25 | #include "ui/color.h" |
@@ -106,11 +128,11 @@ enum iGmLineType { | |||
106 | max_GmLineType, | 128 | max_GmLineType, |
107 | }; | 129 | }; |
108 | 130 | ||
109 | static enum iGmLineType lineType_GmDocument_(const iGmDocument *d, const iRangecc *line) { | 131 | static enum iGmLineType lineType_GmDocument_(const iGmDocument *d, const iRangecc line) { |
110 | if (d->format == plainText_GmDocumentFormat) { | 132 | if (d->format == plainText_GmDocumentFormat) { |
111 | return text_GmLineType; | 133 | return text_GmLineType; |
112 | } | 134 | } |
113 | if (isEmpty_Range(line)) { | 135 | if (isEmpty_Range(&line)) { |
114 | return text_GmLineType; | 136 | return text_GmLineType; |
115 | } | 137 | } |
116 | if (startsWith_Rangecc(line, "=>")) { | 138 | if (startsWith_Rangecc(line, "=>")) { |
@@ -128,10 +150,10 @@ static enum iGmLineType lineType_GmDocument_(const iGmDocument *d, const iRangec | |||
128 | if (startsWith_Rangecc(line, "```")) { | 150 | if (startsWith_Rangecc(line, "```")) { |
129 | return preformatted_GmLineType; | 151 | return preformatted_GmLineType; |
130 | } | 152 | } |
131 | if (*line->start == '>') { | 153 | if (*line.start == '>') { |
132 | return quote_GmLineType; | 154 | return quote_GmLineType; |
133 | } | 155 | } |
134 | if (size_Range(line) >= 2 && line->start[0] == '*' && isspace(line->start[1])) { | 156 | if (size_Range(&line) >= 2 && line.start[0] == '*' && isspace(line.start[1])) { |
135 | return bullet_GmLineType; | 157 | return bullet_GmLineType; |
136 | } | 158 | } |
137 | return text_GmLineType; | 159 | return text_GmLineType; |
@@ -157,11 +179,11 @@ static int lastVisibleRunBottom_GmDocument_(const iGmDocument *d) { | |||
157 | iInt2 measurePreformattedBlock_GmDocument_(const iGmDocument *d, const char *start, int font) { | 179 | iInt2 measurePreformattedBlock_GmDocument_(const iGmDocument *d, const char *start, int font) { |
158 | const iRangecc content = { start, constEnd_String(&d->source) }; | 180 | const iRangecc content = { start, constEnd_String(&d->source) }; |
159 | iRangecc line = iNullRange; | 181 | iRangecc line = iNullRange; |
160 | nextSplit_Rangecc(&content, "\n", &line); | 182 | nextSplit_Rangecc(content, "\n", &line); |
161 | iAssert(startsWith_Rangecc(&line, "```")); | 183 | iAssert(startsWith_Rangecc(line, "```")); |
162 | iRangecc preBlock = { line.end + 1, line.end + 1 }; | 184 | iRangecc preBlock = { line.end + 1, line.end + 1 }; |
163 | while (nextSplit_Rangecc(&content, "\n", &line)) { | 185 | while (nextSplit_Rangecc(content, "\n", &line)) { |
164 | if (startsWith_Rangecc(&line, "```")) { | 186 | if (startsWith_Rangecc(line, "```")) { |
165 | break; | 187 | break; |
166 | } | 188 | } |
167 | preBlock.end = line.end; | 189 | preBlock.end = line.end; |
@@ -170,31 +192,35 @@ iInt2 measurePreformattedBlock_GmDocument_(const iGmDocument *d, const char *sta | |||
170 | } | 192 | } |
171 | 193 | ||
172 | static iRangecc addLink_GmDocument_(iGmDocument *d, iRangecc line, iGmLinkId *linkId) { | 194 | static iRangecc addLink_GmDocument_(iGmDocument *d, iRangecc line, iGmLinkId *linkId) { |
173 | iRegExp *pattern = new_RegExp("=>\\s*([^\\s]+)(\\s.*)?", caseInsensitive_RegExpOption); | 195 | static iRegExp *pattern_; |
196 | if (!pattern_) { | ||
197 | pattern_ = new_RegExp("=>\\s*([^\\s]+)(\\s.*)?", caseInsensitive_RegExpOption); | ||
198 | } | ||
174 | iRegExpMatch m; | 199 | iRegExpMatch m; |
175 | if (matchRange_RegExp(pattern, line, &m)) { | 200 | init_RegExpMatch(&m); |
201 | if (matchRange_RegExp(pattern_, line, &m)) { | ||
176 | iGmLink *link = new_GmLink(); | 202 | iGmLink *link = new_GmLink(); |
177 | setRange_String(&link->url, capturedRange_RegExpMatch(&m, 1)); | 203 | setRange_String(&link->url, capturedRange_RegExpMatch(&m, 1)); |
178 | set_String(&link->url, absoluteUrl_String(&d->url, &link->url)); | 204 | set_String(&link->url, absoluteUrl_String(&d->url, &link->url)); |
179 | /* Check the URL. */ { | 205 | /* Check the URL. */ { |
180 | iUrl parts; | 206 | iUrl parts; |
181 | init_Url(&parts, &link->url); | 207 | init_Url(&parts, &link->url); |
182 | if (!equalCase_Rangecc(&parts.host, cstr_String(&d->localHost))) { | 208 | if (!equalCase_Rangecc(parts.host, cstr_String(&d->localHost))) { |
183 | link->flags |= remote_GmLinkFlag; | 209 | link->flags |= remote_GmLinkFlag; |
184 | } | 210 | } |
185 | if (startsWithCase_Rangecc(&parts.protocol, "gemini")) { | 211 | if (startsWithCase_Rangecc(parts.scheme, "gemini")) { |
186 | link->flags |= gemini_GmLinkFlag; | 212 | link->flags |= gemini_GmLinkFlag; |
187 | } | 213 | } |
188 | else if (startsWithCase_Rangecc(&parts.protocol, "http")) { | 214 | else if (startsWithCase_Rangecc(parts.scheme, "http")) { |
189 | link->flags |= http_GmLinkFlag; | 215 | link->flags |= http_GmLinkFlag; |
190 | } | 216 | } |
191 | else if (equalCase_Rangecc(&parts.protocol, "gopher")) { | 217 | else if (equalCase_Rangecc(parts.scheme, "gopher")) { |
192 | link->flags |= gopher_GmLinkFlag; | 218 | link->flags |= gopher_GmLinkFlag; |
193 | } | 219 | } |
194 | else if (equalCase_Rangecc(&parts.protocol, "file")) { | 220 | else if (equalCase_Rangecc(parts.scheme, "file")) { |
195 | link->flags |= file_GmLinkFlag; | 221 | link->flags |= file_GmLinkFlag; |
196 | } | 222 | } |
197 | else if (equalCase_Rangecc(&parts.protocol, "data")) { | 223 | else if (equalCase_Rangecc(parts.scheme, "data")) { |
198 | link->flags |= data_GmLinkFlag; | 224 | link->flags |= data_GmLinkFlag; |
199 | } | 225 | } |
200 | /* Check the file name extension, if present. */ | 226 | /* Check the file name extension, if present. */ |
@@ -232,7 +258,6 @@ static iRangecc addLink_GmDocument_(iGmDocument *d, iRangecc line, iGmLinkId *li | |||
232 | line = capturedRange_RegExpMatch(&m, 1); /* Show the URL. */ | 258 | line = capturedRange_RegExpMatch(&m, 1); /* Show the URL. */ |
233 | } | 259 | } |
234 | } | 260 | } |
235 | iRelease(pattern); | ||
236 | return line; | 261 | return line; |
237 | } | 262 | } |
238 | 263 | ||
@@ -254,16 +279,21 @@ static size_t findLinkImage_GmDocument_(const iGmDocument *d, iGmLinkId linkId) | |||
254 | return iInvalidPos; | 279 | return iInvalidPos; |
255 | } | 280 | } |
256 | 281 | ||
282 | static iBool isGopher_GmDocument_(const iGmDocument *d) { | ||
283 | return equalCase_Rangecc(urlScheme_String(&d->url), "gopher"); | ||
284 | } | ||
285 | |||
257 | static void doLayout_GmDocument_(iGmDocument *d) { | 286 | static void doLayout_GmDocument_(iGmDocument *d) { |
287 | const iBool isGemini = !isGopher_GmDocument_(d); | ||
258 | /* TODO: Collect these parameters into a GmTheme. */ | 288 | /* TODO: Collect these parameters into a GmTheme. */ |
259 | static const int fonts[max_GmLineType] = { | 289 | const int fonts[max_GmLineType] = { |
260 | paragraph_FontId, | 290 | isGemini ? paragraph_FontId : preformatted_FontId, |
261 | paragraph_FontId, /* bullet */ | 291 | isGemini ? paragraph_FontId : preformatted_FontId, /* bullet */ |
262 | preformatted_FontId, | 292 | preformatted_FontId, |
263 | quote_FontId, | 293 | quote_FontId, |
264 | heading1_FontId, | 294 | isGemini ? heading1_FontId : preformatted_FontId, |
265 | heading2_FontId, | 295 | isGemini ? heading2_FontId : preformatted_FontId, |
266 | heading3_FontId, | 296 | isGemini ? heading3_FontId : preformatted_FontId, |
267 | regular_FontId, | 297 | regular_FontId, |
268 | }; | 298 | }; |
269 | static const int colors[max_GmLineType] = { | 299 | static const int colors[max_GmLineType] = { |
@@ -277,7 +307,7 @@ static void doLayout_GmDocument_(iGmDocument *d) { | |||
277 | tmLinkText_ColorId, | 307 | tmLinkText_ColorId, |
278 | }; | 308 | }; |
279 | static const int indents[max_GmLineType] = { | 309 | static const int indents[max_GmLineType] = { |
280 | 5, 10, 5, 10, 0, 0, 0, 5 | 310 | 6, 12, 6, 12, 0, 0, 0, 6 |
281 | }; | 311 | }; |
282 | static const float topMargin[max_GmLineType] = { | 312 | static const float topMargin[max_GmLineType] = { |
283 | 0.0f, 0.5f, 1.0f, 0.5f, 2.0f, 2.0f, 1.5f, 1.0f | 313 | 0.0f, 0.5f, 1.0f, 0.5f, 2.0f, 2.0f, 1.5f, 1.0f |
@@ -298,20 +328,21 @@ static void doLayout_GmDocument_(iGmDocument *d) { | |||
298 | return; | 328 | return; |
299 | } | 329 | } |
300 | const iRangecc content = range_String(&d->source); | 330 | const iRangecc content = range_String(&d->source); |
331 | iRangecc contentLine = iNullRange; | ||
301 | iInt2 pos = zero_I2(); | 332 | iInt2 pos = zero_I2(); |
302 | iRangecc line = iNullRange; | 333 | iBool isFirstText = isGemini; |
303 | iRangecc preAltText = iNullRange; | ||
304 | enum iGmLineType prevType; // = text_GmLineType; | ||
305 | iBool isPreformat = iFalse; | 334 | iBool isPreformat = iFalse; |
306 | iBool isFirstText = iTrue; | 335 | iRangecc preAltText = iNullRange; |
336 | int preFont = preformatted_FontId; | ||
307 | iBool enableIndents = iFalse; | 337 | iBool enableIndents = iFalse; |
308 | iBool addSiteBanner = iTrue; | 338 | iBool addSiteBanner = iTrue; |
309 | int preFont = preformatted_FontId; | 339 | enum iGmLineType prevType; |
310 | if (d->format == plainText_GmDocumentFormat) { | 340 | if (d->format == plainText_GmDocumentFormat) { |
311 | isPreformat = iTrue; | 341 | isPreformat = iTrue; |
312 | isFirstText = iFalse; | 342 | isFirstText = iFalse; |
313 | } | 343 | } |
314 | while (nextSplit_Rangecc(&content, "\n", &line)) { | 344 | while (nextSplit_Rangecc(content, "\n", &contentLine)) { |
345 | iRangecc line = contentLine; /* `line` will be trimmed later; would confuse nextSplit */ | ||
315 | iGmRun run; | 346 | iGmRun run; |
316 | run.flags = 0; | 347 | run.flags = 0; |
317 | run.color = white_ColorId; | 348 | run.color = white_ColorId; |
@@ -320,7 +351,7 @@ static void doLayout_GmDocument_(iGmDocument *d) { | |||
320 | enum iGmLineType type; | 351 | enum iGmLineType type; |
321 | int indent = 0; | 352 | int indent = 0; |
322 | if (!isPreformat) { | 353 | if (!isPreformat) { |
323 | type = lineType_GmDocument_(d, &line); | 354 | type = lineType_GmDocument_(d, line); |
324 | if (line.start == content.start) { | 355 | if (line.start == content.start) { |
325 | prevType = type; | 356 | prevType = type; |
326 | } | 357 | } |
@@ -358,7 +389,7 @@ static void doLayout_GmDocument_(iGmDocument *d) { | |||
358 | /* Preformatted line. */ | 389 | /* Preformatted line. */ |
359 | type = preformatted_GmLineType; | 390 | type = preformatted_GmLineType; |
360 | if (d->format == gemini_GmDocumentFormat && | 391 | if (d->format == gemini_GmDocumentFormat && |
361 | startsWithSc_Rangecc(&line, "```", &iCaseSensitive)) { | 392 | startsWithSc_Rangecc(line, "```", &iCaseSensitive)) { |
362 | isPreformat = iFalse; | 393 | isPreformat = iFalse; |
363 | preAltText = iNullRange; | 394 | preAltText = iNullRange; |
364 | addSiteBanner = iFalse; /* overrides the banner */ | 395 | addSiteBanner = iFalse; /* overrides the banner */ |
@@ -664,6 +695,20 @@ void setThemeSeed_GmDocument(iGmDocument *d, const iBlock *seed) { | |||
664 | } | 695 | } |
665 | /* Set up colors. */ | 696 | /* Set up colors. */ |
666 | if (d->themeSeed) { | 697 | if (d->themeSeed) { |
698 | enum iHue { | ||
699 | red_Hue, | ||
700 | reddishOrange_Hue, | ||
701 | yellowishOrange_Hue, | ||
702 | yellow_Hue, | ||
703 | greenishYellow_Hue, | ||
704 | green_Hue, | ||
705 | bluishGreen_Hue, | ||
706 | cyan_Hue, | ||
707 | skyBlue_Hue, | ||
708 | blue_Hue, | ||
709 | violet_Hue, | ||
710 | pink_Hue | ||
711 | }; | ||
667 | static const float hues[] = { 5, 25, 40, 56, 80, 120, 160, 180, 208, 231, 270, 324 }; | 712 | static const float hues[] = { 5, 25, 40, 56, 80, 120, 160, 180, 208, 231, 270, 324 }; |
668 | static const struct { | 713 | static const struct { |
669 | int index[2]; | 714 | int index[2]; |
@@ -681,9 +726,9 @@ void setThemeSeed_GmDocument(iGmDocument *d, const iBlock *seed) { | |||
681 | { 8, 9 }, /* violet */ | 726 | { 8, 9 }, /* violet */ |
682 | { 7, 8 }, /* pink */ | 727 | { 7, 8 }, /* pink */ |
683 | }; | 728 | }; |
684 | const float saturationLevel = 1.0f; /* TODO: user setting */ | ||
685 | const iBool isBannerLighter = (d->themeSeed & 0x4000) != 0; | 729 | const iBool isBannerLighter = (d->themeSeed & 0x4000) != 0; |
686 | const size_t primIndex = d->themeSeed ? (d->themeSeed & 0xff) % iElemCount(hues) : 2; | 730 | const size_t primIndex = d->themeSeed ? (d->themeSeed & 0xff) % iElemCount(hues) : 2; |
731 | const float saturationLevel = 1.0f; /* TODO: user setting */ | ||
687 | const iBool isDarkBgSat = | 732 | const iBool isDarkBgSat = |
688 | (d->themeSeed & 0x200000) != 0 && (primIndex < 1 || primIndex > 4); | 733 | (d->themeSeed & 0x200000) != 0 && (primIndex < 1 || primIndex > 4); |
689 | iHSLColor base = { hues[primIndex], | 734 | iHSLColor base = { hues[primIndex], |
@@ -692,9 +737,10 @@ void setThemeSeed_GmDocument(iGmDocument *d, const iBlock *seed) { | |||
692 | 1.0f }; | 737 | 1.0f }; |
693 | // printf("background: %d %f %f\n", (int) base.hue, base.sat, base.lum); | 738 | // printf("background: %d %f %f\n", (int) base.hue, base.sat, base.lum); |
694 | // printf("isDarkBgSat: %d\n", isDarkBgSat); | 739 | // printf("isDarkBgSat: %d\n", isDarkBgSat); |
695 | setHsl_Color(tmBackground_ColorId, base); | 740 | iHSLColor bgBase = base; |
741 | setHsl_Color(tmBackground_ColorId, bgBase); | ||
696 | 742 | ||
697 | setHsl_Color(tmBannerBackground_ColorId, addSatLum_HSLColor(base, 0.1f, 0.04f * (isBannerLighter ? 1 : -1))); | 743 | setHsl_Color(tmBannerBackground_ColorId, addSatLum_HSLColor(bgBase, 0.1f, 0.04f * (isBannerLighter ? 1 : -1))); |
698 | setHsl_Color(tmBannerTitle_ColorId, setLum_HSLColor(addSatLum_HSLColor(base, 0.1f, 0), 0.55f)); | 744 | setHsl_Color(tmBannerTitle_ColorId, setLum_HSLColor(addSatLum_HSLColor(base, 0.1f, 0), 0.55f)); |
699 | setHsl_Color(tmBannerIcon_ColorId, setLum_HSLColor(addSatLum_HSLColor(base, 0.35f, 0), 0.65f)); | 745 | setHsl_Color(tmBannerIcon_ColorId, setLum_HSLColor(addSatLum_HSLColor(base, 0.35f, 0), 0.65f)); |
700 | 746 | ||
@@ -714,7 +760,7 @@ void setThemeSeed_GmDocument(iGmDocument *d, const iBlock *seed) { | |||
714 | setHsl_Color(tmHeading3_ColorId, setLum_HSLColor(altBase, titleLum + 0.60f)); | 760 | setHsl_Color(tmHeading3_ColorId, setLum_HSLColor(altBase, titleLum + 0.60f)); |
715 | 761 | ||
716 | setHsl_Color(tmParagraph_ColorId, addSatLum_HSLColor(base, 0.1f, 0.6f)); | 762 | setHsl_Color(tmParagraph_ColorId, addSatLum_HSLColor(base, 0.1f, 0.6f)); |
717 | setHsl_Color(tmFirstParagraph_ColorId, addSatLum_HSLColor(base, 0.2f, 0.8f)); | 763 | setHsl_Color(tmFirstParagraph_ColorId, addSatLum_HSLColor(base, 0.2f, 0.72f)); |
718 | setHsl_Color(tmPreformatted_ColorId, (iHSLColor){ altHue2, 1.0f, 0.75f, 1.0f }); | 764 | setHsl_Color(tmPreformatted_ColorId, (iHSLColor){ altHue2, 1.0f, 0.75f, 1.0f }); |
719 | set_Color(tmQuote_ColorId, get_Color(tmPreformatted_ColorId)); | 765 | set_Color(tmQuote_ColorId, get_Color(tmPreformatted_ColorId)); |
720 | set_Color(tmInlineContentMetadata_ColorId, get_Color(tmHeading3_ColorId)); | 766 | set_Color(tmInlineContentMetadata_ColorId, get_Color(tmHeading3_ColorId)); |
@@ -736,31 +782,12 @@ void setThemeSeed_GmDocument(iGmDocument *d, const iBlock *seed) { | |||
736 | else if (i == tmHeading3_ColorId) { | 782 | else if (i == tmHeading3_ColorId) { |
737 | color.lum *= 0.75f; | 783 | color.lum *= 0.75f; |
738 | } | 784 | } |
739 | #if 0 | ||
740 | else if (isLink_ColorId(i)) { | ||
741 | /* Darken links generally to improve visibility against a | ||
742 | light background. */ | ||
743 | color.lum *= 0.5f; | ||
744 | color.sat = 1.0f; | ||
745 | } | ||
746 | #endif | ||
747 | else if (i == tmBannerIcon_ColorId || i == tmBannerTitle_ColorId) { | 785 | else if (i == tmBannerIcon_ColorId || i == tmBannerTitle_ColorId) { |
748 | if (isBannerLighter) { | 786 | color.sat = 1.0f; |
749 | color.lum *= 0.75f; | 787 | color.lum = 0.35f; |
750 | } | ||
751 | else { | ||
752 | color.lum = 0.98f; | ||
753 | } | ||
754 | } | 788 | } |
755 | else if (i == tmBannerBackground_ColorId) { | 789 | else if (i == tmBannerBackground_ColorId) { |
756 | if (isBannerLighter) { | 790 | color = hsl_Color(get_Color(tmBackground_ColorId)); |
757 | //color.lum = iMin(0.9, color.lum); | ||
758 | color = hsl_Color(get_Color(tmBackground_ColorId)); | ||
759 | } | ||
760 | else { | ||
761 | color.sat *= 0.8f; | ||
762 | color.lum = 0.6f; | ||
763 | } | ||
764 | } | 791 | } |
765 | else if (isText_ColorId(i)) { | 792 | else if (isText_ColorId(i)) { |
766 | color.sat = 0.9f; | 793 | color.sat = 0.9f; |
@@ -772,7 +799,12 @@ void setThemeSeed_GmDocument(iGmDocument *d, const iBlock *seed) { | |||
772 | if (isDarkBgSat) { | 799 | if (isDarkBgSat) { |
773 | /* Saturate background, desaturate text. */ | 800 | /* Saturate background, desaturate text. */ |
774 | if (isBackground_ColorId(i)) { | 801 | if (isBackground_ColorId(i)) { |
775 | color.sat = (color.sat + 1) / 2; | 802 | if (primIndex != green_Hue) { |
803 | color.sat = (color.sat + 1) / 2; | ||
804 | } | ||
805 | else { | ||
806 | color.sat *= 0.5f; | ||
807 | } | ||
776 | color.lum *= 0.75f; | 808 | color.lum *= 0.75f; |
777 | } | 809 | } |
778 | else if (isText_ColorId(i)) { | 810 | else if (isText_ColorId(i)) { |
@@ -822,11 +854,11 @@ static void normalize_GmDocument(iGmDocument *d) { | |||
822 | iRangecc src = range_String(&d->source); | 854 | iRangecc src = range_String(&d->source); |
823 | iRangecc line = iNullRange; | 855 | iRangecc line = iNullRange; |
824 | iBool isPreformat = iFalse; | 856 | iBool isPreformat = iFalse; |
825 | if (d->format == plainText_GmDocumentFormat) { | 857 | if (d->format == plainText_GmDocumentFormat || isGopher_GmDocument_(d)) { |
826 | isPreformat = iTrue; /* Cannot be turned off. */ | 858 | isPreformat = iTrue; /* Cannot be turned off. */ |
827 | } | 859 | } |
828 | const int preTabWidth = 8; /* TODO: user-configurable parameter */ | 860 | const int preTabWidth = 4; /* TODO: user-configurable parameter */ |
829 | while (nextSplit_Rangecc(&src, "\n", &line)) { | 861 | while (nextSplit_Rangecc(src, "\n", &line)) { |
830 | if (isPreformat) { | 862 | if (isPreformat) { |
831 | /* Replace any tab characters with spaces for visualization. */ | 863 | /* Replace any tab characters with spaces for visualization. */ |
832 | for (const char *ch = line.start; ch != line.end; ch++) { | 864 | for (const char *ch = line.start; ch != line.end; ch++) { |
@@ -842,12 +874,12 @@ static void normalize_GmDocument(iGmDocument *d) { | |||
842 | } | 874 | } |
843 | } | 875 | } |
844 | appendCStr_String(normalized, "\n"); | 876 | appendCStr_String(normalized, "\n"); |
845 | if (lineType_GmDocument_(d, &line) == preformatted_GmLineType) { | 877 | if (lineType_GmDocument_(d, line) == preformatted_GmLineType) { |
846 | isPreformat = iFalse; | 878 | isPreformat = iFalse; |
847 | } | 879 | } |
848 | continue; | 880 | continue; |
849 | } | 881 | } |
850 | if (lineType_GmDocument_(d, &line) == preformatted_GmLineType) { | 882 | if (lineType_GmDocument_(d, line) == preformatted_GmLineType) { |
851 | isPreformat = iTrue; | 883 | isPreformat = iTrue; |
852 | appendRange_String(normalized, line); | 884 | appendRange_String(normalized, line); |
853 | appendCStr_String(normalized, "\n"); | 885 | appendCStr_String(normalized, "\n"); |
diff --git a/src/gmdocument.h b/src/gmdocument.h index 9f4bc1ca..b6c1c6ab 100644 --- a/src/gmdocument.h +++ b/src/gmdocument.h | |||
@@ -1,3 +1,25 @@ | |||
1 | /* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi> | ||
2 | |||
3 | Redistribution and use in source and binary forms, with or without | ||
4 | modification, are permitted provided that the following conditions are met: | ||
5 | |||
6 | 1. Redistributions of source code must retain the above copyright notice, this | ||
7 | list of conditions and the following disclaimer. | ||
8 | 2. Redistributions in binary form must reproduce the above copyright notice, | ||
9 | this list of conditions and the following disclaimer in the documentation | ||
10 | and/or other materials provided with the distribution. | ||
11 | |||
12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | ||
13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | ||
14 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||
15 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR | ||
16 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | ||
17 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | ||
18 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON | ||
19 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||
20 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | ||
21 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | ||
22 | |||
1 | #pragma once | 23 | #pragma once |
2 | 24 | ||
3 | #include "gmutil.h" | 25 | #include "gmutil.h" |
@@ -66,7 +88,7 @@ iDeclareObjectConstruction(GmDocument) | |||
66 | 88 | ||
67 | enum iGmDocumentFormat { | 89 | enum iGmDocumentFormat { |
68 | undefined_GmDocumentFormat = -1, | 90 | undefined_GmDocumentFormat = -1, |
69 | gemini_GmDocumentFormat = 0, | 91 | gemini_GmDocumentFormat = 0, |
70 | plainText_GmDocumentFormat, | 92 | plainText_GmDocumentFormat, |
71 | }; | 93 | }; |
72 | 94 | ||
diff --git a/src/gmrequest.c b/src/gmrequest.c index 0d69861d..137e8303 100644 --- a/src/gmrequest.c +++ b/src/gmrequest.c | |||
@@ -1,8 +1,31 @@ | |||
1 | /* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi> | ||
2 | |||
3 | Redistribution and use in source and binary forms, with or without | ||
4 | modification, are permitted provided that the following conditions are met: | ||
5 | |||
6 | 1. Redistributions of source code must retain the above copyright notice, this | ||
7 | list of conditions and the following disclaimer. | ||
8 | 2. Redistributions in binary form must reproduce the above copyright notice, | ||
9 | this list of conditions and the following disclaimer in the documentation | ||
10 | and/or other materials provided with the distribution. | ||
11 | |||
12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | ||
13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | ||
14 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||
15 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR | ||
16 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | ||
17 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | ||
18 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON | ||
19 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||
20 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | ||
21 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | ||
22 | |||
1 | #include "gmrequest.h" | 23 | #include "gmrequest.h" |
2 | #include "gmutil.h" | 24 | #include "gmutil.h" |
3 | #include "gmcerts.h" | 25 | #include "gmcerts.h" |
4 | #include "app.h" /* dataDir_App() */ | 26 | #include "app.h" /* dataDir_App() */ |
5 | #include "embedded.h" | 27 | #include "embedded.h" |
28 | #include "ui/text.h" | ||
6 | 29 | ||
7 | #include <the_Foundation/file.h> | 30 | #include <the_Foundation/file.h> |
8 | #include <the_Foundation/mutex.h> | 31 | #include <the_Foundation/mutex.h> |
@@ -171,11 +194,22 @@ static void checkServerCertificate_GmRequest_(iGmRequest *d) { | |||
171 | const iTlsCertificate *cert = serverCertificate_TlsRequest(d->req); | 194 | const iTlsCertificate *cert = serverCertificate_TlsRequest(d->req); |
172 | d->resp.certFlags = 0; | 195 | d->resp.certFlags = 0; |
173 | if (cert) { | 196 | if (cert) { |
174 | const iRangecc domain = urlHost_String(&d->url); | 197 | const iRangecc domain = range_String(hostName_Address(address_TlsRequest(d->req))); |
175 | d->resp.certFlags |= available_GmCertFlag; | 198 | d->resp.certFlags |= available_GmCertFlag; |
176 | if (!isExpired_TlsCertificate(cert)) { | 199 | if (!isExpired_TlsCertificate(cert)) { |
177 | d->resp.certFlags |= timeVerified_GmCertFlag; | 200 | d->resp.certFlags |= timeVerified_GmCertFlag; |
178 | } | 201 | } |
202 | /* TODO: Check for IP too (see below), because it may be specified in the SAN. */ | ||
203 | #if 0 | ||
204 | iString *ip = toStringFlags_Address(address_TlsRequest(d->req), noPort_SocketStringFlag, 0); | ||
205 | if (verifyIp_TlsCertificate(cert, ip)) { | ||
206 | printf("[GmRequest] IP address %s matches!\n", cstr_String(ip)); | ||
207 | } | ||
208 | else { | ||
209 | printf("[GmRequest] IP address %s not matched\n", cstr_String(ip)); | ||
210 | } | ||
211 | delete_String(ip); | ||
212 | #endif | ||
179 | if (verifyDomain_TlsCertificate(cert, domain)) { | 213 | if (verifyDomain_TlsCertificate(cert, domain)) { |
180 | d->resp.certFlags |= domainVerified_GmCertFlag; | 214 | d->resp.certFlags |= domainVerified_GmCertFlag; |
181 | } | 215 | } |
@@ -265,31 +299,92 @@ static void requestFinished_GmRequest_(iAnyObject *obj) { | |||
265 | 299 | ||
266 | static const iBlock *aboutPageSource_(iRangecc path) { | 300 | static const iBlock *aboutPageSource_(iRangecc path) { |
267 | const iBlock *src = NULL; | 301 | const iBlock *src = NULL; |
268 | if (equalCase_Rangecc(&path, "lagrange")) { | 302 | if (equalCase_Rangecc(path, "lagrange")) { |
269 | return &blobAbout_Embedded; | 303 | return &blobLagrange_Embedded; |
270 | } | 304 | } |
271 | if (equalCase_Rangecc(&path, "help")) { | 305 | if (equalCase_Rangecc(path, "help")) { |
272 | return &blobHelp_Embedded; | 306 | return &blobHelp_Embedded; |
273 | } | 307 | } |
274 | if (equalCase_Rangecc(&path, "version")) { | 308 | if (equalCase_Rangecc(path, "version")) { |
275 | return &blobVersion_Embedded; | 309 | return &blobVersion_Embedded; |
276 | } | 310 | } |
277 | return src; | 311 | return src; |
278 | } | 312 | } |
279 | 313 | ||
280 | static const iBlock *replaceVariables_(const iBlock *block) { | 314 | static const iBlock *replaceVariables_(const iBlock *block) { |
281 | iRegExp *var = new_RegExp("\\$\\{([A-Z_]+)\\}", 0); | 315 | iRegExp *var = new_RegExp("\\$\\{([^}]+)\\}", 0); |
282 | iRegExpMatch m; | 316 | iRegExpMatch m; |
317 | init_RegExpMatch(&m); | ||
283 | if (matchRange_RegExp(var, range_Block(block), &m)) { | 318 | if (matchRange_RegExp(var, range_Block(block), &m)) { |
284 | iBlock *replaced = collect_Block(copy_Block(block)); | 319 | iBlock *replaced = collect_Block(copy_Block(block)); |
285 | do { | 320 | do { |
286 | const iRangei span = m.range; | 321 | const iRangei span = m.range; |
287 | remove_Block(replaced, span.start, size_Range(&span)); | ||
288 | const iRangecc name = capturedRange_RegExpMatch(&m, 1); | 322 | const iRangecc name = capturedRange_RegExpMatch(&m, 1); |
289 | if (equal_Rangecc(&name, "APP_VERSION")) { | 323 | iRangecc repl = iNullRange; |
290 | insertData_Block(replaced, span.start, | 324 | if (equal_Rangecc(name, "APP_VERSION")) { |
291 | LAGRANGE_APP_VERSION, strlen(LAGRANGE_APP_VERSION)); | 325 | repl = range_CStr(LAGRANGE_APP_VERSION); |
326 | } | ||
327 | else if (startsWith_Rangecc(name, "BT:")) { /* block text */ | ||
328 | repl = range_String(collect_String(renderBlockChars_Text( | ||
329 | &fontFiraSansRegular_Embedded, | ||
330 | 11, /* should be larger if shaded */ | ||
331 | quadrants_TextBlockMode, | ||
332 | &(iString){ iBlockLiteral( | ||
333 | name.start + 3, size_Range(&name) - 3, size_Range(&name) - 3) }))); | ||
334 | } | ||
335 | else if (startsWith_Rangecc(name, "ST:")) { /* shaded text */ | ||
336 | repl = range_String(collect_String(renderBlockChars_Text( | ||
337 | &fontSymbola_Embedded, | ||
338 | 20, | ||
339 | shading_TextBlockMode, | ||
340 | &(iString){ iBlockLiteral( | ||
341 | name.start + 3, size_Range(&name) - 3, size_Range(&name) - 3) }))); | ||
342 | } | ||
343 | else if (equal_Rangecc(name, "ALT")) { | ||
344 | #if defined (iPlatformApple) | ||
345 | repl = range_CStr("\u2325"); | ||
346 | #else | ||
347 | repl = range_CStr("Alt"); | ||
348 | #endif | ||
349 | } | ||
350 | else if (equal_Rangecc(name, "ALT+")) { | ||
351 | #if defined (iPlatformApple) | ||
352 | repl = range_CStr("\u2325"); | ||
353 | #else | ||
354 | repl = range_CStr("Alt+"); | ||
355 | #endif | ||
356 | } | ||
357 | else if (equal_Rangecc(name, "CTRL")) { | ||
358 | #if defined (iPlatformApple) | ||
359 | repl = range_CStr("\u2318"); | ||
360 | #else | ||
361 | repl = range_CStr("Ctrl"); | ||
362 | #endif | ||
292 | } | 363 | } |
364 | else if (equal_Rangecc(name, "CTRL+")) { | ||
365 | #if defined (iPlatformApple) | ||
366 | repl = range_CStr("\u2318"); | ||
367 | #else | ||
368 | repl = range_CStr("Ctrl+"); | ||
369 | #endif | ||
370 | } | ||
371 | else if (equal_Rangecc(name, "SHIFT")) { | ||
372 | #if defined (iPlatformApple) | ||
373 | repl = range_CStr("\u21e7"); | ||
374 | #else | ||
375 | repl = range_CStr("Shift"); | ||
376 | #endif | ||
377 | } | ||
378 | else if (equal_Rangecc(name, "SHIFT+")) { | ||
379 | #if defined (iPlatformApple) | ||
380 | repl = range_CStr("\u21e7"); | ||
381 | #else | ||
382 | repl = range_CStr("Shift+"); | ||
383 | #endif | ||
384 | } | ||
385 | remove_Block(replaced, span.start, size_Range(&span)); | ||
386 | insertData_Block(replaced, span.start, repl.start, size_Range(&repl)); | ||
387 | iZap(m); | ||
293 | } while (matchRange_RegExp(var, range_Block(replaced), &m)); | 388 | } while (matchRange_RegExp(var, range_Block(replaced), &m)); |
294 | block = replaced; | 389 | block = replaced; |
295 | } | 390 | } |
@@ -305,9 +400,12 @@ void submit_GmRequest(iGmRequest *d) { | |||
305 | clear_GmResponse(&d->resp); | 400 | clear_GmResponse(&d->resp); |
306 | iUrl url; | 401 | iUrl url; |
307 | init_Url(&url, &d->url); | 402 | init_Url(&url, &d->url); |
308 | /* Check for special protocols. */ | 403 | /* Check for special schemes. */ |
309 | /* TODO: If this were a library, these could be handled via callbacks. */ | 404 | /* TODO: If this were a library, these could be handled via callbacks. */ |
310 | if (equalCase_Rangecc(&url.protocol, "about")) { | 405 | /* TODO: Handle app's configured proxies and these via the same mechanism. */ |
406 | const iString *host = collect_String(newRange_String(url.host)); | ||
407 | uint16_t port = toInt_String(collect_String(newRange_String(url.port))); | ||
408 | if (equalCase_Rangecc(url.scheme, "about")) { | ||
311 | const iBlock *src = aboutPageSource_(url.path); | 409 | const iBlock *src = aboutPageSource_(url.path); |
312 | if (src) { | 410 | if (src) { |
313 | d->resp.statusCode = success_GmStatusCode; | 411 | d->resp.statusCode = success_GmStatusCode; |
@@ -323,7 +421,7 @@ void submit_GmRequest(iGmRequest *d) { | |||
323 | iNotifyAudience(d, finished, GmRequestFinished); | 421 | iNotifyAudience(d, finished, GmRequestFinished); |
324 | return; | 422 | return; |
325 | } | 423 | } |
326 | else if (equalCase_Rangecc(&url.protocol, "file")) { | 424 | else if (equalCase_Rangecc(url.scheme, "file")) { |
327 | iString *path = collect_String(urlDecode_String(collect_String(newRange_String(url.path)))); | 425 | iString *path = collect_String(urlDecode_String(collect_String(newRange_String(url.path)))); |
328 | iFile * f = new_File(path); | 426 | iFile * f = new_File(path); |
329 | if (open_File(f, readOnly_FileMode)) { | 427 | if (open_File(f, readOnly_FileMode)) { |
@@ -361,9 +459,9 @@ void submit_GmRequest(iGmRequest *d) { | |||
361 | iNotifyAudience(d, finished, GmRequestFinished); | 459 | iNotifyAudience(d, finished, GmRequestFinished); |
362 | return; | 460 | return; |
363 | } | 461 | } |
364 | else if (equalCase_Rangecc(&url.protocol, "data")) { | 462 | else if (equalCase_Rangecc(url.scheme, "data")) { |
365 | d->resp.statusCode = success_GmStatusCode; | 463 | d->resp.statusCode = success_GmStatusCode; |
366 | iString *src = collectNewCStr_String(url.protocol.start + 5); | 464 | iString *src = collectNewCStr_String(url.scheme.start + 5); |
367 | iRangecc header = { constBegin_String(src), constBegin_String(src) }; | 465 | iRangecc header = { constBegin_String(src), constBegin_String(src) }; |
368 | while (header.end < constEnd_String(src) && *header.end != ',') { | 466 | while (header.end < constEnd_String(src) && *header.end != ',') { |
369 | header.end++; | 467 | header.end++; |
@@ -372,8 +470,8 @@ void submit_GmRequest(iGmRequest *d) { | |||
372 | setRange_String(&d->resp.meta, header); | 470 | setRange_String(&d->resp.meta, header); |
373 | /* Check what's in the header. */ { | 471 | /* Check what's in the header. */ { |
374 | iRangecc entry = iNullRange; | 472 | iRangecc entry = iNullRange; |
375 | while (nextSplit_Rangecc(&header, ";", &entry)) { | 473 | while (nextSplit_Rangecc(header, ";", &entry)) { |
376 | if (equal_Rangecc(&entry, "base64")) { | 474 | if (equal_Rangecc(entry, "base64")) { |
377 | isBase64 = iTrue; | 475 | isBase64 = iTrue; |
378 | } | 476 | } |
379 | } | 477 | } |
@@ -392,15 +490,31 @@ void submit_GmRequest(iGmRequest *d) { | |||
392 | iNotifyAudience(d, finished, GmRequestFinished); | 490 | iNotifyAudience(d, finished, GmRequestFinished); |
393 | return; | 491 | return; |
394 | } | 492 | } |
493 | else if (schemeProxy_App(url.scheme)) { | ||
494 | /* User has configured a proxy server for this scheme. */ | ||
495 | const iString *proxy = schemeProxy_App(url.scheme); | ||
496 | if (contains_String(proxy, ':')) { | ||
497 | const size_t cpos = indexOf_String(proxy, ':'); | ||
498 | port = atoi(cstr_String(proxy) + cpos + 1); | ||
499 | host = collect_String(newCStrN_String(cstr_String(proxy), cpos)); | ||
500 | } | ||
501 | else { | ||
502 | host = proxy; | ||
503 | port = 0; | ||
504 | } | ||
505 | } | ||
395 | d->state = receivingHeader_GmRequestState; | 506 | d->state = receivingHeader_GmRequestState; |
396 | d->req = new_TlsRequest(); | 507 | d->req = new_TlsRequest(); |
508 | const iGmIdentity *identity = identityForUrl_GmCerts(d->certs, &d->url); | ||
509 | if (identity) { | ||
510 | setCertificate_TlsRequest(d->req, identity->cert); | ||
511 | } | ||
397 | iConnect(TlsRequest, d->req, readyRead, d, readIncoming_GmRequest_); | 512 | iConnect(TlsRequest, d->req, readyRead, d, readIncoming_GmRequest_); |
398 | iConnect(TlsRequest, d->req, finished, d, requestFinished_GmRequest_); | 513 | iConnect(TlsRequest, d->req, finished, d, requestFinished_GmRequest_); |
399 | uint16_t port = toInt_String(collect_String(newRange_String(url.port))); | ||
400 | if (port == 0) { | 514 | if (port == 0) { |
401 | port = 1965; /* default Gemini port */ | 515 | port = 1965; /* default Gemini port */ |
402 | } | 516 | } |
403 | setUrl_TlsRequest(d->req, collect_String(newRange_String(url.host)), port); | 517 | setUrl_TlsRequest(d->req, host, port); |
404 | setContent_TlsRequest(d->req, | 518 | setContent_TlsRequest(d->req, |
405 | utf8_String(collectNewFormat_String("%s\r\n", cstr_String(&d->url)))); | 519 | utf8_String(collectNewFormat_String("%s\r\n", cstr_String(&d->url)))); |
406 | submit_TlsRequest(d->req); | 520 | submit_TlsRequest(d->req); |
diff --git a/src/gmrequest.h b/src/gmrequest.h index cb7c151a..2f1a4261 100644 --- a/src/gmrequest.h +++ b/src/gmrequest.h | |||
@@ -1,3 +1,25 @@ | |||
1 | /* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi> | ||
2 | |||
3 | Redistribution and use in source and binary forms, with or without | ||
4 | modification, are permitted provided that the following conditions are met: | ||
5 | |||
6 | 1. Redistributions of source code must retain the above copyright notice, this | ||
7 | list of conditions and the following disclaimer. | ||
8 | 2. Redistributions in binary form must reproduce the above copyright notice, | ||
9 | this list of conditions and the following disclaimer in the documentation | ||
10 | and/or other materials provided with the distribution. | ||
11 | |||
12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | ||
13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | ||
14 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||
15 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR | ||
16 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | ||
17 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | ||
18 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON | ||
19 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||
20 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | ||
21 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | ||
22 | |||
1 | #pragma once | 23 | #pragma once |
2 | 24 | ||
3 | #include <the_Foundation/audience.h> | 25 | #include <the_Foundation/audience.h> |
diff --git a/src/gmutil.c b/src/gmutil.c index 2138caa3..131734b2 100644 --- a/src/gmutil.c +++ b/src/gmutil.c | |||
@@ -1,3 +1,25 @@ | |||
1 | /* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi> | ||
2 | |||
3 | Redistribution and use in source and binary forms, with or without | ||
4 | modification, are permitted provided that the following conditions are met: | ||
5 | |||
6 | 1. Redistributions of source code must retain the above copyright notice, this | ||
7 | list of conditions and the following disclaimer. | ||
8 | 2. Redistributions in binary form must reproduce the above copyright notice, | ||
9 | this list of conditions and the following disclaimer in the documentation | ||
10 | and/or other materials provided with the distribution. | ||
11 | |||
12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | ||
13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | ||
14 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||
15 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR | ||
16 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | ||
17 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | ||
18 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON | ||
19 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||
20 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | ||
21 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | ||
22 | |||
1 | #include "gmutil.h" | 23 | #include "gmutil.h" |
2 | 24 | ||
3 | #include <the_Foundation/regexp.h> | 25 | #include <the_Foundation/regexp.h> |
@@ -5,12 +27,17 @@ | |||
5 | #include <the_Foundation/path.h> | 27 | #include <the_Foundation/path.h> |
6 | 28 | ||
7 | void init_Url(iUrl *d, const iString *text) { | 29 | void init_Url(iUrl *d, const iString *text) { |
8 | iRegExp *absPat = | 30 | static iRegExp *absoluteUrlPattern_; |
9 | new_RegExp("([a-z]+:)?(//[^/:?]*)(:[0-9]+)?([^?]*)(\\?.*)?", caseInsensitive_RegExpOption); | 31 | static iRegExp *relativeUrlPattern_; |
32 | if (!absoluteUrlPattern_) { | ||
33 | absoluteUrlPattern_ = new_RegExp("([a-z]+:)?(//[^/:?]*)(:[0-9]+)?([^?]*)(\\?.*)?", | ||
34 | caseInsensitive_RegExpOption); | ||
35 | } | ||
10 | iRegExpMatch m; | 36 | iRegExpMatch m; |
11 | if (matchString_RegExp(absPat, text, &m)) { | 37 | init_RegExpMatch(&m); |
12 | d->protocol = capturedRange_RegExpMatch(&m, 1); | 38 | if (matchString_RegExp(absoluteUrlPattern_, text, &m)) { |
13 | d->host = capturedRange_RegExpMatch(&m, 2); | 39 | d->scheme = capturedRange_RegExpMatch(&m, 1); |
40 | d->host = capturedRange_RegExpMatch(&m, 2); | ||
14 | if (!isEmpty_Range(&d->host)) { | 41 | if (!isEmpty_Range(&d->host)) { |
15 | d->host.start += 2; /* skip the double slash */ | 42 | d->host.start += 2; /* skip the double slash */ |
16 | } | 43 | } |
@@ -18,28 +45,28 @@ void init_Url(iUrl *d, const iString *text) { | |||
18 | if (!isEmpty_Range(&d->port)) { | 45 | if (!isEmpty_Range(&d->port)) { |
19 | d->port.start++; /* omit the colon */ | 46 | d->port.start++; /* omit the colon */ |
20 | } | 47 | } |
21 | d->path = capturedRange_RegExpMatch(&m, 4); | 48 | d->path = capturedRange_RegExpMatch(&m, 4); |
22 | d->query = capturedRange_RegExpMatch(&m, 5); | 49 | d->query = capturedRange_RegExpMatch(&m, 5); |
23 | } | 50 | } |
24 | else { | 51 | else { |
25 | /* Must be a relative path. */ | 52 | /* Must be a relative path. */ |
26 | iZap(*d); | 53 | iZap(*d); |
27 | iRegExp *relPat = new_RegExp("([a-z]+:)?([^?]*)(\\?.*)?", 0); | 54 | if (!relativeUrlPattern_) { |
28 | if (matchString_RegExp(relPat, text, &m)) { | 55 | relativeUrlPattern_ = new_RegExp("([a-z]+:)?([^?]*)(\\?.*)?", 0); |
29 | d->protocol = capturedRange_RegExpMatch(&m, 1); | 56 | } |
30 | d->path = capturedRange_RegExpMatch(&m, 2); | 57 | if (matchString_RegExp(relativeUrlPattern_, text, &m)) { |
31 | d->query = capturedRange_RegExpMatch(&m, 3); | 58 | d->scheme = capturedRange_RegExpMatch(&m, 1); |
59 | d->path = capturedRange_RegExpMatch(&m, 2); | ||
60 | d->query = capturedRange_RegExpMatch(&m, 3); | ||
32 | } | 61 | } |
33 | iRelease(relPat); | ||
34 | } | 62 | } |
35 | iRelease(absPat); | 63 | if (!isEmpty_Range(&d->scheme)) { |
36 | if (!isEmpty_Range(&d->protocol)) { | 64 | d->scheme.end--; /* omit the colon */ |
37 | d->protocol.end--; /* omit the colon */ | ||
38 | } | 65 | } |
39 | } | 66 | } |
40 | 67 | ||
41 | static iRangecc dirPath_(iRangecc path) { | 68 | static iRangecc dirPath_(iRangecc path) { |
42 | const size_t pos = lastIndexOfCStr_Rangecc(&path, "/"); | 69 | const size_t pos = lastIndexOfCStr_Rangecc(path, "/"); |
43 | if (pos == iInvalidPos) return path; | 70 | if (pos == iInvalidPos) return path; |
44 | return (iRangecc){ path.start, path.start + pos }; | 71 | return (iRangecc){ path.start, path.start + pos }; |
45 | } | 72 | } |
@@ -62,13 +89,13 @@ void cleanUrlPath_String(iString *d) { | |||
62 | iUrl parts; | 89 | iUrl parts; |
63 | init_Url(&parts, d); | 90 | init_Url(&parts, d); |
64 | iRangecc seg = iNullRange; | 91 | iRangecc seg = iNullRange; |
65 | while (nextSplit_Rangecc(&parts.path, "/", &seg)) { | 92 | while (nextSplit_Rangecc(parts.path, "/", &seg)) { |
66 | if (equal_Rangecc(&seg, "..")) { | 93 | if (equal_Rangecc(seg, "..")) { |
67 | /* Back up one segment. */ | 94 | /* Back up one segment. */ |
68 | iRangecc last = prevPathSeg_(constEnd_String(&clean), constBegin_String(&clean)); | 95 | iRangecc last = prevPathSeg_(constEnd_String(&clean), constBegin_String(&clean)); |
69 | truncate_Block(&clean.chars, last.start - constBegin_String(&clean)); | 96 | truncate_Block(&clean.chars, last.start - constBegin_String(&clean)); |
70 | } | 97 | } |
71 | else if (equal_Rangecc(&seg, ".")) { | 98 | else if (equal_Rangecc(seg, ".")) { |
72 | /* Skip it. */ | 99 | /* Skip it. */ |
73 | } | 100 | } |
74 | else { | 101 | else { |
@@ -76,11 +103,11 @@ void cleanUrlPath_String(iString *d) { | |||
76 | appendRange_String(&clean, seg); | 103 | appendRange_String(&clean, seg); |
77 | } | 104 | } |
78 | } | 105 | } |
79 | if (endsWith_Rangecc(&parts.path, "/")) { | 106 | if (endsWith_Rangecc(parts.path, "/")) { |
80 | appendCStr_String(&clean, "/"); | 107 | appendCStr_String(&clean, "/"); |
81 | } | 108 | } |
82 | /* Replace with the new path. */ | 109 | /* Replace with the new path. */ |
83 | if (cmpCStrNSc_Rangecc(&parts.path, cstr_String(&clean), size_String(&clean), &iCaseSensitive)) { | 110 | if (cmpCStrNSc_Rangecc(parts.path, cstr_String(&clean), size_String(&clean), &iCaseSensitive)) { |
84 | const size_t pos = parts.path.start - constBegin_String(d); | 111 | const size_t pos = parts.path.start - constBegin_String(d); |
85 | remove_Block(&d->chars, pos, size_Range(&parts.path)); | 112 | remove_Block(&d->chars, pos, size_Range(&parts.path)); |
86 | insertData_Block(&d->chars, pos, cstr_String(&clean), size_String(&clean)); | 113 | insertData_Block(&d->chars, pos, cstr_String(&clean), size_String(&clean)); |
@@ -88,10 +115,10 @@ void cleanUrlPath_String(iString *d) { | |||
88 | deinit_String(&clean); | 115 | deinit_String(&clean); |
89 | } | 116 | } |
90 | 117 | ||
91 | iRangecc urlProtocol_String(const iString *d) { | 118 | iRangecc urlScheme_String(const iString *d) { |
92 | iUrl url; | 119 | iUrl url; |
93 | init_Url(&url, d); | 120 | init_Url(&url, d); |
94 | return url.protocol; | 121 | return url.scheme; |
95 | } | 122 | } |
96 | 123 | ||
97 | iRangecc urlHost_String(const iString *d) { | 124 | iRangecc urlHost_String(const iString *d) { |
@@ -105,20 +132,20 @@ const iString *absoluteUrl_String(const iString *d, const iString *urlMaybeRelat | |||
105 | iUrl rel; | 132 | iUrl rel; |
106 | init_Url(&orig, d); | 133 | init_Url(&orig, d); |
107 | init_Url(&rel, urlMaybeRelative); | 134 | init_Url(&rel, urlMaybeRelative); |
108 | if (equalCase_Rangecc(&rel.protocol, "data") || equalCase_Rangecc(&rel.protocol, "about")) { | 135 | if (equalCase_Rangecc(rel.scheme, "data") || equalCase_Rangecc(rel.scheme, "about")) { |
109 | /* Special case, the contents should be left unparsed. */ | 136 | /* Special case, the contents should be left unparsed. */ |
110 | return urlMaybeRelative; | 137 | return urlMaybeRelative; |
111 | } | 138 | } |
112 | const iBool isRelative = !isDef_(rel.host); | 139 | const iBool isRelative = !isDef_(rel.host); |
113 | iRangecc protocol = range_CStr("gemini"); | 140 | iRangecc scheme = range_CStr("gemini"); |
114 | if (isDef_(rel.protocol)) { | 141 | if (isDef_(rel.scheme)) { |
115 | protocol = rel.protocol; | 142 | scheme = rel.scheme; |
116 | } | 143 | } |
117 | else if (isRelative && isDef_(orig.protocol)) { | 144 | else if (isRelative && isDef_(orig.scheme)) { |
118 | protocol = orig.protocol; | 145 | scheme = orig.scheme; |
119 | } | 146 | } |
120 | iString *absolute = collectNew_String(); | 147 | iString *absolute = collectNew_String(); |
121 | appendRange_String(absolute, protocol); | 148 | appendRange_String(absolute, scheme); |
122 | appendCStr_String(absolute, "://"); { | 149 | appendCStr_String(absolute, "://"); { |
123 | const iUrl *selHost = isDef_(rel.host) ? &rel : &orig; | 150 | const iUrl *selHost = isDef_(rel.host) ? &rel : &orig; |
124 | appendRange_String(absolute, selHost->host); | 151 | appendRange_String(absolute, selHost->host); |
@@ -127,11 +154,11 @@ const iString *absoluteUrl_String(const iString *d, const iString *urlMaybeRelat | |||
127 | appendRange_String(absolute, selHost->port); | 154 | appendRange_String(absolute, selHost->port); |
128 | } | 155 | } |
129 | } | 156 | } |
130 | if (isDef_(rel.protocol) || isDef_(rel.host) || startsWith_Rangecc(&rel.path, "/")) { | 157 | if (isDef_(rel.scheme) || isDef_(rel.host) || startsWith_Rangecc(rel.path, "/")) { |
131 | appendRange_String(absolute, rel.path); /* absolute path */ | 158 | appendRange_String(absolute, isDef_(rel.path) ? rel.path : range_CStr("/")); /* absolute path */ |
132 | } | 159 | } |
133 | else { | 160 | else { |
134 | if (!endsWith_Rangecc(&orig.path, "/")) { | 161 | if (!endsWith_Rangecc(orig.path, "/")) { |
135 | /* Referencing a file. */ | 162 | /* Referencing a file. */ |
136 | appendRange_String(absolute, dirPath_(orig.path)); | 163 | appendRange_String(absolute, dirPath_(orig.path)); |
137 | } | 164 | } |
@@ -200,6 +227,16 @@ static const struct { | |||
200 | "Invalid Redirect", | 227 | "Invalid Redirect", |
201 | "The server responded with a redirect but did not provide a valid destination URL. " | 228 | "The server responded with a redirect but did not provide a valid destination URL. " |
202 | "Perhaps the server is malfunctioning." } }, | 229 | "Perhaps the server is malfunctioning." } }, |
230 | { nonGeminiRedirect_GmStatusCode, | ||
231 | { 0x27a0, /* dashed arrow */ | ||
232 | "Redirect to Non-Gemini URL", | ||
233 | "The server attempted to redirect us to a non-Gemini URL. Here is the link so you " | ||
234 | "can open it manually if appropriate."} }, | ||
235 | { tooManyRedirects_GmStatusCode, | ||
236 | { 0x27a0, /* dashed arrow */ | ||
237 | "Too Many Redirects", | ||
238 | "You may be stuck in a redirection loop. The next redirected URL is below if you " | ||
239 | "want to continue manually."} }, | ||
203 | { temporaryFailure_GmStatusCode, | 240 | { temporaryFailure_GmStatusCode, |
204 | { 0x1f50c, /* electric plug */ | 241 | { 0x1f50c, /* electric plug */ |
205 | "Temporary Failure", | 242 | "Temporary Failure", |
diff --git a/src/gmutil.h b/src/gmutil.h index 9e8c5934..2c017af4 100644 --- a/src/gmutil.h +++ b/src/gmutil.h | |||
@@ -1,3 +1,25 @@ | |||
1 | /* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi> | ||
2 | |||
3 | Redistribution and use in source and binary forms, with or without | ||
4 | modification, are permitted provided that the following conditions are met: | ||
5 | |||
6 | 1. Redistributions of source code must retain the above copyright notice, this | ||
7 | list of conditions and the following disclaimer. | ||
8 | 2. Redistributions in binary form must reproduce the above copyright notice, | ||
9 | this list of conditions and the following disclaimer in the documentation | ||
10 | and/or other materials provided with the distribution. | ||
11 | |||
12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | ||
13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | ||
14 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||
15 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR | ||
16 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | ||
17 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | ||
18 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON | ||
19 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||
20 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | ||
21 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | ||
22 | |||
1 | #pragma once | 23 | #pragma once |
2 | 24 | ||
3 | #include <the_Foundation/range.h> | 25 | #include <the_Foundation/range.h> |
@@ -11,6 +33,8 @@ enum iGmStatusCode { | |||
11 | /* clientside status codes */ | 33 | /* clientside status codes */ |
12 | clientSide_GmStatusCode = -100, | 34 | clientSide_GmStatusCode = -100, |
13 | invalidRedirect_GmStatusCode, | 35 | invalidRedirect_GmStatusCode, |
36 | nonGeminiRedirect_GmStatusCode, | ||
37 | tooManyRedirects_GmStatusCode, | ||
14 | invalidHeader_GmStatusCode, | 38 | invalidHeader_GmStatusCode, |
15 | unsupportedMimeType_GmStatusCode, | 39 | unsupportedMimeType_GmStatusCode, |
16 | failedToOpenFile_GmStatusCode, | 40 | failedToOpenFile_GmStatusCode, |
@@ -61,7 +85,7 @@ iBool isDefined_GmError (enum iGmStatusCode code); | |||
61 | const iGmError * get_GmError (enum iGmStatusCode code); | 85 | const iGmError * get_GmError (enum iGmStatusCode code); |
62 | 86 | ||
63 | struct Impl_Url { | 87 | struct Impl_Url { |
64 | iRangecc protocol; | 88 | iRangecc scheme; |
65 | iRangecc host; | 89 | iRangecc host; |
66 | iRangecc port; | 90 | iRangecc port; |
67 | iRangecc path; | 91 | iRangecc path; |
@@ -70,7 +94,7 @@ struct Impl_Url { | |||
70 | 94 | ||
71 | void init_Url (iUrl *, const iString *text); | 95 | void init_Url (iUrl *, const iString *text); |
72 | 96 | ||
73 | iRangecc urlProtocol_String (const iString *); | 97 | iRangecc urlScheme_String (const iString *); |
74 | iRangecc urlHost_String (const iString *); | 98 | iRangecc urlHost_String (const iString *); |
75 | const iString * absoluteUrl_String (const iString *, const iString *urlMaybeRelative); | 99 | const iString * absoluteUrl_String (const iString *, const iString *urlMaybeRelative); |
76 | iString * makeFileUrl_String (const iString *localFilePath); | 100 | iString * makeFileUrl_String (const iString *localFilePath); |
diff --git a/src/history.c b/src/history.c index 24912d81..6e985903 100644 --- a/src/history.c +++ b/src/history.c | |||
@@ -1,3 +1,25 @@ | |||
1 | /* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi> | ||
2 | |||
3 | Redistribution and use in source and binary forms, with or without | ||
4 | modification, are permitted provided that the following conditions are met: | ||
5 | |||
6 | 1. Redistributions of source code must retain the above copyright notice, this | ||
7 | list of conditions and the following disclaimer. | ||
8 | 2. Redistributions in binary form must reproduce the above copyright notice, | ||
9 | this list of conditions and the following disclaimer in the documentation | ||
10 | and/or other materials provided with the distribution. | ||
11 | |||
12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | ||
13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | ||
14 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||
15 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR | ||
16 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | ||
17 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | ||
18 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON | ||
19 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||
20 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | ||
21 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | ||
22 | |||
1 | #include "history.h" | 23 | #include "history.h" |
2 | #include "app.h" | 24 | #include "app.h" |
3 | 25 | ||
diff --git a/src/history.h b/src/history.h index cf3a46be..22fe8bf8 100644 --- a/src/history.h +++ b/src/history.h | |||
@@ -1,3 +1,25 @@ | |||
1 | /* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi> | ||
2 | |||
3 | Redistribution and use in source and binary forms, with or without | ||
4 | modification, are permitted provided that the following conditions are met: | ||
5 | |||
6 | 1. Redistributions of source code must retain the above copyright notice, this | ||
7 | list of conditions and the following disclaimer. | ||
8 | 2. Redistributions in binary form must reproduce the above copyright notice, | ||
9 | this list of conditions and the following disclaimer in the documentation | ||
10 | and/or other materials provided with the distribution. | ||
11 | |||
12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | ||
13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | ||
14 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||
15 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR | ||
16 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | ||
17 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | ||
18 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON | ||
19 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||
20 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | ||
21 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | ||
22 | |||
1 | #pragma once | 23 | #pragma once |
2 | 24 | ||
3 | #include "gmrequest.h" | 25 | #include "gmrequest.h" |
diff --git a/src/macos.h b/src/macos.h new file mode 100644 index 00000000..07990090 --- /dev/null +++ b/src/macos.h | |||
@@ -0,0 +1,31 @@ | |||
1 | /* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi> | ||
2 | |||
3 | Redistribution and use in source and binary forms, with or without | ||
4 | modification, are permitted provided that the following conditions are met: | ||
5 | |||
6 | 1. Redistributions of source code must retain the above copyright notice, this | ||
7 | list of conditions and the following disclaimer. | ||
8 | 2. Redistributions in binary form must reproduce the above copyright notice, | ||
9 | this list of conditions and the following disclaimer in the documentation | ||
10 | and/or other materials provided with the distribution. | ||
11 | |||
12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | ||
13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | ||
14 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||
15 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR | ||
16 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | ||
17 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | ||
18 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON | ||
19 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||
20 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | ||
21 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | ||
22 | |||
23 | #pragma once | ||
24 | |||
25 | #include "ui/util.h" | ||
26 | |||
27 | /* Platform-specific functionality for macOS */ | ||
28 | |||
29 | void setupApplication_MacOS (void); | ||
30 | void insertMenuItems_MacOS (const char *menuLabel, int atIndex, const iMenuItem *items, size_t count); | ||
31 | void handleCommand_MacOS (const char *cmd); | ||
diff --git a/src/ui/macos.m b/src/macos.m index 9ff2f96e..edbb6df0 100644 --- a/src/ui/macos.m +++ b/src/macos.m | |||
@@ -1,8 +1,30 @@ | |||
1 | /* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi> | ||
2 | |||
3 | Redistribution and use in source and binary forms, with or without | ||
4 | modification, are permitted provided that the following conditions are met: | ||
5 | |||
6 | 1. Redistributions of source code must retain the above copyright notice, this | ||
7 | list of conditions and the following disclaimer. | ||
8 | 2. Redistributions in binary form must reproduce the above copyright notice, | ||
9 | this list of conditions and the following disclaimer in the documentation | ||
10 | and/or other materials provided with the distribution. | ||
11 | |||
12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | ||
13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | ||
14 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||
15 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR | ||
16 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | ||
17 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | ||
18 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON | ||
19 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||
20 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | ||
21 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | ||
22 | |||
1 | #include "macos.h" | 23 | #include "macos.h" |
2 | #include "app.h" | 24 | #include "app.h" |
3 | #include "command.h" | 25 | #include "ui/command.h" |
4 | #include "widget.h" | 26 | #include "ui/widget.h" |
5 | #include "color.h" | 27 | #include "ui/color.h" |
6 | 28 | ||
7 | #import <AppKit/AppKit.h> | 29 | #import <AppKit/AppKit.h> |
8 | 30 | ||
@@ -91,6 +113,7 @@ enum iTouchBarVariant { | |||
91 | 113 | ||
92 | @interface MyDelegate : NSResponder<NSApplicationDelegate, NSTouchBarDelegate> { | 114 | @interface MyDelegate : NSResponder<NSApplicationDelegate, NSTouchBarDelegate> { |
93 | enum iTouchBarVariant touchBarVariant; | 115 | enum iTouchBarVariant touchBarVariant; |
116 | NSString *currentAppearanceName; | ||
94 | NSObject<NSApplicationDelegate> *sdlDelegate; | 117 | NSObject<NSApplicationDelegate> *sdlDelegate; |
95 | NSMutableDictionary<NSString *, NSString*> *menuCommands; | 118 | NSMutableDictionary<NSString *, NSString*> *menuCommands; |
96 | } | 119 | } |
@@ -105,6 +128,7 @@ enum iTouchBarVariant { | |||
105 | 128 | ||
106 | - (id)initWithSDLDelegate:(NSObject<NSApplicationDelegate> *)sdl { | 129 | - (id)initWithSDLDelegate:(NSObject<NSApplicationDelegate> *)sdl { |
107 | [super init]; | 130 | [super init]; |
131 | currentAppearanceName = nil; | ||
108 | menuCommands = [[NSMutableDictionary<NSString *, NSString *> alloc] init]; | 132 | menuCommands = [[NSMutableDictionary<NSString *, NSString *> alloc] init]; |
109 | touchBarVariant = default_TouchBarVariant; | 133 | touchBarVariant = default_TouchBarVariant; |
110 | sdlDelegate = sdl; | 134 | sdlDelegate = sdl; |
@@ -113,6 +137,7 @@ enum iTouchBarVariant { | |||
113 | 137 | ||
114 | - (void)dealloc { | 138 | - (void)dealloc { |
115 | [menuCommands release]; | 139 | [menuCommands release]; |
140 | [currentAppearanceName release]; | ||
116 | [super dealloc]; | 141 | [super dealloc]; |
117 | } | 142 | } |
118 | 143 | ||
@@ -121,6 +146,24 @@ enum iTouchBarVariant { | |||
121 | self.touchBar = nil; | 146 | self.touchBar = nil; |
122 | } | 147 | } |
123 | 148 | ||
149 | static void appearanceChanged_MacOS_(NSString *name) { | ||
150 | const iBool isDark = [name containsString:@"Dark"]; | ||
151 | const iBool isHighContrast = [name containsString:@"HighContrast"]; | ||
152 | postCommandf_App("os.theme.changed dark:%d contrast:%d", isDark ? 1 : 0, isHighContrast ? 1 : 0); | ||
153 | // printf("Effective appearance changed: %s\n", [name cStringUsingEncoding:NSUTF8StringEncoding]); | ||
154 | // fflush(stdout); | ||
155 | } | ||
156 | |||
157 | - (void)setAppearance:(NSString *)name { | ||
158 | if (!currentAppearanceName || ![name isEqualToString:currentAppearanceName]) { | ||
159 | if (currentAppearanceName) { | ||
160 | [currentAppearanceName release]; | ||
161 | } | ||
162 | currentAppearanceName = [name retain]; | ||
163 | appearanceChanged_MacOS_(currentAppearanceName); | ||
164 | } | ||
165 | } | ||
166 | |||
124 | - (void)setCommand:(NSString *)command forMenuItem:(NSMenuItem *)menuItem { | 167 | - (void)setCommand:(NSString *)command forMenuItem:(NSMenuItem *)menuItem { |
125 | [menuCommands setObject:command forKey:[menuItem title]]; | 168 | [menuCommands setObject:command forKey:[menuItem title]]; |
126 | } | 169 | } |
@@ -133,6 +176,16 @@ enum iTouchBarVariant { | |||
133 | [sdlDelegate applicationDidFinishLaunching:notification]; | 176 | [sdlDelegate applicationDidFinishLaunching:notification]; |
134 | } | 177 | } |
135 | 178 | ||
179 | - (void)observeValueForKeyPath:(NSString *)keyPath | ||
180 | ofObject:(id)object | ||
181 | change:(NSDictionary *)change | ||
182 | context:(void *)context { | ||
183 | iUnused(object, change); | ||
184 | if ([keyPath isEqualToString:@"effectiveAppearance"] && context == self) { | ||
185 | [self setAppearance:[[NSApp effectiveAppearance] name]]; | ||
186 | } | ||
187 | } | ||
188 | |||
136 | #if 0 | 189 | #if 0 |
137 | - (NSTouchBar *)makeTouchBar { | 190 | - (NSTouchBar *)makeTouchBar { |
138 | NSTouchBar *bar = [[NSTouchBar alloc] init]; | 191 | NSTouchBar *bar = [[NSTouchBar alloc] init]; |
@@ -225,7 +278,7 @@ enum iTouchBarVariant { | |||
225 | return [[CommandButton alloc] initWithIdentifier:identifier | 278 | return [[CommandButton alloc] initWithIdentifier:identifier |
226 | title:@"Go to…" | 279 | title:@"Go to…" |
227 | command:@"pattern.goto arg:-1"]; | 280 | command:@"pattern.goto arg:-1"]; |
228 | } | 281 | } |
229 | else if ([identifier isEqualToString:event_TouchId_]) { | 282 | else if ([identifier isEqualToString:event_TouchId_]) { |
230 | NSTouchBar *events = [[NSTouchBar alloc] init]; | 283 | NSTouchBar *events = [[NSTouchBar alloc] init]; |
231 | events.delegate = self; | 284 | events.delegate = self; |
@@ -344,8 +397,10 @@ void enableMomentumScroll_MacOS(void) { | |||
344 | 397 | ||
345 | void setupApplication_MacOS(void) { | 398 | void setupApplication_MacOS(void) { |
346 | NSApplication *app = [NSApplication sharedApplication]; | 399 | NSApplication *app = [NSApplication sharedApplication]; |
400 | //appearanceChanged_MacOS_([[app effectiveAppearance] name]); | ||
347 | /* Our delegate will override SDL's delegate. */ | 401 | /* Our delegate will override SDL's delegate. */ |
348 | MyDelegate *myDel = [[MyDelegate alloc] initWithSDLDelegate:app.delegate]; | 402 | MyDelegate *myDel = [[MyDelegate alloc] initWithSDLDelegate:app.delegate]; |
403 | [myDel setAppearance:[[app effectiveAppearance] name]]; | ||
349 | app.delegate = myDel; | 404 | app.delegate = myDel; |
350 | NSMenu *appMenu = [[[NSApp mainMenu] itemAtIndex:0] submenu]; | 405 | NSMenu *appMenu = [[[NSApp mainMenu] itemAtIndex:0] submenu]; |
351 | NSMenuItem *prefsItem = [appMenu itemWithTitle:@"Preferences…"]; | 406 | NSMenuItem *prefsItem = [appMenu itemWithTitle:@"Preferences…"]; |
@@ -361,6 +416,10 @@ void setupApplication_MacOS(void) { | |||
361 | void insertMenuItems_MacOS(const char *menuLabel, int atIndex, const iMenuItem *items, size_t count) { | 416 | void insertMenuItems_MacOS(const char *menuLabel, int atIndex, const iMenuItem *items, size_t count) { |
362 | NSApplication *app = [NSApplication sharedApplication]; | 417 | NSApplication *app = [NSApplication sharedApplication]; |
363 | MyDelegate *myDel = (MyDelegate *) app.delegate; | 418 | MyDelegate *myDel = (MyDelegate *) app.delegate; |
419 | [app addObserver:myDel | ||
420 | forKeyPath:@"effectiveAppearance" | ||
421 | options:0 | ||
422 | context:myDel]; | ||
364 | NSMenu *appMenu = [app mainMenu]; | 423 | NSMenu *appMenu = [app mainMenu]; |
365 | NSMenuItem *mainItem = [appMenu insertItemWithTitle:[NSString stringWithUTF8String:menuLabel] | 424 | NSMenuItem *mainItem = [appMenu insertItemWithTitle:[NSString stringWithUTF8String:menuLabel] |
366 | action:nil | 425 | action:nil |
@@ -417,6 +476,11 @@ void insertMenuItems_MacOS(const char *menuLabel, int atIndex, const iMenuItem * | |||
417 | } | 476 | } |
418 | 477 | ||
419 | void handleCommand_MacOS(const char *cmd) { | 478 | void handleCommand_MacOS(const char *cmd) { |
479 | if (equal_Command(cmd, "prefs.ostheme.changed")) { | ||
480 | if (arg_Command(cmd)) { | ||
481 | appearanceChanged_MacOS_([[NSApp effectiveAppearance] name]); | ||
482 | } | ||
483 | } | ||
420 | #if 0 | 484 | #if 0 |
421 | if (equal_Command(cmd, "tabs.changed")) { | 485 | if (equal_Command(cmd, "tabs.changed")) { |
422 | MyDelegate *myDel = (MyDelegate *) [[NSApplication sharedApplication] delegate]; | 486 | MyDelegate *myDel = (MyDelegate *) [[NSApplication sharedApplication] delegate]; |
@@ -1,3 +1,25 @@ | |||
1 | /* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi> | ||
2 | |||
3 | Redistribution and use in source and binary forms, with or without | ||
4 | modification, are permitted provided that the following conditions are met: | ||
5 | |||
6 | 1. Redistributions of source code must retain the above copyright notice, this | ||
7 | list of conditions and the following disclaimer. | ||
8 | 2. Redistributions in binary form must reproduce the above copyright notice, | ||
9 | this list of conditions and the following disclaimer in the documentation | ||
10 | and/or other materials provided with the distribution. | ||
11 | |||
12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | ||
13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | ||
14 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||
15 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR | ||
16 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | ||
17 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | ||
18 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON | ||
19 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||
20 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | ||
21 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | ||
22 | |||
1 | #include <the_Foundation/commandline.h> | 23 | #include <the_Foundation/commandline.h> |
2 | #include <stdio.h> | 24 | #include <stdio.h> |
3 | #if defined (iPlatformMsys) | 25 | #if defined (iPlatformMsys) |
diff --git a/src/stb_image.h b/src/stb_image.h index 2857f05d..accef483 100644 --- a/src/stb_image.h +++ b/src/stb_image.h | |||
@@ -1,4 +1,4 @@ | |||
1 | /* stb_image - v2.25 - public domain image loader - http://nothings.org/stb | 1 | /* stb_image - v2.26 - public domain image loader - http://nothings.org/stb |
2 | no warranty implied; use at your own risk | 2 | no warranty implied; use at your own risk |
3 | 3 | ||
4 | Do this: | 4 | Do this: |
@@ -48,6 +48,7 @@ LICENSE | |||
48 | 48 | ||
49 | RECENT REVISION HISTORY: | 49 | RECENT REVISION HISTORY: |
50 | 50 | ||
51 | 2.26 (2020-07-13) many minor fixes | ||
51 | 2.25 (2020-02-02) fix warnings | 52 | 2.25 (2020-02-02) fix warnings |
52 | 2.24 (2020-02-02) fix warnings; thread-local failure_reason and flip_vertically | 53 | 2.24 (2020-02-02) fix warnings; thread-local failure_reason and flip_vertically |
53 | 2.23 (2019-08-11) fix clang static analysis warning | 54 | 2.23 (2019-08-11) fix clang static analysis warning |
@@ -93,22 +94,30 @@ RECENT REVISION HISTORY: | |||
93 | Carmelo J Fdez-Aguera | 94 | Carmelo J Fdez-Aguera |
94 | 95 | ||
95 | Bug & warning fixes | 96 | Bug & warning fixes |
96 | Marc LeBlanc David Woo Guillaume George Martins Mozeiko | 97 | Marc LeBlanc David Woo Guillaume George Martins Mozeiko |
97 | Christpher Lloyd Jerry Jansson Joseph Thomson Phil Jordan | 98 | Christpher Lloyd Jerry Jansson Joseph Thomson Blazej Dariusz Roszkowski |
98 | Dave Moore Roy Eltham Hayaki Saito Nathan Reed | 99 | Phil Jordan Dave Moore Roy Eltham |
99 | Won Chun Luke Graham Johan Duparc Nick Verigakis | 100 | Hayaki Saito Nathan Reed Won Chun |
100 | the Horde3D community Thomas Ruf Ronny Chevalier github:rlyeh | 101 | Luke Graham Johan Duparc Nick Verigakis the Horde3D community |
101 | Janez Zemva John Bartholomew Michal Cichon github:romigrou | 102 | Thomas Ruf Ronny Chevalier github:rlyeh |
102 | Jonathan Blow Ken Hamada Tero Hanninen github:svdijk | 103 | Janez Zemva John Bartholomew Michal Cichon github:romigrou |
103 | Laurent Gomila Cort Stratton Sergio Gonzalez github:snagar | 104 | Jonathan Blow Ken Hamada Tero Hanninen github:svdijk |
104 | Aruelien Pocheville Thibault Reuille Cass Everitt github:Zelex | 105 | Laurent Gomila Cort Stratton github:snagar |
105 | Ryamond Barbiero Paul Du Bois Engin Manap github:grim210 | 106 | Aruelien Pocheville Sergio Gonzalez Thibault Reuille github:Zelex |
106 | Aldo Culquicondor Philipp Wiesemann Dale Weiler github:sammyhw | 107 | Cass Everitt Ryamond Barbiero github:grim210 |
107 | Oriol Ferrer Mesia Josh Tobin Matthew Gregan github:phprus | 108 | Paul Du Bois Engin Manap Aldo Culquicondor github:sammyhw |
108 | Julian Raschke Gregory Mullen Baldur Karlsson github:poppolopoppo | 109 | Philipp Wiesemann Dale Weiler Oriol Ferrer Mesia github:phprus |
109 | Christian Floisand Kevin Schmidt JR Smith github:darealshinji | 110 | Josh Tobin Matthew Gregan github:poppolopoppo |
110 | Brad Weinberger Matvey Cherevko github:Michaelangel007 | 111 | Julian Raschke Gregory Mullen Christian Floisand github:darealshinji |
111 | Blazej Dariusz Roszkowski Alexander Veselov | 112 | Baldur Karlsson Kevin Schmidt JR Smith github:Michaelangel007 |
113 | Brad Weinberger Matvey Cherevko [reserved] | ||
114 | Luca Sas Alexander Veselov Zack Middleton [reserved] | ||
115 | Ryan C. Gordon [reserved] [reserved] | ||
116 | DO NOT ADD YOUR NAME HERE | ||
117 | |||
118 | To add your name to the credits, pick a random blank space in the middle and fill it. | ||
119 | 80% of merge conflicts on stb PRs are due to people adding their name at the end | ||
120 | of the credits. | ||
112 | */ | 121 | */ |
113 | 122 | ||
114 | #ifndef STBI_INCLUDE_STB_IMAGE_H | 123 | #ifndef STBI_INCLUDE_STB_IMAGE_H |
@@ -318,7 +327,14 @@ RECENT REVISION HISTORY: | |||
318 | // - If you use STBI_NO_PNG (or _ONLY_ without PNG), and you still | 327 | // - If you use STBI_NO_PNG (or _ONLY_ without PNG), and you still |
319 | // want the zlib decoder to be available, #define STBI_SUPPORT_ZLIB | 328 | // want the zlib decoder to be available, #define STBI_SUPPORT_ZLIB |
320 | // | 329 | // |
321 | 330 | // - If you define STBI_MAX_DIMENSIONS, stb_image will reject images greater | |
331 | // than that size (in either width or height) without further processing. | ||
332 | // This is to let programs in the wild set an upper bound to prevent | ||
333 | // denial-of-service attacks on untrusted data, as one could generate a | ||
334 | // valid image of gigantic dimensions and force stb_image to allocate a | ||
335 | // huge block of memory and spend disproportionate time decoding it. By | ||
336 | // default this is set to (1 << 24), which is 16777216, but that's still | ||
337 | // very big. | ||
322 | 338 | ||
323 | #ifndef STBI_NO_STDIO | 339 | #ifndef STBI_NO_STDIO |
324 | #include <stdio.h> | 340 | #include <stdio.h> |
@@ -574,13 +590,19 @@ STBIDEF int stbi_zlib_decode_noheader_buffer(char *obuffer, int olen, const ch | |||
574 | #ifndef STBI_NO_THREAD_LOCALS | 590 | #ifndef STBI_NO_THREAD_LOCALS |
575 | #if defined(__cplusplus) && __cplusplus >= 201103L | 591 | #if defined(__cplusplus) && __cplusplus >= 201103L |
576 | #define STBI_THREAD_LOCAL thread_local | 592 | #define STBI_THREAD_LOCAL thread_local |
577 | #elif defined (__STDC_VERSION__) && __STDC_VERSION__ >= 201112L | 593 | #elif defined(__GNUC__) && __GNUC__ < 5 |
578 | #define STBI_THREAD_LOCAL _Thread_local | ||
579 | #elif defined(__GNUC__) | ||
580 | #define STBI_THREAD_LOCAL __thread | 594 | #define STBI_THREAD_LOCAL __thread |
581 | #elif defined(_MSC_VER) | 595 | #elif defined(_MSC_VER) |
582 | #define STBI_THREAD_LOCAL __declspec(thread) | 596 | #define STBI_THREAD_LOCAL __declspec(thread) |
583 | #endif | 597 | #elif defined (__STDC_VERSION__) && __STDC_VERSION__ >= 201112L && !defined(__STDC_NO_THREADS__) |
598 | #define STBI_THREAD_LOCAL _Thread_local | ||
599 | #endif | ||
600 | |||
601 | #ifndef STBI_THREAD_LOCAL | ||
602 | #if defined(__GNUC__) | ||
603 | #define STBI_THREAD_LOCAL __thread | ||
604 | #endif | ||
605 | #endif | ||
584 | #endif | 606 | #endif |
585 | 607 | ||
586 | #ifdef _MSC_VER | 608 | #ifdef _MSC_VER |
@@ -734,6 +756,10 @@ static int stbi__sse2_available(void) | |||
734 | #define STBI_SIMD_ALIGN(type, name) type name | 756 | #define STBI_SIMD_ALIGN(type, name) type name |
735 | #endif | 757 | #endif |
736 | 758 | ||
759 | #ifndef STBI_MAX_DIMENSIONS | ||
760 | #define STBI_MAX_DIMENSIONS (1 << 24) | ||
761 | #endif | ||
762 | |||
737 | /////////////////////////////////////////////// | 763 | /////////////////////////////////////////////// |
738 | // | 764 | // |
739 | // stbi__context struct and start_xxx functions | 765 | // stbi__context struct and start_xxx functions |
@@ -751,6 +777,7 @@ typedef struct | |||
751 | int read_from_callbacks; | 777 | int read_from_callbacks; |
752 | int buflen; | 778 | int buflen; |
753 | stbi_uc buffer_start[128]; | 779 | stbi_uc buffer_start[128]; |
780 | int callback_already_read; | ||
754 | 781 | ||
755 | stbi_uc *img_buffer, *img_buffer_end; | 782 | stbi_uc *img_buffer, *img_buffer_end; |
756 | stbi_uc *img_buffer_original, *img_buffer_original_end; | 783 | stbi_uc *img_buffer_original, *img_buffer_original_end; |
@@ -764,6 +791,7 @@ static void stbi__start_mem(stbi__context *s, stbi_uc const *buffer, int len) | |||
764 | { | 791 | { |
765 | s->io.read = NULL; | 792 | s->io.read = NULL; |
766 | s->read_from_callbacks = 0; | 793 | s->read_from_callbacks = 0; |
794 | s->callback_already_read = 0; | ||
767 | s->img_buffer = s->img_buffer_original = (stbi_uc *) buffer; | 795 | s->img_buffer = s->img_buffer_original = (stbi_uc *) buffer; |
768 | s->img_buffer_end = s->img_buffer_original_end = (stbi_uc *) buffer+len; | 796 | s->img_buffer_end = s->img_buffer_original_end = (stbi_uc *) buffer+len; |
769 | } | 797 | } |
@@ -775,7 +803,8 @@ static void stbi__start_callbacks(stbi__context *s, stbi_io_callbacks *c, void * | |||
775 | s->io_user_data = user; | 803 | s->io_user_data = user; |
776 | s->buflen = sizeof(s->buffer_start); | 804 | s->buflen = sizeof(s->buffer_start); |
777 | s->read_from_callbacks = 1; | 805 | s->read_from_callbacks = 1; |
778 | s->img_buffer_original = s->buffer_start; | 806 | s->callback_already_read = 0; |
807 | s->img_buffer = s->img_buffer_original = s->buffer_start; | ||
779 | stbi__refill_buffer(s); | 808 | stbi__refill_buffer(s); |
780 | s->img_buffer_original_end = s->img_buffer_end; | 809 | s->img_buffer_original_end = s->img_buffer_end; |
781 | } | 810 | } |
@@ -789,12 +818,17 @@ static int stbi__stdio_read(void *user, char *data, int size) | |||
789 | 818 | ||
790 | static void stbi__stdio_skip(void *user, int n) | 819 | static void stbi__stdio_skip(void *user, int n) |
791 | { | 820 | { |
821 | int ch; | ||
792 | fseek((FILE*) user, n, SEEK_CUR); | 822 | fseek((FILE*) user, n, SEEK_CUR); |
823 | ch = fgetc((FILE*) user); /* have to read a byte to reset feof()'s flag */ | ||
824 | if (ch != EOF) { | ||
825 | ungetc(ch, (FILE *) user); /* push byte back onto stream if valid. */ | ||
826 | } | ||
793 | } | 827 | } |
794 | 828 | ||
795 | static int stbi__stdio_eof(void *user) | 829 | static int stbi__stdio_eof(void *user) |
796 | { | 830 | { |
797 | return feof((FILE*) user); | 831 | return feof((FILE*) user) || ferror((FILE *) user); |
798 | } | 832 | } |
799 | 833 | ||
800 | static stbi_io_callbacks stbi__stdio_callbacks = | 834 | static stbi_io_callbacks stbi__stdio_callbacks = |
@@ -1171,8 +1205,10 @@ static unsigned char *stbi__load_and_postprocess_8bit(stbi__context *s, int *x, | |||
1171 | if (result == NULL) | 1205 | if (result == NULL) |
1172 | return NULL; | 1206 | return NULL; |
1173 | 1207 | ||
1208 | // it is the responsibility of the loaders to make sure we get either 8 or 16 bit. | ||
1209 | STBI_ASSERT(ri.bits_per_channel == 8 || ri.bits_per_channel == 16); | ||
1210 | |||
1174 | if (ri.bits_per_channel != 8) { | 1211 | if (ri.bits_per_channel != 8) { |
1175 | STBI_ASSERT(ri.bits_per_channel == 16); | ||
1176 | result = stbi__convert_16_to_8((stbi__uint16 *) result, *x, *y, req_comp == 0 ? *comp : req_comp); | 1212 | result = stbi__convert_16_to_8((stbi__uint16 *) result, *x, *y, req_comp == 0 ? *comp : req_comp); |
1177 | ri.bits_per_channel = 8; | 1213 | ri.bits_per_channel = 8; |
1178 | } | 1214 | } |
@@ -1195,8 +1231,10 @@ static stbi__uint16 *stbi__load_and_postprocess_16bit(stbi__context *s, int *x, | |||
1195 | if (result == NULL) | 1231 | if (result == NULL) |
1196 | return NULL; | 1232 | return NULL; |
1197 | 1233 | ||
1234 | // it is the responsibility of the loaders to make sure we get either 8 or 16 bit. | ||
1235 | STBI_ASSERT(ri.bits_per_channel == 8 || ri.bits_per_channel == 16); | ||
1236 | |||
1198 | if (ri.bits_per_channel != 16) { | 1237 | if (ri.bits_per_channel != 16) { |
1199 | STBI_ASSERT(ri.bits_per_channel == 8); | ||
1200 | result = stbi__convert_8_to_16((stbi_uc *) result, *x, *y, req_comp == 0 ? *comp : req_comp); | 1238 | result = stbi__convert_8_to_16((stbi_uc *) result, *x, *y, req_comp == 0 ? *comp : req_comp); |
1201 | ri.bits_per_channel = 16; | 1239 | ri.bits_per_channel = 16; |
1202 | } | 1240 | } |
@@ -1499,6 +1537,7 @@ enum | |||
1499 | static void stbi__refill_buffer(stbi__context *s) | 1537 | static void stbi__refill_buffer(stbi__context *s) |
1500 | { | 1538 | { |
1501 | int n = (s->io.read)(s->io_user_data,(char*)s->buffer_start,s->buflen); | 1539 | int n = (s->io.read)(s->io_user_data,(char*)s->buffer_start,s->buflen); |
1540 | s->callback_already_read += (int) (s->img_buffer - s->img_buffer_original); | ||
1502 | if (n == 0) { | 1541 | if (n == 0) { |
1503 | // at end of file, treat same as if from memory, but need to handle case | 1542 | // at end of file, treat same as if from memory, but need to handle case |
1504 | // where s->img_buffer isn't pointing to safe memory, e.g. 0-byte file | 1543 | // where s->img_buffer isn't pointing to safe memory, e.g. 0-byte file |
@@ -1544,6 +1583,7 @@ stbi_inline static int stbi__at_eof(stbi__context *s) | |||
1544 | #else | 1583 | #else |
1545 | static void stbi__skip(stbi__context *s, int n) | 1584 | static void stbi__skip(stbi__context *s, int n) |
1546 | { | 1585 | { |
1586 | if (n == 0) return; // already there! | ||
1547 | if (n < 0) { | 1587 | if (n < 0) { |
1548 | s->img_buffer = s->img_buffer_end; | 1588 | s->img_buffer = s->img_buffer_end; |
1549 | return; | 1589 | return; |
@@ -1686,7 +1726,7 @@ static unsigned char *stbi__convert_format(unsigned char *data, int img_n, int r | |||
1686 | STBI__CASE(4,1) { dest[0]=stbi__compute_y(src[0],src[1],src[2]); } break; | 1726 | STBI__CASE(4,1) { dest[0]=stbi__compute_y(src[0],src[1],src[2]); } break; |
1687 | STBI__CASE(4,2) { dest[0]=stbi__compute_y(src[0],src[1],src[2]); dest[1] = src[3]; } break; | 1727 | STBI__CASE(4,2) { dest[0]=stbi__compute_y(src[0],src[1],src[2]); dest[1] = src[3]; } break; |
1688 | STBI__CASE(4,3) { dest[0]=src[0];dest[1]=src[1];dest[2]=src[2]; } break; | 1728 | STBI__CASE(4,3) { dest[0]=src[0];dest[1]=src[1];dest[2]=src[2]; } break; |
1689 | default: STBI_ASSERT(0); | 1729 | default: STBI_ASSERT(0); STBI_FREE(data); STBI_FREE(good); return stbi__errpuc("unsupported", "Unsupported format conversion"); |
1690 | } | 1730 | } |
1691 | #undef STBI__CASE | 1731 | #undef STBI__CASE |
1692 | } | 1732 | } |
@@ -1743,7 +1783,7 @@ static stbi__uint16 *stbi__convert_format16(stbi__uint16 *data, int img_n, int r | |||
1743 | STBI__CASE(4,1) { dest[0]=stbi__compute_y_16(src[0],src[1],src[2]); } break; | 1783 | STBI__CASE(4,1) { dest[0]=stbi__compute_y_16(src[0],src[1],src[2]); } break; |
1744 | STBI__CASE(4,2) { dest[0]=stbi__compute_y_16(src[0],src[1],src[2]); dest[1] = src[3]; } break; | 1784 | STBI__CASE(4,2) { dest[0]=stbi__compute_y_16(src[0],src[1],src[2]); dest[1] = src[3]; } break; |
1745 | STBI__CASE(4,3) { dest[0]=src[0];dest[1]=src[1];dest[2]=src[2]; } break; | 1785 | STBI__CASE(4,3) { dest[0]=src[0];dest[1]=src[1];dest[2]=src[2]; } break; |
1746 | default: STBI_ASSERT(0); | 1786 | default: STBI_ASSERT(0); STBI_FREE(data); STBI_FREE(good); return (stbi__uint16*) stbi__errpuc("unsupported", "Unsupported format conversion"); |
1747 | } | 1787 | } |
1748 | #undef STBI__CASE | 1788 | #undef STBI__CASE |
1749 | } | 1789 | } |
@@ -2052,7 +2092,7 @@ stbi_inline static int stbi__extend_receive(stbi__jpeg *j, int n) | |||
2052 | 2092 | ||
2053 | sgn = (stbi__int32)j->code_buffer >> 31; // sign bit is always in MSB | 2093 | sgn = (stbi__int32)j->code_buffer >> 31; // sign bit is always in MSB |
2054 | k = stbi_lrot(j->code_buffer, n); | 2094 | k = stbi_lrot(j->code_buffer, n); |
2055 | STBI_ASSERT(n >= 0 && n < (int) (sizeof(stbi__bmask)/sizeof(*stbi__bmask))); | 2095 | if (n < 0 || n >= (int) (sizeof(stbi__bmask)/sizeof(*stbi__bmask))) return 0; |
2056 | j->code_buffer = k & ~stbi__bmask[n]; | 2096 | j->code_buffer = k & ~stbi__bmask[n]; |
2057 | k &= stbi__bmask[n]; | 2097 | k &= stbi__bmask[n]; |
2058 | j->code_bits -= n; | 2098 | j->code_bits -= n; |
@@ -2163,6 +2203,7 @@ static int stbi__jpeg_decode_block_prog_dc(stbi__jpeg *j, short data[64], stbi__ | |||
2163 | // first scan for DC coefficient, must be first | 2203 | // first scan for DC coefficient, must be first |
2164 | memset(data,0,64*sizeof(data[0])); // 0 all the ac values now | 2204 | memset(data,0,64*sizeof(data[0])); // 0 all the ac values now |
2165 | t = stbi__jpeg_huff_decode(j, hdc); | 2205 | t = stbi__jpeg_huff_decode(j, hdc); |
2206 | if (t == -1) return stbi__err("can't merge dc and ac", "Corrupt JPEG"); | ||
2166 | diff = t ? stbi__extend_receive(j, t) : 0; | 2207 | diff = t ? stbi__extend_receive(j, t) : 0; |
2167 | 2208 | ||
2168 | dc = j->img_comp[b].dc_pred + diff; | 2209 | dc = j->img_comp[b].dc_pred + diff; |
@@ -3153,6 +3194,8 @@ static int stbi__process_frame_header(stbi__jpeg *z, int scan) | |||
3153 | p = stbi__get8(s); if (p != 8) return stbi__err("only 8-bit","JPEG format not supported: 8-bit only"); // JPEG baseline | 3194 | p = stbi__get8(s); if (p != 8) return stbi__err("only 8-bit","JPEG format not supported: 8-bit only"); // JPEG baseline |
3154 | s->img_y = stbi__get16be(s); if (s->img_y == 0) return stbi__err("no header height", "JPEG format not supported: delayed height"); // Legal, but we don't handle it--but neither does IJG | 3195 | s->img_y = stbi__get16be(s); if (s->img_y == 0) return stbi__err("no header height", "JPEG format not supported: delayed height"); // Legal, but we don't handle it--but neither does IJG |
3155 | s->img_x = stbi__get16be(s); if (s->img_x == 0) return stbi__err("0 width","Corrupt JPEG"); // JPEG requires | 3196 | s->img_x = stbi__get16be(s); if (s->img_x == 0) return stbi__err("0 width","Corrupt JPEG"); // JPEG requires |
3197 | if (s->img_y > STBI_MAX_DIMENSIONS) return stbi__err("too large","Very large image (corrupt?)"); | ||
3198 | if (s->img_x > STBI_MAX_DIMENSIONS) return stbi__err("too large","Very large image (corrupt?)"); | ||
3156 | c = stbi__get8(s); | 3199 | c = stbi__get8(s); |
3157 | if (c != 3 && c != 1 && c != 4) return stbi__err("bad component count","Corrupt JPEG"); | 3200 | if (c != 3 && c != 1 && c != 4) return stbi__err("bad component count","Corrupt JPEG"); |
3158 | s->img_n = c; | 3201 | s->img_n = c; |
@@ -4033,16 +4076,23 @@ typedef struct | |||
4033 | stbi__zhuffman z_length, z_distance; | 4076 | stbi__zhuffman z_length, z_distance; |
4034 | } stbi__zbuf; | 4077 | } stbi__zbuf; |
4035 | 4078 | ||
4079 | stbi_inline static int stbi__zeof(stbi__zbuf *z) | ||
4080 | { | ||
4081 | return (z->zbuffer >= z->zbuffer_end); | ||
4082 | } | ||
4083 | |||
4036 | stbi_inline static stbi_uc stbi__zget8(stbi__zbuf *z) | 4084 | stbi_inline static stbi_uc stbi__zget8(stbi__zbuf *z) |
4037 | { | 4085 | { |
4038 | if (z->zbuffer >= z->zbuffer_end) return 0; | 4086 | return stbi__zeof(z) ? 0 : *z->zbuffer++; |
4039 | return *z->zbuffer++; | ||
4040 | } | 4087 | } |
4041 | 4088 | ||
4042 | static void stbi__fill_bits(stbi__zbuf *z) | 4089 | static void stbi__fill_bits(stbi__zbuf *z) |
4043 | { | 4090 | { |
4044 | do { | 4091 | do { |
4045 | STBI_ASSERT(z->code_buffer < (1U << z->num_bits)); | 4092 | if (z->code_buffer >= (1U << z->num_bits)) { |
4093 | z->zbuffer = z->zbuffer_end; /* treat this as EOF so we fail. */ | ||
4094 | return; | ||
4095 | } | ||
4046 | z->code_buffer |= (unsigned int) stbi__zget8(z) << z->num_bits; | 4096 | z->code_buffer |= (unsigned int) stbi__zget8(z) << z->num_bits; |
4047 | z->num_bits += 8; | 4097 | z->num_bits += 8; |
4048 | } while (z->num_bits <= 24); | 4098 | } while (z->num_bits <= 24); |
@@ -4067,10 +4117,11 @@ static int stbi__zhuffman_decode_slowpath(stbi__zbuf *a, stbi__zhuffman *z) | |||
4067 | for (s=STBI__ZFAST_BITS+1; ; ++s) | 4117 | for (s=STBI__ZFAST_BITS+1; ; ++s) |
4068 | if (k < z->maxcode[s]) | 4118 | if (k < z->maxcode[s]) |
4069 | break; | 4119 | break; |
4070 | if (s == 16) return -1; // invalid code! | 4120 | if (s >= 16) return -1; // invalid code! |
4071 | // code size is s, so: | 4121 | // code size is s, so: |
4072 | b = (k >> (16-s)) - z->firstcode[s] + z->firstsymbol[s]; | 4122 | b = (k >> (16-s)) - z->firstcode[s] + z->firstsymbol[s]; |
4073 | STBI_ASSERT(z->size[b] == s); | 4123 | if (b >= sizeof (z->size)) return -1; // some data was corrupt somewhere! |
4124 | if (z->size[b] != s) return -1; // was originally an assert, but report failure instead. | ||
4074 | a->code_buffer >>= s; | 4125 | a->code_buffer >>= s; |
4075 | a->num_bits -= s; | 4126 | a->num_bits -= s; |
4076 | return z->value[b]; | 4127 | return z->value[b]; |
@@ -4079,7 +4130,12 @@ static int stbi__zhuffman_decode_slowpath(stbi__zbuf *a, stbi__zhuffman *z) | |||
4079 | stbi_inline static int stbi__zhuffman_decode(stbi__zbuf *a, stbi__zhuffman *z) | 4130 | stbi_inline static int stbi__zhuffman_decode(stbi__zbuf *a, stbi__zhuffman *z) |
4080 | { | 4131 | { |
4081 | int b,s; | 4132 | int b,s; |
4082 | if (a->num_bits < 16) stbi__fill_bits(a); | 4133 | if (a->num_bits < 16) { |
4134 | if (stbi__zeof(a)) { | ||
4135 | return -1; /* report error for unexpected end of data. */ | ||
4136 | } | ||
4137 | stbi__fill_bits(a); | ||
4138 | } | ||
4083 | b = z->fast[a->code_buffer & STBI__ZFAST_MASK]; | 4139 | b = z->fast[a->code_buffer & STBI__ZFAST_MASK]; |
4084 | if (b) { | 4140 | if (b) { |
4085 | s = b >> 9; | 4141 | s = b >> 9; |
@@ -4093,13 +4149,16 @@ stbi_inline static int stbi__zhuffman_decode(stbi__zbuf *a, stbi__zhuffman *z) | |||
4093 | static int stbi__zexpand(stbi__zbuf *z, char *zout, int n) // need to make room for n bytes | 4149 | static int stbi__zexpand(stbi__zbuf *z, char *zout, int n) // need to make room for n bytes |
4094 | { | 4150 | { |
4095 | char *q; | 4151 | char *q; |
4096 | int cur, limit, old_limit; | 4152 | unsigned int cur, limit, old_limit; |
4097 | z->zout = zout; | 4153 | z->zout = zout; |
4098 | if (!z->z_expandable) return stbi__err("output buffer limit","Corrupt PNG"); | 4154 | if (!z->z_expandable) return stbi__err("output buffer limit","Corrupt PNG"); |
4099 | cur = (int) (z->zout - z->zout_start); | 4155 | cur = (unsigned int) (z->zout - z->zout_start); |
4100 | limit = old_limit = (int) (z->zout_end - z->zout_start); | 4156 | limit = old_limit = (unsigned) (z->zout_end - z->zout_start); |
4101 | while (cur + n > limit) | 4157 | if (UINT_MAX - cur < (unsigned) n) return stbi__err("outofmem", "Out of memory"); |
4158 | while (cur + n > limit) { | ||
4159 | if(limit > UINT_MAX / 2) return stbi__err("outofmem", "Out of memory"); | ||
4102 | limit *= 2; | 4160 | limit *= 2; |
4161 | } | ||
4103 | q = (char *) STBI_REALLOC_SIZED(z->zout_start, old_limit, limit); | 4162 | q = (char *) STBI_REALLOC_SIZED(z->zout_start, old_limit, limit); |
4104 | STBI_NOTUSED(old_limit); | 4163 | STBI_NOTUSED(old_limit); |
4105 | if (q == NULL) return stbi__err("outofmem", "Out of memory"); | 4164 | if (q == NULL) return stbi__err("outofmem", "Out of memory"); |
@@ -4197,11 +4256,12 @@ static int stbi__compute_huffman_codes(stbi__zbuf *a) | |||
4197 | c = stbi__zreceive(a,2)+3; | 4256 | c = stbi__zreceive(a,2)+3; |
4198 | if (n == 0) return stbi__err("bad codelengths", "Corrupt PNG"); | 4257 | if (n == 0) return stbi__err("bad codelengths", "Corrupt PNG"); |
4199 | fill = lencodes[n-1]; | 4258 | fill = lencodes[n-1]; |
4200 | } else if (c == 17) | 4259 | } else if (c == 17) { |
4201 | c = stbi__zreceive(a,3)+3; | 4260 | c = stbi__zreceive(a,3)+3; |
4202 | else { | 4261 | } else if (c == 18) { |
4203 | STBI_ASSERT(c == 18); | ||
4204 | c = stbi__zreceive(a,7)+11; | 4262 | c = stbi__zreceive(a,7)+11; |
4263 | } else { | ||
4264 | return stbi__err("bad codelengths", "Corrupt PNG"); | ||
4205 | } | 4265 | } |
4206 | if (ntot - n < c) return stbi__err("bad codelengths", "Corrupt PNG"); | 4266 | if (ntot - n < c) return stbi__err("bad codelengths", "Corrupt PNG"); |
4207 | memset(lencodes+n, fill, c); | 4267 | memset(lencodes+n, fill, c); |
@@ -4227,7 +4287,7 @@ static int stbi__parse_uncompressed_block(stbi__zbuf *a) | |||
4227 | a->code_buffer >>= 8; | 4287 | a->code_buffer >>= 8; |
4228 | a->num_bits -= 8; | 4288 | a->num_bits -= 8; |
4229 | } | 4289 | } |
4230 | STBI_ASSERT(a->num_bits == 0); | 4290 | if (a->num_bits < 0) return stbi__err("zlib corrupt","Corrupt PNG"); |
4231 | // now fill header the normal way | 4291 | // now fill header the normal way |
4232 | while (k < 4) | 4292 | while (k < 4) |
4233 | header[k++] = stbi__zget8(a); | 4293 | header[k++] = stbi__zget8(a); |
@@ -4249,6 +4309,7 @@ static int stbi__parse_zlib_header(stbi__zbuf *a) | |||
4249 | int cm = cmf & 15; | 4309 | int cm = cmf & 15; |
4250 | /* int cinfo = cmf >> 4; */ | 4310 | /* int cinfo = cmf >> 4; */ |
4251 | int flg = stbi__zget8(a); | 4311 | int flg = stbi__zget8(a); |
4312 | if (stbi__zeof(a)) return stbi__err("bad zlib header","Corrupt PNG"); // zlib spec | ||
4252 | if ((cmf*256+flg) % 31 != 0) return stbi__err("bad zlib header","Corrupt PNG"); // zlib spec | 4313 | if ((cmf*256+flg) % 31 != 0) return stbi__err("bad zlib header","Corrupt PNG"); // zlib spec |
4253 | if (flg & 32) return stbi__err("no preset dict","Corrupt PNG"); // preset dictionary not allowed in png | 4314 | if (flg & 32) return stbi__err("no preset dict","Corrupt PNG"); // preset dictionary not allowed in png |
4254 | if (cm != 8) return stbi__err("bad compression","Corrupt PNG"); // DEFLATE required for png | 4315 | if (cm != 8) return stbi__err("bad compression","Corrupt PNG"); // DEFLATE required for png |
@@ -4510,7 +4571,7 @@ static int stbi__create_png_image_raw(stbi__png *a, stbi_uc *raw, stbi__uint32 r | |||
4510 | return stbi__err("invalid filter","Corrupt PNG"); | 4571 | return stbi__err("invalid filter","Corrupt PNG"); |
4511 | 4572 | ||
4512 | if (depth < 8) { | 4573 | if (depth < 8) { |
4513 | STBI_ASSERT(img_width_bytes <= x); | 4574 | if (img_width_bytes > x) return stbi__err("invalid width","Corrupt PNG"); |
4514 | cur += x*out_n - img_width_bytes; // store output to the rightmost img_len bytes, so we can decode in place | 4575 | cur += x*out_n - img_width_bytes; // store output to the rightmost img_len bytes, so we can decode in place |
4515 | filter_bytes = 1; | 4576 | filter_bytes = 1; |
4516 | width = img_width_bytes; | 4577 | width = img_width_bytes; |
@@ -4905,8 +4966,10 @@ static int stbi__parse_png_file(stbi__png *z, int scan, int req_comp) | |||
4905 | if (!first) return stbi__err("multiple IHDR","Corrupt PNG"); | 4966 | if (!first) return stbi__err("multiple IHDR","Corrupt PNG"); |
4906 | first = 0; | 4967 | first = 0; |
4907 | if (c.length != 13) return stbi__err("bad IHDR len","Corrupt PNG"); | 4968 | if (c.length != 13) return stbi__err("bad IHDR len","Corrupt PNG"); |
4908 | s->img_x = stbi__get32be(s); if (s->img_x > (1 << 24)) return stbi__err("too large","Very large image (corrupt?)"); | 4969 | s->img_x = stbi__get32be(s); |
4909 | s->img_y = stbi__get32be(s); if (s->img_y > (1 << 24)) return stbi__err("too large","Very large image (corrupt?)"); | 4970 | s->img_y = stbi__get32be(s); |
4971 | if (s->img_y > STBI_MAX_DIMENSIONS) return stbi__err("too large","Very large image (corrupt?)"); | ||
4972 | if (s->img_x > STBI_MAX_DIMENSIONS) return stbi__err("too large","Very large image (corrupt?)"); | ||
4910 | z->depth = stbi__get8(s); if (z->depth != 1 && z->depth != 2 && z->depth != 4 && z->depth != 8 && z->depth != 16) return stbi__err("1/2/4/8/16-bit only","PNG not supported: 1/2/4/8/16-bit only"); | 4973 | z->depth = stbi__get8(s); if (z->depth != 1 && z->depth != 2 && z->depth != 4 && z->depth != 8 && z->depth != 16) return stbi__err("1/2/4/8/16-bit only","PNG not supported: 1/2/4/8/16-bit only"); |
4911 | color = stbi__get8(s); if (color > 6) return stbi__err("bad ctype","Corrupt PNG"); | 4974 | color = stbi__get8(s); if (color > 6) return stbi__err("bad ctype","Corrupt PNG"); |
4912 | if (color == 3 && z->depth == 16) return stbi__err("bad ctype","Corrupt PNG"); | 4975 | if (color == 3 && z->depth == 16) return stbi__err("bad ctype","Corrupt PNG"); |
@@ -5055,10 +5118,12 @@ static void *stbi__do_png(stbi__png *p, int *x, int *y, int *n, int req_comp, st | |||
5055 | void *result=NULL; | 5118 | void *result=NULL; |
5056 | if (req_comp < 0 || req_comp > 4) return stbi__errpuc("bad req_comp", "Internal error"); | 5119 | if (req_comp < 0 || req_comp > 4) return stbi__errpuc("bad req_comp", "Internal error"); |
5057 | if (stbi__parse_png_file(p, STBI__SCAN_load, req_comp)) { | 5120 | if (stbi__parse_png_file(p, STBI__SCAN_load, req_comp)) { |
5058 | if (p->depth < 8) | 5121 | if (p->depth <= 8) |
5059 | ri->bits_per_channel = 8; | 5122 | ri->bits_per_channel = 8; |
5123 | else if (p->depth == 16) | ||
5124 | ri->bits_per_channel = 16; | ||
5060 | else | 5125 | else |
5061 | ri->bits_per_channel = p->depth; | 5126 | return stbi__errpuc("bad bits_per_channel", "PNG not supported: unsupported color depth"); |
5062 | result = p->out; | 5127 | result = p->out; |
5063 | p->out = NULL; | 5128 | p->out = NULL; |
5064 | if (req_comp && req_comp != p->s->img_out_n) { | 5129 | if (req_comp && req_comp != p->s->img_out_n) { |
@@ -5219,6 +5284,8 @@ static void *stbi__bmp_parse_header(stbi__context *s, stbi__bmp_data *info) | |||
5219 | info->mr = info->mg = info->mb = info->ma = 0; | 5284 | info->mr = info->mg = info->mb = info->ma = 0; |
5220 | info->extra_read = 14; | 5285 | info->extra_read = 14; |
5221 | 5286 | ||
5287 | if (info->offset < 0) return stbi__errpuc("bad BMP", "bad BMP"); | ||
5288 | |||
5222 | if (hsz != 12 && hsz != 40 && hsz != 56 && hsz != 108 && hsz != 124) return stbi__errpuc("unknown BMP", "BMP type not supported: unknown"); | 5289 | if (hsz != 12 && hsz != 40 && hsz != 56 && hsz != 108 && hsz != 124) return stbi__errpuc("unknown BMP", "BMP type not supported: unknown"); |
5223 | if (hsz == 12) { | 5290 | if (hsz == 12) { |
5224 | s->img_x = stbi__get16le(s); | 5291 | s->img_x = stbi__get16le(s); |
@@ -5310,6 +5377,9 @@ static void *stbi__bmp_load(stbi__context *s, int *x, int *y, int *comp, int req | |||
5310 | flip_vertically = ((int) s->img_y) > 0; | 5377 | flip_vertically = ((int) s->img_y) > 0; |
5311 | s->img_y = abs((int) s->img_y); | 5378 | s->img_y = abs((int) s->img_y); |
5312 | 5379 | ||
5380 | if (s->img_y > STBI_MAX_DIMENSIONS) return stbi__errpuc("too large","Very large image (corrupt?)"); | ||
5381 | if (s->img_x > STBI_MAX_DIMENSIONS) return stbi__errpuc("too large","Very large image (corrupt?)"); | ||
5382 | |||
5313 | mr = info.mr; | 5383 | mr = info.mr; |
5314 | mg = info.mg; | 5384 | mg = info.mg; |
5315 | mb = info.mb; | 5385 | mb = info.mb; |
@@ -5324,7 +5394,10 @@ static void *stbi__bmp_load(stbi__context *s, int *x, int *y, int *comp, int req | |||
5324 | psize = (info.offset - info.extra_read - info.hsz) >> 2; | 5394 | psize = (info.offset - info.extra_read - info.hsz) >> 2; |
5325 | } | 5395 | } |
5326 | if (psize == 0) { | 5396 | if (psize == 0) { |
5327 | STBI_ASSERT(info.offset == (s->img_buffer - s->buffer_start)); | 5397 | STBI_ASSERT(info.offset == s->callback_already_read + (int) (s->img_buffer - s->img_buffer_original)); |
5398 | if (info.offset != s->callback_already_read + (s->img_buffer - s->buffer_start)) { | ||
5399 | return stbi__errpuc("bad offset", "Corrupt BMP"); | ||
5400 | } | ||
5328 | } | 5401 | } |
5329 | 5402 | ||
5330 | if (info.bpp == 24 && ma == 0xff000000) | 5403 | if (info.bpp == 24 && ma == 0xff000000) |
@@ -5419,6 +5492,7 @@ static void *stbi__bmp_load(stbi__context *s, int *x, int *y, int *comp, int req | |||
5419 | gshift = stbi__high_bit(mg)-7; gcount = stbi__bitcount(mg); | 5492 | gshift = stbi__high_bit(mg)-7; gcount = stbi__bitcount(mg); |
5420 | bshift = stbi__high_bit(mb)-7; bcount = stbi__bitcount(mb); | 5493 | bshift = stbi__high_bit(mb)-7; bcount = stbi__bitcount(mb); |
5421 | ashift = stbi__high_bit(ma)-7; acount = stbi__bitcount(ma); | 5494 | ashift = stbi__high_bit(ma)-7; acount = stbi__bitcount(ma); |
5495 | if (rcount > 8 || gcount > 8 || bcount > 8 || acount > 8) { STBI_FREE(out); return stbi__errpuc("bad masks", "Corrupt BMP"); } | ||
5422 | } | 5496 | } |
5423 | for (j=0; j < (int) s->img_y; ++j) { | 5497 | for (j=0; j < (int) s->img_y; ++j) { |
5424 | if (easy) { | 5498 | if (easy) { |
@@ -5643,6 +5717,9 @@ static void *stbi__tga_load(stbi__context *s, int *x, int *y, int *comp, int req | |||
5643 | STBI_NOTUSED(tga_x_origin); // @TODO | 5717 | STBI_NOTUSED(tga_x_origin); // @TODO |
5644 | STBI_NOTUSED(tga_y_origin); // @TODO | 5718 | STBI_NOTUSED(tga_y_origin); // @TODO |
5645 | 5719 | ||
5720 | if (tga_height > STBI_MAX_DIMENSIONS) return stbi__errpuc("too large","Very large image (corrupt?)"); | ||
5721 | if (tga_width > STBI_MAX_DIMENSIONS) return stbi__errpuc("too large","Very large image (corrupt?)"); | ||
5722 | |||
5646 | // do a tiny bit of precessing | 5723 | // do a tiny bit of precessing |
5647 | if ( tga_image_type >= 8 ) | 5724 | if ( tga_image_type >= 8 ) |
5648 | { | 5725 | { |
@@ -5682,6 +5759,11 @@ static void *stbi__tga_load(stbi__context *s, int *x, int *y, int *comp, int req | |||
5682 | // do I need to load a palette? | 5759 | // do I need to load a palette? |
5683 | if ( tga_indexed) | 5760 | if ( tga_indexed) |
5684 | { | 5761 | { |
5762 | if (tga_palette_len == 0) { /* you have to have at least one entry! */ | ||
5763 | STBI_FREE(tga_data); | ||
5764 | return stbi__errpuc("bad palette", "Corrupt TGA"); | ||
5765 | } | ||
5766 | |||
5685 | // any data to skip? (offset usually = 0) | 5767 | // any data to skip? (offset usually = 0) |
5686 | stbi__skip(s, tga_palette_start ); | 5768 | stbi__skip(s, tga_palette_start ); |
5687 | // load the palette | 5769 | // load the palette |
@@ -5890,6 +5972,9 @@ static void *stbi__psd_load(stbi__context *s, int *x, int *y, int *comp, int req | |||
5890 | h = stbi__get32be(s); | 5972 | h = stbi__get32be(s); |
5891 | w = stbi__get32be(s); | 5973 | w = stbi__get32be(s); |
5892 | 5974 | ||
5975 | if (h > STBI_MAX_DIMENSIONS) return stbi__errpuc("too large","Very large image (corrupt?)"); | ||
5976 | if (w > STBI_MAX_DIMENSIONS) return stbi__errpuc("too large","Very large image (corrupt?)"); | ||
5977 | |||
5893 | // Make sure the depth is 8 bits. | 5978 | // Make sure the depth is 8 bits. |
5894 | bitdepth = stbi__get16be(s); | 5979 | bitdepth = stbi__get16be(s); |
5895 | if (bitdepth != 8 && bitdepth != 16) | 5980 | if (bitdepth != 8 && bitdepth != 16) |
@@ -6244,6 +6329,10 @@ static void *stbi__pic_load(stbi__context *s,int *px,int *py,int *comp,int req_c | |||
6244 | 6329 | ||
6245 | x = stbi__get16be(s); | 6330 | x = stbi__get16be(s); |
6246 | y = stbi__get16be(s); | 6331 | y = stbi__get16be(s); |
6332 | |||
6333 | if (y > STBI_MAX_DIMENSIONS) return stbi__errpuc("too large","Very large image (corrupt?)"); | ||
6334 | if (x > STBI_MAX_DIMENSIONS) return stbi__errpuc("too large","Very large image (corrupt?)"); | ||
6335 | |||
6247 | if (stbi__at_eof(s)) return stbi__errpuc("bad file","file too short (pic header)"); | 6336 | if (stbi__at_eof(s)) return stbi__errpuc("bad file","file too short (pic header)"); |
6248 | if (!stbi__mad3sizes_valid(x, y, 4, 0)) return stbi__errpuc("too large", "PIC image too large to decode"); | 6337 | if (!stbi__mad3sizes_valid(x, y, 4, 0)) return stbi__errpuc("too large", "PIC image too large to decode"); |
6249 | 6338 | ||
@@ -6352,6 +6441,9 @@ static int stbi__gif_header(stbi__context *s, stbi__gif *g, int *comp, int is_in | |||
6352 | g->ratio = stbi__get8(s); | 6441 | g->ratio = stbi__get8(s); |
6353 | g->transparent = -1; | 6442 | g->transparent = -1; |
6354 | 6443 | ||
6444 | if (g->w > STBI_MAX_DIMENSIONS) return stbi__err("too large","Very large image (corrupt?)"); | ||
6445 | if (g->h > STBI_MAX_DIMENSIONS) return stbi__err("too large","Very large image (corrupt?)"); | ||
6446 | |||
6355 | if (comp != 0) *comp = 4; // can't actually tell whether it's 3 or 4 until we parse the comments | 6447 | if (comp != 0) *comp = 4; // can't actually tell whether it's 3 or 4 until we parse the comments |
6356 | 6448 | ||
6357 | if (is_info) return 1; | 6449 | if (is_info) return 1; |
@@ -6529,7 +6621,7 @@ static stbi_uc *stbi__gif_load_next(stbi__context *s, stbi__gif *g, int *comp, i | |||
6529 | memset(g->history, 0x00, pcount); // pixels that were affected previous frame | 6621 | memset(g->history, 0x00, pcount); // pixels that were affected previous frame |
6530 | first_frame = 1; | 6622 | first_frame = 1; |
6531 | } else { | 6623 | } else { |
6532 | // second frame - how do we dispoase of the previous one? | 6624 | // second frame - how do we dispose of the previous one? |
6533 | dispose = (g->eflags & 0x1C) >> 2; | 6625 | dispose = (g->eflags & 0x1C) >> 2; |
6534 | pcount = g->w * g->h; | 6626 | pcount = g->w * g->h; |
6535 | 6627 | ||
@@ -6683,6 +6775,8 @@ static void *stbi__load_gif_main(stbi__context *s, int **delays, int *x, int *y, | |||
6683 | stbi_uc *two_back = 0; | 6775 | stbi_uc *two_back = 0; |
6684 | stbi__gif g; | 6776 | stbi__gif g; |
6685 | int stride; | 6777 | int stride; |
6778 | int out_size = 0; | ||
6779 | int delays_size = 0; | ||
6686 | memset(&g, 0, sizeof(g)); | 6780 | memset(&g, 0, sizeof(g)); |
6687 | if (delays) { | 6781 | if (delays) { |
6688 | *delays = 0; | 6782 | *delays = 0; |
@@ -6699,22 +6793,28 @@ static void *stbi__load_gif_main(stbi__context *s, int **delays, int *x, int *y, | |||
6699 | stride = g.w * g.h * 4; | 6793 | stride = g.w * g.h * 4; |
6700 | 6794 | ||
6701 | if (out) { | 6795 | if (out) { |
6702 | void *tmp = (stbi_uc*) STBI_REALLOC( out, layers * stride ); | 6796 | void *tmp = (stbi_uc*) STBI_REALLOC_SIZED( out, out_size, layers * stride ); |
6703 | if (NULL == tmp) { | 6797 | if (NULL == tmp) { |
6704 | STBI_FREE(g.out); | 6798 | STBI_FREE(g.out); |
6705 | STBI_FREE(g.history); | 6799 | STBI_FREE(g.history); |
6706 | STBI_FREE(g.background); | 6800 | STBI_FREE(g.background); |
6707 | return stbi__errpuc("outofmem", "Out of memory"); | 6801 | return stbi__errpuc("outofmem", "Out of memory"); |
6708 | } | 6802 | } |
6709 | else | 6803 | else { |
6710 | out = (stbi_uc*) tmp; | 6804 | out = (stbi_uc*) tmp; |
6805 | out_size = layers * stride; | ||
6806 | } | ||
6807 | |||
6711 | if (delays) { | 6808 | if (delays) { |
6712 | *delays = (int*) STBI_REALLOC( *delays, sizeof(int) * layers ); | 6809 | *delays = (int*) STBI_REALLOC_SIZED( *delays, delays_size, sizeof(int) * layers ); |
6810 | delays_size = layers * sizeof(int); | ||
6713 | } | 6811 | } |
6714 | } else { | 6812 | } else { |
6715 | out = (stbi_uc*)stbi__malloc( layers * stride ); | 6813 | out = (stbi_uc*)stbi__malloc( layers * stride ); |
6814 | out_size = layers * stride; | ||
6716 | if (delays) { | 6815 | if (delays) { |
6717 | *delays = (int*) stbi__malloc( layers * sizeof(int) ); | 6816 | *delays = (int*) stbi__malloc( layers * sizeof(int) ); |
6817 | delays_size = layers * sizeof(int); | ||
6718 | } | 6818 | } |
6719 | } | 6819 | } |
6720 | memcpy( out + ((layers - 1) * stride), u, stride ); | 6820 | memcpy( out + ((layers - 1) * stride), u, stride ); |
@@ -6893,6 +6993,9 @@ static float *stbi__hdr_load(stbi__context *s, int *x, int *y, int *comp, int re | |||
6893 | token += 3; | 6993 | token += 3; |
6894 | width = (int) strtol(token, NULL, 10); | 6994 | width = (int) strtol(token, NULL, 10); |
6895 | 6995 | ||
6996 | if (height > STBI_MAX_DIMENSIONS) return stbi__errpf("too large","Very large image (corrupt?)"); | ||
6997 | if (width > STBI_MAX_DIMENSIONS) return stbi__errpf("too large","Very large image (corrupt?)"); | ||
6998 | |||
6896 | *x = width; | 6999 | *x = width; |
6897 | *y = height; | 7000 | *y = height; |
6898 | 7001 | ||
@@ -7207,6 +7310,9 @@ static void *stbi__pnm_load(stbi__context *s, int *x, int *y, int *comp, int req | |||
7207 | if (!stbi__pnm_info(s, (int *)&s->img_x, (int *)&s->img_y, (int *)&s->img_n)) | 7310 | if (!stbi__pnm_info(s, (int *)&s->img_x, (int *)&s->img_y, (int *)&s->img_n)) |
7208 | return 0; | 7311 | return 0; |
7209 | 7312 | ||
7313 | if (s->img_y > STBI_MAX_DIMENSIONS) return stbi__errpuc("too large","Very large image (corrupt?)"); | ||
7314 | if (s->img_x > STBI_MAX_DIMENSIONS) return stbi__errpuc("too large","Very large image (corrupt?)"); | ||
7315 | |||
7210 | *x = s->img_x; | 7316 | *x = s->img_x; |
7211 | *y = s->img_y; | 7317 | *y = s->img_y; |
7212 | if (comp) *comp = s->img_n; | 7318 | if (comp) *comp = s->img_n; |
diff --git a/src/stb_truetype.h b/src/stb_truetype.h index 935a6de2..62595a15 100644 --- a/src/stb_truetype.h +++ b/src/stb_truetype.h | |||
@@ -53,8 +53,8 @@ | |||
53 | // Johan Duparc Thomas Fields | 53 | // Johan Duparc Thomas Fields |
54 | // Hou Qiming Derek Vinyard | 54 | // Hou Qiming Derek Vinyard |
55 | // Rob Loach Cort Stratton | 55 | // Rob Loach Cort Stratton |
56 | // Kenney Phillis Jr. Brian Costabile | 56 | // Kenney Phillis Jr. Brian Costabile |
57 | // Ken Voskuil (kaesve) | 57 | // Ken Voskuil (kaesve) |
58 | // | 58 | // |
59 | // VERSION HISTORY | 59 | // VERSION HISTORY |
60 | // | 60 | // |
diff --git a/src/ui/color.c b/src/ui/color.c index 7d6ab2c2..723e9805 100644 --- a/src/ui/color.c +++ b/src/ui/color.c | |||
@@ -1,3 +1,25 @@ | |||
1 | /* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi> | ||
2 | |||
3 | Redistribution and use in source and binary forms, with or without | ||
4 | modification, are permitted provided that the following conditions are met: | ||
5 | |||
6 | 1. Redistributions of source code must retain the above copyright notice, this | ||
7 | list of conditions and the following disclaimer. | ||
8 | 2. Redistributions in binary form must reproduce the above copyright notice, | ||
9 | this list of conditions and the following disclaimer in the documentation | ||
10 | and/or other materials provided with the distribution. | ||
11 | |||
12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | ||
13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | ||
14 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||
15 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR | ||
16 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | ||
17 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | ||
18 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON | ||
19 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||
20 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | ||
21 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | ||
22 | |||
1 | #include "color.h" | 23 | #include "color.h" |
2 | 24 | ||
3 | #include <the_Foundation/string.h> | 25 | #include <the_Foundation/string.h> |
@@ -62,6 +84,7 @@ void setThemePalette_Color(enum iColorTheme theme) { | |||
62 | copy_(uiTextStrong_ColorId, white_ColorId); | 84 | copy_(uiTextStrong_ColorId, white_ColorId); |
63 | copy_(uiTextSelected_ColorId, white_ColorId); | 85 | copy_(uiTextSelected_ColorId, white_ColorId); |
64 | copy_(uiTextFramelessHover_ColorId, white_ColorId); | 86 | copy_(uiTextFramelessHover_ColorId, white_ColorId); |
87 | copy_(uiTextDisabled_ColorId, gray25_ColorId); | ||
65 | copy_(uiTextShortcut_ColorId, cyan_ColorId); | 88 | copy_(uiTextShortcut_ColorId, cyan_ColorId); |
66 | copy_(uiTextAction_ColorId, cyan_ColorId); | 89 | copy_(uiTextAction_ColorId, cyan_ColorId); |
67 | copy_(uiTextCaution_ColorId, orange_ColorId); | 90 | copy_(uiTextCaution_ColorId, orange_ColorId); |
@@ -86,7 +109,8 @@ void setThemePalette_Color(enum iColorTheme theme) { | |||
86 | copy_(uiInputCursor_ColorId, orange_ColorId); | 109 | copy_(uiInputCursor_ColorId, orange_ColorId); |
87 | copy_(uiInputCursorText_ColorId, black_ColorId); | 110 | copy_(uiInputCursorText_ColorId, black_ColorId); |
88 | copy_(uiHeading_ColorId, cyan_ColorId); | 111 | copy_(uiHeading_ColorId, cyan_ColorId); |
89 | copy_(uiIcon_ColorId, teal_ColorId); | 112 | copy_(uiAnnotation_ColorId, teal_ColorId); |
113 | copy_(uiIcon_ColorId, cyan_ColorId); | ||
90 | copy_(uiIconHover_ColorId, cyan_ColorId); | 114 | copy_(uiIconHover_ColorId, cyan_ColorId); |
91 | copy_(uiSeparator_ColorId, gray25_ColorId); | 115 | copy_(uiSeparator_ColorId, gray25_ColorId); |
92 | copy_(uiMarked_ColorId, brown_ColorId); | 116 | copy_(uiMarked_ColorId, brown_ColorId); |
@@ -103,6 +127,7 @@ void setThemePalette_Color(enum iColorTheme theme) { | |||
103 | copy_(uiTextPressed_ColorId, black_ColorId); | 127 | copy_(uiTextPressed_ColorId, black_ColorId); |
104 | copy_(uiTextStrong_ColorId, white_ColorId); | 128 | copy_(uiTextStrong_ColorId, white_ColorId); |
105 | copy_(uiTextSelected_ColorId, white_ColorId); | 129 | copy_(uiTextSelected_ColorId, white_ColorId); |
130 | copy_(uiTextDisabled_ColorId, gray50_ColorId); | ||
106 | copy_(uiTextFramelessHover_ColorId, white_ColorId); | 131 | copy_(uiTextFramelessHover_ColorId, white_ColorId); |
107 | copy_(uiTextShortcut_ColorId, cyan_ColorId); | 132 | copy_(uiTextShortcut_ColorId, cyan_ColorId); |
108 | copy_(uiTextAction_ColorId, cyan_ColorId); | 133 | copy_(uiTextAction_ColorId, cyan_ColorId); |
@@ -128,6 +153,7 @@ void setThemePalette_Color(enum iColorTheme theme) { | |||
128 | copy_(uiInputCursor_ColorId, orange_ColorId); | 153 | copy_(uiInputCursor_ColorId, orange_ColorId); |
129 | copy_(uiInputCursorText_ColorId, black_ColorId); | 154 | copy_(uiInputCursorText_ColorId, black_ColorId); |
130 | copy_(uiHeading_ColorId, cyan_ColorId); | 155 | copy_(uiHeading_ColorId, cyan_ColorId); |
156 | copy_(uiAnnotation_ColorId, teal_ColorId); | ||
131 | copy_(uiIcon_ColorId, cyan_ColorId); | 157 | copy_(uiIcon_ColorId, cyan_ColorId); |
132 | copy_(uiIconHover_ColorId, cyan_ColorId); | 158 | copy_(uiIconHover_ColorId, cyan_ColorId); |
133 | copy_(uiSeparator_ColorId, black_ColorId); | 159 | copy_(uiSeparator_ColorId, black_ColorId); |
@@ -141,9 +167,10 @@ void setThemePalette_Color(enum iColorTheme theme) { | |||
141 | copy_(uiBackgroundPressed_ColorId, cyan_ColorId); | 167 | copy_(uiBackgroundPressed_ColorId, cyan_ColorId); |
142 | copy_(uiBackgroundFramelessHover_ColorId, orange_ColorId); | 168 | copy_(uiBackgroundFramelessHover_ColorId, orange_ColorId); |
143 | copy_(uiText_ColorId, black_ColorId); | 169 | copy_(uiText_ColorId, black_ColorId); |
144 | copy_(uiTextStrong_ColorId, brown_ColorId); | 170 | copy_(uiTextStrong_ColorId, teal_ColorId); |
145 | copy_(uiTextPressed_ColorId, black_ColorId); | 171 | copy_(uiTextPressed_ColorId, black_ColorId); |
146 | copy_(uiTextSelected_ColorId, black_ColorId); | 172 | copy_(uiTextSelected_ColorId, black_ColorId); |
173 | copy_(uiTextDisabled_ColorId, gray50_ColorId); | ||
147 | copy_(uiTextFramelessHover_ColorId, black_ColorId); | 174 | copy_(uiTextFramelessHover_ColorId, black_ColorId); |
148 | copy_(uiTextShortcut_ColorId, brown_ColorId); | 175 | copy_(uiTextShortcut_ColorId, brown_ColorId); |
149 | copy_(uiTextAction_ColorId, brown_ColorId); | 176 | copy_(uiTextAction_ColorId, brown_ColorId); |
@@ -163,15 +190,16 @@ void setThemePalette_Color(enum iColorTheme theme) { | |||
163 | copy_(uiInputBackgroundFocused_ColorId, white_ColorId); | 190 | copy_(uiInputBackgroundFocused_ColorId, white_ColorId); |
164 | copy_(uiInputText_ColorId, gray25_ColorId); | 191 | copy_(uiInputText_ColorId, gray25_ColorId); |
165 | copy_(uiInputTextFocused_ColorId, black_ColorId); | 192 | copy_(uiInputTextFocused_ColorId, black_ColorId); |
166 | copy_(uiInputFrame_ColorId, gray25_ColorId); | 193 | copy_(uiInputFrame_ColorId, gray50_ColorId); |
167 | copy_(uiInputFrameHover_ColorId, brown_ColorId); | 194 | copy_(uiInputFrameHover_ColorId, brown_ColorId); |
168 | copy_(uiInputFrameFocused_ColorId, teal_ColorId); | 195 | copy_(uiInputFrameFocused_ColorId, teal_ColorId); |
169 | copy_(uiInputCursor_ColorId, teal_ColorId); | 196 | copy_(uiInputCursor_ColorId, teal_ColorId); |
170 | copy_(uiInputCursorText_ColorId, white_ColorId); | 197 | copy_(uiInputCursorText_ColorId, white_ColorId); |
171 | copy_(uiHeading_ColorId, brown_ColorId); | 198 | copy_(uiHeading_ColorId, brown_ColorId); |
199 | copy_(uiAnnotation_ColorId, gray50_ColorId); | ||
172 | copy_(uiIcon_ColorId, brown_ColorId); | 200 | copy_(uiIcon_ColorId, brown_ColorId); |
173 | copy_(uiIconHover_ColorId, brown_ColorId); | 201 | copy_(uiIconHover_ColorId, brown_ColorId); |
174 | copy_(uiSeparator_ColorId, gray25_ColorId); | 202 | copy_(uiSeparator_ColorId, gray50_ColorId); |
175 | copy_(uiMarked_ColorId, cyan_ColorId); | 203 | copy_(uiMarked_ColorId, cyan_ColorId); |
176 | copy_(uiMatching_ColorId, orange_ColorId); | 204 | copy_(uiMatching_ColorId, orange_ColorId); |
177 | break; | 205 | break; |
@@ -183,6 +211,7 @@ void setThemePalette_Color(enum iColorTheme theme) { | |||
183 | copy_(uiBackgroundFramelessHover_ColorId, orange_ColorId); | 211 | copy_(uiBackgroundFramelessHover_ColorId, orange_ColorId); |
184 | copy_(uiText_ColorId, gray25_ColorId); | 212 | copy_(uiText_ColorId, gray25_ColorId); |
185 | copy_(uiTextPressed_ColorId, black_ColorId); | 213 | copy_(uiTextPressed_ColorId, black_ColorId); |
214 | copy_(uiTextDisabled_ColorId, gray75_ColorId); | ||
186 | copy_(uiTextStrong_ColorId, black_ColorId); | 215 | copy_(uiTextStrong_ColorId, black_ColorId); |
187 | copy_(uiTextSelected_ColorId, black_ColorId); | 216 | copy_(uiTextSelected_ColorId, black_ColorId); |
188 | copy_(uiTextFramelessHover_ColorId, black_ColorId); | 217 | copy_(uiTextFramelessHover_ColorId, black_ColorId); |
@@ -210,13 +239,16 @@ void setThemePalette_Color(enum iColorTheme theme) { | |||
210 | copy_(uiInputCursor_ColorId, teal_ColorId); | 239 | copy_(uiInputCursor_ColorId, teal_ColorId); |
211 | copy_(uiInputCursorText_ColorId, white_ColorId); | 240 | copy_(uiInputCursorText_ColorId, white_ColorId); |
212 | copy_(uiHeading_ColorId, brown_ColorId); | 241 | copy_(uiHeading_ColorId, brown_ColorId); |
242 | copy_(uiAnnotation_ColorId, gray50_ColorId); | ||
213 | copy_(uiIcon_ColorId, brown_ColorId); | 243 | copy_(uiIcon_ColorId, brown_ColorId); |
214 | copy_(uiIconHover_ColorId, brown_ColorId); | 244 | copy_(uiIconHover_ColorId, brown_ColorId); |
215 | copy_(uiSeparator_ColorId, gray50_ColorId); | 245 | copy_(uiSeparator_ColorId, gray75_ColorId); |
216 | copy_(uiMarked_ColorId, cyan_ColorId); | 246 | copy_(uiMarked_ColorId, cyan_ColorId); |
217 | copy_(uiMatching_ColorId, orange_ColorId); | 247 | copy_(uiMatching_ColorId, orange_ColorId); |
218 | break; | 248 | break; |
219 | } | 249 | } |
250 | palette_[uiMarked_ColorId].a = 128; | ||
251 | palette_[uiMatching_ColorId].a = 128; | ||
220 | } | 252 | } |
221 | 253 | ||
222 | iColor get_Color(int color) { | 254 | iColor get_Color(int color) { |
@@ -233,6 +265,14 @@ void set_Color(int color, iColor rgba) { | |||
233 | } | 265 | } |
234 | } | 266 | } |
235 | 267 | ||
268 | iColor mix_Color(iColor c1, iColor c2, float t) { | ||
269 | t = iClamp(t, 0.0f, 1.0f); | ||
270 | return (iColor){ c1.r * (1 - t) + c2.r * t, | ||
271 | c1.g * (1 - t) + c2.g * t, | ||
272 | c1.b * (1 - t) + c2.b * t, | ||
273 | c1.a * (1 - t) + c2.a * t }; | ||
274 | } | ||
275 | |||
236 | iLocalDef iBool equal_Color_(const iColor *x, const iColor *y) { | 276 | iLocalDef iBool equal_Color_(const iColor *x, const iColor *y) { |
237 | return memcmp(x, y, sizeof(iColor)) == 0; | 277 | return memcmp(x, y, sizeof(iColor)) == 0; |
238 | } | 278 | } |
@@ -361,7 +401,8 @@ const char *escape_Color(int color) { | |||
361 | if (color >= 0 && color < (int) iElemCount(esc)) { | 401 | if (color >= 0 && color < (int) iElemCount(esc)) { |
362 | return esc[color]; | 402 | return esc[color]; |
363 | } | 403 | } |
364 | return format_CStr("\r%c", color + '0'); | 404 | iAssert(asciiBase_ColorEscape + color <= 127); |
405 | return format_CStr("\r%c", asciiBase_ColorEscape + color); | ||
365 | } | 406 | } |
366 | 407 | ||
367 | iHSLColor setSat_HSLColor(iHSLColor d, float sat) { | 408 | iHSLColor setSat_HSLColor(iHSLColor d, float sat) { |
diff --git a/src/ui/color.h b/src/ui/color.h index 596fec7a..76e5b2a7 100644 --- a/src/ui/color.h +++ b/src/ui/color.h | |||
@@ -1,3 +1,25 @@ | |||
1 | /* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi> | ||
2 | |||
3 | Redistribution and use in source and binary forms, with or without | ||
4 | modification, are permitted provided that the following conditions are met: | ||
5 | |||
6 | 1. Redistributions of source code must retain the above copyright notice, this | ||
7 | list of conditions and the following disclaimer. | ||
8 | 2. Redistributions in binary form must reproduce the above copyright notice, | ||
9 | this list of conditions and the following disclaimer in the documentation | ||
10 | and/or other materials provided with the distribution. | ||
11 | |||
12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | ||
13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | ||
14 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||
15 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR | ||
16 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | ||
17 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | ||
18 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON | ||
19 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||
20 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | ||
21 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | ||
22 | |||
1 | #pragma once | 23 | #pragma once |
2 | 24 | ||
3 | #include <the_Foundation/range.h> | 25 | #include <the_Foundation/range.h> |
@@ -46,6 +68,7 @@ enum iColorId { | |||
46 | uiText_ColorId, | 68 | uiText_ColorId, |
47 | uiTextPressed_ColorId, | 69 | uiTextPressed_ColorId, |
48 | uiTextSelected_ColorId, | 70 | uiTextSelected_ColorId, |
71 | uiTextDisabled_ColorId, | ||
49 | uiTextFramelessHover_ColorId, | 72 | uiTextFramelessHover_ColorId, |
50 | uiTextFramelessSelected_ColorId, | 73 | uiTextFramelessSelected_ColorId, |
51 | uiTextStrong_ColorId, | 74 | uiTextStrong_ColorId, |
@@ -73,6 +96,7 @@ enum iColorId { | |||
73 | uiInputCursor_ColorId, | 96 | uiInputCursor_ColorId, |
74 | uiInputCursorText_ColorId, | 97 | uiInputCursorText_ColorId, |
75 | uiHeading_ColorId, | 98 | uiHeading_ColorId, |
99 | uiAnnotation_ColorId, | ||
76 | uiIcon_ColorId, | 100 | uiIcon_ColorId, |
77 | uiIconHover_ColorId, | 101 | uiIconHover_ColorId, |
78 | uiSeparator_ColorId, | 102 | uiSeparator_ColorId, |
@@ -139,24 +163,26 @@ iLocalDef iBool isRegularText_ColorId(enum iColorId d) { | |||
139 | #define mask_ColorId 0x7f | 163 | #define mask_ColorId 0x7f |
140 | #define permanent_ColorId 0x80 /* cannot be changed via escapes */ | 164 | #define permanent_ColorId 0x80 /* cannot be changed via escapes */ |
141 | 165 | ||
142 | #define black_ColorEscape "\r0" | 166 | #define asciiBase_ColorEscape 33 |
143 | #define gray25_ColorEscape "\r1" | 167 | |
144 | #define gray50_ColorEscape "\r2" | 168 | #define black_ColorEscape "\r!" |
145 | #define gray75_ColorEscape "\r3" | 169 | #define gray25_ColorEscape "\r\"" |
146 | #define white_ColorEscape "\r4" | 170 | #define gray50_ColorEscape "\r#" |
147 | #define brown_ColorEscape "\r5" | 171 | #define gray75_ColorEscape "\r$" |
148 | #define orange_ColorEscape "\r6" | 172 | #define white_ColorEscape "\r%" |
149 | #define teal_ColorEscape "\r7" | 173 | #define brown_ColorEscape "\r&" |
150 | #define cyan_ColorEscape "\r8" | 174 | #define orange_ColorEscape "\r'" |
151 | #define yellow_ColorEscape "\r9" | 175 | #define teal_ColorEscape "\r(" |
152 | #define red_ColorEscape "\r:" | 176 | #define cyan_ColorEscape "\r)" |
153 | #define magenta_ColorEscape "\r;" | 177 | #define yellow_ColorEscape "\r*" |
154 | #define blue_ColorEscape "\r<" | 178 | #define red_ColorEscape "\r+" |
155 | #define green_ColorEscape "\r=" | 179 | #define magenta_ColorEscape "\r," |
156 | #define uiText_ColorEscape "\rC" | 180 | #define blue_ColorEscape "\r-" |
157 | #define uiTextAction_ColorEscape "\rJ" | 181 | #define green_ColorEscape "\r." |
158 | #define uiTextCaution_ColorEscape "\rK" | 182 | #define uiText_ColorEscape "\r4" |
159 | #define uiHeading_ColorEscape "\r`" | 183 | #define uiTextAction_ColorEscape "\r<" |
184 | #define uiTextCaution_ColorEscape "\r=" | ||
185 | #define uiHeading_ColorEscape "\rR" | ||
160 | 186 | ||
161 | iDeclareType(Color) | 187 | iDeclareType(Color) |
162 | iDeclareType(HSLColor) | 188 | iDeclareType(HSLColor) |
@@ -180,6 +206,7 @@ iColor get_Color (int color); | |||
180 | int darker_Color (int color); | 206 | int darker_Color (int color); |
181 | int lighter_Color (int color); | 207 | int lighter_Color (int color); |
182 | void set_Color (int color, iColor rgba); | 208 | void set_Color (int color, iColor rgba); |
209 | iColor mix_Color (iColor c1, iColor c2, float t); | ||
183 | 210 | ||
184 | iLocalDef void setHsl_Color(int color, iHSLColor hsl) { | 211 | iLocalDef void setHsl_Color(int color, iHSLColor hsl) { |
185 | set_Color(color, rgb_HSLColor(hsl)); | 212 | set_Color(color, rgb_HSLColor(hsl)); |
diff --git a/src/ui/command.c b/src/ui/command.c index 19228e46..cf8c7032 100644 --- a/src/ui/command.c +++ b/src/ui/command.c | |||
@@ -1,3 +1,25 @@ | |||
1 | /* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi> | ||
2 | |||
3 | Redistribution and use in source and binary forms, with or without | ||
4 | modification, are permitted provided that the following conditions are met: | ||
5 | |||
6 | 1. Redistributions of source code must retain the above copyright notice, this | ||
7 | list of conditions and the following disclaimer. | ||
8 | 2. Redistributions in binary form must reproduce the above copyright notice, | ||
9 | this list of conditions and the following disclaimer in the documentation | ||
10 | and/or other materials provided with the distribution. | ||
11 | |||
12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | ||
13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | ||
14 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||
15 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR | ||
16 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | ||
17 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | ||
18 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON | ||
19 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||
20 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | ||
21 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | ||
22 | |||
1 | #include "command.h" | 23 | #include "command.h" |
2 | #include "app.h" | 24 | #include "app.h" |
3 | 25 | ||
diff --git a/src/ui/command.h b/src/ui/command.h index 2124d527..4798baa6 100644 --- a/src/ui/command.h +++ b/src/ui/command.h | |||
@@ -1,3 +1,25 @@ | |||
1 | /* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi> | ||
2 | |||
3 | Redistribution and use in source and binary forms, with or without | ||
4 | modification, are permitted provided that the following conditions are met: | ||
5 | |||
6 | 1. Redistributions of source code must retain the above copyright notice, this | ||
7 | list of conditions and the following disclaimer. | ||
8 | 2. Redistributions in binary form must reproduce the above copyright notice, | ||
9 | this list of conditions and the following disclaimer in the documentation | ||
10 | and/or other materials provided with the distribution. | ||
11 | |||
12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | ||
13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | ||
14 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||
15 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR | ||
16 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | ||
17 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | ||
18 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON | ||
19 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||
20 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | ||
21 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | ||
22 | |||
1 | #pragma once | 23 | #pragma once |
2 | 24 | ||
3 | #include <the_Foundation/vec2.h> | 25 | #include <the_Foundation/vec2.h> |
diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c index 29803252..6ca6e4c2 100644 --- a/src/ui/documentwidget.c +++ b/src/ui/documentwidget.c | |||
@@ -1,3 +1,25 @@ | |||
1 | /* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi> | ||
2 | |||
3 | Redistribution and use in source and binary forms, with or without | ||
4 | modification, are permitted provided that the following conditions are met: | ||
5 | |||
6 | 1. Redistributions of source code must retain the above copyright notice, this | ||
7 | list of conditions and the following disclaimer. | ||
8 | 2. Redistributions in binary form must reproduce the above copyright notice, | ||
9 | this list of conditions and the following disclaimer in the documentation | ||
10 | and/or other materials provided with the distribution. | ||
11 | |||
12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | ||
13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | ||
14 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||
15 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR | ||
16 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | ||
17 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | ||
18 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON | ||
19 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||
20 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | ||
21 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | ||
22 | |||
1 | #include "documentwidget.h" | 23 | #include "documentwidget.h" |
2 | #include "scrollwidget.h" | 24 | #include "scrollwidget.h" |
3 | #include "inputwidget.h" | 25 | #include "inputwidget.h" |
@@ -105,6 +127,43 @@ iDefineTypeConstruction(Model) | |||
105 | 127 | ||
106 | /*----------------------------------------------------------------------------------------------*/ | 128 | /*----------------------------------------------------------------------------------------------*/ |
107 | 129 | ||
130 | iDeclareType(VisBuffer) | ||
131 | iDeclareTypeConstruction(VisBuffer) | ||
132 | |||
133 | struct Impl_VisBuffer { | ||
134 | SDL_Texture * texture[2]; | ||
135 | int index; | ||
136 | iInt2 size; | ||
137 | iRangei validRange; | ||
138 | }; | ||
139 | |||
140 | void init_VisBuffer(iVisBuffer *d) { | ||
141 | iZap(*d); | ||
142 | } | ||
143 | |||
144 | void deinit_VisBuffer(iVisBuffer *d) { | ||
145 | iForIndices(i, d->texture) { | ||
146 | if (d->texture[i]) { | ||
147 | SDL_DestroyTexture(d->texture[i]); | ||
148 | } | ||
149 | } | ||
150 | } | ||
151 | |||
152 | void dealloc_VisBuffer(iVisBuffer *d) { | ||
153 | d->size = zero_I2(); | ||
154 | iZap(d->validRange); | ||
155 | iForIndices(i, d->texture) { | ||
156 | SDL_DestroyTexture(d->texture[i]); | ||
157 | d->texture[i] = NULL; | ||
158 | } | ||
159 | } | ||
160 | |||
161 | iDefineTypeConstruction(VisBuffer) | ||
162 | |||
163 | /*----------------------------------------------------------------------------------------------*/ | ||
164 | |||
165 | static const int smoothSpeed_DocumentWidget_ = 120; /* unit: gap_Text per second */ | ||
166 | |||
108 | enum iRequestState { | 167 | enum iRequestState { |
109 | blank_RequestState, | 168 | blank_RequestState, |
110 | fetching_RequestState, | 169 | fetching_RequestState, |
@@ -124,26 +183,26 @@ struct Impl_DocumentWidget { | |||
124 | int certFlags; | 183 | int certFlags; |
125 | iDate certExpiry; | 184 | iDate certExpiry; |
126 | iString * certSubject; | 185 | iString * certSubject; |
186 | int redirectCount; | ||
127 | iBool selecting; | 187 | iBool selecting; |
128 | iRangecc selectMark; | 188 | iRangecc selectMark; |
129 | iRangecc foundMark; | 189 | iRangecc foundMark; |
130 | int pageMargin; | 190 | int pageMargin; |
131 | iPtrArray visibleLinks; | 191 | iPtrArray visibleLinks; |
132 | const iGmRun * hoverLink; | 192 | const iGmRun * hoverLink; |
193 | const iGmRun * contextLink; | ||
133 | iBool noHoverWhileScrolling; | 194 | iBool noHoverWhileScrolling; |
134 | iBool showLinkNumbers; | 195 | iBool showLinkNumbers; |
135 | iClick click; | 196 | iClick click; |
136 | float initNormScrollY; | 197 | float initNormScrollY; |
137 | int scrollY; | 198 | int scrollY; |
138 | iScrollWidget *scroll; | 199 | iScrollWidget *scroll; |
200 | int smoothScroll; | ||
201 | int smoothSpeed; | ||
202 | int smoothLastOffset; | ||
203 | iBool smoothContinue; | ||
139 | iWidget * menu; | 204 | iWidget * menu; |
140 | SDL_Cursor * arrowCursor; /* TODO: cursors belong in Window */ | 205 | iVisBuffer * visBuffer; |
141 | SDL_Cursor * beamCursor; | ||
142 | SDL_Cursor * handCursor; | ||
143 | SDL_Texture * visBuffer[2]; | ||
144 | int visBufferIndex; | ||
145 | iInt2 visBufferSize; | ||
146 | iRangei visBufferValidRange; | ||
147 | }; | 206 | }; |
148 | 207 | ||
149 | iDefineObjectConstruction(DocumentWidget) | 208 | iDefineObjectConstruction(DocumentWidget) |
@@ -163,22 +222,22 @@ void init_DocumentWidget(iDocumentWidget *d) { | |||
163 | d->isRequestUpdated = iFalse; | 222 | d->isRequestUpdated = iFalse; |
164 | d->media = new_ObjectList(); | 223 | d->media = new_ObjectList(); |
165 | d->doc = new_GmDocument(); | 224 | d->doc = new_GmDocument(); |
225 | d->redirectCount = 0; | ||
166 | d->initNormScrollY = 0; | 226 | d->initNormScrollY = 0; |
167 | d->scrollY = 0; | 227 | d->scrollY = 0; |
228 | d->smoothScroll = 0; | ||
229 | d->smoothSpeed = 0; | ||
230 | d->smoothLastOffset = 0; | ||
231 | d->smoothContinue = iFalse; | ||
168 | d->selecting = iFalse; | 232 | d->selecting = iFalse; |
169 | d->selectMark = iNullRange; | 233 | d->selectMark = iNullRange; |
170 | d->foundMark = iNullRange; | 234 | d->foundMark = iNullRange; |
171 | d->pageMargin = 5; | 235 | d->pageMargin = 5; |
172 | d->hoverLink = NULL; | 236 | d->hoverLink = NULL; |
237 | d->contextLink = NULL; | ||
173 | d->noHoverWhileScrolling = iFalse; | 238 | d->noHoverWhileScrolling = iFalse; |
174 | d->showLinkNumbers = iFalse; | 239 | d->showLinkNumbers = iFalse; |
175 | iZap(d->visBuffer); | 240 | d->visBuffer = new_VisBuffer(); |
176 | d->visBufferIndex = 0; | ||
177 | d->visBufferSize = zero_I2(); | ||
178 | iZap(d->visBufferValidRange); | ||
179 | d->arrowCursor = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_ARROW); | ||
180 | d->beamCursor = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_IBEAM); | ||
181 | d->handCursor = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_HAND); | ||
182 | init_PtrArray(&d->visibleLinks); | 241 | init_PtrArray(&d->visibleLinks); |
183 | init_Click(&d->click, d, SDL_BUTTON_LEFT); | 242 | init_Click(&d->click, d, SDL_BUTTON_LEFT); |
184 | addChild_Widget(w, iClob(d->scroll = new_ScrollWidget())); | 243 | addChild_Widget(w, iClob(d->scroll = new_ScrollWidget())); |
@@ -197,18 +256,23 @@ void init_DocumentWidget(iDocumentWidget *d) { | |||
197 | } | 256 | } |
198 | 257 | ||
199 | void deinit_DocumentWidget(iDocumentWidget *d) { | 258 | void deinit_DocumentWidget(iDocumentWidget *d) { |
259 | delete_VisBuffer(d->visBuffer); | ||
200 | iRelease(d->media); | 260 | iRelease(d->media); |
201 | iRelease(d->request); | 261 | iRelease(d->request); |
202 | iRelease(d->doc); | 262 | iRelease(d->doc); |
203 | deinit_PtrArray(&d->visibleLinks); | 263 | deinit_PtrArray(&d->visibleLinks); |
204 | delete_String(d->certSubject); | 264 | delete_String(d->certSubject); |
205 | delete_String(d->titleUser); | 265 | delete_String(d->titleUser); |
206 | SDL_FreeCursor(d->arrowCursor); | ||
207 | SDL_FreeCursor(d->beamCursor); | ||
208 | SDL_FreeCursor(d->handCursor); | ||
209 | deinit_Model(&d->mod); | 266 | deinit_Model(&d->mod); |
210 | } | 267 | } |
211 | 268 | ||
269 | static void resetSmoothScroll_DocumentWidget_(iDocumentWidget *d) { | ||
270 | d->smoothSpeed = 0; | ||
271 | d->smoothScroll = 0; | ||
272 | d->smoothLastOffset = 0; | ||
273 | d->smoothContinue = iFalse; | ||
274 | } | ||
275 | |||
212 | static int documentWidth_DocumentWidget_(const iDocumentWidget *d) { | 276 | static int documentWidth_DocumentWidget_(const iDocumentWidget *d) { |
213 | const iWidget *w = constAs_Widget(d); | 277 | const iWidget *w = constAs_Widget(d); |
214 | const iRect bounds = bounds_Widget(w); | 278 | const iRect bounds = bounds_Widget(w); |
@@ -242,9 +306,11 @@ iLocalDef int documentToWindowY_DocumentWidget_(const iDocumentWidget *d, int do | |||
242 | return docY - d->scrollY + documentBounds_DocumentWidget_(d).pos.y; | 306 | return docY - d->scrollY + documentBounds_DocumentWidget_(d).pos.y; |
243 | } | 307 | } |
244 | 308 | ||
309 | #if 0 | ||
245 | iLocalDef int windowToDocumentY_DocumentWidget_(const iDocumentWidget *d, int localY) { | 310 | iLocalDef int windowToDocumentY_DocumentWidget_(const iDocumentWidget *d, int localY) { |
246 | return localY + d->scrollY - documentBounds_DocumentWidget_(d).pos.y; | 311 | return localY + d->scrollY - documentBounds_DocumentWidget_(d).pos.y; |
247 | } | 312 | } |
313 | #endif | ||
248 | 314 | ||
249 | static iInt2 documentPos_DocumentWidget_(const iDocumentWidget *d, iInt2 pos) { | 315 | static iInt2 documentPos_DocumentWidget_(const iDocumentWidget *d, iInt2 pos) { |
250 | return addY_I2(sub_I2(pos, topLeft_Rect(documentBounds_DocumentWidget_(d))), d->scrollY); | 316 | return addY_I2(sub_I2(pos, topLeft_Rect(documentBounds_DocumentWidget_(d))), d->scrollY); |
@@ -291,15 +357,16 @@ static float normScrollPos_DocumentWidget_(const iDocumentWidget *d) { | |||
291 | 357 | ||
292 | static int scrollMax_DocumentWidget_(const iDocumentWidget *d) { | 358 | static int scrollMax_DocumentWidget_(const iDocumentWidget *d) { |
293 | return size_GmDocument(d->doc).y - height_Rect(bounds_Widget(constAs_Widget(d))) + | 359 | return size_GmDocument(d->doc).y - height_Rect(bounds_Widget(constAs_Widget(d))) + |
294 | 2 * d->pageMargin * gap_UI; | 360 | (hasSiteBanner_GmDocument(d->doc) ? 1 : 2) * d->pageMargin * gap_UI; |
295 | } | 361 | } |
296 | 362 | ||
297 | static void updateHover_DocumentWidget_(iDocumentWidget *d, iInt2 mouse) { | 363 | static void updateHover_DocumentWidget_(iDocumentWidget *d, iInt2 mouse) { |
298 | const iRect docBounds = documentBounds_DocumentWidget_(d); | 364 | const iWidget *w = constAs_Widget(d); |
299 | const iGmRun *oldHoverLink = d->hoverLink; | 365 | const iRect docBounds = documentBounds_DocumentWidget_(d); |
300 | d->hoverLink = NULL; | 366 | const iGmRun * oldHoverLink = d->hoverLink; |
301 | const iInt2 hoverPos = addY_I2(sub_I2(mouse, topLeft_Rect(docBounds)), d->scrollY); | 367 | d->hoverLink = NULL; |
302 | if (!d->noHoverWhileScrolling && | 368 | const iInt2 hoverPos = addY_I2(sub_I2(mouse, topLeft_Rect(docBounds)), d->scrollY); |
369 | if (isHover_Widget(w) && !d->noHoverWhileScrolling && | ||
303 | (d->state == ready_RequestState || d->state == receivedPartialResponse_RequestState)) { | 370 | (d->state == ready_RequestState || d->state == receivedPartialResponse_RequestState)) { |
304 | iConstForEach(PtrArray, i, &d->visibleLinks) { | 371 | iConstForEach(PtrArray, i, &d->visibleLinks) { |
305 | const iGmRun *run = i.ptr; | 372 | const iGmRun *run = i.ptr; |
@@ -312,12 +379,9 @@ static void updateHover_DocumentWidget_(iDocumentWidget *d, iInt2 mouse) { | |||
312 | if (d->hoverLink != oldHoverLink) { | 379 | if (d->hoverLink != oldHoverLink) { |
313 | refresh_Widget(as_Widget(d)); | 380 | refresh_Widget(as_Widget(d)); |
314 | } | 381 | } |
315 | if (!contains_Widget(constAs_Widget(d), mouse) || | 382 | if (isHover_Widget(w) && !contains_Widget(constAs_Widget(d->scroll), mouse)) { |
316 | contains_Widget(constAs_Widget(d->scroll), mouse)) { | 383 | setCursor_Window(get_Window(), |
317 | SDL_SetCursor(d->arrowCursor); | 384 | d->hoverLink ? SDL_SYSTEM_CURSOR_HAND : SDL_SYSTEM_CURSOR_IBEAM); |
318 | } | ||
319 | else { | ||
320 | SDL_SetCursor(d->hoverLink ? d->handCursor : d->beamCursor); | ||
321 | } | 385 | } |
322 | } | 386 | } |
323 | 387 | ||
@@ -424,38 +488,52 @@ static void updateWindowTitle_DocumentWidget_(const iDocumentWidget *d) { | |||
424 | } | 488 | } |
425 | } | 489 | } |
426 | 490 | ||
491 | static void invalidate_DocumentWidget_(iDocumentWidget *d) { | ||
492 | iZap(d->visBuffer->validRange); | ||
493 | } | ||
494 | |||
427 | static void setSource_DocumentWidget_(iDocumentWidget *d, const iString *source) { | 495 | static void setSource_DocumentWidget_(iDocumentWidget *d, const iString *source) { |
428 | setUrl_GmDocument(d->doc, d->mod.url); | 496 | setUrl_GmDocument(d->doc, d->mod.url); |
429 | setSource_GmDocument(d->doc, source, documentWidth_DocumentWidget_(d)); | 497 | setSource_GmDocument(d->doc, source, documentWidth_DocumentWidget_(d)); |
430 | d->foundMark = iNullRange; | 498 | d->foundMark = iNullRange; |
431 | d->selectMark = iNullRange; | 499 | d->selectMark = iNullRange; |
432 | d->hoverLink = NULL; | 500 | d->hoverLink = NULL; |
501 | d->contextLink = NULL; | ||
433 | updateWindowTitle_DocumentWidget_(d); | 502 | updateWindowTitle_DocumentWidget_(d); |
434 | updateVisible_DocumentWidget_(d); | 503 | updateVisible_DocumentWidget_(d); |
504 | invalidate_DocumentWidget_(d); | ||
435 | refresh_Widget(as_Widget(d)); | 505 | refresh_Widget(as_Widget(d)); |
436 | } | 506 | } |
437 | 507 | ||
438 | static void showErrorPage_DocumentWidget_(iDocumentWidget *d, enum iGmStatusCode code) { | 508 | static void showErrorPage_DocumentWidget_(iDocumentWidget *d, enum iGmStatusCode code, |
509 | const iString *meta) { | ||
439 | iString *src = collectNewCStr_String("# "); | 510 | iString *src = collectNewCStr_String("# "); |
440 | const iGmError *msg = get_GmError(code); | 511 | const iGmError *msg = get_GmError(code); |
441 | appendChar_String(src, msg->icon ? msg->icon : 0x2327); /* X in a box */ | 512 | appendChar_String(src, msg->icon ? msg->icon : 0x2327); /* X in a box */ |
442 | appendFormat_String(src, " %s\n%s", msg->title, msg->info); | 513 | appendFormat_String(src, " %s\n%s", msg->title, msg->info); |
443 | switch (code) { | 514 | if (meta) { |
444 | case failedToOpenFile_GmStatusCode: | 515 | switch (code) { |
445 | case certificateNotValid_GmStatusCode: | 516 | case nonGeminiRedirect_GmStatusCode: |
446 | appendFormat_String(src, "\n\n%s", cstr_String(meta_GmRequest(d->request))); | 517 | case tooManyRedirects_GmStatusCode: |
447 | break; | 518 | appendFormat_String(src, "\n=> %s\n", cstr_String(meta)); |
448 | case unsupportedMimeType_GmStatusCode: | 519 | break; |
449 | appendFormat_String(src, "\n```\n%s\n```\n", cstr_String(meta_GmRequest(d->request))); | 520 | case failedToOpenFile_GmStatusCode: |
450 | break; | 521 | case certificateNotValid_GmStatusCode: |
451 | case slowDown_GmStatusCode: | 522 | appendFormat_String(src, "\n\n%s", cstr_String(meta)); |
452 | appendFormat_String(src, "\n\nWait %s seconds before your next request.", | 523 | break; |
453 | cstr_String(meta_GmRequest(d->request))); | 524 | case unsupportedMimeType_GmStatusCode: |
454 | break; | 525 | appendFormat_String(src, "\n```\n%s\n```\n", cstr_String(meta)); |
455 | default: | 526 | break; |
456 | break; | 527 | case slowDown_GmStatusCode: |
528 | appendFormat_String(src, "\n\nWait %s seconds before your next request.", | ||
529 | cstr_String(meta)); | ||
530 | break; | ||
531 | default: | ||
532 | break; | ||
533 | } | ||
457 | } | 534 | } |
458 | setSource_DocumentWidget_(d, src); | 535 | setSource_DocumentWidget_(d, src); |
536 | resetSmoothScroll_DocumentWidget_(d); | ||
459 | d->scrollY = 0; | 537 | d->scrollY = 0; |
460 | d->state = ready_RequestState; | 538 | d->state = ready_RequestState; |
461 | } | 539 | } |
@@ -470,10 +548,6 @@ static void updateTheme_DocumentWidget_(iDocumentWidget *d) { | |||
470 | } | 548 | } |
471 | } | 549 | } |
472 | 550 | ||
473 | static void invalidate_DocumentWidget_(iDocumentWidget *d) { | ||
474 | iZap(d->visBufferValidRange); | ||
475 | } | ||
476 | |||
477 | static void updateDocument_DocumentWidget_(iDocumentWidget *d, const iGmResponse *response) { | 551 | static void updateDocument_DocumentWidget_(iDocumentWidget *d, const iGmResponse *response) { |
478 | if (d->state == ready_RequestState) { | 552 | if (d->state == ready_RequestState) { |
479 | return; | 553 | return; |
@@ -493,16 +567,16 @@ static void updateDocument_DocumentWidget_(iDocumentWidget *d, const iGmResponse | |||
493 | const iString *mimeStr = collect_String(lower_String(&response->meta)); /* for convenience */ | 567 | const iString *mimeStr = collect_String(lower_String(&response->meta)); /* for convenience */ |
494 | iRangecc mime = range_String(mimeStr); | 568 | iRangecc mime = range_String(mimeStr); |
495 | iRangecc seg = iNullRange; | 569 | iRangecc seg = iNullRange; |
496 | while (nextSplit_Rangecc(&mime, ";", &seg)) { | 570 | while (nextSplit_Rangecc(mime, ";", &seg)) { |
497 | iRangecc param = seg; | 571 | iRangecc param = seg; |
498 | trim_Rangecc(¶m); | 572 | trim_Rangecc(¶m); |
499 | if (equal_Rangecc(¶m, "text/plain")) { | 573 | if (equal_Rangecc(param, "text/plain")) { |
500 | docFormat = plainText_GmDocumentFormat; | 574 | docFormat = plainText_GmDocumentFormat; |
501 | } | 575 | } |
502 | else if (equal_Rangecc(¶m, "text/gemini")) { | 576 | else if (equal_Rangecc(param, "text/gemini")) { |
503 | docFormat = gemini_GmDocumentFormat; | 577 | docFormat = gemini_GmDocumentFormat; |
504 | } | 578 | } |
505 | else if (startsWith_Rangecc(¶m, "image/")) { | 579 | else if (startsWith_Rangecc(param, "image/")) { |
506 | docFormat = gemini_GmDocumentFormat; | 580 | docFormat = gemini_GmDocumentFormat; |
507 | if (!d->request || isFinished_GmRequest(d->request)) { | 581 | if (!d->request || isFinished_GmRequest(d->request)) { |
508 | /* Make a simple document with an image. */ | 582 | /* Make a simple document with an image. */ |
@@ -521,7 +595,7 @@ static void updateDocument_DocumentWidget_(iDocumentWidget *d, const iGmResponse | |||
521 | clear_String(&str); | 595 | clear_String(&str); |
522 | } | 596 | } |
523 | } | 597 | } |
524 | else if (startsWith_Rangecc(¶m, "charset=")) { | 598 | else if (startsWith_Rangecc(param, "charset=")) { |
525 | charset = (iRangecc){ param.start + 8, param.end }; | 599 | charset = (iRangecc){ param.start + 8, param.end }; |
526 | /* Remove whitespace and quotes. */ | 600 | /* Remove whitespace and quotes. */ |
527 | trim_Rangecc(&charset); | 601 | trim_Rangecc(&charset); |
@@ -532,12 +606,12 @@ static void updateDocument_DocumentWidget_(iDocumentWidget *d, const iGmResponse | |||
532 | } | 606 | } |
533 | } | 607 | } |
534 | if (docFormat == undefined_GmDocumentFormat) { | 608 | if (docFormat == undefined_GmDocumentFormat) { |
535 | showErrorPage_DocumentWidget_(d, unsupportedMimeType_GmStatusCode); | 609 | showErrorPage_DocumentWidget_(d, unsupportedMimeType_GmStatusCode, &response->meta); |
536 | deinit_String(&str); | 610 | deinit_String(&str); |
537 | return; | 611 | return; |
538 | } | 612 | } |
539 | /* Convert the source to UTF-8 if needed. */ | 613 | /* Convert the source to UTF-8 if needed. */ |
540 | if (!equalCase_Rangecc(&charset, "utf-8")) { | 614 | if (!equalCase_Rangecc(charset, "utf-8")) { |
541 | set_String(&str, | 615 | set_String(&str, |
542 | collect_String(decode_Block(&str.chars, cstr_Rangecc(charset)))); | 616 | collect_String(decode_Block(&str.chars, cstr_Rangecc(charset)))); |
543 | } | 617 | } |
@@ -609,6 +683,7 @@ static void parseUser_DocumentWidget_(iDocumentWidget *d) { | |||
609 | iRegExp *userPats[2] = { new_RegExp("~([^/?]+)", 0), | 683 | iRegExp *userPats[2] = { new_RegExp("~([^/?]+)", 0), |
610 | new_RegExp("/users/([^/?]+)", caseInsensitive_RegExpOption) }; | 684 | new_RegExp("/users/([^/?]+)", caseInsensitive_RegExpOption) }; |
611 | iRegExpMatch m; | 685 | iRegExpMatch m; |
686 | init_RegExpMatch(&m); | ||
612 | iForIndices(i, userPats) { | 687 | iForIndices(i, userPats) { |
613 | if (matchString_RegExp(userPats[i], d->mod.url, &m)) { | 688 | if (matchString_RegExp(userPats[i], d->mod.url, &m)) { |
614 | setRange_String(d->titleUser, capturedRange_RegExpMatch(&m, 1)); | 689 | setRange_String(d->titleUser, capturedRange_RegExpMatch(&m, 1)); |
@@ -621,6 +696,8 @@ static iBool updateFromHistory_DocumentWidget_(iDocumentWidget *d) { | |||
621 | const iRecentUrl *recent = findUrl_History(d->mod.history, d->mod.url); | 696 | const iRecentUrl *recent = findUrl_History(d->mod.history, d->mod.url); |
622 | if (recent && recent->cachedResponse) { | 697 | if (recent && recent->cachedResponse) { |
623 | const iGmResponse *resp = recent->cachedResponse; | 698 | const iGmResponse *resp = recent->cachedResponse; |
699 | clear_ObjectList(d->media); | ||
700 | reset_GmDocument(d->doc); | ||
624 | d->state = fetching_RequestState; | 701 | d->state = fetching_RequestState; |
625 | d->initNormScrollY = recent->normScrollY; | 702 | d->initNormScrollY = recent->normScrollY; |
626 | /* Use the cached response data. */ | 703 | /* Use the cached response data. */ |
@@ -676,6 +753,10 @@ void setInitialScroll_DocumentWidget(iDocumentWidget *d, float normScrollY) { | |||
676 | d->initNormScrollY = normScrollY; | 753 | d->initNormScrollY = normScrollY; |
677 | } | 754 | } |
678 | 755 | ||
756 | void setRedirectCount_DocumentWidget(iDocumentWidget *d, int count) { | ||
757 | d->redirectCount = count; | ||
758 | } | ||
759 | |||
679 | iBool isRequestOngoing_DocumentWidget(const iDocumentWidget *d) { | 760 | iBool isRequestOngoing_DocumentWidget(const iDocumentWidget *d) { |
680 | return d->state == fetching_RequestState || d->state == receivedPartialResponse_RequestState; | 761 | return d->state == fetching_RequestState || d->state == receivedPartialResponse_RequestState; |
681 | } | 762 | } |
@@ -696,6 +777,37 @@ static void scroll_DocumentWidget_(iDocumentWidget *d, int offset) { | |||
696 | refresh_Widget(as_Widget(d)); | 777 | refresh_Widget(as_Widget(d)); |
697 | } | 778 | } |
698 | 779 | ||
780 | static void doScroll_DocumentWidget_(iAny *ptr) { | ||
781 | iDocumentWidget *d = ptr; | ||
782 | if (!d->smoothScroll) return; /* was cancelled */ | ||
783 | const double elapsed = (double) elapsedSinceLastTicker_App() / 1000.0; | ||
784 | int delta = d->smoothSpeed * elapsed * iSign(d->smoothScroll); | ||
785 | if (iAbs(d->smoothScroll) <= iAbs(delta)) { | ||
786 | if (d->smoothContinue) { | ||
787 | d->smoothScroll += d->smoothLastOffset; | ||
788 | } | ||
789 | else { | ||
790 | delta = d->smoothScroll; | ||
791 | } | ||
792 | } | ||
793 | scroll_DocumentWidget_(d, delta); | ||
794 | d->smoothScroll -= delta; | ||
795 | if (d->smoothScroll != 0) { | ||
796 | addTicker_App(doScroll_DocumentWidget_, d); | ||
797 | } | ||
798 | } | ||
799 | |||
800 | static void smoothScroll_DocumentWidget_(iDocumentWidget *d, int offset, int speed) { | ||
801 | if (speed == 0) { | ||
802 | scroll_DocumentWidget_(d, offset); | ||
803 | return; | ||
804 | } | ||
805 | d->smoothSpeed = speed; | ||
806 | d->smoothScroll += offset; | ||
807 | d->smoothLastOffset = offset; | ||
808 | addTicker_App(doScroll_DocumentWidget_, d); | ||
809 | } | ||
810 | |||
699 | static void scrollTo_DocumentWidget_(iDocumentWidget *d, int documentY, iBool centered) { | 811 | static void scrollTo_DocumentWidget_(iDocumentWidget *d, int documentY, iBool centered) { |
700 | d->scrollY = documentY - (centered ? documentBounds_DocumentWidget_(d).size.y / 2 : | 812 | d->scrollY = documentY - (centered ? documentBounds_DocumentWidget_(d).size.y / 2 : |
701 | lineHeight_Text(paragraph_FontId)); | 813 | lineHeight_Text(paragraph_FontId)); |
@@ -733,32 +845,43 @@ static void checkResponse_DocumentWidget_(iDocumentWidget *d) { | |||
733 | } | 845 | } |
734 | case categorySuccess_GmStatusCode: | 846 | case categorySuccess_GmStatusCode: |
735 | d->scrollY = 0; | 847 | d->scrollY = 0; |
848 | resetSmoothScroll_DocumentWidget_(d); | ||
736 | reset_GmDocument(d->doc); /* new content incoming */ | 849 | reset_GmDocument(d->doc); /* new content incoming */ |
737 | updateDocument_DocumentWidget_(d, response_GmRequest(d->request)); | 850 | updateDocument_DocumentWidget_(d, response_GmRequest(d->request)); |
738 | break; | 851 | break; |
739 | case categoryRedirect_GmStatusCode: | 852 | case categoryRedirect_GmStatusCode: |
740 | if (isEmpty_String(meta_GmRequest(d->request))) { | 853 | if (isEmpty_String(meta_GmRequest(d->request))) { |
741 | showErrorPage_DocumentWidget_(d, invalidRedirect_GmStatusCode); | 854 | showErrorPage_DocumentWidget_(d, invalidRedirect_GmStatusCode, NULL); |
742 | } | 855 | } |
743 | else { | 856 | else { |
744 | /* TODO: only accept redirects that use gemini protocol */ | 857 | /* Only accept redirects that use gemini scheme. */ |
745 | postCommandf_App( | 858 | const iString *dstUrl = absoluteUrl_String(d->mod.url, meta_GmRequest(d->request)); |
746 | "open redirect:1 url:%s", | 859 | if (d->redirectCount >= 5) { |
747 | cstr_String(absoluteUrl_String(d->mod.url, meta_GmRequest(d->request)))); | 860 | showErrorPage_DocumentWidget_(d, tooManyRedirects_GmStatusCode, dstUrl); |
861 | } | ||
862 | else if (equalCase_Rangecc(urlScheme_String(dstUrl), "gemini")) { | ||
863 | postCommandf_App( | ||
864 | "open redirect:%d url:%s", d->redirectCount + 1, cstr_String(dstUrl)); | ||
865 | } | ||
866 | else { | ||
867 | showErrorPage_DocumentWidget_(d, nonGeminiRedirect_GmStatusCode, dstUrl); | ||
868 | } | ||
748 | iReleasePtr(&d->request); | 869 | iReleasePtr(&d->request); |
749 | } | 870 | } |
750 | break; | 871 | break; |
751 | default: | 872 | default: |
752 | if (isDefined_GmError(statusCode)) { | 873 | if (isDefined_GmError(statusCode)) { |
753 | showErrorPage_DocumentWidget_(d, statusCode); | 874 | showErrorPage_DocumentWidget_(d, statusCode, meta_GmRequest(d->request)); |
754 | } | 875 | } |
755 | else if (category_GmStatusCode(statusCode) == | 876 | else if (category_GmStatusCode(statusCode) == |
756 | categoryTemporaryFailure_GmStatusCode) { | 877 | categoryTemporaryFailure_GmStatusCode) { |
757 | showErrorPage_DocumentWidget_(d, temporaryFailure_GmStatusCode); | 878 | showErrorPage_DocumentWidget_( |
879 | d, temporaryFailure_GmStatusCode, meta_GmRequest(d->request)); | ||
758 | } | 880 | } |
759 | else if (category_GmStatusCode(statusCode) == | 881 | else if (category_GmStatusCode(statusCode) == |
760 | categoryPermanentFailure_GmStatusCode) { | 882 | categoryPermanentFailure_GmStatusCode) { |
761 | showErrorPage_DocumentWidget_(d, permanentFailure_GmStatusCode); | 883 | showErrorPage_DocumentWidget_( |
884 | d, permanentFailure_GmStatusCode, meta_GmRequest(d->request)); | ||
762 | } | 885 | } |
763 | break; | 886 | break; |
764 | } | 887 | } |
@@ -872,32 +995,33 @@ static iBool handleMediaCommand_DocumentWidget_(iDocumentWidget *d, const char * | |||
872 | return iFalse; | 995 | return iFalse; |
873 | } | 996 | } |
874 | 997 | ||
875 | static void deallocVisBuffer_DocumentWidget_(iDocumentWidget *d) { | 998 | static void deallocVisBuffer_DocumentWidget_(const iDocumentWidget *d) { |
876 | d->visBufferSize = zero_I2(); | 999 | d->visBuffer->size = zero_I2(); |
877 | iZap(d->visBufferValidRange); | 1000 | iZap(d->visBuffer->validRange); |
878 | iForIndices(i, d->visBuffer) { | 1001 | iForIndices(i, d->visBuffer->texture) { |
879 | SDL_DestroyTexture(d->visBuffer[i]); | 1002 | SDL_DestroyTexture(d->visBuffer->texture[i]); |
880 | d->visBuffer[i] = NULL; | 1003 | d->visBuffer->texture[i] = NULL; |
881 | } | 1004 | } |
882 | } | 1005 | } |
883 | 1006 | ||
884 | static void allocVisBuffer_DocumentWidget_(iDocumentWidget *d) { | 1007 | static void allocVisBuffer_DocumentWidget_(const iDocumentWidget *d) { |
885 | iWidget *w = as_Widget(d); | 1008 | const iWidget *w = constAs_Widget(d); |
886 | const iBool isVisible = isVisible_Widget(w); | 1009 | const iBool isVisible = isVisible_Widget(w); |
887 | const iInt2 size = bounds_Widget(w).size; | 1010 | const iInt2 size = bounds_Widget(w).size; |
888 | if (!isEqual_I2(size, d->visBufferSize) || !isVisible) { | 1011 | if (!isEqual_I2(size, d->visBuffer->size) || !isVisible) { |
889 | deallocVisBuffer_DocumentWidget_(d); | 1012 | dealloc_VisBuffer(d->visBuffer); |
890 | } | 1013 | } |
891 | if (isVisible && !d->visBuffer[0]) { | 1014 | if (isVisible && !d->visBuffer->texture[0]) { |
892 | iZap(d->visBufferValidRange); | 1015 | iZap(d->visBuffer->validRange); |
893 | d->visBufferSize = size; | 1016 | d->visBuffer->size = size; |
894 | iForIndices(i, d->visBuffer) { | 1017 | iForIndices(i, d->visBuffer->texture) { |
895 | d->visBuffer[i] = SDL_CreateTexture(renderer_Window(get_Window()), | 1018 | d->visBuffer->texture[i] = |
896 | SDL_PIXELFORMAT_RGBA8888, | 1019 | SDL_CreateTexture(renderer_Window(get_Window()), |
897 | SDL_TEXTUREACCESS_STATIC | SDL_TEXTUREACCESS_TARGET, | 1020 | SDL_PIXELFORMAT_RGBA8888, |
898 | size.x, | 1021 | SDL_TEXTUREACCESS_STATIC | SDL_TEXTUREACCESS_TARGET, |
899 | size.y); | 1022 | size.x, |
900 | SDL_SetTextureBlendMode(d->visBuffer[i], SDL_BLENDMODE_NONE); | 1023 | size.y); |
1024 | SDL_SetTextureBlendMode(d->visBuffer->texture[i], SDL_BLENDMODE_NONE); | ||
901 | } | 1025 | } |
902 | } | 1026 | } |
903 | } | 1027 | } |
@@ -973,7 +1097,7 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) | |||
973 | : "Not trusted")); | 1097 | : "Not trusted")); |
974 | return iTrue; | 1098 | return iTrue; |
975 | } | 1099 | } |
976 | else if (equal_Command(cmd, "copy") && document_App() == d) { | 1100 | else if (equal_Command(cmd, "copy") && document_App() == d && !focus_Widget()) { |
977 | iString *copied; | 1101 | iString *copied; |
978 | if (d->selectMark.start) { | 1102 | if (d->selectMark.start) { |
979 | iRangecc mark = d->selectMark; | 1103 | iRangecc mark = d->selectMark; |
@@ -990,10 +1114,10 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) | |||
990 | delete_String(copied); | 1114 | delete_String(copied); |
991 | return iTrue; | 1115 | return iTrue; |
992 | } | 1116 | } |
993 | else if (equalWidget_Command(cmd, w, "document.copylink")) { | 1117 | else if (equal_Command(cmd, "document.copylink") && document_App() == d) { |
994 | if (d->hoverLink) { | 1118 | if (d->contextLink) { |
995 | SDL_SetClipboardText(cstr_String( | 1119 | SDL_SetClipboardText(cstr_String( |
996 | absoluteUrl_String(d->mod.url, linkUrl_GmDocument(d->doc, d->hoverLink->linkId)))); | 1120 | absoluteUrl_String(d->mod.url, linkUrl_GmDocument(d->doc, d->contextLink->linkId)))); |
997 | } | 1121 | } |
998 | else { | 1122 | else { |
999 | SDL_SetClipboardText(cstr_String(d->mod.url)); | 1123 | SDL_SetClipboardText(cstr_String(d->mod.url)); |
@@ -1001,18 +1125,16 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) | |||
1001 | return iTrue; | 1125 | return iTrue; |
1002 | } | 1126 | } |
1003 | else if (equal_Command(cmd, "document.input.submit")) { | 1127 | else if (equal_Command(cmd, "document.input.submit")) { |
1004 | if (arg_Command(cmd)) { | 1128 | iString *value = collect_String(suffix_Command(cmd, "value")); |
1005 | iString *value = collect_String(suffix_Command(cmd, "value")); | 1129 | urlEncode_String(value); |
1006 | urlEncode_String(value); | 1130 | iString *url = collect_String(copy_String(d->mod.url)); |
1007 | iString *url = collect_String(copy_String(d->mod.url)); | 1131 | const size_t qPos = indexOfCStr_String(url, "?"); |
1008 | const size_t qPos = indexOfCStr_String(url, "?"); | 1132 | if (qPos != iInvalidPos) { |
1009 | if (qPos != iInvalidPos) { | 1133 | remove_Block(&url->chars, qPos, iInvalidSize); |
1010 | remove_Block(&url->chars, qPos, iInvalidSize); | ||
1011 | } | ||
1012 | appendCStr_String(url, "?"); | ||
1013 | append_String(url, value); | ||
1014 | postCommandf_App("open url:%s", cstr_String(url)); | ||
1015 | } | 1134 | } |
1135 | appendCStr_String(url, "?"); | ||
1136 | append_String(url, value); | ||
1137 | postCommandf_App("open url:%s", cstr_String(url)); | ||
1016 | return iTrue; | 1138 | return iTrue; |
1017 | } | 1139 | } |
1018 | else if (equal_Command(cmd, "valueinput.cancelled") && | 1140 | else if (equal_Command(cmd, "valueinput.cancelled") && |
@@ -1028,11 +1150,11 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) | |||
1028 | else if (equalWidget_Command(cmd, w, "document.request.finished") && | 1150 | else if (equalWidget_Command(cmd, w, "document.request.finished") && |
1029 | pointerLabel_Command(cmd, "request") == d->request) { | 1151 | pointerLabel_Command(cmd, "request") == d->request) { |
1030 | checkResponse_DocumentWidget_(d); | 1152 | checkResponse_DocumentWidget_(d); |
1153 | resetSmoothScroll_DocumentWidget_(d); | ||
1031 | d->scrollY = d->initNormScrollY * size_GmDocument(d->doc).y; | 1154 | d->scrollY = d->initNormScrollY * size_GmDocument(d->doc).y; |
1032 | d->state = ready_RequestState; | 1155 | d->state = ready_RequestState; |
1033 | /* The response may be cached. */ { | 1156 | /* The response may be cached. */ { |
1034 | const iRangecc proto = urlProtocol_String(d->mod.url); | 1157 | if (!equal_Rangecc(urlScheme_String(d->mod.url), "about")) { |
1035 | if (!equal_Rangecc(&proto, "about")) { | ||
1036 | setCachedResponse_History(d->mod.history, response_GmRequest(d->request)); | 1158 | setCachedResponse_History(d->mod.history, response_GmRequest(d->request)); |
1037 | } | 1159 | } |
1038 | } | 1160 | } |
@@ -1076,12 +1198,24 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) | |||
1076 | } | 1198 | } |
1077 | else if (equalWidget_Command(cmd, w, "scroll.moved")) { | 1199 | else if (equalWidget_Command(cmd, w, "scroll.moved")) { |
1078 | d->scrollY = arg_Command(cmd); | 1200 | d->scrollY = arg_Command(cmd); |
1201 | resetSmoothScroll_DocumentWidget_(d); | ||
1079 | updateVisible_DocumentWidget_(d); | 1202 | updateVisible_DocumentWidget_(d); |
1080 | return iTrue; | 1203 | return iTrue; |
1081 | } | 1204 | } |
1082 | else if (equalWidget_Command(cmd, w, "scroll.page")) { | 1205 | else if (equalWidget_Command(cmd, w, "scroll.page")) { |
1083 | scroll_DocumentWidget_(d, | 1206 | if (argLabel_Command(cmd, "repeat")) { |
1084 | arg_Command(cmd) * height_Rect(documentBounds_DocumentWidget_(d))); | 1207 | if (!d->smoothContinue) { |
1208 | d->smoothContinue = iTrue; | ||
1209 | } | ||
1210 | else { | ||
1211 | return iTrue; | ||
1212 | } | ||
1213 | } | ||
1214 | smoothScroll_DocumentWidget_(d, | ||
1215 | arg_Command(cmd) * | ||
1216 | (0.5f * height_Rect(documentBounds_DocumentWidget_(d)) - | ||
1217 | 0 * lineHeight_Text(paragraph_FontId)), | ||
1218 | 25 * smoothSpeed_DocumentWidget_); | ||
1085 | return iTrue; | 1219 | return iTrue; |
1086 | } | 1220 | } |
1087 | else if (equal_Command(cmd, "document.goto") && document_App() == d) { | 1221 | else if (equal_Command(cmd, "document.goto") && document_App() == d) { |
@@ -1164,6 +1298,13 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e | |||
1164 | refresh_Widget(w); | 1298 | refresh_Widget(w); |
1165 | } | 1299 | } |
1166 | break; | 1300 | break; |
1301 | case SDLK_PAGEUP: | ||
1302 | case SDLK_PAGEDOWN: | ||
1303 | case SDLK_SPACE: | ||
1304 | case SDLK_UP: | ||
1305 | case SDLK_DOWN: | ||
1306 | d->smoothContinue = iFalse; | ||
1307 | break; | ||
1167 | } | 1308 | } |
1168 | } | 1309 | } |
1169 | if (ev->type == SDL_KEYDOWN) { | 1310 | if (ev->type == SDL_KEYDOWN) { |
@@ -1194,12 +1335,14 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e | |||
1194 | break; | 1335 | break; |
1195 | case SDLK_HOME: | 1336 | case SDLK_HOME: |
1196 | d->scrollY = 0; | 1337 | d->scrollY = 0; |
1338 | resetSmoothScroll_DocumentWidget_(d); | ||
1197 | scroll_DocumentWidget_(d, 0); | 1339 | scroll_DocumentWidget_(d, 0); |
1198 | updateVisible_DocumentWidget_(d); | 1340 | updateVisible_DocumentWidget_(d); |
1199 | refresh_Widget(w); | 1341 | refresh_Widget(w); |
1200 | return iTrue; | 1342 | return iTrue; |
1201 | case SDLK_END: | 1343 | case SDLK_END: |
1202 | d->scrollY = scrollMax_DocumentWidget_(d); | 1344 | d->scrollY = scrollMax_DocumentWidget_(d); |
1345 | resetSmoothScroll_DocumentWidget_(d); | ||
1203 | scroll_DocumentWidget_(d, 0); | 1346 | scroll_DocumentWidget_(d, 0); |
1204 | updateVisible_DocumentWidget_(d); | 1347 | updateVisible_DocumentWidget_(d); |
1205 | refresh_Widget(w); | 1348 | refresh_Widget(w); |
@@ -1207,24 +1350,37 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e | |||
1207 | case SDLK_UP: | 1350 | case SDLK_UP: |
1208 | case SDLK_DOWN: | 1351 | case SDLK_DOWN: |
1209 | if (mods == 0) { | 1352 | if (mods == 0) { |
1210 | scroll_DocumentWidget_(d, 2 * lineHeight_Text(default_FontId) * | 1353 | if (ev->key.repeat) { |
1211 | (key == SDLK_UP ? -1 : 1)); | 1354 | if (!d->smoothContinue) { |
1355 | d->smoothContinue = iTrue; | ||
1356 | } | ||
1357 | else return iTrue; | ||
1358 | } | ||
1359 | smoothScroll_DocumentWidget_(d, | ||
1360 | 3 * lineHeight_Text(paragraph_FontId) * | ||
1361 | (key == SDLK_UP ? -1 : 1), | ||
1362 | gap_Text * smoothSpeed_DocumentWidget_); | ||
1212 | return iTrue; | 1363 | return iTrue; |
1213 | } | 1364 | } |
1214 | break; | 1365 | break; |
1215 | case SDLK_PAGEUP: | 1366 | case SDLK_PAGEUP: |
1216 | case SDLK_PAGEDOWN: | 1367 | case SDLK_PAGEDOWN: |
1217 | case ' ': | 1368 | case SDLK_SPACE: |
1218 | postCommand_Widget(w, "scroll.page arg:%d", key == SDLK_PAGEUP ? -1 : +1); | 1369 | postCommand_Widget( |
1370 | w, | ||
1371 | "scroll.page arg:%d repeat:%d", | ||
1372 | (key == SDLK_SPACE && mods & KMOD_SHIFT) || key == SDLK_PAGEUP ? -1 : +1, | ||
1373 | ev->key.repeat != 0); | ||
1219 | return iTrue; | 1374 | return iTrue; |
1220 | #if 0 | 1375 | #if 1 |
1221 | case SDLK_9: { | 1376 | case SDLK_KP_1: { |
1222 | iBlock *seed = new_Block(64); | 1377 | iBlock *seed = new_Block(64); |
1223 | for (size_t i = 0; i < 64; ++i) { | 1378 | for (size_t i = 0; i < 64; ++i) { |
1224 | setByte_Block(seed, i, iRandom(0, 255)); | 1379 | setByte_Block(seed, i, iRandom(0, 255)); |
1225 | } | 1380 | } |
1226 | setThemeSeed_GmDocument(d->doc, seed); | 1381 | setThemeSeed_GmDocument(d->doc, seed); |
1227 | delete_Block(seed); | 1382 | delete_Block(seed); |
1383 | invalidate_DocumentWidget_(d); | ||
1228 | refresh_Widget(w); | 1384 | refresh_Widget(w); |
1229 | break; | 1385 | break; |
1230 | } | 1386 | } |
@@ -1258,7 +1414,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e | |||
1258 | else if (ev->type == SDL_MOUSEMOTION) { | 1414 | else if (ev->type == SDL_MOUSEMOTION) { |
1259 | d->noHoverWhileScrolling = iFalse; | 1415 | d->noHoverWhileScrolling = iFalse; |
1260 | if (isVisible_Widget(d->menu)) { | 1416 | if (isVisible_Widget(d->menu)) { |
1261 | SDL_SetCursor(d->arrowCursor); | 1417 | setCursor_Window(get_Window(), SDL_SYSTEM_CURSOR_ARROW); |
1262 | } | 1418 | } |
1263 | else { | 1419 | else { |
1264 | updateHover_DocumentWidget_(d, init_I2(ev->motion.x, ev->motion.y)); | 1420 | updateHover_DocumentWidget_(d, init_I2(ev->motion.x, ev->motion.y)); |
@@ -1274,6 +1430,9 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e | |||
1274 | return iTrue; | 1430 | return iTrue; |
1275 | } | 1431 | } |
1276 | } | 1432 | } |
1433 | if (!isVisible_Widget(d->menu)) { | ||
1434 | d->contextLink = d->hoverLink; | ||
1435 | } | ||
1277 | processContextMenuEvent_Widget(d->menu, ev, d->hoverLink = NULL); | 1436 | processContextMenuEvent_Widget(d->menu, ev, d->hoverLink = NULL); |
1278 | switch (processEvent_Click(&d->click, ev)) { | 1437 | switch (processEvent_Click(&d->click, ev)) { |
1279 | case started_ClickResult: | 1438 | case started_ClickResult: |
@@ -1282,6 +1441,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e | |||
1282 | case drag_ClickResult: { | 1441 | case drag_ClickResult: { |
1283 | /* Begin selecting a range of text. */ | 1442 | /* Begin selecting a range of text. */ |
1284 | if (!d->selecting) { | 1443 | if (!d->selecting) { |
1444 | setFocus_Widget(NULL); /* TODO: Focus this document? */ | ||
1285 | d->selecting = iTrue; | 1445 | d->selecting = iTrue; |
1286 | d->selectMark.start = d->selectMark.end = | 1446 | d->selectMark.start = d->selectMark.end = |
1287 | sourceLoc_DocumentWidget_(d, d->click.startPos); | 1447 | sourceLoc_DocumentWidget_(d, d->click.startPos); |
@@ -1418,8 +1578,12 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) { | |||
1418 | } | 1578 | } |
1419 | /* Text markers. */ | 1579 | /* Text markers. */ |
1420 | if (d->pass == dynamic_DrawRunPass) { | 1580 | if (d->pass == dynamic_DrawRunPass) { |
1581 | SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), | ||
1582 | isDark_ColorTheme(colorTheme_App()) ? SDL_BLENDMODE_ADD | ||
1583 | : SDL_BLENDMODE_BLEND); | ||
1421 | fillRange_DrawContext_(d, run, uiMatching_ColorId, d->widget->foundMark, &d->inFoundMark); | 1584 | fillRange_DrawContext_(d, run, uiMatching_ColorId, d->widget->foundMark, &d->inFoundMark); |
1422 | fillRange_DrawContext_(d, run, uiMarked_ColorId, d->widget->selectMark, &d->inSelectMark); | 1585 | fillRange_DrawContext_(d, run, uiMarked_ColorId, d->widget->selectMark, &d->inSelectMark); |
1586 | SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_NONE); | ||
1423 | } | 1587 | } |
1424 | enum iColorId fg = run->color; | 1588 | enum iColorId fg = run->color; |
1425 | const iGmDocument *doc = d->widget->doc; | 1589 | const iGmDocument *doc = d->widget->doc; |
@@ -1505,6 +1669,12 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) { | |||
1505 | appendFormat_String( | 1669 | appendFormat_String( |
1506 | &text, " %s\u2a2f", isHover ? escape_Color(tmLinkText_ColorId) : ""); | 1670 | &text, " %s\u2a2f", isHover ? escape_Color(tmLinkText_ColorId) : ""); |
1507 | } | 1671 | } |
1672 | const iInt2 size = measureRange_Text(metaFont, range_String(&text)); | ||
1673 | fillRect_Paint( | ||
1674 | &d->paint, | ||
1675 | (iRect){ add_I2(origin, addX_I2(topRight_Rect(run->bounds), -size.x - gap_UI)), | ||
1676 | addX_I2(size, 2 * gap_UI) }, | ||
1677 | tmBackground_ColorId); | ||
1508 | drawAlign_Text(metaFont, | 1678 | drawAlign_Text(metaFont, |
1509 | add_I2(topRight_Rect(run->bounds), origin), | 1679 | add_I2(topRight_Rect(run->bounds), origin), |
1510 | fg, | 1680 | fg, |
@@ -1527,9 +1697,8 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) { | |||
1527 | const int flags = linkFlags_GmDocument(doc, linkId); | 1697 | const int flags = linkFlags_GmDocument(doc, linkId); |
1528 | iUrl parts; | 1698 | iUrl parts; |
1529 | init_Url(&parts, url); | 1699 | init_Url(&parts, url); |
1530 | const iString *host = collect_String(newRange_String(parts.host)); | ||
1531 | fg = linkColor_GmDocument(doc, linkId, textHover_GmLinkPart); | 1700 | fg = linkColor_GmDocument(doc, linkId, textHover_GmLinkPart); |
1532 | const iBool showHost = (!isEmpty_String(host) && flags & userFriendly_GmLinkFlag); | 1701 | const iBool showHost = (!isEmpty_Range(&parts.host) && flags & userFriendly_GmLinkFlag); |
1533 | const iBool showImage = (flags & imageFileExtension_GmLinkFlag) != 0; | 1702 | const iBool showImage = (flags & imageFileExtension_GmLinkFlag) != 0; |
1534 | const iBool showAudio = (flags & audioFileExtension_GmLinkFlag) != 0; | 1703 | const iBool showAudio = (flags & audioFileExtension_GmLinkFlag) != 0; |
1535 | iString str; | 1704 | iString str; |
@@ -1541,11 +1710,16 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) { | |||
1541 | &str, | 1710 | &str, |
1542 | " \u2014%s%s%s\r%c%s", | 1711 | " \u2014%s%s%s\r%c%s", |
1543 | showHost ? " " : "", | 1712 | showHost ? " " : "", |
1544 | showHost ? cstr_String(host) : "", | 1713 | showHost ? (!equalCase_Rangecc(parts.scheme, "gemini") |
1714 | ? format_CStr("%s://%s", | ||
1715 | cstr_Rangecc(parts.scheme), | ||
1716 | cstr_Rangecc(parts.host)) | ||
1717 | : cstr_Rangecc(parts.host)) | ||
1718 | : "", | ||
1545 | showHost && (showImage || showAudio) ? " \u2014" : "", | 1719 | showHost && (showImage || showAudio) ? " \u2014" : "", |
1546 | showImage || showAudio | 1720 | showImage || showAudio |
1547 | ? '0' + fg | 1721 | ? asciiBase_ColorEscape + fg |
1548 | : ('0' + linkColor_GmDocument(doc, run->linkId, domain_GmLinkPart)), | 1722 | : (asciiBase_ColorEscape + linkColor_GmDocument(doc, run->linkId, domain_GmLinkPart)), |
1549 | showImage ? " View Image \U0001f5bc" | 1723 | showImage ? " View Image \U0001f5bc" |
1550 | : showAudio ? " Play Audio \U0001f3b5" : ""); | 1724 | : showAudio ? " Play Audio \U0001f3b5" : ""); |
1551 | } | 1725 | } |
@@ -1599,8 +1773,9 @@ static void draw_DocumentWidget_(const iDocumentWidget *d) { | |||
1599 | const iRect bounds = bounds_Widget(w); | 1773 | const iRect bounds = bounds_Widget(w); |
1600 | const iInt2 origin = topLeft_Rect(bounds); | 1774 | const iInt2 origin = topLeft_Rect(bounds); |
1601 | const iRangei visRange = visibleRange_DocumentWidget_(d); | 1775 | const iRangei visRange = visibleRange_DocumentWidget_(d); |
1776 | iVisBuffer * visBuf = d->visBuffer; /* this may be updated/modified here */ | ||
1602 | draw_Widget(w); | 1777 | draw_Widget(w); |
1603 | allocVisBuffer_DocumentWidget_(iConstCast(iDocumentWidget *, d)); | 1778 | allocVisBuffer_DocumentWidget_(d); |
1604 | iDrawContext ctxDynamic = { | 1779 | iDrawContext ctxDynamic = { |
1605 | .pass = dynamic_DrawRunPass, | 1780 | .pass = dynamic_DrawRunPass, |
1606 | .widget = d, | 1781 | .widget = d, |
@@ -1618,36 +1793,38 @@ static void draw_DocumentWidget_(const iDocumentWidget *d) { | |||
1618 | /* Static content. */ { | 1793 | /* Static content. */ { |
1619 | iPaint *p = &ctxStatic.paint; | 1794 | iPaint *p = &ctxStatic.paint; |
1620 | init_Paint(p); | 1795 | init_Paint(p); |
1621 | const int vbSrc = d->visBufferIndex; | 1796 | const int vbSrc = visBuf->index; |
1622 | const int vbDst = d->visBufferIndex ^ 1; | 1797 | const int vbDst = visBuf->index ^ 1; |
1623 | iRangei drawRange = visRange; | 1798 | iRangei drawRange = visRange; |
1624 | iAssert(d->visBuffer[vbDst]); | 1799 | iAssert(visBuf->texture[vbDst]); |
1625 | beginTarget_Paint(p, d->visBuffer[vbDst]); | 1800 | beginTarget_Paint(p, visBuf->texture[vbDst]); |
1626 | const iRect visBufferRect = { zero_I2(), d->visBufferSize }; | 1801 | const iRect visBufferRect = { zero_I2(), visBuf->size }; |
1627 | iRect drawRect = visBufferRect; | 1802 | iRect drawRect = visBufferRect; |
1628 | if (!isEmpty_Rangei_(intersect_Rangei_(visRange, d->visBufferValidRange))) { | 1803 | if (!isEmpty_Rangei_(intersect_Rangei_(visRange, visBuf->validRange))) { |
1629 | if (visRange.start < d->visBufferValidRange.start) { | 1804 | if (visRange.start < visBuf->validRange.start) { |
1630 | drawRange = (iRangei){ visRange.start, d->visBufferValidRange.start }; | 1805 | drawRange = (iRangei){ visRange.start, visBuf->validRange.start }; |
1631 | } | 1806 | } |
1632 | else { | 1807 | else { |
1633 | drawRange = (iRangei){ d->visBufferValidRange.end, visRange.end }; | 1808 | drawRange = (iRangei){ visBuf->validRange.end, visRange.end }; |
1634 | } | 1809 | } |
1635 | if (isEmpty_Range(&drawRange)) { | 1810 | if (isEmpty_Range(&drawRange)) { |
1636 | SDL_RenderCopy(render, d->visBuffer[vbSrc], NULL, NULL); | 1811 | SDL_RenderCopy(render, visBuf->texture[vbSrc], NULL, NULL); |
1637 | } | 1812 | } |
1638 | else { | 1813 | else { |
1639 | SDL_RenderCopy( | 1814 | SDL_RenderCopy( |
1640 | render, | 1815 | render, |
1641 | d->visBuffer[vbSrc], | 1816 | visBuf->texture[vbSrc], |
1642 | NULL, | 1817 | NULL, |
1643 | &(SDL_Rect){ 0, | 1818 | &(SDL_Rect){ 0, |
1644 | documentToWindowY_DocumentWidget_(d, d->visBufferValidRange.start) - origin.y, | 1819 | documentToWindowY_DocumentWidget_(d, visBuf->validRange.start) - |
1645 | d->visBufferSize.x, | 1820 | origin.y, |
1646 | d->visBufferSize.y }); | 1821 | visBuf->size.x, |
1647 | drawRect = init_Rect(0, | 1822 | visBuf->size.y }); |
1648 | documentToWindowY_DocumentWidget_(d, drawRange.start) - origin.y, | 1823 | drawRect = |
1649 | d->visBufferSize.x, | 1824 | init_Rect(0, |
1650 | size_Range(&drawRange)); | 1825 | documentToWindowY_DocumentWidget_(d, drawRange.start) - origin.y, |
1826 | visBuf->size.x, | ||
1827 | size_Range(&drawRange)); | ||
1651 | } | 1828 | } |
1652 | } | 1829 | } |
1653 | if (!isEmpty_Range(&drawRange)) { | 1830 | if (!isEmpty_Range(&drawRange)) { |
@@ -1657,17 +1834,20 @@ static void draw_DocumentWidget_(const iDocumentWidget *d) { | |||
1657 | unsetClip_Paint(p); | 1834 | unsetClip_Paint(p); |
1658 | } | 1835 | } |
1659 | endTarget_Paint(p); | 1836 | endTarget_Paint(p); |
1660 | SDL_RenderCopy(render, d->visBuffer[vbDst], NULL, | 1837 | SDL_RenderCopy(render, visBuf->texture[vbDst], NULL, |
1661 | &(SDL_Rect){ origin.x, origin.y, bounds.size.x, bounds.size.y } ); | 1838 | &(SDL_Rect){ origin.x, origin.y, bounds.size.x, bounds.size.y } ); |
1662 | iConstCast(iDocumentWidget *, d)->visBufferValidRange = visRange; | 1839 | visBuf->validRange = visRange; |
1663 | iConstCast(iDocumentWidget *, d)->visBufferIndex = vbDst; | 1840 | visBuf->index = vbDst; |
1664 | } | 1841 | } |
1665 | /* Dynamic content. */ { | 1842 | /* Dynamic content. */ { |
1843 | extern int enableKerning_Text; | ||
1844 | enableKerning_Text = iFalse; /* need to be fast, these is redone on every redraw */ | ||
1666 | iPaint *p = &ctxDynamic.paint; | 1845 | iPaint *p = &ctxDynamic.paint; |
1667 | init_Paint(p); | 1846 | init_Paint(p); |
1668 | setClip_Paint(p, bounds); | 1847 | setClip_Paint(p, bounds); |
1669 | render_GmDocument(d->doc, visRange, drawRun_DrawContext_, &ctxDynamic); | 1848 | render_GmDocument(d->doc, visRange, drawRun_DrawContext_, &ctxDynamic); |
1670 | unsetClip_Paint(p); | 1849 | unsetClip_Paint(p); |
1850 | enableKerning_Text = iTrue; | ||
1671 | } | 1851 | } |
1672 | 1852 | ||
1673 | // drawRect_Paint(&ctx.paint, | 1853 | // drawRect_Paint(&ctx.paint, |
diff --git a/src/ui/documentwidget.h b/src/ui/documentwidget.h index 074516c3..5a3125af 100644 --- a/src/ui/documentwidget.h +++ b/src/ui/documentwidget.h | |||
@@ -1,3 +1,25 @@ | |||
1 | /* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi> | ||
2 | |||
3 | Redistribution and use in source and binary forms, with or without | ||
4 | modification, are permitted provided that the following conditions are met: | ||
5 | |||
6 | 1. Redistributions of source code must retain the above copyright notice, this | ||
7 | list of conditions and the following disclaimer. | ||
8 | 2. Redistributions in binary form must reproduce the above copyright notice, | ||
9 | this list of conditions and the following disclaimer in the documentation | ||
10 | and/or other materials provided with the distribution. | ||
11 | |||
12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | ||
13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | ||
14 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||
15 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR | ||
16 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | ||
17 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | ||
18 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON | ||
19 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||
20 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | ||
21 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | ||
22 | |||
1 | #pragma once | 23 | #pragma once |
2 | 24 | ||
3 | #include "widget.h" | 25 | #include "widget.h" |
@@ -23,5 +45,6 @@ const iString * bookmarkTitle_DocumentWidget (const iDocumentWidget *); | |||
23 | void setUrl_DocumentWidget (iDocumentWidget *, const iString *url); | 45 | void setUrl_DocumentWidget (iDocumentWidget *, const iString *url); |
24 | void setUrlFromCache_DocumentWidget (iDocumentWidget *, const iString *url, iBool isFromCache); | 46 | void setUrlFromCache_DocumentWidget (iDocumentWidget *, const iString *url, iBool isFromCache); |
25 | void setInitialScroll_DocumentWidget (iDocumentWidget *, float normScrollY); /* set after content received */ | 47 | void setInitialScroll_DocumentWidget (iDocumentWidget *, float normScrollY); /* set after content received */ |
48 | void setRedirectCount_DocumentWidget (iDocumentWidget *, int count); | ||
26 | 49 | ||
27 | void updateSize_DocumentWidget (iDocumentWidget *); | 50 | void updateSize_DocumentWidget (iDocumentWidget *); |
diff --git a/src/ui/inputwidget.c b/src/ui/inputwidget.c index d367952d..d583b109 100644 --- a/src/ui/inputwidget.c +++ b/src/ui/inputwidget.c | |||
@@ -1,3 +1,25 @@ | |||
1 | /* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi> | ||
2 | |||
3 | Redistribution and use in source and binary forms, with or without | ||
4 | modification, are permitted provided that the following conditions are met: | ||
5 | |||
6 | 1. Redistributions of source code must retain the above copyright notice, this | ||
7 | list of conditions and the following disclaimer. | ||
8 | 2. Redistributions in binary form must reproduce the above copyright notice, | ||
9 | this list of conditions and the following disclaimer in the documentation | ||
10 | and/or other materials provided with the distribution. | ||
11 | |||
12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | ||
13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | ||
14 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||
15 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR | ||
16 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | ||
17 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | ||
18 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON | ||
19 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||
20 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | ||
21 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | ||
22 | |||
1 | #include "inputwidget.h" | 23 | #include "inputwidget.h" |
2 | #include "paint.h" | 24 | #include "paint.h" |
3 | #include "util.h" | 25 | #include "util.h" |
@@ -7,17 +29,40 @@ | |||
7 | #include <SDL_clipboard.h> | 29 | #include <SDL_clipboard.h> |
8 | #include <SDL_timer.h> | 30 | #include <SDL_timer.h> |
9 | 31 | ||
10 | static const int REFRESH_INTERVAL = 256; | 32 | static const int refreshInterval_InputWidget_ = 256; |
33 | static const size_t maxUndo_InputWidget_ = 64; | ||
34 | |||
35 | iDeclareType(InputUndo) | ||
36 | |||
37 | struct Impl_InputUndo { | ||
38 | iArray text; | ||
39 | size_t cursor; | ||
40 | }; | ||
41 | |||
42 | static void init_InputUndo_(iInputUndo *d, const iArray *text, size_t cursor) { | ||
43 | initCopy_Array(&d->text, text); | ||
44 | d->cursor = cursor; | ||
45 | } | ||
46 | |||
47 | static void deinit_InputUndo_(iInputUndo *d) { | ||
48 | deinit_Array(&d->text); | ||
49 | } | ||
11 | 50 | ||
12 | struct Impl_InputWidget { | 51 | struct Impl_InputWidget { |
13 | iWidget widget; | 52 | iWidget widget; |
14 | enum iInputMode mode; | 53 | enum iInputMode mode; |
15 | iBool isSensitive; | 54 | iBool isSensitive; |
16 | iBool enterPressed; | 55 | iBool enterPressed; |
56 | iBool selectAllOnFocus; | ||
17 | size_t maxLen; | 57 | size_t maxLen; |
18 | iArray text; /* iChar[] */ | 58 | iArray text; /* iChar[] */ |
19 | iArray oldText; /* iChar[] */ | 59 | iArray oldText; /* iChar[] */ |
60 | iString hint; | ||
20 | size_t cursor; | 61 | size_t cursor; |
62 | size_t lastCursor; | ||
63 | iBool isMarking; | ||
64 | iRanges mark; | ||
65 | iArray undoStack; | ||
21 | int font; | 66 | int font; |
22 | iClick click; | 67 | iClick click; |
23 | uint32_t timer; | 68 | uint32_t timer; |
@@ -25,16 +70,29 @@ struct Impl_InputWidget { | |||
25 | 70 | ||
26 | iDefineObjectConstructionArgs(InputWidget, (size_t maxLen), maxLen) | 71 | iDefineObjectConstructionArgs(InputWidget, (size_t maxLen), maxLen) |
27 | 72 | ||
73 | static void clearUndo_InputWidget_(iInputWidget *d) { | ||
74 | iForEach(Array, i, &d->undoStack) { | ||
75 | deinit_InputUndo_(i.value); | ||
76 | } | ||
77 | clear_Array(&d->undoStack); | ||
78 | } | ||
79 | |||
28 | void init_InputWidget(iInputWidget *d, size_t maxLen) { | 80 | void init_InputWidget(iInputWidget *d, size_t maxLen) { |
29 | iWidget *w = &d->widget; | 81 | iWidget *w = &d->widget; |
30 | init_Widget(w); | 82 | init_Widget(w); |
31 | setFlags_Widget(w, focusable_WidgetFlag | hover_WidgetFlag, iTrue); | 83 | setFlags_Widget(w, focusable_WidgetFlag | hover_WidgetFlag, iTrue); |
32 | init_Array(&d->text, sizeof(iChar)); | 84 | init_Array(&d->text, sizeof(iChar)); |
33 | init_Array(&d->oldText, sizeof(iChar)); | 85 | init_Array(&d->oldText, sizeof(iChar)); |
86 | init_String(&d->hint); | ||
87 | init_Array(&d->undoStack, sizeof(iInputUndo)); | ||
34 | d->font = uiInput_FontId; | 88 | d->font = uiInput_FontId; |
35 | d->cursor = 0; | 89 | d->cursor = 0; |
36 | d->isSensitive = iFalse; | 90 | d->lastCursor = 0; |
37 | d->enterPressed = iFalse; | 91 | d->isMarking = iFalse; |
92 | iZap(d->mark); | ||
93 | d->isSensitive = iFalse; | ||
94 | d->enterPressed = iFalse; | ||
95 | d->selectAllOnFocus = iFalse; | ||
38 | setMaxLen_InputWidget(d, maxLen); | 96 | setMaxLen_InputWidget(d, maxLen); |
39 | /* Caller must arrange the width, but the height is fixed. */ | 97 | /* Caller must arrange the width, but the height is fixed. */ |
40 | w->rect.size.y = lineHeight_Text(default_FontId) + 2 * gap_UI; | 98 | w->rect.size.y = lineHeight_Text(default_FontId) + 2 * gap_UI; |
@@ -44,13 +102,39 @@ void init_InputWidget(iInputWidget *d, size_t maxLen) { | |||
44 | } | 102 | } |
45 | 103 | ||
46 | void deinit_InputWidget(iInputWidget *d) { | 104 | void deinit_InputWidget(iInputWidget *d) { |
105 | clearUndo_InputWidget_(d); | ||
106 | deinit_Array(&d->undoStack); | ||
47 | if (d->timer) { | 107 | if (d->timer) { |
48 | SDL_RemoveTimer(d->timer); | 108 | SDL_RemoveTimer(d->timer); |
49 | } | 109 | } |
110 | deinit_String(&d->hint); | ||
50 | deinit_Array(&d->oldText); | 111 | deinit_Array(&d->oldText); |
51 | deinit_Array(&d->text); | 112 | deinit_Array(&d->text); |
52 | } | 113 | } |
53 | 114 | ||
115 | static void pushUndo_InputWidget_(iInputWidget *d) { | ||
116 | iInputUndo undo; | ||
117 | init_InputUndo_(&undo, &d->text, d->cursor); | ||
118 | pushBack_Array(&d->undoStack, &undo); | ||
119 | if (size_Array(&d->undoStack) > maxUndo_InputWidget_) { | ||
120 | deinit_InputUndo_(front_Array(&d->undoStack)); | ||
121 | popFront_Array(&d->undoStack); | ||
122 | } | ||
123 | } | ||
124 | |||
125 | static iBool popUndo_InputWidget_(iInputWidget *d) { | ||
126 | if (!isEmpty_Array(&d->undoStack)) { | ||
127 | iInputUndo *undo = back_Array(&d->undoStack); | ||
128 | setCopy_Array(&d->text, &undo->text); | ||
129 | d->cursor = undo->cursor; | ||
130 | deinit_InputUndo_(undo); | ||
131 | popBack_Array(&d->undoStack); | ||
132 | iZap(d->mark); | ||
133 | return iTrue; | ||
134 | } | ||
135 | return iFalse; | ||
136 | } | ||
137 | |||
54 | void setMode_InputWidget(iInputWidget *d, enum iInputMode mode) { | 138 | void setMode_InputWidget(iInputWidget *d, enum iInputMode mode) { |
55 | d->mode = mode; | 139 | d->mode = mode; |
56 | } | 140 | } |
@@ -78,7 +162,12 @@ void setMaxLen_InputWidget(iInputWidget *d, size_t maxLen) { | |||
78 | } | 162 | } |
79 | } | 163 | } |
80 | 164 | ||
165 | void setHint_InputWidget(iInputWidget *d, const char *hintText) { | ||
166 | setCStr_String(&d->hint, hintText); | ||
167 | } | ||
168 | |||
81 | void setText_InputWidget(iInputWidget *d, const iString *text) { | 169 | void setText_InputWidget(iInputWidget *d, const iString *text) { |
170 | clearUndo_InputWidget_(d); | ||
82 | clear_Array(&d->text); | 171 | clear_Array(&d->text); |
83 | iConstForEach(String, i, text) { | 172 | iConstForEach(String, i, text) { |
84 | pushBack_Array(&d->text, &i.value); | 173 | pushBack_Array(&d->text, &i.value); |
@@ -92,10 +181,6 @@ void setTextCStr_InputWidget(iInputWidget *d, const char *cstr) { | |||
92 | delete_String(str); | 181 | delete_String(str); |
93 | } | 182 | } |
94 | 183 | ||
95 | void setCursor_InputWidget(iInputWidget *d, size_t pos) { | ||
96 | d->cursor = iMin(pos, size_Array(&d->text)); | ||
97 | } | ||
98 | |||
99 | static uint32_t refreshTimer_(uint32_t interval, void *d) { | 184 | static uint32_t refreshTimer_(uint32_t interval, void *d) { |
100 | refresh_Widget(d); | 185 | refresh_Widget(d); |
101 | return interval; | 186 | return interval; |
@@ -118,8 +203,14 @@ void begin_InputWidget(iInputWidget *d) { | |||
118 | SDL_StartTextInput(); | 203 | SDL_StartTextInput(); |
119 | setFlags_Widget(w, selected_WidgetFlag, iTrue); | 204 | setFlags_Widget(w, selected_WidgetFlag, iTrue); |
120 | refresh_Widget(w); | 205 | refresh_Widget(w); |
121 | d->timer = SDL_AddTimer(REFRESH_INTERVAL, refreshTimer_, d); | 206 | d->timer = SDL_AddTimer(refreshInterval_InputWidget_, refreshTimer_, d); |
122 | d->enterPressed = iFalse; | 207 | d->enterPressed = iFalse; |
208 | if (d->selectAllOnFocus) { | ||
209 | d->mark = (iRanges){ 0, size_Array(&d->text) }; | ||
210 | } | ||
211 | else { | ||
212 | iZap(d->mark); | ||
213 | } | ||
123 | } | 214 | } |
124 | 215 | ||
125 | void end_InputWidget(iInputWidget *d, iBool accept) { | 216 | void end_InputWidget(iInputWidget *d, iBool accept) { |
@@ -159,6 +250,168 @@ static void insertChar_InputWidget_(iInputWidget *d, iChar chr) { | |||
159 | refresh_Widget(as_Widget(d)); | 250 | refresh_Widget(as_Widget(d)); |
160 | } | 251 | } |
161 | 252 | ||
253 | iLocalDef size_t cursorMax_InputWidget_(const iInputWidget *d) { | ||
254 | return iMin(size_Array(&d->text), d->maxLen - 1); | ||
255 | } | ||
256 | |||
257 | iLocalDef iBool isMarking_(void) { | ||
258 | return (SDL_GetModState() & KMOD_SHIFT) != 0; | ||
259 | } | ||
260 | |||
261 | void setCursor_InputWidget(iInputWidget *d, size_t pos) { | ||
262 | if (isEmpty_Array(&d->text)) { | ||
263 | d->cursor = 0; | ||
264 | } | ||
265 | else { | ||
266 | d->cursor = iClamp(pos, 0, cursorMax_InputWidget_(d)); | ||
267 | } | ||
268 | /* Update selection. */ | ||
269 | if (isMarking_()) { | ||
270 | if (isEmpty_Range(&d->mark)) { | ||
271 | d->mark.start = d->lastCursor; | ||
272 | d->mark.end = d->cursor; | ||
273 | } | ||
274 | else { | ||
275 | d->mark.end = d->cursor; | ||
276 | } | ||
277 | } | ||
278 | else { | ||
279 | iZap(d->mark); | ||
280 | } | ||
281 | } | ||
282 | |||
283 | void setSelectAllOnFocus_InputWidget(iInputWidget *d, iBool selectAllOnFocus) { | ||
284 | d->selectAllOnFocus = selectAllOnFocus; | ||
285 | } | ||
286 | |||
287 | static iRanges mark_InputWidget_(const iInputWidget *d) { | ||
288 | return (iRanges){ iMin(d->mark.start, d->mark.end), iMax(d->mark.start, d->mark.end) }; | ||
289 | } | ||
290 | |||
291 | static iBool deleteMarked_InputWidget_(iInputWidget *d) { | ||
292 | const iRanges m = mark_InputWidget_(d); | ||
293 | if (!isEmpty_Range(&m)) { | ||
294 | removeRange_Array(&d->text, m); | ||
295 | setCursor_InputWidget(d, m.start); | ||
296 | iZap(d->mark); | ||
297 | return iTrue; | ||
298 | } | ||
299 | return iFalse; | ||
300 | } | ||
301 | |||
302 | static iBool isWordChar_InputWidget_(const iInputWidget *d, size_t pos) { | ||
303 | const iChar ch = pos < size_Array(&d->text) ? constValue_Array(&d->text, pos, iChar) : ' '; | ||
304 | return isAlphaNumeric_Char(ch); | ||
305 | } | ||
306 | |||
307 | iLocalDef iBool movePos_InputWidget_(const iInputWidget *d, size_t *pos, int dir) { | ||
308 | if (dir < 0) { | ||
309 | if (*pos > 0) (*pos)--; else return iFalse; | ||
310 | } | ||
311 | else { | ||
312 | if (*pos < cursorMax_InputWidget_(d)) (*pos)++; else return iFalse; | ||
313 | } | ||
314 | return iTrue; | ||
315 | } | ||
316 | |||
317 | static size_t skipWord_InputWidget_(const iInputWidget *d, size_t pos, int dir) { | ||
318 | const iBool startedAtNonWord = !isWordChar_InputWidget_(d, pos); | ||
319 | if (!movePos_InputWidget_(d, &pos, dir)) { | ||
320 | return pos; | ||
321 | } | ||
322 | /* Skip any non-word characters at start position. */ | ||
323 | while (!isWordChar_InputWidget_(d, pos)) { | ||
324 | if (!movePos_InputWidget_(d, &pos, dir)) { | ||
325 | return pos; | ||
326 | } | ||
327 | } | ||
328 | if (startedAtNonWord && dir > 0) { | ||
329 | return pos; /* Found the start of a word. */ | ||
330 | } | ||
331 | /* Skip the word. */ | ||
332 | while (isWordChar_InputWidget_(d, pos)) { | ||
333 | if (!movePos_InputWidget_(d, &pos, dir)) { | ||
334 | return pos; | ||
335 | } | ||
336 | } | ||
337 | if (dir > 0) { | ||
338 | /* Skip to the beginning of the word. */ | ||
339 | while (!isWordChar_InputWidget_(d, pos)) { | ||
340 | if (!movePos_InputWidget_(d, &pos, dir)) { | ||
341 | return pos; | ||
342 | } | ||
343 | } | ||
344 | } | ||
345 | else { | ||
346 | movePos_InputWidget_(d, &pos, +1); | ||
347 | } | ||
348 | return pos; | ||
349 | } | ||
350 | |||
351 | static const iChar sensitiveChar_ = 0x25cf; /* black circle */ | ||
352 | |||
353 | static iString *visText_InputWidget_(const iInputWidget *d) { | ||
354 | iString *text; | ||
355 | if (!d->isSensitive) { | ||
356 | text = newUnicodeN_String(constData_Array(&d->text), size_Array(&d->text)); | ||
357 | } | ||
358 | else { | ||
359 | text = new_String(); | ||
360 | for (size_t i = 0; i < size_Array(&d->text); ++i) { | ||
361 | appendChar_String(text, sensitiveChar_); | ||
362 | } | ||
363 | } | ||
364 | return text; | ||
365 | } | ||
366 | |||
367 | iLocalDef iInt2 padding_(void) { | ||
368 | return init_I2(gap_UI / 2, gap_UI / 2); | ||
369 | } | ||
370 | |||
371 | static iInt2 textOrigin_InputWidget_(const iInputWidget *d, const char *visText) { | ||
372 | const iWidget *w = constAs_Widget(d); | ||
373 | iRect bounds = adjusted_Rect(bounds_Widget(w), padding_(), neg_I2(padding_())); | ||
374 | const iInt2 emSize = advance_Text(d->font, "M"); | ||
375 | const int textWidth = advance_Text(d->font, visText).x; | ||
376 | const int cursorX = advanceN_Text(d->font, visText, d->cursor).x; | ||
377 | int xOff = 0; | ||
378 | shrink_Rect(&bounds, init_I2(gap_UI * (flags_Widget(w) & tight_WidgetFlag ? 1 : 2), 0)); | ||
379 | if (d->maxLen == 0) { | ||
380 | if (textWidth > width_Rect(bounds) - emSize.x) { | ||
381 | xOff = width_Rect(bounds) - emSize.x - textWidth; | ||
382 | } | ||
383 | if (cursorX + xOff < width_Rect(bounds) / 2) { | ||
384 | xOff = width_Rect(bounds) / 2 - cursorX; | ||
385 | } | ||
386 | xOff = iMin(xOff, 0); | ||
387 | } | ||
388 | const int yOff = (height_Rect(bounds) - lineHeight_Text(d->font)) / 2; | ||
389 | return add_I2(topLeft_Rect(bounds), init_I2(xOff, yOff)); | ||
390 | } | ||
391 | |||
392 | static size_t coordIndex_InputWidget_(const iInputWidget *d, iInt2 coord) { | ||
393 | iString *visText = visText_InputWidget_(d); | ||
394 | iInt2 pos = sub_I2(coord, textOrigin_InputWidget_(d, cstr_String(visText))); | ||
395 | size_t index = 0; | ||
396 | if (pos.x > 0) { | ||
397 | const char *endPos; | ||
398 | tryAdvanceNoWrap_Text(d->font, range_String(visText), pos.x, &endPos); | ||
399 | if (endPos == constEnd_String(visText)) { | ||
400 | index = cursorMax_InputWidget_(d); | ||
401 | } | ||
402 | else { | ||
403 | /* Need to know the actual character index. */ | ||
404 | /* TODO: tryAdvance could tell us this directly with an extra return value */ | ||
405 | iConstForEach(String, i, visText) { | ||
406 | if (i.pos >= endPos) break; | ||
407 | index++; | ||
408 | } | ||
409 | } | ||
410 | } | ||
411 | delete_String(visText); | ||
412 | return index; | ||
413 | } | ||
414 | |||
162 | static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) { | 415 | static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) { |
163 | iWidget *w = as_Widget(d); | 416 | iWidget *w = as_Widget(d); |
164 | if (isCommand_Widget(w, ev, "focus.gained")) { | 417 | if (isCommand_Widget(w, ev, "focus.gained")) { |
@@ -173,25 +426,51 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) { | |||
173 | case none_ClickResult: | 426 | case none_ClickResult: |
174 | break; | 427 | break; |
175 | case started_ClickResult: | 428 | case started_ClickResult: |
176 | case drag_ClickResult: | 429 | setFocus_Widget(w); |
430 | setCursor_InputWidget(d, coordIndex_InputWidget_(d, pos_Click(&d->click))); | ||
431 | iZap(d->mark); | ||
432 | d->isMarking = iFalse; | ||
433 | return iTrue; | ||
177 | case double_ClickResult: | 434 | case double_ClickResult: |
178 | case aborted_ClickResult: | 435 | case aborted_ClickResult: |
179 | return iTrue; | 436 | return iTrue; |
437 | case drag_ClickResult: | ||
438 | d->cursor = coordIndex_InputWidget_(d, pos_Click(&d->click)); | ||
439 | if (!d->isMarking) { | ||
440 | d->isMarking = iTrue; | ||
441 | d->mark.start = d->cursor; | ||
442 | } | ||
443 | d->mark.end = d->cursor; | ||
444 | refresh_Widget(w); | ||
445 | return iTrue; | ||
180 | case finished_ClickResult: | 446 | case finished_ClickResult: |
181 | setFocus_Widget(as_Widget(d)); | ||
182 | return iTrue; | 447 | return iTrue; |
183 | } | 448 | } |
184 | if (ev->type == SDL_KEYUP && isFocused_Widget(w)) { | 449 | if (ev->type == SDL_KEYUP && isFocused_Widget(w)) { |
185 | return iTrue; | 450 | return iTrue; |
186 | } | 451 | } |
187 | const size_t curMax = iMin(size_Array(&d->text), d->maxLen - 1); | 452 | const size_t curMax = cursorMax_InputWidget_(d); |
188 | if (ev->type == SDL_KEYDOWN && isFocused_Widget(w)) { | 453 | if (ev->type == SDL_KEYDOWN && isFocused_Widget(w)) { |
189 | const int key = ev->key.keysym.sym; | 454 | const int key = ev->key.keysym.sym; |
190 | const int mods = keyMods_Sym(ev->key.keysym.mod); | 455 | const int mods = keyMods_Sym(ev->key.keysym.mod); |
191 | if (mods == KMOD_PRIMARY) { | 456 | if (mods == KMOD_PRIMARY) { |
192 | switch (key) { | 457 | switch (key) { |
458 | case 'c': | ||
459 | case 'x': | ||
460 | if (!isEmpty_Range(&d->mark)) { | ||
461 | const iRanges m = mark_InputWidget_(d); | ||
462 | SDL_SetClipboardText(cstrCollect_String( | ||
463 | newUnicodeN_String(constAt_Array(&d->text, m.start), size_Range(&m)))); | ||
464 | if (key == 'x') { | ||
465 | pushUndo_InputWidget_(d); | ||
466 | deleteMarked_InputWidget_(d); | ||
467 | } | ||
468 | } | ||
469 | return iTrue; | ||
193 | case 'v': | 470 | case 'v': |
194 | if (SDL_HasClipboardText()) { | 471 | if (SDL_HasClipboardText()) { |
472 | pushUndo_InputWidget_(d); | ||
473 | deleteMarked_InputWidget_(d); | ||
195 | char *text = SDL_GetClipboardText(); | 474 | char *text = SDL_GetClipboardText(); |
196 | iString *paste = collect_String(newCStr_String(text)); | 475 | iString *paste = collect_String(newCStr_String(text)); |
197 | SDL_free(text); | 476 | SDL_free(text); |
@@ -200,8 +479,14 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) { | |||
200 | } | 479 | } |
201 | } | 480 | } |
202 | return iTrue; | 481 | return iTrue; |
482 | case 'z': | ||
483 | if (popUndo_InputWidget_(d)) { | ||
484 | refresh_Widget(w); | ||
485 | } | ||
486 | return iTrue; | ||
203 | } | 487 | } |
204 | } | 488 | } |
489 | d->lastCursor = d->cursor; | ||
205 | switch (key) { | 490 | switch (key) { |
206 | case SDLK_RETURN: | 491 | case SDLK_RETURN: |
207 | case SDLK_KP_ENTER: | 492 | case SDLK_KP_ENTER: |
@@ -213,11 +498,18 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) { | |||
213 | setFocus_Widget(NULL); | 498 | setFocus_Widget(NULL); |
214 | return iTrue; | 499 | return iTrue; |
215 | case SDLK_BACKSPACE: | 500 | case SDLK_BACKSPACE: |
216 | if (mods & KMOD_ALT) { | 501 | if (!isEmpty_Range(&d->mark)) { |
217 | clear_Array(&d->text); | 502 | pushUndo_InputWidget_(d); |
218 | d->cursor = 0; | 503 | deleteMarked_InputWidget_(d); |
504 | } | ||
505 | else if (mods & KMOD_ALT) { | ||
506 | pushUndo_InputWidget_(d); | ||
507 | d->mark.start = d->cursor; | ||
508 | d->mark.end = skipWord_InputWidget_(d, d->cursor, -1); | ||
509 | deleteMarked_InputWidget_(d); | ||
219 | } | 510 | } |
220 | else if (d->cursor > 0) { | 511 | else if (d->cursor > 0) { |
512 | pushUndo_InputWidget_(d); | ||
221 | remove_Array(&d->text, --d->cursor); | 513 | remove_Array(&d->text, --d->cursor); |
222 | } | 514 | } |
223 | refresh_Widget(w); | 515 | refresh_Widget(w); |
@@ -225,49 +517,79 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) { | |||
225 | case SDLK_d: | 517 | case SDLK_d: |
226 | if (mods != KMOD_CTRL) break; | 518 | if (mods != KMOD_CTRL) break; |
227 | case SDLK_DELETE: | 519 | case SDLK_DELETE: |
228 | if (d->cursor < size_Array(&d->text)) { | 520 | if (!isEmpty_Range(&d->mark)) { |
521 | pushUndo_InputWidget_(d); | ||
522 | deleteMarked_InputWidget_(d); | ||
523 | } | ||
524 | else if (mods & KMOD_ALT) { | ||
525 | pushUndo_InputWidget_(d); | ||
526 | d->mark.start = d->cursor; | ||
527 | d->mark.end = skipWord_InputWidget_(d, d->cursor, +1); | ||
528 | deleteMarked_InputWidget_(d); | ||
529 | } | ||
530 | else if (d->cursor < size_Array(&d->text)) { | ||
531 | pushUndo_InputWidget_(d); | ||
229 | remove_Array(&d->text, d->cursor); | 532 | remove_Array(&d->text, d->cursor); |
230 | refresh_Widget(w); | ||
231 | } | 533 | } |
534 | refresh_Widget(w); | ||
232 | return iTrue; | 535 | return iTrue; |
233 | case SDLK_k: | 536 | case SDLK_k: |
234 | if (mods == KMOD_CTRL) { | 537 | if (mods == KMOD_CTRL) { |
235 | removeN_Array(&d->text, d->cursor, size_Array(&d->text) - d->cursor); | 538 | if (!isEmpty_Range(&d->mark)) { |
539 | pushUndo_InputWidget_(d); | ||
540 | deleteMarked_InputWidget_(d); | ||
541 | } | ||
542 | else { | ||
543 | pushUndo_InputWidget_(d); | ||
544 | removeN_Array(&d->text, d->cursor, size_Array(&d->text) - d->cursor); | ||
545 | } | ||
236 | refresh_Widget(w); | 546 | refresh_Widget(w); |
237 | return iTrue; | 547 | return iTrue; |
238 | } | 548 | } |
239 | break; | 549 | break; |
240 | case SDLK_HOME: | 550 | case SDLK_HOME: |
241 | case SDLK_END: | 551 | case SDLK_END: |
242 | d->cursor = (key == SDLK_HOME ? 0 : curMax); | 552 | setCursor_InputWidget(d, key == SDLK_HOME ? 0 : curMax); |
243 | refresh_Widget(w); | 553 | refresh_Widget(w); |
244 | return iTrue; | 554 | return iTrue; |
245 | case SDLK_a: | 555 | case SDLK_a: |
556 | #if defined (iPlatformApple) | ||
557 | if (mods == KMOD_PRIMARY) { | ||
558 | d->mark.start = 0; | ||
559 | d->mark.end = curMax; | ||
560 | d->cursor = curMax; | ||
561 | refresh_Widget(w); | ||
562 | return iTrue; | ||
563 | } | ||
564 | #endif | ||
565 | /* fall through for Emacs-style Home/End */ | ||
246 | case SDLK_e: | 566 | case SDLK_e: |
247 | if (mods == KMOD_CTRL) { | 567 | if (mods == KMOD_CTRL || mods == (KMOD_CTRL | KMOD_SHIFT)) { |
248 | d->cursor = (key == 'a' ? 0 : curMax); | 568 | setCursor_InputWidget(d, key == 'a' ? 0 : curMax); |
249 | refresh_Widget(w); | 569 | refresh_Widget(w); |
250 | return iTrue; | 570 | return iTrue; |
251 | } | 571 | } |
252 | break; | 572 | break; |
253 | case SDLK_LEFT: | 573 | case SDLK_LEFT: |
574 | case SDLK_RIGHT: { | ||
575 | const int dir = (key == SDLK_LEFT ? -1 : +1); | ||
254 | if (mods & KMOD_PRIMARY) { | 576 | if (mods & KMOD_PRIMARY) { |
255 | d->cursor = 0; | 577 | setCursor_InputWidget(d, dir < 0 ? 0 : curMax); |
256 | } | 578 | } |
257 | else if (d->cursor > 0) { | 579 | else if (mods & KMOD_ALT) { |
258 | d->cursor--; | 580 | setCursor_InputWidget(d, skipWord_InputWidget_(d, d->cursor, dir)); |
259 | } | 581 | } |
260 | refresh_Widget(w); | 582 | else if (!isMarking_() && !isEmpty_Range(&d->mark)) { |
261 | return iTrue; | 583 | const iRanges m = mark_InputWidget_(d); |
262 | case SDLK_RIGHT: | 584 | setCursor_InputWidget(d, dir < 0 ? m.start : m.end); |
263 | if (mods & KMOD_PRIMARY) { | 585 | iZap(d->mark); |
264 | d->cursor = curMax; | ||
265 | } | 586 | } |
266 | else if (d->cursor < curMax) { | 587 | else if ((dir < 0 && d->cursor > 0) || (dir > 0 && d->cursor < curMax)) { |
267 | d->cursor++; | 588 | setCursor_InputWidget(d, d->cursor + dir); |
268 | } | 589 | } |
269 | refresh_Widget(w); | 590 | refresh_Widget(w); |
270 | return iTrue; | 591 | return iTrue; |
592 | } | ||
271 | case SDLK_TAB: | 593 | case SDLK_TAB: |
272 | /* Allow focus switching. */ | 594 | /* Allow focus switching. */ |
273 | return processEvent_Widget(as_Widget(d), ev); | 595 | return processEvent_Widget(as_Widget(d), ev); |
@@ -278,6 +600,8 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) { | |||
278 | return iTrue; | 600 | return iTrue; |
279 | } | 601 | } |
280 | else if (ev->type == SDL_TEXTINPUT && isFocused_Widget(w)) { | 602 | else if (ev->type == SDL_TEXTINPUT && isFocused_Widget(w)) { |
603 | pushUndo_InputWidget_(d); | ||
604 | deleteMarked_InputWidget_(d); | ||
281 | const iString *uni = collectNewCStr_String(ev->text.text); | 605 | const iString *uni = collectNewCStr_String(ev->text.text); |
282 | iConstForEach(String, i, uni) { | 606 | iConstForEach(String, i, uni) { |
283 | insertChar_InputWidget_(d, i.value); | 607 | insertChar_InputWidget_(d, i.value); |
@@ -287,27 +611,29 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) { | |||
287 | return processEvent_Widget(w, ev); | 611 | return processEvent_Widget(w, ev); |
288 | } | 612 | } |
289 | 613 | ||
290 | static const iChar sensitiveChar_ = 0x25cf; /* black circle */ | 614 | static iBool isWhite_(const iString *str) { |
615 | iConstForEach(String, i, str) { | ||
616 | if (!isSpace_Char(i.value)) { | ||
617 | return iFalse; | ||
618 | } | ||
619 | } | ||
620 | return iTrue; | ||
621 | } | ||
291 | 622 | ||
292 | static void draw_InputWidget_(const iInputWidget *d) { | 623 | static void draw_InputWidget_(const iInputWidget *d) { |
293 | const iWidget *w = constAs_Widget(d); | 624 | const iWidget *w = constAs_Widget(d); |
294 | const uint32_t time = frameTime_Window(get_Window()); | 625 | const uint32_t time = frameTime_Window(get_Window()); |
295 | const iInt2 padding = init_I2(gap_UI / 2, gap_UI / 2); | 626 | iRect bounds = adjusted_Rect(bounds_Widget(w), padding_(), neg_I2(padding_())); |
296 | iRect bounds = adjusted_Rect(bounds_Widget(w), padding, neg_I2(padding)); | 627 | iBool isHint = iFalse; |
297 | const iBool isFocused = isFocused_Widget(w); | 628 | const iBool isFocused = isFocused_Widget(w); |
298 | const iBool isHover = isHover_Widget(w) && | 629 | const iBool isHover = isHover_Widget(w) && |
299 | contains_Widget(w, mouseCoord_Window(get_Window())); | 630 | contains_Widget(w, mouseCoord_Window(get_Window())); |
300 | iPaint p; | 631 | iPaint p; |
301 | init_Paint(&p); | 632 | init_Paint(&p); |
302 | iString text; | 633 | iString *text = visText_InputWidget_(d); |
303 | if (!d->isSensitive) { | 634 | if (isWhite_(text) && !isEmpty_String(&d->hint)) { |
304 | initUnicodeN_String(&text, constData_Array(&d->text), size_Array(&d->text)); | 635 | set_String(text, &d->hint); |
305 | } | 636 | isHint = iTrue; |
306 | else { | ||
307 | init_String(&text); | ||
308 | for (size_t i = 0; i < size_Array(&d->text); ++i) { | ||
309 | appendChar_String(&text, sensitiveChar_); | ||
310 | } | ||
311 | } | 637 | } |
312 | fillRect_Paint( | 638 | fillRect_Paint( |
313 | &p, bounds, isFocused ? uiInputBackgroundFocused_ColorId : uiInputBackground_ColorId); | 639 | &p, bounds, isFocused ? uiInputBackgroundFocused_ColorId : uiInputBackground_ColorId); |
@@ -317,34 +643,27 @@ static void draw_InputWidget_(const iInputWidget *d) { | |||
317 | isFocused ? uiInputFrameFocused_ColorId | 643 | isFocused ? uiInputFrameFocused_ColorId |
318 | : isHover ? uiInputFrameHover_ColorId : uiInputFrame_ColorId); | 644 | : isHover ? uiInputFrameHover_ColorId : uiInputFrame_ColorId); |
319 | setClip_Paint(&p, bounds); | 645 | setClip_Paint(&p, bounds); |
320 | shrink_Rect(&bounds, init_I2(gap_UI * (flags_Widget(w) & tight_WidgetFlag ? 1 : 2), 0)); | 646 | const iInt2 textOrigin = textOrigin_InputWidget_(d, cstr_String(text)); |
321 | const iInt2 emSize = advance_Text(d->font, "M"); | 647 | if (isFocused && !isEmpty_Range(&d->mark)) { |
322 | const int textWidth = advance_Text(d->font, cstr_String(&text)).x; | 648 | /* Draw the selected range. */ |
323 | const int cursorX = advanceN_Text(d->font, cstr_String(&text), d->cursor).x; | 649 | const int m1 = advanceN_Text(d->font, cstr_String(text), d->mark.start).x; |
324 | int xOff = 0; | 650 | const int m2 = advanceN_Text(d->font, cstr_String(text), d->mark.end).x; |
325 | if (d->maxLen == 0) { | 651 | fillRect_Paint(&p, |
326 | if (textWidth > width_Rect(bounds) - emSize.x) { | 652 | (iRect){ addX_I2(textOrigin, iMin(m1, m2)), |
327 | xOff = width_Rect(bounds) - emSize.x - textWidth; | 653 | init_I2(iAbs(m2 - m1), lineHeight_Text(d->font)) }, |
328 | } | 654 | uiMarked_ColorId); |
329 | if (cursorX + xOff < width_Rect(bounds) / 2) { | ||
330 | xOff = width_Rect(bounds) / 2 - cursorX; | ||
331 | } | ||
332 | xOff = iMin(xOff, 0); | ||
333 | } | 655 | } |
334 | const int yOff = (height_Rect(bounds) - lineHeight_Text(d->font)) / 2; | ||
335 | draw_Text(d->font, | 656 | draw_Text(d->font, |
336 | add_I2(topLeft_Rect(bounds), init_I2(xOff, yOff)), | 657 | textOrigin, |
337 | isFocused ? uiInputTextFocused_ColorId : uiInputText_ColorId, | 658 | isHint ? uiAnnotation_ColorId |
659 | : isFocused && !isEmpty_Array(&d->text) ? uiInputTextFocused_ColorId | ||
660 | : uiInputText_ColorId, | ||
338 | "%s", | 661 | "%s", |
339 | cstr_String(&text)); | 662 | cstr_String(text)); |
340 | unsetClip_Paint(&p); | 663 | unsetClip_Paint(&p); |
341 | /* Cursor blinking. */ | 664 | /* Cursor blinking. */ |
342 | if (isFocused && (time & 256)) { | 665 | if (isFocused && (time & 256)) { |
343 | const iInt2 prefixSize = advanceN_Text(d->font, cstr_String(&text), d->cursor); | 666 | iString cur; |
344 | const iInt2 curPos = init_I2(xOff + left_Rect(bounds) + prefixSize.x, | ||
345 | yOff + top_Rect(bounds)); | ||
346 | const iRect curRect = { curPos, addX_I2(emSize, 1) }; | ||
347 | iString cur; | ||
348 | if (d->cursor < size_Array(&d->text)) { | 667 | if (d->cursor < size_Array(&d->text)) { |
349 | if (!d->isSensitive) { | 668 | if (!d->isSensitive) { |
350 | initUnicodeN_String(&cur, constAt_Array(&d->text, d->cursor), 1); | 669 | initUnicodeN_String(&cur, constAt_Array(&d->text, d->cursor), 1); |
@@ -356,11 +675,14 @@ static void draw_InputWidget_(const iInputWidget *d) { | |||
356 | else { | 675 | else { |
357 | initCStr_String(&cur, " "); | 676 | initCStr_String(&cur, " "); |
358 | } | 677 | } |
678 | const iInt2 prefixSize = advanceN_Text(d->font, cstr_String(text), d->cursor); | ||
679 | const iInt2 curPos = addX_I2(textOrigin, prefixSize.x); | ||
680 | const iRect curRect = { curPos, addX_I2(advance_Text(d->font, cstr_String(&cur)), 1) }; | ||
359 | fillRect_Paint(&p, curRect, uiInputCursor_ColorId); | 681 | fillRect_Paint(&p, curRect, uiInputCursor_ColorId); |
360 | draw_Text(d->font, curPos, uiInputCursorText_ColorId, cstr_String(&cur)); | 682 | draw_Text(d->font, curPos, uiInputCursorText_ColorId, cstr_String(&cur)); |
361 | deinit_String(&cur); | 683 | deinit_String(&cur); |
362 | } | 684 | } |
363 | deinit_String(&text); | 685 | delete_String(text); |
364 | } | 686 | } |
365 | 687 | ||
366 | iBeginDefineSubclass(InputWidget, Widget) | 688 | iBeginDefineSubclass(InputWidget, Widget) |
diff --git a/src/ui/inputwidget.h b/src/ui/inputwidget.h index 903e428a..fed9c3d9 100644 --- a/src/ui/inputwidget.h +++ b/src/ui/inputwidget.h | |||
@@ -1,3 +1,25 @@ | |||
1 | /* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi> | ||
2 | |||
3 | Redistribution and use in source and binary forms, with or without | ||
4 | modification, are permitted provided that the following conditions are met: | ||
5 | |||
6 | 1. Redistributions of source code must retain the above copyright notice, this | ||
7 | list of conditions and the following disclaimer. | ||
8 | 2. Redistributions in binary form must reproduce the above copyright notice, | ||
9 | this list of conditions and the following disclaimer in the documentation | ||
10 | and/or other materials provided with the distribution. | ||
11 | |||
12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | ||
13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | ||
14 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||
15 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR | ||
16 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | ||
17 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | ||
18 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON | ||
19 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||
20 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | ||
21 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | ||
22 | |||
1 | #pragma once | 23 | #pragma once |
2 | 24 | ||
3 | #include "widget.h" | 25 | #include "widget.h" |
@@ -10,13 +32,21 @@ enum iInputMode { | |||
10 | overwrite_InputMode, | 32 | overwrite_InputMode, |
11 | }; | 33 | }; |
12 | 34 | ||
35 | void setHint_InputWidget (iInputWidget *, const char *hintText); | ||
13 | void setSensitive_InputWidget(iInputWidget *, iBool isSensitive); | 36 | void setSensitive_InputWidget(iInputWidget *, iBool isSensitive); |
14 | void setMode_InputWidget (iInputWidget *, enum iInputMode mode); | 37 | void setMode_InputWidget (iInputWidget *, enum iInputMode mode); |
15 | void setMaxLen_InputWidget (iInputWidget *, size_t maxLen); | 38 | void setMaxLen_InputWidget (iInputWidget *, size_t maxLen); |
16 | void setText_InputWidget (iInputWidget *, const iString *text); | 39 | void setText_InputWidget (iInputWidget *, const iString *text); |
17 | void setTextCStr_InputWidget (iInputWidget *, const char *cstr); | 40 | void setTextCStr_InputWidget (iInputWidget *, const char *cstr); |
18 | void setCursor_InputWidget (iInputWidget *, size_t pos); | 41 | void setCursor_InputWidget (iInputWidget *, size_t pos); |
42 | void setSelectAllOnFocus_InputWidget(iInputWidget *, iBool selectAllOnFocus); | ||
19 | void begin_InputWidget (iInputWidget *); | 43 | void begin_InputWidget (iInputWidget *); |
20 | void end_InputWidget (iInputWidget *, iBool accept); | 44 | void end_InputWidget (iInputWidget *, iBool accept); |
21 | 45 | ||
22 | const iString * text_InputWidget (const iInputWidget *); | 46 | const iString * text_InputWidget (const iInputWidget *); |
47 | |||
48 | iLocalDef iInputWidget *newHint_InputWidget(size_t maxLen, const char *hint) { | ||
49 | iInputWidget *d = new_InputWidget(maxLen); | ||
50 | setHint_InputWidget(d, hint); | ||
51 | return d; | ||
52 | } | ||
diff --git a/src/ui/labelwidget.c b/src/ui/labelwidget.c index a3daff44..8b2506e7 100644 --- a/src/ui/labelwidget.c +++ b/src/ui/labelwidget.c | |||
@@ -1,3 +1,25 @@ | |||
1 | /* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi> | ||
2 | |||
3 | Redistribution and use in source and binary forms, with or without | ||
4 | modification, are permitted provided that the following conditions are met: | ||
5 | |||
6 | 1. Redistributions of source code must retain the above copyright notice, this | ||
7 | list of conditions and the following disclaimer. | ||
8 | 2. Redistributions in binary form must reproduce the above copyright notice, | ||
9 | this list of conditions and the following disclaimer in the documentation | ||
10 | and/or other materials provided with the distribution. | ||
11 | |||
12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | ||
13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | ||
14 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||
15 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR | ||
16 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | ||
17 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | ||
18 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON | ||
19 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||
20 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | ||
21 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | ||
22 | |||
1 | #include "labelwidget.h" | 23 | #include "labelwidget.h" |
2 | #include "text.h" | 24 | #include "text.h" |
3 | #include "color.h" | 25 | #include "color.h" |
@@ -133,6 +155,9 @@ static void getColors_LabelWidget_(const iLabelWidget *d, int *bg, int *fg, int | |||
133 | *fg = uiText_ColorId; | 155 | *fg = uiText_ColorId; |
134 | *frame1 = isButton ? uiEmboss1_ColorId : uiFrame_ColorId; | 156 | *frame1 = isButton ? uiEmboss1_ColorId : uiFrame_ColorId; |
135 | *frame2 = isButton ? uiEmboss2_ColorId : *frame1; | 157 | *frame2 = isButton ? uiEmboss2_ColorId : *frame1; |
158 | if (flags_Widget(w) & disabled_WidgetFlag && isButton) { | ||
159 | *fg = uiTextDisabled_ColorId; | ||
160 | } | ||
136 | if (isSel) { | 161 | if (isSel) { |
137 | *bg = uiBackgroundSelected_ColorId; | 162 | *bg = uiBackgroundSelected_ColorId; |
138 | *fg = uiTextSelected_ColorId; | 163 | *fg = uiTextSelected_ColorId; |
@@ -150,11 +175,11 @@ static void getColors_LabelWidget_(const iLabelWidget *d, int *bg, int *fg, int | |||
150 | /* Frames matching color escaped text. */ | 175 | /* Frames matching color escaped text. */ |
151 | if (startsWith_String(&d->label, "\r")) { | 176 | if (startsWith_String(&d->label, "\r")) { |
152 | if (isDark_ColorTheme(colorTheme_App())) { | 177 | if (isDark_ColorTheme(colorTheme_App())) { |
153 | *frame1 = cstr_String(&d->label)[1] - '0'; | 178 | *frame1 = cstr_String(&d->label)[1] - asciiBase_ColorEscape; |
154 | *frame2 = darker_Color(*frame1); | 179 | *frame2 = darker_Color(*frame1); |
155 | } | 180 | } |
156 | else { | 181 | else { |
157 | *bg = *frame1 = *frame2 = cstr_String(&d->label)[1] - '0'; | 182 | *bg = *frame1 = *frame2 = cstr_String(&d->label)[1] - asciiBase_ColorEscape; |
158 | *fg = uiBackground_ColorId | permanent_ColorId; | 183 | *fg = uiBackground_ColorId | permanent_ColorId; |
159 | } | 184 | } |
160 | } | 185 | } |
@@ -311,10 +336,20 @@ void setTextCStr_LabelWidget(iLabelWidget *d, const char *text) { | |||
311 | updateSize_LabelWidget(d); | 336 | updateSize_LabelWidget(d); |
312 | } | 337 | } |
313 | 338 | ||
339 | const iString *label_LabelWidget(const iLabelWidget *d) { | ||
340 | return &d->label; | ||
341 | } | ||
342 | |||
314 | const iString *command_LabelWidget(const iLabelWidget *d) { | 343 | const iString *command_LabelWidget(const iLabelWidget *d) { |
315 | return &d->command; | 344 | return &d->command; |
316 | } | 345 | } |
317 | 346 | ||
347 | iLabelWidget *newColor_LabelWidget(const char *text, int color) { | ||
348 | iLabelWidget *d = new_LabelWidget(format_CStr("%s%s", escape_Color(color), text), 0, 0, NULL); | ||
349 | setFlags_Widget(as_Widget(d), frameless_WidgetFlag, iTrue); | ||
350 | return d; | ||
351 | } | ||
352 | |||
318 | iBeginDefineSubclass(LabelWidget, Widget) | 353 | iBeginDefineSubclass(LabelWidget, Widget) |
319 | .processEvent = (iAny *) processEvent_LabelWidget_, | 354 | .processEvent = (iAny *) processEvent_LabelWidget_, |
320 | .draw = (iAny *) draw_LabelWidget_, | 355 | .draw = (iAny *) draw_LabelWidget_, |
diff --git a/src/ui/labelwidget.h b/src/ui/labelwidget.h index aaa897c9..c55ecd08 100644 --- a/src/ui/labelwidget.h +++ b/src/ui/labelwidget.h | |||
@@ -1,3 +1,25 @@ | |||
1 | /* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi> | ||
2 | |||
3 | Redistribution and use in source and binary forms, with or without | ||
4 | modification, are permitted provided that the following conditions are met: | ||
5 | |||
6 | 1. Redistributions of source code must retain the above copyright notice, this | ||
7 | list of conditions and the following disclaimer. | ||
8 | 2. Redistributions in binary form must reproduce the above copyright notice, | ||
9 | this list of conditions and the following disclaimer in the documentation | ||
10 | and/or other materials provided with the distribution. | ||
11 | |||
12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | ||
13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | ||
14 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||
15 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR | ||
16 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | ||
17 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | ||
18 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON | ||
19 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||
20 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | ||
21 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | ||
22 | |||
1 | #pragma once | 23 | #pragma once |
2 | 24 | ||
3 | /* Text label/button. */ | 25 | /* Text label/button. */ |
@@ -16,8 +38,11 @@ void updateSize_LabelWidget (iLabelWidget *); | |||
16 | void updateText_LabelWidget (iLabelWidget *, const iString *text); /* not resized */ | 38 | void updateText_LabelWidget (iLabelWidget *, const iString *text); /* not resized */ |
17 | void updateTextCStr_LabelWidget (iLabelWidget *, const char *text); /* not resized */ | 39 | void updateTextCStr_LabelWidget (iLabelWidget *, const char *text); /* not resized */ |
18 | 40 | ||
41 | const iString *label_LabelWidget (const iLabelWidget *); | ||
19 | const iString *command_LabelWidget (const iLabelWidget *); | 42 | const iString *command_LabelWidget (const iLabelWidget *); |
20 | 43 | ||
44 | iLabelWidget *newColor_LabelWidget(const char *text, int color); | ||
45 | |||
21 | iLocalDef iLabelWidget *newEmpty_LabelWidget(void) { | 46 | iLocalDef iLabelWidget *newEmpty_LabelWidget(void) { |
22 | return new_LabelWidget("", 0, 0, NULL); | 47 | return new_LabelWidget("", 0, 0, NULL); |
23 | } | 48 | } |
diff --git a/src/ui/macos.h b/src/ui/macos.h deleted file mode 100644 index a491e721..00000000 --- a/src/ui/macos.h +++ /dev/null | |||
@@ -1,9 +0,0 @@ | |||
1 | #pragma once | ||
2 | |||
3 | #include "util.h" | ||
4 | |||
5 | /* Platform-specific functionality for macOS */ | ||
6 | |||
7 | void setupApplication_MacOS (void); | ||
8 | void insertMenuItems_MacOS (const char *menuLabel, int atIndex, const iMenuItem *items, size_t count); | ||
9 | void handleCommand_MacOS (const char *cmd); | ||
diff --git a/src/ui/metrics.c b/src/ui/metrics.c index 3c4bfd75..537aaf98 100644 --- a/src/ui/metrics.c +++ b/src/ui/metrics.c | |||
@@ -1,3 +1,25 @@ | |||
1 | /* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi> | ||
2 | |||
3 | Redistribution and use in source and binary forms, with or without | ||
4 | modification, are permitted provided that the following conditions are met: | ||
5 | |||
6 | 1. Redistributions of source code must retain the above copyright notice, this | ||
7 | list of conditions and the following disclaimer. | ||
8 | 2. Redistributions in binary form must reproduce the above copyright notice, | ||
9 | this list of conditions and the following disclaimer in the documentation | ||
10 | and/or other materials provided with the distribution. | ||
11 | |||
12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | ||
13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | ||
14 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||
15 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR | ||
16 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | ||
17 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | ||
18 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON | ||
19 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||
20 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | ||
21 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | ||
22 | |||
1 | #include "metrics.h" | 23 | #include "metrics.h" |
2 | 24 | ||
3 | #include <the_Foundation/math.h> | 25 | #include <the_Foundation/math.h> |
diff --git a/src/ui/metrics.h b/src/ui/metrics.h index 39fc6415..207dd59d 100644 --- a/src/ui/metrics.h +++ b/src/ui/metrics.h | |||
@@ -1,3 +1,25 @@ | |||
1 | /* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi> | ||
2 | |||
3 | Redistribution and use in source and binary forms, with or without | ||
4 | modification, are permitted provided that the following conditions are met: | ||
5 | |||
6 | 1. Redistributions of source code must retain the above copyright notice, this | ||
7 | list of conditions and the following disclaimer. | ||
8 | 2. Redistributions in binary form must reproduce the above copyright notice, | ||
9 | this list of conditions and the following disclaimer in the documentation | ||
10 | and/or other materials provided with the distribution. | ||
11 | |||
12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | ||
13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | ||
14 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||
15 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR | ||
16 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | ||
17 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | ||
18 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON | ||
19 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||
20 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | ||
21 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | ||
22 | |||
1 | #pragma once | 23 | #pragma once |
2 | 24 | ||
3 | #include <the_Foundation/vec2.h> | 25 | #include <the_Foundation/vec2.h> |
diff --git a/src/ui/paint.c b/src/ui/paint.c index 264ca0d8..0166e398 100644 --- a/src/ui/paint.c +++ b/src/ui/paint.c | |||
@@ -1,3 +1,25 @@ | |||
1 | /* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi> | ||
2 | |||
3 | Redistribution and use in source and binary forms, with or without | ||
4 | modification, are permitted provided that the following conditions are met: | ||
5 | |||
6 | 1. Redistributions of source code must retain the above copyright notice, this | ||
7 | list of conditions and the following disclaimer. | ||
8 | 2. Redistributions in binary form must reproduce the above copyright notice, | ||
9 | this list of conditions and the following disclaimer in the documentation | ||
10 | and/or other materials provided with the distribution. | ||
11 | |||
12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | ||
13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | ||
14 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||
15 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR | ||
16 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | ||
17 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | ||
18 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON | ||
19 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||
20 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | ||
21 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | ||
22 | |||
1 | #include "paint.h" | 23 | #include "paint.h" |
2 | 24 | ||
3 | #include <SDL_version.h> | 25 | #include <SDL_version.h> |
diff --git a/src/ui/paint.h b/src/ui/paint.h index 5b29b176..7c9e5e51 100644 --- a/src/ui/paint.h +++ b/src/ui/paint.h | |||
@@ -1,3 +1,25 @@ | |||
1 | /* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi> | ||
2 | |||
3 | Redistribution and use in source and binary forms, with or without | ||
4 | modification, are permitted provided that the following conditions are met: | ||
5 | |||
6 | 1. Redistributions of source code must retain the above copyright notice, this | ||
7 | list of conditions and the following disclaimer. | ||
8 | 2. Redistributions in binary form must reproduce the above copyright notice, | ||
9 | this list of conditions and the following disclaimer in the documentation | ||
10 | and/or other materials provided with the distribution. | ||
11 | |||
12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | ||
13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | ||
14 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||
15 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR | ||
16 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | ||
17 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | ||
18 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON | ||
19 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||
20 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | ||
21 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | ||
22 | |||
1 | #pragma once | 23 | #pragma once |
2 | 24 | ||
3 | #include <the_Foundation/rect.h> | 25 | #include <the_Foundation/rect.h> |
diff --git a/src/ui/scrollwidget.c b/src/ui/scrollwidget.c index 3d8f5eaa..f5340897 100644 --- a/src/ui/scrollwidget.c +++ b/src/ui/scrollwidget.c | |||
@@ -1,3 +1,25 @@ | |||
1 | /* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi> | ||
2 | |||
3 | Redistribution and use in source and binary forms, with or without | ||
4 | modification, are permitted provided that the following conditions are met: | ||
5 | |||
6 | 1. Redistributions of source code must retain the above copyright notice, this | ||
7 | list of conditions and the following disclaimer. | ||
8 | 2. Redistributions in binary form must reproduce the above copyright notice, | ||
9 | this list of conditions and the following disclaimer in the documentation | ||
10 | and/or other materials provided with the distribution. | ||
11 | |||
12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | ||
13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | ||
14 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||
15 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR | ||
16 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | ||
17 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | ||
18 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON | ||
19 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||
20 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | ||
21 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | ||
22 | |||
1 | #include "scrollwidget.h" | 23 | #include "scrollwidget.h" |
2 | #include "paint.h" | 24 | #include "paint.h" |
3 | #include "util.h" | 25 | #include "util.h" |
@@ -39,6 +61,7 @@ static iRect thumbRect_ScrollWidget_(const iScrollWidget *d) { | |||
39 | const int total = size_Range(&d->range); | 61 | const int total = size_Range(&d->range); |
40 | if (total > 0) { | 62 | if (total > 0) { |
41 | const int tsize = thumbSize_ScrollWidget_(d); | 63 | const int tsize = thumbSize_ScrollWidget_(d); |
64 | // iAssert(tsize <= height_Rect(bounds)); | ||
42 | const int tpos = | 65 | const int tpos = |
43 | iClamp((float) d->thumb / (float) total, 0, 1) * (height_Rect(bounds) - tsize); | 66 | iClamp((float) d->thumb / (float) total, 0, 1) * (height_Rect(bounds) - tsize); |
44 | rect.pos.y = bounds.pos.y + tpos; | 67 | rect.pos.y = bounds.pos.y + tpos; |
diff --git a/src/ui/scrollwidget.h b/src/ui/scrollwidget.h index 7b44dced..e6cda03d 100644 --- a/src/ui/scrollwidget.h +++ b/src/ui/scrollwidget.h | |||
@@ -1,3 +1,25 @@ | |||
1 | /* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi> | ||
2 | |||
3 | Redistribution and use in source and binary forms, with or without | ||
4 | modification, are permitted provided that the following conditions are met: | ||
5 | |||
6 | 1. Redistributions of source code must retain the above copyright notice, this | ||
7 | list of conditions and the following disclaimer. | ||
8 | 2. Redistributions in binary form must reproduce the above copyright notice, | ||
9 | this list of conditions and the following disclaimer in the documentation | ||
10 | and/or other materials provided with the distribution. | ||
11 | |||
12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | ||
13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | ||
14 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||
15 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR | ||
16 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | ||
17 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | ||
18 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON | ||
19 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||
20 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | ||
21 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | ||
22 | |||
1 | #pragma once | 23 | #pragma once |
2 | 24 | ||
3 | #include "widget.h" | 25 | #include "widget.h" |
diff --git a/src/ui/sidebarwidget.c b/src/ui/sidebarwidget.c index 71b641d4..40c3f55e 100644 --- a/src/ui/sidebarwidget.c +++ b/src/ui/sidebarwidget.c | |||
@@ -1,16 +1,41 @@ | |||
1 | /* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi> | ||
2 | |||
3 | Redistribution and use in source and binary forms, with or without | ||
4 | modification, are permitted provided that the following conditions are met: | ||
5 | |||
6 | 1. Redistributions of source code must retain the above copyright notice, this | ||
7 | list of conditions and the following disclaimer. | ||
8 | 2. Redistributions in binary form must reproduce the above copyright notice, | ||
9 | this list of conditions and the following disclaimer in the documentation | ||
10 | and/or other materials provided with the distribution. | ||
11 | |||
12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | ||
13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | ||
14 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||
15 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR | ||
16 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | ||
17 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | ||
18 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON | ||
19 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||
20 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | ||
21 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | ||
22 | |||
1 | #include "sidebarwidget.h" | 23 | #include "sidebarwidget.h" |
2 | #include "labelwidget.h" | 24 | |
3 | #include "scrollwidget.h" | 25 | #include "app.h" |
26 | #include "bookmarks.h" | ||
27 | #include "command.h" | ||
4 | #include "documentwidget.h" | 28 | #include "documentwidget.h" |
29 | #include "gmcerts.h" | ||
30 | #include "gmdocument.h" | ||
5 | #include "inputwidget.h" | 31 | #include "inputwidget.h" |
6 | #include "bookmarks.h" | 32 | #include "labelwidget.h" |
7 | #include "paint.h" | 33 | #include "paint.h" |
34 | #include "scrollwidget.h" | ||
8 | #include "util.h" | 35 | #include "util.h" |
9 | #include "command.h" | 36 | #include "visited.h" |
10 | #include "../gmdocument.h" | ||
11 | #include "app.h" | ||
12 | 37 | ||
13 | #include <the_Foundation/array.h> | 38 | #include <the_Foundation/stringarray.h> |
14 | #include <SDL_clipboard.h> | 39 | #include <SDL_clipboard.h> |
15 | #include <SDL_mouse.h> | 40 | #include <SDL_mouse.h> |
16 | 41 | ||
@@ -23,6 +48,8 @@ struct Impl_SidebarItem { | |||
23 | iString label; | 48 | iString label; |
24 | iString meta; | 49 | iString meta; |
25 | iString url; | 50 | iString url; |
51 | iBool isSeparator; | ||
52 | iBool isSelected; | ||
26 | }; | 53 | }; |
27 | 54 | ||
28 | void init_SidebarItem(iSidebarItem *d) { | 55 | void init_SidebarItem(iSidebarItem *d) { |
@@ -32,6 +59,8 @@ void init_SidebarItem(iSidebarItem *d) { | |||
32 | init_String(&d->label); | 59 | init_String(&d->label); |
33 | init_String(&d->meta); | 60 | init_String(&d->meta); |
34 | init_String(&d->url); | 61 | init_String(&d->url); |
62 | d->isSeparator = iFalse; | ||
63 | d->isSelected = iFalse; | ||
35 | } | 64 | } |
36 | 65 | ||
37 | void deinit_SidebarItem(iSidebarItem *d) { | 66 | void deinit_SidebarItem(iSidebarItem *d) { |
@@ -49,6 +78,7 @@ struct Impl_SidebarWidget { | |||
49 | enum iSidebarMode mode; | 78 | enum iSidebarMode mode; |
50 | iScrollWidget *scroll; | 79 | iScrollWidget *scroll; |
51 | int scrollY; | 80 | int scrollY; |
81 | int modeScroll[max_SidebarMode]; | ||
52 | int width; | 82 | int width; |
53 | iLabelWidget *modeButtons[max_SidebarMode]; | 83 | iLabelWidget *modeButtons[max_SidebarMode]; |
54 | int itemHeight; | 84 | int itemHeight; |
@@ -141,12 +171,7 @@ static void updateItems_SidebarWidget_(iSidebarWidget *d) { | |||
141 | item.icon = bm->icon; | 171 | item.icon = bm->icon; |
142 | set_String(&item.url, &bm->url); | 172 | set_String(&item.url, &bm->url); |
143 | set_String(&item.label, &bm->title); | 173 | set_String(&item.label, &bm->title); |
144 | // iDate date; | ||
145 | // init_Date(&date, &bm->when); | ||
146 | // iString *ds = format_Date(&date, "%Y %b %d"); | ||
147 | // set_String(&item.meta, ds); | ||
148 | set_String(&item.meta, &bm->tags); | 174 | set_String(&item.meta, &bm->tags); |
149 | // delete_String(ds); | ||
150 | pushBack_Array(&d->items, &item); | 175 | pushBack_Array(&d->items, &item); |
151 | } | 176 | } |
152 | d->menu = makeMenu_Widget( | 177 | d->menu = makeMenu_Widget( |
@@ -158,8 +183,97 @@ static void updateItems_SidebarWidget_(iSidebarWidget *d) { | |||
158 | 4); | 183 | 4); |
159 | break; | 184 | break; |
160 | } | 185 | } |
161 | case history_SidebarMode: | 186 | case history_SidebarMode: { |
187 | iDate on; | ||
188 | initCurrent_Date(&on); | ||
189 | const int thisYear = on.year; | ||
190 | iConstForEach(PtrArray, i, list_Visited(visited_App(), 200)) { | ||
191 | const iVisitedUrl *visit = i.ptr; | ||
192 | iSidebarItem item; | ||
193 | init_SidebarItem(&item); | ||
194 | set_String(&item.url, &visit->url); | ||
195 | iDate date; | ||
196 | init_Date(&date, &visit->when); | ||
197 | if (date.day != on.day || date.month != on.month || date.year != on.year) { | ||
198 | on = date; | ||
199 | /* Date separator. */ | ||
200 | iSidebarItem sep; | ||
201 | init_SidebarItem(&sep); | ||
202 | sep.isSeparator = iTrue; | ||
203 | set_String(&sep.meta, | ||
204 | collect_String(format_Date( | ||
205 | &date, date.year != thisYear ? "%b %d %Y" : "%b %d"))); | ||
206 | pushBack_Array(&d->items, &sep); | ||
207 | /* Date separators are two items tall. */ | ||
208 | init_SidebarItem(&sep); | ||
209 | sep.isSeparator = iTrue; | ||
210 | pushBack_Array(&d->items, &sep); | ||
211 | } | ||
212 | pushBack_Array(&d->items, &item); | ||
213 | } | ||
214 | d->menu = makeMenu_Widget( | ||
215 | as_Widget(d), | ||
216 | (iMenuItem[]){ | ||
217 | { "Copy URL", 0, 0, "history.copy" }, | ||
218 | { "Add Bookmark...", 0, 0, "history.addbookmark" }, | ||
219 | { "---", 0, 0, NULL }, | ||
220 | { "Remove URL", 0, 0, "history.delete" }, | ||
221 | { "---", 0, 0, NULL }, | ||
222 | { uiTextCaution_ColorEscape "Clear History...", 0, 0, "history.clear confirm:1" }, | ||
223 | }, 6); | ||
224 | break; | ||
225 | } | ||
226 | case identities_SidebarMode: { | ||
227 | const iString *tabUrl = url_DocumentWidget(document_App()); | ||
228 | iConstForEach(PtrArray, i, identities_GmCerts(certs_App())) { | ||
229 | const iGmIdentity *ident = i.ptr; | ||
230 | iSidebarItem item; | ||
231 | init_SidebarItem(&item); | ||
232 | item.id = index_PtrArrayConstIterator(&i); | ||
233 | item.icon = ident->icon; | ||
234 | set_String(&item.label, collect_String(subject_TlsCertificate(ident->cert))); | ||
235 | iDate until; | ||
236 | validUntil_TlsCertificate(ident->cert, &until); | ||
237 | const iBool isActive = isUsedOn_GmIdentity(ident, tabUrl); | ||
238 | format_String( | ||
239 | &item.meta, | ||
240 | "%s", | ||
241 | isActive ? "Using" | ||
242 | : isUsed_GmIdentity(ident) | ||
243 | ? format_CStr("Used on %zu URLs", size_StringSet(ident->useUrls)) | ||
244 | : "Not used"); | ||
245 | const char *expiry = | ||
246 | ident->flags & temporary_GmIdentityFlag | ||
247 | ? "Temporary" | ||
248 | : cstrCollect_String(format_Date(&until, "Expires %b %d, %Y")); | ||
249 | if (isEmpty_String(&ident->notes)) { | ||
250 | appendFormat_String(&item.meta, "\n%s", expiry); | ||
251 | } | ||
252 | else { | ||
253 | appendFormat_String(&item.meta, | ||
254 | " \u2014 %s\n%s%s", | ||
255 | expiry, | ||
256 | escape_Color(uiHeading_ColorId), | ||
257 | cstr_String(&ident->notes)); | ||
258 | } | ||
259 | item.isSelected = isActive; | ||
260 | pushBack_Array(&d->items, &item); | ||
261 | } | ||
262 | const iMenuItem menuItems[] = { | ||
263 | { "Use on This Page", 0, 0, "ident.use arg:1" }, | ||
264 | { "Stop Using This Page", 0, 0, "ident.use arg:0" }, | ||
265 | { "Stop Using Everywhere", 0, 0, "ident.use arg:0 clear:1" }, | ||
266 | { "Show Usage", 0, 0, "ident.showuse" }, | ||
267 | { "---", 0, 0, NULL }, | ||
268 | { "Edit Notes...", 0, 0, "ident.edit" }, | ||
269 | { "Pick Icon...", 0, 0, "ident.pickicon" }, | ||
270 | { "---", 0, 0, NULL }, | ||
271 | { "Reveal Files", 0, 0, "ident.reveal" }, | ||
272 | { uiTextCaution_ColorEscape "Delete Identity...", 0, 0, "ident.delete confirm:1" }, | ||
273 | }; | ||
274 | d->menu = makeMenu_Widget(as_Widget(d), menuItems, iElemCount(menuItems)); | ||
162 | break; | 275 | break; |
276 | } | ||
163 | default: | 277 | default: |
164 | break; | 278 | break; |
165 | } | 279 | } |
@@ -167,14 +281,35 @@ static void updateItems_SidebarWidget_(iSidebarWidget *d) { | |||
167 | invalidate_SidebarWidget_(d); | 281 | invalidate_SidebarWidget_(d); |
168 | } | 282 | } |
169 | 283 | ||
284 | static void scroll_SidebarWidget_(iSidebarWidget *d, int offset) { | ||
285 | const int oldScroll = d->scrollY; | ||
286 | d->scrollY += offset; | ||
287 | if (d->scrollY < 0) { | ||
288 | d->scrollY = 0; | ||
289 | } | ||
290 | const int scrollMax = scrollMax_SidebarWidget_(d); | ||
291 | d->scrollY = iMin(d->scrollY, scrollMax); | ||
292 | if (oldScroll != d->scrollY) { | ||
293 | d->hoverItem = iInvalidPos; | ||
294 | updateVisible_SidebarWidget_(d); | ||
295 | invalidate_SidebarWidget_(d); | ||
296 | } | ||
297 | } | ||
298 | |||
170 | void setMode_SidebarWidget(iSidebarWidget *d, enum iSidebarMode mode) { | 299 | void setMode_SidebarWidget(iSidebarWidget *d, enum iSidebarMode mode) { |
171 | if (d->mode == mode) return; | 300 | if (d->mode == mode) return; |
301 | if (d->mode >= 0 && d->mode < max_SidebarMode) { | ||
302 | d->modeScroll[d->mode] = d->scrollY; /* saved for later */ | ||
303 | } | ||
172 | d->mode = mode; | 304 | d->mode = mode; |
173 | for (enum iSidebarMode i = 0; i < max_SidebarMode; i++) { | 305 | for (enum iSidebarMode i = 0; i < max_SidebarMode; i++) { |
174 | setFlags_Widget(as_Widget(d->modeButtons[i]), selected_WidgetFlag, i == d->mode); | 306 | setFlags_Widget(as_Widget(d->modeButtons[i]), selected_WidgetFlag, i == d->mode); |
175 | } | 307 | } |
176 | const float heights[max_SidebarMode] = { 1.333f, 3, 3, 1.2f }; | 308 | const float heights[max_SidebarMode] = { 1.333f, 1.333f, 3.5f, 1.2f }; |
177 | d->itemHeight = heights[mode] * lineHeight_Text(uiContent_FontId); | 309 | d->itemHeight = heights[mode] * lineHeight_Text(uiContent_FontId); |
310 | invalidate_SidebarWidget_(d); | ||
311 | /* Restore previous scroll position. */ | ||
312 | d->scrollY = d->modeScroll[mode]; | ||
178 | } | 313 | } |
179 | 314 | ||
180 | enum iSidebarMode mode_SidebarWidget(const iSidebarWidget *d) { | 315 | enum iSidebarMode mode_SidebarWidget(const iSidebarWidget *d) { |
@@ -209,8 +344,9 @@ void init_SidebarWidget(iSidebarWidget *d) { | |||
209 | resizeWidthOfChildren_WidgetFlag | collapse_WidgetFlag, | 344 | resizeWidthOfChildren_WidgetFlag | collapse_WidgetFlag, |
210 | iTrue); | 345 | iTrue); |
211 | d->scrollY = 0; | 346 | d->scrollY = 0; |
212 | d->mode = -1; | 347 | iZap(d->modeScroll); |
213 | d->width = 75 * gap_UI; | 348 | d->mode = -1; |
349 | d->width = 60 * gap_UI; | ||
214 | init_Array(&d->items, sizeof(iSidebarItem)); | 350 | init_Array(&d->items, sizeof(iSidebarItem)); |
215 | d->hoverItem = iInvalidPos; | 351 | d->hoverItem = iInvalidPos; |
216 | init_Click(&d->click, d, SDL_BUTTON_LEFT); | 352 | init_Click(&d->click, d, SDL_BUTTON_LEFT); |
@@ -220,7 +356,7 @@ void init_SidebarWidget(iSidebarWidget *d) { | |||
220 | d->modeButtons[i] = addChildFlags_Widget( | 356 | d->modeButtons[i] = addChildFlags_Widget( |
221 | w, | 357 | w, |
222 | iClob( | 358 | iClob( |
223 | new_LabelWidget(normalModeLabels_[i], 0, 0, format_CStr("sidebar.mode arg:%d", i))), | 359 | new_LabelWidget(tightModeLabels_[i], 0, 0, format_CStr("sidebar.mode arg:%d", i))), |
224 | frameless_WidgetFlag | expand_WidgetFlag); | 360 | frameless_WidgetFlag | expand_WidgetFlag); |
225 | d->maxButtonLabelWidth = | 361 | d->maxButtonLabelWidth = |
226 | iMaxi(d->maxButtonLabelWidth, | 362 | iMaxi(d->maxButtonLabelWidth, |
@@ -270,7 +406,53 @@ static size_t itemIndex_SidebarWidget_(const iSidebarWidget *d, iInt2 pos) { | |||
270 | return index; | 406 | return index; |
271 | } | 407 | } |
272 | 408 | ||
409 | static const iSidebarItem *constHoverItem_SidebarWidget_(const iSidebarWidget *d) { | ||
410 | if (d->hoverItem < size_Array(&d->items)) { | ||
411 | return constAt_Array(&d->items, d->hoverItem); | ||
412 | } | ||
413 | return NULL; | ||
414 | } | ||
415 | |||
416 | static iSidebarItem *hoverItem_SidebarWidget_(iSidebarWidget *d) { | ||
417 | if (d->hoverItem < size_Array(&d->items)) { | ||
418 | return at_Array(&d->items, d->hoverItem); | ||
419 | } | ||
420 | return NULL; | ||
421 | } | ||
422 | |||
423 | static const iGmIdentity *constHoverIdentity_SidebarWidget_(const iSidebarWidget *d) { | ||
424 | if (d->mode == identities_SidebarMode) { | ||
425 | const iSidebarItem *hoverItem = constHoverItem_SidebarWidget_(d); | ||
426 | if (hoverItem) { | ||
427 | return identity_GmCerts(certs_App(), hoverItem->id); | ||
428 | } | ||
429 | } | ||
430 | return NULL; | ||
431 | } | ||
432 | |||
433 | static iGmIdentity *hoverIdentity_SidebarWidget_(const iSidebarWidget *d) { | ||
434 | return iConstCast(iGmIdentity *, constHoverIdentity_SidebarWidget_(d)); | ||
435 | } | ||
436 | |||
437 | static void setHoverItem_SidebarWidget_(iSidebarWidget *d, size_t index) { | ||
438 | if (index < size_Array(&d->items)) { | ||
439 | if (constValue_Array(&d->items, index, iSidebarItem).isSeparator) { | ||
440 | index = iInvalidPos; | ||
441 | } | ||
442 | } | ||
443 | if (d->hoverItem != index) { | ||
444 | d->hoverItem = index; | ||
445 | invalidate_SidebarWidget_(d); | ||
446 | } | ||
447 | } | ||
448 | |||
449 | static void updateMouseHover_SidebarWidget_(iSidebarWidget *d) { | ||
450 | const iInt2 mouse = mouseCoord_Window(get_Window()); | ||
451 | setHoverItem_SidebarWidget_(d, itemIndex_SidebarWidget_(d, mouse)); | ||
452 | } | ||
453 | |||
273 | static void itemClicked_SidebarWidget_(iSidebarWidget *d, size_t index) { | 454 | static void itemClicked_SidebarWidget_(iSidebarWidget *d, size_t index) { |
455 | setFocus_Widget(NULL); | ||
274 | const iSidebarItem *item = constAt_Array(&d->items, index); | 456 | const iSidebarItem *item = constAt_Array(&d->items, index); |
275 | switch (d->mode) { | 457 | switch (d->mode) { |
276 | case documentOutline_SidebarMode: { | 458 | case documentOutline_SidebarMode: { |
@@ -279,25 +461,30 @@ static void itemClicked_SidebarWidget_(iSidebarWidget *d, size_t index) { | |||
279 | postCommandf_App("document.goto loc:%p", head->text.start); | 461 | postCommandf_App("document.goto loc:%p", head->text.start); |
280 | break; | 462 | break; |
281 | } | 463 | } |
282 | case bookmarks_SidebarMode: { | 464 | case bookmarks_SidebarMode: |
283 | postCommandf_App("open url:%s", cstr_String(&item->url)); | 465 | case history_SidebarMode: { |
466 | if (!isEmpty_String(&item->url)) { | ||
467 | postCommandf_App("open url:%s", cstr_String(&item->url)); | ||
468 | } | ||
284 | break; | 469 | break; |
285 | } | 470 | } |
286 | } | 471 | case identities_SidebarMode: { |
287 | } | 472 | iGmIdentity *ident = hoverIdentity_SidebarWidget_(d); |
288 | 473 | if (ident) { | |
289 | static void scroll_SidebarWidget_(iSidebarWidget *d, int offset) { | 474 | const iString *tabUrl = url_DocumentWidget(document_App()); |
290 | const int oldScroll = d->scrollY; | 475 | if (isUsedOn_GmIdentity(ident, tabUrl)) { |
291 | d->scrollY += offset; | 476 | signOut_GmCerts(certs_App(), tabUrl); |
292 | if (d->scrollY < 0) { | 477 | } |
293 | d->scrollY = 0; | 478 | else { |
294 | } | 479 | signIn_GmCerts(certs_App(), ident, tabUrl); |
295 | const int scrollMax = scrollMax_SidebarWidget_(d); | 480 | } |
296 | d->scrollY = iMin(d->scrollY, scrollMax); | 481 | updateItems_SidebarWidget_(d); |
297 | if (oldScroll != d->scrollY) { | 482 | updateMouseHover_SidebarWidget_(d); |
298 | d->hoverItem = iInvalidPos; | 483 | } |
299 | updateVisible_SidebarWidget_(d); | 484 | break; |
300 | invalidate_SidebarWidget_(d); | 485 | } |
486 | default: | ||
487 | break; | ||
301 | } | 488 | } |
302 | } | 489 | } |
303 | 490 | ||
@@ -316,13 +503,6 @@ static void checkModeButtonLayout_SidebarWidget_(iSidebarWidget *d) { | |||
316 | } | 503 | } |
317 | } | 504 | } |
318 | 505 | ||
319 | static iSidebarItem *hoverItem_SidebarWidget_(iSidebarWidget *d) { | ||
320 | if (d->hoverItem < size_Array(&d->items)) { | ||
321 | return at_Array(&d->items, d->hoverItem); | ||
322 | } | ||
323 | return NULL; | ||
324 | } | ||
325 | |||
326 | void setWidth_SidebarWidget(iSidebarWidget *d, int width) { | 506 | void setWidth_SidebarWidget(iSidebarWidget *d, int width) { |
327 | iWidget *w = as_Widget(d); | 507 | iWidget *w = as_Widget(d); |
328 | width = iMax(30 * gap_UI, width); | 508 | width = iMax(30 * gap_UI, width); |
@@ -339,15 +519,18 @@ void setWidth_SidebarWidget(iSidebarWidget *d, int width) { | |||
339 | } | 519 | } |
340 | 520 | ||
341 | iBool handleBookmarkEditorCommands_SidebarWidget_(iWidget *editor, const char *cmd) { | 521 | iBool handleBookmarkEditorCommands_SidebarWidget_(iWidget *editor, const char *cmd) { |
342 | iSidebarWidget *d = findWidget_App("sidebar"); | ||
343 | if (equal_Command(cmd, "bmed.accept") || equal_Command(cmd, "cancel")) { | 522 | if (equal_Command(cmd, "bmed.accept") || equal_Command(cmd, "cancel")) { |
523 | iSidebarWidget *d = findWidget_App("sidebar"); | ||
344 | if (equal_Command(cmd, "bmed.accept")) { | 524 | if (equal_Command(cmd, "bmed.accept")) { |
525 | const iString *title = text_InputWidget(findChild_Widget(editor, "bmed.title")); | ||
526 | const iString *url = text_InputWidget(findChild_Widget(editor, "bmed.url")); | ||
527 | const iString *tags = text_InputWidget(findChild_Widget(editor, "bmed.tags")); | ||
345 | const iSidebarItem *item = hoverItem_SidebarWidget_(d); | 528 | const iSidebarItem *item = hoverItem_SidebarWidget_(d); |
346 | iAssert(item); /* hover item cannot have been changed */ | 529 | iAssert(item); /* hover item cannot have been changed */ |
347 | iBookmark *bm = get_Bookmarks(bookmarks_App(), item->id); | 530 | iBookmark *bm = get_Bookmarks(bookmarks_App(), item->id); |
348 | set_String(&bm->title, text_InputWidget(findChild_Widget(editor, "bmed.title"))); | 531 | set_String(&bm->title, title); |
349 | set_String(&bm->url, text_InputWidget(findChild_Widget(editor, "bmed.url"))); | 532 | set_String(&bm->url, url); |
350 | set_String(&bm->tags, text_InputWidget(findChild_Widget(editor, "bmed.tags"))); | 533 | set_String(&bm->tags, tags); |
351 | postCommand_App("bookmarks.changed"); | 534 | postCommand_App("bookmarks.changed"); |
352 | } | 535 | } |
353 | setFlags_Widget(as_Widget(d), disabled_WidgetFlag, iFalse); | 536 | setFlags_Widget(as_Widget(d), disabled_WidgetFlag, iFalse); |
@@ -399,11 +582,15 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev) | |||
399 | setMode_SidebarWidget(d, arg_Command(cmd)); | 582 | setMode_SidebarWidget(d, arg_Command(cmd)); |
400 | updateItems_SidebarWidget_(d); | 583 | updateItems_SidebarWidget_(d); |
401 | if (argLabel_Command(cmd, "show") && !isVisible_Widget(w)) { | 584 | if (argLabel_Command(cmd, "show") && !isVisible_Widget(w)) { |
402 | postCommand_App("sidebar.toggle"); | 585 | postCommand_App("sidebar.toggle arg:1"); |
403 | } | 586 | } |
587 | scroll_SidebarWidget_(d, 0); | ||
404 | return iTrue; | 588 | return iTrue; |
405 | } | 589 | } |
406 | else if (equal_Command(cmd, "sidebar.toggle")) { | 590 | else if (equal_Command(cmd, "sidebar.toggle")) { |
591 | if (arg_Command(cmd) && isVisible_Widget(w)) { | ||
592 | return iTrue; | ||
593 | } | ||
407 | setFlags_Widget(w, hidden_WidgetFlag, isVisible_Widget(w)); | 594 | setFlags_Widget(w, hidden_WidgetFlag, isVisible_Widget(w)); |
408 | if (isVisible_Widget(w)) { | 595 | if (isVisible_Widget(w)) { |
409 | w->rect.size.x = d->width; | 596 | w->rect.size.x = d->width; |
@@ -421,7 +608,6 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev) | |||
421 | return iTrue; | 608 | return iTrue; |
422 | } | 609 | } |
423 | else if (equal_Command(cmd, "tabs.changed") || equal_Command(cmd, "document.changed")) { | 610 | else if (equal_Command(cmd, "tabs.changed") || equal_Command(cmd, "document.changed")) { |
424 | d->scrollY = 0; | ||
425 | updateItems_SidebarWidget_(d); | 611 | updateItems_SidebarWidget_(d); |
426 | } | 612 | } |
427 | else if (equal_Command(cmd, "theme.changed")) { | 613 | else if (equal_Command(cmd, "theme.changed")) { |
@@ -455,25 +641,156 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev) | |||
455 | } | 641 | } |
456 | return iTrue; | 642 | return iTrue; |
457 | } | 643 | } |
458 | else if (equal_Command(cmd, "bookmarks.changed")) { | 644 | else if (equal_Command(cmd, "bookmarks.changed") && d->mode == bookmarks_SidebarMode) { |
645 | updateItems_SidebarWidget_(d); | ||
646 | } | ||
647 | else if (equal_Command(cmd, "idents.changed") && d->mode == identities_SidebarMode) { | ||
648 | updateItems_SidebarWidget_(d); | ||
649 | } | ||
650 | else if (isCommand_Widget(w, ev, "ident.use")) { | ||
651 | iGmIdentity * ident = hoverIdentity_SidebarWidget_(d); | ||
652 | const iString *tabUrl = url_DocumentWidget(document_App()); | ||
653 | if (ident) { | ||
654 | if (argLabel_Command(cmd, "clear")) { | ||
655 | clearUse_GmIdentity(ident); | ||
656 | } | ||
657 | else if (arg_Command(cmd)) { | ||
658 | signIn_GmCerts(certs_App(), ident, tabUrl); | ||
659 | } | ||
660 | else { | ||
661 | signOut_GmCerts(certs_App(), tabUrl); | ||
662 | } | ||
663 | updateItems_SidebarWidget_(d); | ||
664 | } | ||
665 | return iTrue; | ||
666 | } | ||
667 | else if (isCommand_Widget(w, ev, "ident.showuse")) { | ||
668 | const iGmIdentity *ident = constHoverIdentity_SidebarWidget_(d); | ||
669 | if (ident) { | ||
670 | makeMessage_Widget(uiHeading_ColorEscape "IDENTITY USAGE", | ||
671 | cstrCollect_String(joinCStr_StringSet(ident->useUrls, "\n"))); | ||
672 | } | ||
673 | return iTrue; | ||
674 | } | ||
675 | else if (isCommand_Widget(w, ev, "ident.edit")) { | ||
676 | const iGmIdentity *ident = constHoverIdentity_SidebarWidget_(d); | ||
677 | if (ident) { | ||
678 | makeValueInput_Widget(get_Window()->root, | ||
679 | &ident->notes, | ||
680 | uiHeading_ColorEscape "IDENTITY NOTES", | ||
681 | format_CStr("Notes about %s:", cstr_String(name_GmIdentity(ident))), | ||
682 | uiTextAction_ColorEscape "OK", | ||
683 | format_CStr("ident.setnotes ident:%p", ident)); | ||
684 | } | ||
685 | return iTrue; | ||
686 | } | ||
687 | else if (equal_Command(cmd, "ident.setnotes")) { | ||
688 | iGmIdentity *ident = pointerLabel_Command(cmd, "ident"); | ||
689 | if (ident) { | ||
690 | setCStr_String(&ident->notes, suffixPtr_Command(cmd, "value")); | ||
691 | updateItems_SidebarWidget_(d); | ||
692 | } | ||
693 | return iTrue; | ||
694 | } | ||
695 | else if (isCommand_Widget(w, ev, "ident.pickicon")) { | ||
696 | return iTrue; | ||
697 | } | ||
698 | else if (isCommand_Widget(w, ev, "ident.reveal")) { | ||
699 | const iGmIdentity *ident = constHoverIdentity_SidebarWidget_(d); | ||
700 | if (ident) { | ||
701 | const iString *crtPath = certificatePath_GmCerts(certs_App(), ident); | ||
702 | if (crtPath) { | ||
703 | revealPath_App(crtPath); | ||
704 | } | ||
705 | } | ||
706 | return iTrue; | ||
707 | } | ||
708 | else if (equal_Command(cmd, "ident.delete")) { | ||
709 | iSidebarItem *item = hoverItem_SidebarWidget_(d); | ||
710 | if (argLabel_Command(cmd, "confirm")) { | ||
711 | makeQuestion_Widget(uiTextCaution_ColorEscape "DELETE IDENTITY", | ||
712 | format_CStr("Do you really want to delete the identity\n" | ||
713 | uiTextAction_ColorEscape "%s\n" | ||
714 | uiText_ColorEscape | ||
715 | "including its certificate and private key files?", | ||
716 | cstr_String(&item->label)), | ||
717 | (const char *[]){ "Cancel", | ||
718 | uiTextCaution_ColorEscape | ||
719 | "Delete Identity and Files" }, | ||
720 | (const char *[]){ "cancel", "ident.delete confirm:0" }, | ||
721 | 2); | ||
722 | return iTrue; | ||
723 | } | ||
724 | deleteIdentity_GmCerts(certs_App(), hoverIdentity_SidebarWidget_(d)); | ||
459 | updateItems_SidebarWidget_(d); | 725 | updateItems_SidebarWidget_(d); |
726 | return iTrue; | ||
727 | } | ||
728 | else if (equal_Command(cmd, "history.delete")) { | ||
729 | const iSidebarItem *item = hoverItem_SidebarWidget_(d); | ||
730 | if (item && !isEmpty_String(&item->url)) { | ||
731 | removeUrl_Visited(visited_App(), &item->url); | ||
732 | updateItems_SidebarWidget_(d); | ||
733 | scroll_SidebarWidget_(d, 0); | ||
734 | } | ||
735 | return iTrue; | ||
736 | } | ||
737 | else if (equal_Command(cmd, "history.copy")) { | ||
738 | const iSidebarItem *item = hoverItem_SidebarWidget_(d); | ||
739 | if (item && !isEmpty_String(&item->url)) { | ||
740 | SDL_SetClipboardText(cstr_String(&item->url)); | ||
741 | } | ||
742 | return iTrue; | ||
743 | } | ||
744 | else if (equal_Command(cmd, "history.addbookmark")) { | ||
745 | const iSidebarItem *item = hoverItem_SidebarWidget_(d); | ||
746 | if (!isEmpty_String(&item->url)) { | ||
747 | makeBookmarkCreation_Widget( | ||
748 | &item->url, | ||
749 | collect_String(newRange_String(urlHost_String(&item->url))), | ||
750 | 0x1f310 /* globe */); | ||
751 | } | ||
752 | } | ||
753 | else if (equal_Command(cmd, "history.clear")) { | ||
754 | if (argLabel_Command(cmd, "confirm")) { | ||
755 | makeQuestion_Widget( | ||
756 | uiTextCaution_ColorEscape "CLEAR HISTORY", | ||
757 | "Do you really want to erase the history of all visited pages?", | ||
758 | (const char *[]){ "Cancel", uiTextCaution_ColorEscape "Clear History" }, | ||
759 | (const char *[]){ "cancel", "history.clear confirm:0" }, | ||
760 | 2); | ||
761 | } | ||
762 | else { | ||
763 | clear_Visited(visited_App()); | ||
764 | updateItems_SidebarWidget_(d); | ||
765 | scroll_SidebarWidget_(d, 0); | ||
766 | } | ||
767 | return iTrue; | ||
460 | } | 768 | } |
461 | } | 769 | } |
462 | if (ev->type == SDL_MOUSEMOTION && !isVisible_Widget(d->menu)) { | 770 | if (ev->type == SDL_MOUSEMOTION && !isVisible_Widget(d->menu)) { |
463 | const iInt2 mouse = init_I2(ev->motion.x, ev->motion.y); | 771 | const iInt2 mouse = init_I2(ev->motion.x, ev->motion.y); |
464 | size_t hover = iInvalidPos; | 772 | size_t hover = iInvalidPos; |
465 | if (contains_Widget(d->resizer, mouse)) { | 773 | if (contains_Widget(d->resizer, mouse)) { |
466 | SDL_SetCursor(d->resizeCursor); | 774 | setCursor_Window(get_Window(), SDL_SYSTEM_CURSOR_SIZEWE); |
467 | } | 775 | } |
468 | else { | 776 | else if (contains_Widget(constAs_Widget(d->scroll), mouse)) { |
469 | SDL_SetCursor(NULL); | 777 | setCursor_Window(get_Window(), SDL_SYSTEM_CURSOR_ARROW); |
470 | if (contains_Widget(w, mouse)) { | ||
471 | hover = itemIndex_SidebarWidget_(d, mouse); | ||
472 | } | ||
473 | } | 778 | } |
474 | if (hover != d->hoverItem) { | 779 | else if (contains_Widget(w, mouse)) { |
475 | d->hoverItem = hover; | 780 | hover = itemIndex_SidebarWidget_(d, mouse); |
476 | invalidate_SidebarWidget_(d); | 781 | } |
782 | setHoverItem_SidebarWidget_(d, hover); | ||
783 | /* Update cursor. */ | ||
784 | if (contains_Widget(w, mouse) && !contains_Widget(d->resizer, mouse) && | ||
785 | !contains_Widget(constAs_Widget(d->scroll), mouse)) { | ||
786 | const iSidebarItem *item = constHoverItem_SidebarWidget_(d); | ||
787 | if (item && d->mode != identities_SidebarMode) { | ||
788 | setCursor_Window(get_Window(), item->isSeparator ? SDL_SYSTEM_CURSOR_ARROW | ||
789 | : SDL_SYSTEM_CURSOR_HAND); | ||
790 | } | ||
791 | else { | ||
792 | setCursor_Window(get_Window(), SDL_SYSTEM_CURSOR_ARROW); | ||
793 | } | ||
477 | } | 794 | } |
478 | } | 795 | } |
479 | if (ev->type == SDL_MOUSEWHEEL && isHover_Widget(w)) { | 796 | if (ev->type == SDL_MOUSEWHEEL && isHover_Widget(w)) { |
@@ -486,10 +803,42 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev) | |||
486 | return iTrue; | 803 | return iTrue; |
487 | } | 804 | } |
488 | if (d->menu && ev->type == SDL_MOUSEBUTTONDOWN) { | 805 | if (d->menu && ev->type == SDL_MOUSEBUTTONDOWN) { |
489 | if (d->hoverItem != iInvalidPos || isVisible_Widget(d->menu)) { | 806 | if (ev->button.button == SDL_BUTTON_RIGHT) { |
490 | processContextMenuEvent_Widget(d->menu, ev, {}); | 807 | if (!isVisible_Widget(d->menu)) { |
808 | setHoverItem_SidebarWidget_( | ||
809 | d, itemIndex_SidebarWidget_(d, init_I2(ev->button.x, ev->button.y))); | ||
810 | } | ||
811 | if (d->hoverItem != iInvalidPos || isVisible_Widget(d->menu)) { | ||
812 | /* Update menu items. */ | ||
813 | if (d->mode == identities_SidebarMode) { | ||
814 | const iGmIdentity *ident = constHoverIdentity_SidebarWidget_(d); | ||
815 | const iString * docUrl = url_DocumentWidget(document_App()); | ||
816 | iForEach(ObjectList, i, children_Widget(d->menu)) { | ||
817 | if (isInstance_Object(i.object, &Class_LabelWidget)) { | ||
818 | iLabelWidget *menuItem = i.object; | ||
819 | const char * cmdItem = cstr_String(command_LabelWidget(menuItem)); | ||
820 | if (equal_Command(cmdItem, "ident.use")) { | ||
821 | const iBool cmdUse = arg_Command(cmdItem) != 0; | ||
822 | const iBool cmdClear = argLabel_Command(cmdItem, "clear") != 0; | ||
823 | setFlags_Widget( | ||
824 | as_Widget(menuItem), | ||
825 | disabled_WidgetFlag, | ||
826 | (cmdClear && !isUsed_GmIdentity(ident)) || | ||
827 | (!cmdClear && cmdUse && isUsedOn_GmIdentity(ident, docUrl)) || | ||
828 | (!cmdClear && !cmdUse && !isUsedOn_GmIdentity(ident, docUrl))); | ||
829 | } | ||
830 | else if (equal_Command(cmdItem, "ident.showuse")) { | ||
831 | setFlags_Widget(as_Widget(menuItem), | ||
832 | disabled_WidgetFlag, | ||
833 | !isUsed_GmIdentity(ident)); | ||
834 | } | ||
835 | } | ||
836 | } | ||
837 | } | ||
838 | } | ||
491 | } | 839 | } |
492 | } | 840 | } |
841 | processContextMenuEvent_Widget(d->menu, ev, {}); | ||
493 | switch (processEvent_Click(&d->click, ev)) { | 842 | switch (processEvent_Click(&d->click, ev)) { |
494 | case started_ClickResult: | 843 | case started_ClickResult: |
495 | invalidate_SidebarWidget_(d); | 844 | invalidate_SidebarWidget_(d); |
@@ -524,8 +873,8 @@ static void allocVisBuffer_SidebarWidget_(iSidebarWidget *d) { | |||
524 | } | 873 | } |
525 | 874 | ||
526 | static void draw_SidebarWidget_(const iSidebarWidget *d) { | 875 | static void draw_SidebarWidget_(const iSidebarWidget *d) { |
527 | const iWidget *w = constAs_Widget(d); | 876 | const iWidget *w = constAs_Widget(d); |
528 | const iRect bounds = contentBounds_SidebarWidget_(d); | 877 | const iRect bounds = contentBounds_SidebarWidget_(d); |
529 | const iBool isPressing = d->click.isActive && contains_Rect(bounds, pos_Click(&d->click)); | 878 | const iBool isPressing = d->click.isActive && contains_Rect(bounds, pos_Click(&d->click)); |
530 | iPaint p; | 879 | iPaint p; |
531 | init_Paint(&p); | 880 | init_Paint(&p); |
@@ -545,9 +894,12 @@ static void draw_SidebarWidget_(const iSidebarWidget *d) { | |||
545 | for (size_t i = visRange.start; i < visRange.end; i++) { | 894 | for (size_t i = visRange.start; i < visRange.end; i++) { |
546 | const iSidebarItem *item = constAt_Array(&d->items, i); | 895 | const iSidebarItem *item = constAt_Array(&d->items, i); |
547 | const iRect itemRect = { pos, init_I2(width_Rect(bufBounds), d->itemHeight) }; | 896 | const iRect itemRect = { pos, init_I2(width_Rect(bufBounds), d->itemHeight) }; |
548 | const iBool isHover = (d->hoverItem == i); | 897 | const iBool isHover = isHover_Widget(w) && (d->hoverItem == i); |
898 | const int iconColor = | ||
899 | isHover ? (isPressing ? uiTextPressed_ColorId : uiIconHover_ColorId) | ||
900 | : uiIcon_ColorId; | ||
549 | setClip_Paint(&p, intersect_Rect(itemRect, bufBounds)); | 901 | setClip_Paint(&p, intersect_Rect(itemRect, bufBounds)); |
550 | if (isHover) { | 902 | if (isHover && !item->isSeparator) { |
551 | fillRect_Paint(&p, | 903 | fillRect_Paint(&p, |
552 | itemRect, | 904 | itemRect, |
553 | isPressing ? uiBackgroundPressed_ColorId | 905 | isPressing ? uiBackgroundPressed_ColorId |
@@ -576,8 +928,7 @@ static void draw_SidebarWidget_(const iSidebarWidget *d) { | |||
576 | font, | 928 | font, |
577 | iconArea, | 929 | iconArea, |
578 | iTrue, | 930 | iTrue, |
579 | isHover ? (isPressing ? uiTextPressed_ColorId : uiIconHover_ColorId) | 931 | iconColor, |
580 | : uiIcon_ColorId, | ||
581 | "%s", | 932 | "%s", |
582 | cstr_String(&str)); | 933 | cstr_String(&str)); |
583 | deinit_String(&str); | 934 | deinit_String(&str); |
@@ -585,6 +936,81 @@ static void draw_SidebarWidget_(const iSidebarWidget *d) { | |||
585 | (d->itemHeight - lineHeight_Text(font)) / 2); | 936 | (d->itemHeight - lineHeight_Text(font)) / 2); |
586 | drawRange_Text(font, textPos, fg, range_String(&item->label)); | 937 | drawRange_Text(font, textPos, fg, range_String(&item->label)); |
587 | } | 938 | } |
939 | else if (d->mode == history_SidebarMode) { | ||
940 | iBeginCollect(); | ||
941 | const int fg = isHover ? (isPressing ? uiTextPressed_ColorId | ||
942 | : uiTextFramelessHover_ColorId) | ||
943 | : uiText_ColorId; | ||
944 | if (item->isSeparator) { | ||
945 | if (!isEmpty_String(&item->meta)) { | ||
946 | unsetClip_Paint(&p); | ||
947 | iInt2 drawPos = addY_I2(topLeft_Rect(itemRect), d->itemHeight * 0.666f); | ||
948 | drawHLine_Paint( | ||
949 | &p, drawPos, width_Rect(itemRect), uiIcon_ColorId); | ||
950 | drawRange_Text( | ||
951 | default_FontId, | ||
952 | add_I2(drawPos, | ||
953 | init_I2(3 * gap_UI, | ||
954 | (d->itemHeight - lineHeight_Text(default_FontId)) / 2)), | ||
955 | uiIcon_ColorId, | ||
956 | range_String(&item->meta)); | ||
957 | } | ||
958 | } | ||
959 | else { | ||
960 | iUrl parts; | ||
961 | init_Url(&parts, &item->url); | ||
962 | const iBool isGemini = equalCase_Rangecc(parts.scheme, "gemini"); | ||
963 | draw_Text( | ||
964 | font, | ||
965 | add_I2(topLeft_Rect(itemRect), | ||
966 | init_I2(3 * gap_UI, (d->itemHeight - lineHeight_Text(font)) / 2)), | ||
967 | fg, | ||
968 | "%s%s%s%s%s%s", | ||
969 | isGemini ? "" : cstr_Rangecc(parts.scheme), | ||
970 | isGemini ? "" : "://", | ||
971 | escape_Color(isHover ? (isPressing ? uiTextPressed_ColorId | ||
972 | : uiTextFramelessHover_ColorId) | ||
973 | : uiTextStrong_ColorId), | ||
974 | cstr_Rangecc(parts.host), | ||
975 | escape_Color(fg), | ||
976 | cstr_Rangecc(parts.path)); | ||
977 | } | ||
978 | iEndCollect(); | ||
979 | } | ||
980 | else if (d->mode == identities_SidebarMode) { | ||
981 | const int fg = isHover ? (isPressing ? uiTextPressed_ColorId | ||
982 | : uiTextFramelessHover_ColorId) | ||
983 | : uiTextStrong_ColorId; | ||
984 | if (item->isSelected) { | ||
985 | drawRectThickness_Paint(&p, | ||
986 | adjusted_Rect(itemRect, zero_I2(), init_I2(-2, -1)), | ||
987 | gap_UI / 4, | ||
988 | isHover && isPressing ? uiTextPressed_ColorId | ||
989 | : uiIcon_ColorId); | ||
990 | } | ||
991 | iString icon; | ||
992 | initUnicodeN_String(&icon, &item->icon, 1); | ||
993 | iInt2 cPos = topLeft_Rect(itemRect); | ||
994 | addv_I2(&cPos, | ||
995 | init_I2(3 * gap_UI, | ||
996 | (d->itemHeight - lineHeight_Text(default_FontId) * 2 - | ||
997 | lineHeight_Text(font)) / | ||
998 | 2)); | ||
999 | const int metaFg = | ||
1000 | isHover ? permanent_ColorId | (isPressing ? uiTextPressed_ColorId | ||
1001 | : uiTextFramelessHover_ColorId) | ||
1002 | : uiText_ColorId; | ||
1003 | drawRange_Text( | ||
1004 | font, cPos, item->isSelected ? iconColor : metaFg, range_String(&icon)); | ||
1005 | deinit_String(&icon); | ||
1006 | drawRange_Text(font, add_I2(cPos, init_I2(6 * gap_UI, 0)), | ||
1007 | fg, range_String(&item->label)); | ||
1008 | drawRange_Text( | ||
1009 | default_FontId, | ||
1010 | add_I2(cPos, init_I2(6 * gap_UI, lineHeight_Text(font))), | ||
1011 | metaFg, | ||
1012 | range_String(&item->meta)); | ||
1013 | } | ||
588 | unsetClip_Paint(&p); | 1014 | unsetClip_Paint(&p); |
589 | pos.y += d->itemHeight; | 1015 | pos.y += d->itemHeight; |
590 | } | 1016 | } |
diff --git a/src/ui/sidebarwidget.h b/src/ui/sidebarwidget.h index 3ff20b7f..7ccc64dd 100644 --- a/src/ui/sidebarwidget.h +++ b/src/ui/sidebarwidget.h | |||
@@ -1,3 +1,25 @@ | |||
1 | /* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi> | ||
2 | |||
3 | Redistribution and use in source and binary forms, with or without | ||
4 | modification, are permitted provided that the following conditions are met: | ||
5 | |||
6 | 1. Redistributions of source code must retain the above copyright notice, this | ||
7 | list of conditions and the following disclaimer. | ||
8 | 2. Redistributions in binary form must reproduce the above copyright notice, | ||
9 | this list of conditions and the following disclaimer in the documentation | ||
10 | and/or other materials provided with the distribution. | ||
11 | |||
12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | ||
13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | ||
14 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||
15 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR | ||
16 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | ||
17 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | ||
18 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON | ||
19 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||
20 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | ||
21 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | ||
22 | |||
1 | #pragma once | 23 | #pragma once |
2 | 24 | ||
3 | #include "widget.h" | 25 | #include "widget.h" |
diff --git a/src/ui/text.c b/src/ui/text.c index 1e702eee..836d540f 100644 --- a/src/ui/text.c +++ b/src/ui/text.c | |||
@@ -1,3 +1,25 @@ | |||
1 | /* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi> | ||
2 | |||
3 | Redistribution and use in source and binary forms, with or without | ||
4 | modification, are permitted provided that the following conditions are met: | ||
5 | |||
6 | 1. Redistributions of source code must retain the above copyright notice, this | ||
7 | list of conditions and the following disclaimer. | ||
8 | 2. Redistributions in binary form must reproduce the above copyright notice, | ||
9 | this list of conditions and the following disclaimer in the documentation | ||
10 | and/or other materials provided with the distribution. | ||
11 | |||
12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | ||
13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | ||
14 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||
15 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR | ||
16 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | ||
17 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | ||
18 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON | ||
19 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||
20 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | ||
21 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | ||
22 | |||
1 | #include "text.h" | 23 | #include "text.h" |
2 | #include "color.h" | 24 | #include "color.h" |
3 | #include "metrics.h" | 25 | #include "metrics.h" |
@@ -11,6 +33,7 @@ | |||
11 | #include <the_Foundation/file.h> | 33 | #include <the_Foundation/file.h> |
12 | #include <the_Foundation/hash.h> | 34 | #include <the_Foundation/hash.h> |
13 | #include <the_Foundation/math.h> | 35 | #include <the_Foundation/math.h> |
36 | #include <the_Foundation/stringlist.h> | ||
14 | #include <the_Foundation/regexp.h> | 37 | #include <the_Foundation/regexp.h> |
15 | #include <the_Foundation/path.h> | 38 | #include <the_Foundation/path.h> |
16 | #include <the_Foundation/vec2.h> | 39 | #include <the_Foundation/vec2.h> |
@@ -25,6 +48,7 @@ iDeclareTypeConstructionArgs(Glyph, iChar ch) | |||
25 | 48 | ||
26 | int gap_Text; /* cf. gap_UI in metrics.h */ | 49 | int gap_Text; /* cf. gap_UI in metrics.h */ |
27 | int enableHalfPixelGlyphs_Text = iTrue; /* debug setting */ | 50 | int enableHalfPixelGlyphs_Text = iTrue; /* debug setting */ |
51 | int enableKerning_Text = iTrue; /* looking up kern pairs is slow */ | ||
28 | 52 | ||
29 | struct Impl_Glyph { | 53 | struct Impl_Glyph { |
30 | iHashNode node; | 54 | iHashNode node; |
@@ -62,6 +86,7 @@ struct Impl_Font { | |||
62 | int baseline; | 86 | int baseline; |
63 | iHash glyphs; | 87 | iHash glyphs; |
64 | iBool isMonospaced; | 88 | iBool isMonospaced; |
89 | iBool manualKernOnly; | ||
65 | enum iFontId symbolsFont; /* font to use for symbols */ | 90 | enum iFontId symbolsFont; /* font to use for symbols */ |
66 | }; | 91 | }; |
67 | 92 | ||
@@ -120,7 +145,7 @@ static void initFonts_Text_(iText *d) { | |||
120 | int symbolsFont; | 145 | int symbolsFont; |
121 | } fontData[max_FontId] = { | 146 | } fontData[max_FontId] = { |
122 | { &fontSourceSansProRegular_Embedded, fontSize_UI, defaultSymbols_FontId }, | 147 | { &fontSourceSansProRegular_Embedded, fontSize_UI, defaultSymbols_FontId }, |
123 | { &fontSourceSansProRegular_Embedded, fontSize_UI * 1.150f, defaultMediumSymbols_FontId }, | 148 | { &fontSourceSansProRegular_Embedded, fontSize_UI * 1.125f, defaultMediumSymbols_FontId }, |
124 | { &fontFiraMonoRegular_Embedded, fontSize_UI * 0.866f, defaultSymbols_FontId }, | 149 | { &fontFiraMonoRegular_Embedded, fontSize_UI * 0.866f, defaultSymbols_FontId }, |
125 | { &fontFiraSansRegular_Embedded, textSize, symbols_FontId }, | 150 | { &fontFiraSansRegular_Embedded, textSize, symbols_FontId }, |
126 | { &fontFiraMonoRegular_Embedded, textSize * 0.866f, smallSymbols_FontId }, | 151 | { &fontFiraMonoRegular_Embedded, textSize * 0.866f, smallSymbols_FontId }, |
@@ -133,14 +158,14 @@ static void initFonts_Text_(iText *d) { | |||
133 | { &fontFiraSansBold_Embedded, textSize * 2.000f, hugeSymbols_FontId }, | 158 | { &fontFiraSansBold_Embedded, textSize * 2.000f, hugeSymbols_FontId }, |
134 | { &fontFiraSansLight_Embedded, textSize * 1.666f, largeSymbols_FontId }, | 159 | { &fontFiraSansLight_Embedded, textSize * 1.666f, largeSymbols_FontId }, |
135 | { &fontSymbola_Embedded, fontSize_UI, defaultSymbols_FontId }, | 160 | { &fontSymbola_Embedded, fontSize_UI, defaultSymbols_FontId }, |
136 | { &fontSymbola_Embedded, fontSize_UI * 1.150f, defaultMediumSymbols_FontId }, | 161 | { &fontSymbola_Embedded, fontSize_UI * 1.125f, defaultMediumSymbols_FontId }, |
137 | { &fontSymbola_Embedded, textSize, symbols_FontId }, | 162 | { &fontSymbola_Embedded, textSize, symbols_FontId }, |
138 | { &fontSymbola_Embedded, textSize * 1.333f, mediumSymbols_FontId }, | 163 | { &fontSymbola_Embedded, textSize * 1.333f, mediumSymbols_FontId }, |
139 | { &fontSymbola_Embedded, textSize * 1.666f, largeSymbols_FontId }, | 164 | { &fontSymbola_Embedded, textSize * 1.666f, largeSymbols_FontId }, |
140 | { &fontSymbola_Embedded, textSize * 2.000f, hugeSymbols_FontId }, | 165 | { &fontSymbola_Embedded, textSize * 2.000f, hugeSymbols_FontId }, |
141 | { &fontSymbola_Embedded, textSize * 0.866f, smallSymbols_FontId }, | 166 | { &fontSymbola_Embedded, textSize * 0.866f, smallSymbols_FontId }, |
142 | { &fontNotoEmojiRegular_Embedded, fontSize_UI, defaultSymbols_FontId }, | 167 | { &fontNotoEmojiRegular_Embedded, fontSize_UI, defaultSymbols_FontId }, |
143 | { &fontNotoEmojiRegular_Embedded, fontSize_UI * 1.150f, defaultMediumSymbols_FontId }, | 168 | { &fontNotoEmojiRegular_Embedded, fontSize_UI * 1.125f, defaultMediumSymbols_FontId }, |
144 | { &fontNotoEmojiRegular_Embedded, textSize, symbols_FontId }, | 169 | { &fontNotoEmojiRegular_Embedded, textSize, symbols_FontId }, |
145 | { &fontNotoEmojiRegular_Embedded, textSize * 1.333f, mediumSymbols_FontId }, | 170 | { &fontNotoEmojiRegular_Embedded, textSize * 1.333f, mediumSymbols_FontId }, |
146 | { &fontNotoEmojiRegular_Embedded, textSize * 1.666f, largeSymbols_FontId }, | 171 | { &fontNotoEmojiRegular_Embedded, textSize * 1.666f, largeSymbols_FontId }, |
@@ -153,6 +178,9 @@ static void initFonts_Text_(iText *d) { | |||
153 | if (fontData[i].ttf == &fontFiraMonoRegular_Embedded) { | 178 | if (fontData[i].ttf == &fontFiraMonoRegular_Embedded) { |
154 | font->isMonospaced = iTrue; | 179 | font->isMonospaced = iTrue; |
155 | } | 180 | } |
181 | if (i == default_FontId || i == defaultMedium_FontId) { | ||
182 | font->manualKernOnly = iTrue; | ||
183 | } | ||
156 | } | 184 | } |
157 | gap_Text = iRound(gap_UI * d->contentFontSize); | 185 | gap_Text = iRound(gap_UI * d->contentFontSize); |
158 | } | 186 | } |
@@ -287,8 +315,8 @@ static void cache_Font_(iFont *d, iGlyph *glyph, int hoff) { | |||
287 | /* Rasterize the glyph using stbtt. */ { | 315 | /* Rasterize the glyph using stbtt. */ { |
288 | surface = rasterizeGlyph_Font_(d, ch, hoff * 0.5f); | 316 | surface = rasterizeGlyph_Font_(d, ch, hoff * 0.5f); |
289 | if (hoff == 0) { | 317 | if (hoff == 0) { |
290 | int adv, lsb; | 318 | int adv; |
291 | stbtt_GetCodepointHMetrics(&d->font, ch, &adv, &lsb); | 319 | stbtt_GetCodepointHMetrics(&d->font, ch, &adv, NULL); |
292 | glyph->advance = d->scale * adv; | 320 | glyph->advance = d->scale * adv; |
293 | } | 321 | } |
294 | stbtt_GetCodepointBitmapBoxSubpixel(&d->font, | 322 | stbtt_GetCodepointBitmapBoxSubpixel(&d->font, |
@@ -406,6 +434,7 @@ static iRect run_Font_(iFont *d, enum iRunMode mode, iRangecc text, size_t maxLe | |||
406 | /* ANSI escape. */ | 434 | /* ANSI escape. */ |
407 | chPos++; | 435 | chPos++; |
408 | iRegExpMatch m; | 436 | iRegExpMatch m; |
437 | init_RegExpMatch(&m); | ||
409 | if (match_RegExp(text_.ansiEscape, chPos, text.end - chPos, &m)) { | 438 | if (match_RegExp(text_.ansiEscape, chPos, text.end - chPos, &m)) { |
410 | if (mode == draw_RunMode) { | 439 | if (mode == draw_RunMode) { |
411 | /* Change the color. */ | 440 | /* Change the color. */ |
@@ -431,7 +460,7 @@ static iRect run_Font_(iFont *d, enum iRunMode mode, iRangecc text, size_t maxLe | |||
431 | if (ch == '\r') { | 460 | if (ch == '\r') { |
432 | const iChar esc = nextChar_(&chPos, text.end); | 461 | const iChar esc = nextChar_(&chPos, text.end); |
433 | if (mode == draw_RunMode) { | 462 | if (mode == draw_RunMode) { |
434 | const iColor clr = get_Color(esc - '0'); | 463 | const iColor clr = get_Color(esc - asciiBase_ColorEscape); |
435 | SDL_SetTextureColorMod(text_.cache, clr.r, clr.g, clr.b); | 464 | SDL_SetTextureColorMod(text_.cache, clr.r, clr.g, clr.b); |
436 | } | 465 | } |
437 | prevCh = 0; | 466 | prevCh = 0; |
@@ -488,9 +517,11 @@ static iRect run_Font_(iFont *d, enum iRunMode mode, iRangecc text, size_t maxLe | |||
488 | /* Manual kerning for double-slash. */ | 517 | /* Manual kerning for double-slash. */ |
489 | xpos -= glyph->rect[hoff].size.x * 0.5f; | 518 | xpos -= glyph->rect[hoff].size.x * 0.5f; |
490 | } | 519 | } |
491 | else if (next) { | 520 | #if defined (LAGRANGE_ENABLE_KERNING) |
521 | else if (enableKerning_Text && !d->manualKernOnly && next) { | ||
492 | xpos += d->scale * stbtt_GetCodepointKernAdvance(&d->font, ch, next); | 522 | xpos += d->scale * stbtt_GetCodepointKernAdvance(&d->font, ch, next); |
493 | } | 523 | } |
524 | #endif | ||
494 | } | 525 | } |
495 | prevCh = ch; | 526 | prevCh = ch; |
496 | if (--maxLen == 0) { | 527 | if (--maxLen == 0) { |
@@ -658,6 +689,111 @@ SDL_Texture *glyphCache_Text(void) { | |||
658 | return text_.cache; | 689 | return text_.cache; |
659 | } | 690 | } |
660 | 691 | ||
692 | static void freeBitmap_(void *ptr) { | ||
693 | stbtt_FreeBitmap(ptr, NULL); | ||
694 | } | ||
695 | |||
696 | iString *renderBlockChars_Text(const iBlock *fontData, int height, enum iTextBlockMode mode, | ||
697 | const iString *text) { | ||
698 | iBeginCollect(); | ||
699 | stbtt_fontinfo font; | ||
700 | iZap(font); | ||
701 | stbtt_InitFont(&font, constData_Block(fontData), 0); | ||
702 | int ascent; | ||
703 | stbtt_GetFontVMetrics(&font, &ascent, NULL, NULL); | ||
704 | iDeclareType(CharBuf); | ||
705 | struct Impl_CharBuf { | ||
706 | uint8_t *pixels; | ||
707 | iInt2 size; | ||
708 | int dy; | ||
709 | int advance; | ||
710 | }; | ||
711 | iArray * chars = collectNew_Array(sizeof(iCharBuf)); | ||
712 | int pxRatio = (mode == quadrants_TextBlockMode ? 2 : 1); | ||
713 | int pxHeight = height * pxRatio; | ||
714 | const float scale = stbtt_ScaleForPixelHeight(&font, pxHeight); | ||
715 | const float xScale = scale * 2; /* character aspect ratio */ | ||
716 | const int baseline = ascent * scale; | ||
717 | int width = 0; | ||
718 | size_t strRemain = length_String(text); | ||
719 | iConstForEach(String, i, text) { | ||
720 | if (!strRemain) break; | ||
721 | if (i.value == variationSelectorEmoji_Char) { | ||
722 | strRemain--; | ||
723 | continue; | ||
724 | } | ||
725 | iCharBuf buf; | ||
726 | buf.pixels = stbtt_GetCodepointBitmap( | ||
727 | &font, xScale, scale, i.value, &buf.size.x, &buf.size.y, 0, &buf.dy); | ||
728 | stbtt_GetCodepointHMetrics(&font, i.value, &buf.advance, NULL); | ||
729 | buf.advance *= xScale; | ||
730 | if (!isSpace_Char(i.value)) { | ||
731 | if (mode == quadrants_TextBlockMode) { | ||
732 | buf.advance = (buf.size.x - 1) / 2 * 2 + 2; | ||
733 | } | ||
734 | else { | ||
735 | buf.advance = buf.size.x + 1; | ||
736 | } | ||
737 | } | ||
738 | pushBack_Array(chars, &buf); | ||
739 | collect_Garbage(buf.pixels, freeBitmap_); | ||
740 | width += buf.advance; | ||
741 | strRemain--; | ||
742 | } | ||
743 | const size_t len = (mode == quadrants_TextBlockMode ? height * ((width + 1) / 2 + 1) | ||
744 | : (height * (width + 1))); | ||
745 | iChar *outBuf = iCollectMem(malloc(sizeof(iChar) * len)); | ||
746 | for (size_t i = 0; i < len; ++i) { | ||
747 | outBuf[i] = 0x20; | ||
748 | } | ||
749 | iChar *outPos = outBuf; | ||
750 | for (int y = 0; y < pxHeight; y += pxRatio) { | ||
751 | const iCharBuf *ch = constData_Array(chars); | ||
752 | int lx = 0; | ||
753 | for (int x = 0; x < width; x += pxRatio, lx += pxRatio) { | ||
754 | if (lx >= ch->advance) { | ||
755 | ch++; | ||
756 | lx = 0; | ||
757 | } | ||
758 | const int ly = y - baseline - ch->dy; | ||
759 | if (mode == quadrants_TextBlockMode) { | ||
760 | #define checkPixel_(offx, offy) \ | ||
761 | (lx + offx < ch->size.x && ly + offy < ch->size.y && ly + offy >= 0 ? \ | ||
762 | ch->pixels[(lx + offx) + (ly + offy) * ch->size.x] > 155 \ | ||
763 | : iFalse) | ||
764 | const int mask = (checkPixel_(0, 0) ? 1 : 0) | | ||
765 | (checkPixel_(1, 0) ? 2 : 0) | | ||
766 | (checkPixel_(0, 1) ? 4 : 0) | | ||
767 | (checkPixel_(1, 1) ? 8 : 0); | ||
768 | #undef checkPixel_ | ||
769 | static const iChar blocks[16] = { 0x0020, 0x2598, 0x259D, 0x2580, 0x2596, 0x258C, | ||
770 | 0x259E, 0x259B, 0x2597, 0x259A, 0x2590, 0x259C, | ||
771 | 0x2584, 0x2599, 0x259F, 0x2588 }; | ||
772 | *outPos++ = blocks[mask]; | ||
773 | } | ||
774 | else { | ||
775 | static const iChar shades[5] = { 0x0020, 0x2591, 0x2592, 0x2593, 0x2588 }; | ||
776 | *outPos++ = shades[lx < ch->size.x && ly < ch->size.y && ly >= 0 ? | ||
777 | ch->pixels[lx + ly * ch->size.x] * 5 / 256 : 0]; | ||
778 | } | ||
779 | } | ||
780 | *outPos++ = '\n'; | ||
781 | } | ||
782 | /* We could compose the lines separately, but we'd still need to convert them to Strings | ||
783 | individually to trim them. */ | ||
784 | iStringList *lines = split_String(collect_String(newUnicodeN_String(outBuf, len)), "\n"); | ||
785 | while (!isEmpty_StringList(lines) && | ||
786 | isEmpty_String(collect_String(trimmed_String(at_StringList(lines, 0))))) { | ||
787 | popFront_StringList(lines); | ||
788 | } | ||
789 | while (!isEmpty_StringList(lines) && isEmpty_String(collect_String(trimmed_String( | ||
790 | at_StringList(lines, size_StringList(lines) - 1))))) { | ||
791 | popBack_StringList(lines); | ||
792 | } | ||
793 | iEndCollect(); | ||
794 | return joinCStr_StringList(iClob(lines), "\n"); | ||
795 | } | ||
796 | |||
661 | /*-----------------------------------------------------------------------------------------------*/ | 797 | /*-----------------------------------------------------------------------------------------------*/ |
662 | 798 | ||
663 | iDefineTypeConstructionArgs(TextBuf, (int font, const char *text), font, text) | 799 | iDefineTypeConstructionArgs(TextBuf, (int font, const char *text), font, text) |
diff --git a/src/ui/text.h b/src/ui/text.h index 2d49a1a6..2b4ec5c3 100644 --- a/src/ui/text.h +++ b/src/ui/text.h | |||
@@ -1,3 +1,25 @@ | |||
1 | /* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi> | ||
2 | |||
3 | Redistribution and use in source and binary forms, with or without | ||
4 | modification, are permitted provided that the following conditions are met: | ||
5 | |||
6 | 1. Redistributions of source code must retain the above copyright notice, this | ||
7 | list of conditions and the following disclaimer. | ||
8 | 2. Redistributions in binary form must reproduce the above copyright notice, | ||
9 | this list of conditions and the following disclaimer in the documentation | ||
10 | and/or other materials provided with the distribution. | ||
11 | |||
12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | ||
13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | ||
14 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||
15 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR | ||
16 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | ||
17 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | ||
18 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON | ||
19 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||
20 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | ||
21 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | ||
22 | |||
1 | #pragma once | 23 | #pragma once |
2 | 24 | ||
3 | #include <the_Foundation/rect.h> | 25 | #include <the_Foundation/rect.h> |
@@ -36,6 +58,7 @@ enum iFontId { | |||
36 | hugeEmoji_FontId, | 58 | hugeEmoji_FontId, |
37 | smallEmoji_FontId, | 59 | smallEmoji_FontId, |
38 | max_FontId, | 60 | max_FontId, |
61 | |||
39 | /* Meta: */ | 62 | /* Meta: */ |
40 | fromSymbolsToEmojiOffset_FontId = 7, | 63 | fromSymbolsToEmojiOffset_FontId = 7, |
41 | /* UI fonts: */ | 64 | /* UI fonts: */ |
@@ -90,6 +113,11 @@ void drawRange_Text (int fontId, iInt2 pos, int color, iRangecc text); | |||
90 | 113 | ||
91 | SDL_Texture * glyphCache_Text (void); | 114 | SDL_Texture * glyphCache_Text (void); |
92 | 115 | ||
116 | enum iTextBlockMode { quadrants_TextBlockMode, shading_TextBlockMode }; | ||
117 | |||
118 | iString * renderBlockChars_Text (const iBlock *fontData, int height, enum iTextBlockMode, | ||
119 | const iString *text); | ||
120 | |||
93 | /*-----------------------------------------------------------------------------------------------*/ | 121 | /*-----------------------------------------------------------------------------------------------*/ |
94 | 122 | ||
95 | iDeclareType(TextBuf) | 123 | iDeclareType(TextBuf) |
diff --git a/src/ui/util.c b/src/ui/util.c index 9f4768d9..dfe364a5 100644 --- a/src/ui/util.c +++ b/src/ui/util.c | |||
@@ -1,8 +1,32 @@ | |||
1 | /* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi> | ||
2 | |||
3 | Redistribution and use in source and binary forms, with or without | ||
4 | modification, are permitted provided that the following conditions are met: | ||
5 | |||
6 | 1. Redistributions of source code must retain the above copyright notice, this | ||
7 | list of conditions and the following disclaimer. | ||
8 | 2. Redistributions in binary form must reproduce the above copyright notice, | ||
9 | this list of conditions and the following disclaimer in the documentation | ||
10 | and/or other materials provided with the distribution. | ||
11 | |||
12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | ||
13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | ||
14 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||
15 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR | ||
16 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | ||
17 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | ||
18 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON | ||
19 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||
20 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | ||
21 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | ||
22 | |||
1 | #include "util.h" | 23 | #include "util.h" |
2 | 24 | ||
3 | #include "app.h" | 25 | #include "app.h" |
26 | #include "bookmarks.h" | ||
4 | #include "color.h" | 27 | #include "color.h" |
5 | #include "command.h" | 28 | #include "command.h" |
29 | #include "gmutil.h" | ||
6 | #include "labelwidget.h" | 30 | #include "labelwidget.h" |
7 | #include "inputwidget.h" | 31 | #include "inputwidget.h" |
8 | #include "widget.h" | 32 | #include "widget.h" |
@@ -157,7 +181,13 @@ static iBool menuHandler_(iWidget *menu, const char *cmd) { | |||
157 | /* Don't reopen self; instead, root will close the menu. */ | 181 | /* Don't reopen self; instead, root will close the menu. */ |
158 | return iFalse; | 182 | return iFalse; |
159 | } | 183 | } |
160 | if (!equal_Command(cmd, "window.resized")) { | 184 | if (equal_Command(cmd, "mouse.clicked") && arg_Command(cmd)) { |
185 | /* Dismiss open menus when clicking outside them. */ | ||
186 | closeMenu_Widget(menu); | ||
187 | return iTrue; | ||
188 | } | ||
189 | if (!equal_Command(cmd, "window.resized") && | ||
190 | !(equal_Command(cmd, "mouse.clicked") && !arg_Command(cmd)) /* ignore button release */) { | ||
161 | closeMenu_Widget(menu); | 191 | closeMenu_Widget(menu); |
162 | } | 192 | } |
163 | } | 193 | } |
@@ -234,7 +264,7 @@ void closeMenu_Widget(iWidget *d) { | |||
234 | } | 264 | } |
235 | 265 | ||
236 | int checkContextMenu_Widget(iWidget *menu, const SDL_Event *ev) { | 266 | int checkContextMenu_Widget(iWidget *menu, const SDL_Event *ev) { |
237 | if (ev->type == SDL_MOUSEBUTTONDOWN && ev->button.button == SDL_BUTTON_RIGHT) { | 267 | if (menu && ev->type == SDL_MOUSEBUTTONDOWN && ev->button.button == SDL_BUTTON_RIGHT) { |
238 | if (isVisible_Widget(menu)) { | 268 | if (isVisible_Widget(menu)) { |
239 | closeMenu_Widget(menu); | 269 | closeMenu_Widget(menu); |
240 | return 0x1; | 270 | return 0x1; |
@@ -476,6 +506,7 @@ iBool filePathHandler_(iWidget *dlg, const char *cmd) { | |||
476 | 506 | ||
477 | iWidget *makeSheet_Widget(const char *id) { | 507 | iWidget *makeSheet_Widget(const char *id) { |
478 | iWidget *sheet = new_Widget(); | 508 | iWidget *sheet = new_Widget(); |
509 | setPadding1_Widget(sheet, 3 * gap_UI); | ||
479 | setId_Widget(sheet, id); | 510 | setId_Widget(sheet, id); |
480 | setFrameColor_Widget(sheet, uiSeparator_ColorId); | 511 | setFrameColor_Widget(sheet, uiSeparator_ColorId); |
481 | setBackgroundColor_Widget(sheet, uiBackground_ColorId); | 512 | setBackgroundColor_Widget(sheet, uiBackground_ColorId); |
@@ -483,9 +514,6 @@ iWidget *makeSheet_Widget(const char *id) { | |||
483 | mouseModal_WidgetFlag | keepOnTop_WidgetFlag | arrangeVertical_WidgetFlag | | 514 | mouseModal_WidgetFlag | keepOnTop_WidgetFlag | arrangeVertical_WidgetFlag | |
484 | arrangeSize_WidgetFlag, | 515 | arrangeSize_WidgetFlag, |
485 | iTrue); | 516 | iTrue); |
486 | // const iInt2 rootSize = rootSize_Window(get_Window()); | ||
487 | // setSize_Widget(sheet, init_I2(rootSize.x / 2, 0)); | ||
488 | // setFlags_Widget(sheet, fixedHeight_WidgetFlag, iFalse); | ||
489 | return sheet; | 517 | return sheet; |
490 | } | 518 | } |
491 | 519 | ||
@@ -529,11 +557,13 @@ void makeFilePath_Widget(iWidget * parent, | |||
529 | 557 | ||
530 | static void acceptValueInput_(iWidget *dlg) { | 558 | static void acceptValueInput_(iWidget *dlg) { |
531 | const iInputWidget *input = findChild_Widget(dlg, "input"); | 559 | const iInputWidget *input = findChild_Widget(dlg, "input"); |
532 | const iString *val = text_InputWidget(input); | 560 | if (!isEmpty_String(id_Widget(dlg))) { |
533 | postCommandf_App("%s arg:%d value:%s", | 561 | const iString *val = text_InputWidget(input); |
534 | cstr_String(id_Widget(dlg)), | 562 | postCommandf_App("%s arg:%d value:%s", |
535 | toInt_String(val), | 563 | cstr_String(id_Widget(dlg)), |
536 | cstr_String(val)); | 564 | toInt_String(val), |
565 | cstr_String(val)); | ||
566 | } | ||
537 | } | 567 | } |
538 | 568 | ||
539 | static void updateValueInputWidth_(iWidget *dlg) { | 569 | static void updateValueInputWidth_(iWidget *dlg) { |
@@ -560,6 +590,7 @@ iBool valueInputHandler_(iWidget *dlg, const char *cmd) { | |||
560 | } | 590 | } |
561 | else { | 591 | else { |
562 | postCommandf_App("valueinput.cancelled id:%s", cstr_String(id_Widget(dlg))); | 592 | postCommandf_App("valueinput.cancelled id:%s", cstr_String(id_Widget(dlg))); |
593 | setId_Widget(dlg, ""); /* no further commands to emit */ | ||
563 | } | 594 | } |
564 | destroy_Widget(dlg); | 595 | destroy_Widget(dlg); |
565 | return iTrue; | 596 | return iTrue; |
@@ -568,6 +599,7 @@ iBool valueInputHandler_(iWidget *dlg, const char *cmd) { | |||
568 | } | 599 | } |
569 | else if (equal_Command(cmd, "cancel")) { | 600 | else if (equal_Command(cmd, "cancel")) { |
570 | postCommandf_App("valueinput.cancelled id:%s", cstr_String(id_Widget(dlg))); | 601 | postCommandf_App("valueinput.cancelled id:%s", cstr_String(id_Widget(dlg))); |
602 | setId_Widget(dlg, ""); /* no further commands to emit */ | ||
571 | destroy_Widget(dlg); | 603 | destroy_Widget(dlg); |
572 | return iTrue; | 604 | return iTrue; |
573 | } | 605 | } |
@@ -633,11 +665,12 @@ static iBool messageHandler_(iWidget *msg, const char *cmd) { | |||
633 | return iFalse; | 665 | return iFalse; |
634 | } | 666 | } |
635 | 667 | ||
636 | void makeMessage_Widget(const char *title, const char *msg) { | 668 | iWidget *makeMessage_Widget(const char *title, const char *msg) { |
637 | iWidget *dlg = makeQuestion_Widget( | 669 | iWidget *dlg = makeQuestion_Widget( |
638 | title, msg, (const char *[]){ "Continue" }, (const char *[]){ "message.ok" }, 1); | 670 | title, msg, (const char *[]){ "Continue" }, (const char *[]){ "message.ok" }, 1); |
639 | addAction_Widget(dlg, SDLK_ESCAPE, 0, "message.ok"); | 671 | addAction_Widget(dlg, SDLK_ESCAPE, 0, "message.ok"); |
640 | addAction_Widget(dlg, SDLK_SPACE, 0, "message.ok"); | 672 | addAction_Widget(dlg, SDLK_SPACE, 0, "message.ok"); |
673 | return dlg; | ||
641 | } | 674 | } |
642 | 675 | ||
643 | iWidget *makeQuestion_Widget(const char *title, | 676 | iWidget *makeQuestion_Widget(const char *title, |
@@ -666,9 +699,11 @@ iWidget *makeQuestion_Widget(const char *title, | |||
666 | } | 699 | } |
667 | 700 | ||
668 | void setToggle_Widget(iWidget *d, iBool active) { | 701 | void setToggle_Widget(iWidget *d, iBool active) { |
669 | setFlags_Widget(d, selected_WidgetFlag, active); | 702 | if (d) { |
670 | updateText_LabelWidget((iLabelWidget *) d, | 703 | setFlags_Widget(d, selected_WidgetFlag, active); |
671 | collectNewFormat_String("%s", isSelected_Widget(d) ? "YES" : "NO")); | 704 | updateText_LabelWidget((iLabelWidget *) d, |
705 | collectNewFormat_String("%s", isSelected_Widget(d) ? "YES" : "NO")); | ||
706 | } | ||
672 | } | 707 | } |
673 | 708 | ||
674 | static iBool toggleHandler_(iWidget *d, const char *cmd) { | 709 | static iBool toggleHandler_(iWidget *d, const char *cmd) { |
@@ -684,8 +719,9 @@ static iBool toggleHandler_(iWidget *d, const char *cmd) { | |||
684 | } | 719 | } |
685 | 720 | ||
686 | iWidget *makeToggle_Widget(const char *id) { | 721 | iWidget *makeToggle_Widget(const char *id) { |
687 | iWidget *toggle = as_Widget(new_LabelWidget("YES", 0, 0, "toggle")); | 722 | iWidget *toggle = as_Widget(new_LabelWidget("YES", 0, 0, "toggle")); /* "YES" for sizing */ |
688 | setId_Widget(toggle, id); | 723 | setId_Widget(toggle, id); |
724 | updateTextCStr_LabelWidget((iLabelWidget *) toggle, "NO"); /* actual initial value */ | ||
689 | setCommandHandler_Widget(toggle, toggleHandler_); | 725 | setCommandHandler_Widget(toggle, toggleHandler_); |
690 | return toggle; | 726 | return toggle; |
691 | } | 727 | } |
@@ -702,9 +738,11 @@ iWidget *makePreferences_Widget(void) { | |||
702 | page, iClob(new_Widget()), arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag); | 738 | page, iClob(new_Widget()), arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag); |
703 | iWidget *values = addChildFlags_Widget( | 739 | iWidget *values = addChildFlags_Widget( |
704 | page, iClob(new_Widget()), arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag); | 740 | page, iClob(new_Widget()), arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag); |
705 | // setBackgroundColor_Widget(headings, none_ColorId); | 741 | #if defined (iPlatformApple) || defined (iPlatformMSys) |
706 | // setBackgroundColor_Widget(values, none_ColorId); | 742 | addChild_Widget(headings, iClob(makeHeading_Widget("Use system theme:"))); |
707 | addChild_Widget(headings, iClob(makeHeading_Widget("Theme:"))); | 743 | addChild_Widget(values, iClob(makeToggle_Widget("prefs.ostheme"))); |
744 | #endif | ||
745 | addChild_Widget(headings, iClob(makeHeading_Widget("Theme:"))); | ||
708 | iWidget *themes = new_Widget(); | 746 | iWidget *themes = new_Widget(); |
709 | /* Themes. */ { | 747 | /* Themes. */ { |
710 | setId_Widget(addChild_Widget(themes, iClob(new_LabelWidget("Pure Black", 0, 0, "theme.set arg:0"))), "prefs.theme.0"); | 748 | setId_Widget(addChild_Widget(themes, iClob(new_LabelWidget("Pure Black", 0, 0, "theme.set arg:0"))), "prefs.theme.0"); |
@@ -717,8 +755,18 @@ iWidget *makePreferences_Widget(void) { | |||
717 | addChild_Widget(values, iClob(makeToggle_Widget("prefs.retainwindow"))); | 755 | addChild_Widget(values, iClob(makeToggle_Widget("prefs.retainwindow"))); |
718 | addChild_Widget(headings, iClob(makeHeading_Widget("UI scale factor:"))); | 756 | addChild_Widget(headings, iClob(makeHeading_Widget("UI scale factor:"))); |
719 | setId_Widget(addChild_Widget(values, iClob(new_InputWidget(8))), "prefs.uiscale"); | 757 | setId_Widget(addChild_Widget(values, iClob(new_InputWidget(8))), "prefs.uiscale"); |
758 | addChild_Widget(headings, iClob(makeHeading_Widget(uiHeading_ColorEscape "Proxies"))); | ||
759 | addChild_Widget(values, iClob(makeHeading_Widget(""))); | ||
760 | addChild_Widget(headings, iClob(makeHeading_Widget("HTTP proxy:"))); | ||
761 | setId_Widget(addChild_Widget(values, iClob(new_InputWidget(0))), "prefs.proxy.http"); | ||
762 | addChild_Widget(headings, iClob(makeHeading_Widget("Gopher proxy:"))); | ||
763 | setId_Widget(addChild_Widget(values, iClob(new_InputWidget(0))), "prefs.proxy.gopher"); | ||
720 | arrange_Widget(dlg); | 764 | arrange_Widget(dlg); |
721 | // as_Widget(songDir)->rect.size.x = dlg->rect.size.x - headings->rect.size.x; | 765 | /* Text input widths. */ { |
766 | const int inputWidth = width_Rect(page->rect) - width_Rect(headings->rect); | ||
767 | as_Widget(findChild_Widget(values, "prefs.proxy.http"))->rect.size.x = inputWidth; | ||
768 | as_Widget(findChild_Widget(values, "prefs.proxy.gopher"))->rect.size.x = inputWidth; | ||
769 | } | ||
722 | iWidget *div = new_Widget(); { | 770 | iWidget *div = new_Widget(); { |
723 | setFlags_Widget(div, arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag, iTrue); | 771 | setFlags_Widget(div, arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag, iTrue); |
724 | addChild_Widget(div, iClob(new_LabelWidget("Dismiss", SDLK_ESCAPE, 0, "prefs.dismiss"))); | 772 | addChild_Widget(div, iClob(new_LabelWidget("Dismiss", SDLK_ESCAPE, 0, "prefs.dismiss"))); |
@@ -731,9 +779,11 @@ iWidget *makePreferences_Widget(void) { | |||
731 | 779 | ||
732 | iWidget *makeBookmarkEditor_Widget(void) { | 780 | iWidget *makeBookmarkEditor_Widget(void) { |
733 | iWidget *dlg = makeSheet_Widget("bmed"); | 781 | iWidget *dlg = makeSheet_Widget("bmed"); |
734 | addChildFlags_Widget(dlg, | 782 | setId_Widget(addChildFlags_Widget( |
735 | iClob(new_LabelWidget(uiHeading_ColorEscape "EDIT BOOKMARK", 0, 0, NULL)), | 783 | dlg, |
736 | frameless_WidgetFlag); | 784 | iClob(new_LabelWidget(uiHeading_ColorEscape "EDIT BOOKMARK", 0, 0, NULL)), |
785 | frameless_WidgetFlag), | ||
786 | "bmed.heading"); | ||
737 | iWidget *page = new_Widget(); | 787 | iWidget *page = new_Widget(); |
738 | addChild_Widget(dlg, iClob(page)); | 788 | addChild_Widget(dlg, iClob(page)); |
739 | setFlags_Widget(page, arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag, iTrue); | 789 | setFlags_Widget(page, arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag, iTrue); |
@@ -741,9 +791,8 @@ iWidget *makeBookmarkEditor_Widget(void) { | |||
741 | page, iClob(new_Widget()), arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag); | 791 | page, iClob(new_Widget()), arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag); |
742 | iWidget *values = addChildFlags_Widget( | 792 | iWidget *values = addChildFlags_Widget( |
743 | page, iClob(new_Widget()), arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag); | 793 | page, iClob(new_Widget()), arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag); |
744 | iInputWidget *inputs[4]; | 794 | iInputWidget *inputs[3]; |
745 | iWidget *hd; | 795 | addChild_Widget(headings, iClob(makeHeading_Widget("Title:"))); |
746 | addChild_Widget(headings, iClob(hd = makeHeading_Widget("Title:"))); | ||
747 | setId_Widget(addChild_Widget(values, iClob(inputs[0] = new_InputWidget(0))), "bmed.title"); | 796 | setId_Widget(addChild_Widget(values, iClob(inputs[0] = new_InputWidget(0))), "bmed.title"); |
748 | addChild_Widget(headings, iClob(makeHeading_Widget("URL:"))); | 797 | addChild_Widget(headings, iClob(makeHeading_Widget("URL:"))); |
749 | setId_Widget(addChild_Widget(values, iClob(inputs[1] = new_InputWidget(0))), "bmed.url"); | 798 | setId_Widget(addChild_Widget(values, iClob(inputs[1] = new_InputWidget(0))), "bmed.url"); |
@@ -759,7 +808,100 @@ iWidget *makeBookmarkEditor_Widget(void) { | |||
759 | addChild_Widget( | 808 | addChild_Widget( |
760 | div, | 809 | div, |
761 | iClob(new_LabelWidget( | 810 | iClob(new_LabelWidget( |
762 | uiTextCaution_ColorEscape "Save", SDLK_RETURN, KMOD_PRIMARY, "bmed.accept"))); | 811 | uiTextCaution_ColorEscape "Save Bookmark", SDLK_RETURN, KMOD_PRIMARY, "bmed.accept"))); |
812 | } | ||
813 | addChild_Widget(dlg, iClob(div)); | ||
814 | addChild_Widget(get_Window()->root, iClob(dlg)); | ||
815 | centerSheet_Widget(dlg); | ||
816 | return dlg; | ||
817 | } | ||
818 | |||
819 | static iBool handleBookmarkCreationCommands_SidebarWidget_(iWidget *editor, const char *cmd) { | ||
820 | if (equal_Command(cmd, "bmed.accept") || equal_Command(cmd, "cancel")) { | ||
821 | if (equal_Command(cmd, "bmed.accept")) { | ||
822 | const iString *title = text_InputWidget(findChild_Widget(editor, "bmed.title")); | ||
823 | const iString *url = text_InputWidget(findChild_Widget(editor, "bmed.url")); | ||
824 | const iString *tags = text_InputWidget(findChild_Widget(editor, "bmed.tags")); | ||
825 | add_Bookmarks(bookmarks_App(), | ||
826 | url, | ||
827 | title, | ||
828 | tags, | ||
829 | first_String(label_LabelWidget(findChild_Widget(editor, "bmed.icon")))); | ||
830 | postCommand_App("bookmarks.changed"); | ||
831 | } | ||
832 | destroy_Widget(editor); | ||
833 | return iTrue; | ||
834 | } | ||
835 | return iFalse; | ||
836 | } | ||
837 | |||
838 | iWidget *makeBookmarkCreation_Widget(const iString *url, const iString *title, iChar icon) { | ||
839 | iWidget *dlg = makeBookmarkEditor_Widget(); | ||
840 | setId_Widget(dlg, "bmed.create"); | ||
841 | setTextCStr_LabelWidget(findChild_Widget(dlg, "bmed.heading"), | ||
842 | uiHeading_ColorEscape "ADD BOOKMARK"); | ||
843 | iUrl parts; | ||
844 | init_Url(&parts, url); | ||
845 | setTextCStr_InputWidget(findChild_Widget(dlg, "bmed.title"), | ||
846 | title ? cstr_String(title) : cstr_Rangecc(parts.host)); | ||
847 | setText_InputWidget(findChild_Widget(dlg, "bmed.url"), url); | ||
848 | setId_Widget( | ||
849 | addChildFlags_Widget( | ||
850 | dlg, | ||
851 | iClob(new_LabelWidget(cstrCollect_String(newUnicodeN_String(&icon, 1)), 0, 0, NULL)), | ||
852 | collapse_WidgetFlag | hidden_WidgetFlag | disabled_WidgetFlag), | ||
853 | "bmed.icon"); | ||
854 | setCommandHandler_Widget(dlg, handleBookmarkCreationCommands_SidebarWidget_); | ||
855 | return dlg; | ||
856 | } | ||
857 | |||
858 | iWidget *makeIdentityCreation_Widget(void) { | ||
859 | iWidget *dlg = makeSheet_Widget("ident"); | ||
860 | setId_Widget(addChildFlags_Widget( | ||
861 | dlg, | ||
862 | iClob(new_LabelWidget(uiHeading_ColorEscape "NEW IDENTITY", 0, 0, NULL)), | ||
863 | frameless_WidgetFlag), | ||
864 | "ident.heading"); | ||
865 | iWidget *page = new_Widget(); | ||
866 | addChildFlags_Widget( | ||
867 | dlg, | ||
868 | iClob( | ||
869 | new_LabelWidget("Creating a 2048-bit self-signed RSA certificate.", 0, 0, NULL)), | ||
870 | frameless_WidgetFlag); | ||
871 | addChild_Widget(dlg, iClob(page)); | ||
872 | setFlags_Widget(page, arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag, iTrue); | ||
873 | iWidget *headings = addChildFlags_Widget( | ||
874 | page, iClob(new_Widget()), arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag); | ||
875 | iWidget *values = addChildFlags_Widget( | ||
876 | page, iClob(new_Widget()), arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag); | ||
877 | iInputWidget *inputs[6]; | ||
878 | addChild_Widget(headings, iClob(makeHeading_Widget("Common name:"))); | ||
879 | setId_Widget(addChild_Widget(values, iClob(inputs[0] = new_InputWidget(0))), "ident.common"); | ||
880 | addChild_Widget(headings, iClob(makeHeading_Widget("Email:"))); | ||
881 | setId_Widget(addChild_Widget(values, iClob(inputs[1] = newHint_InputWidget(0, "optional"))), "ident.email"); | ||
882 | addChild_Widget(headings, iClob(makeHeading_Widget("User ID:"))); | ||
883 | setId_Widget(addChild_Widget(values, iClob(inputs[2] = newHint_InputWidget(0, "optional"))), "ident.userid"); | ||
884 | addChild_Widget(headings, iClob(makeHeading_Widget("Domain:"))); | ||
885 | setId_Widget(addChild_Widget(values, iClob(inputs[3] = newHint_InputWidget(0, "optional"))), "ident.domain"); | ||
886 | addChild_Widget(headings, iClob(makeHeading_Widget("Organization:"))); | ||
887 | setId_Widget(addChild_Widget(values, iClob(inputs[4] = newHint_InputWidget(0, "optional"))), "ident.org"); | ||
888 | addChild_Widget(headings, iClob(makeHeading_Widget("Country:"))); | ||
889 | setId_Widget(addChild_Widget(values, iClob(inputs[5] = newHint_InputWidget(0, "optional"))), "ident.country"); | ||
890 | addChild_Widget(headings, iClob(makeHeading_Widget("Valid until:"))); | ||
891 | setId_Widget(addChild_Widget(values, iClob(newHint_InputWidget(19, "YYYY-MM-DD HH:MM:SS"))), "ident.until"); | ||
892 | addChild_Widget(headings, iClob(makeHeading_Widget("Temporary:"))); | ||
893 | addChild_Widget(values, iClob(makeToggle_Widget("ident.temp"))); | ||
894 | arrange_Widget(dlg); | ||
895 | for (size_t i = 0; i < iElemCount(inputs); ++i) { | ||
896 | as_Widget(inputs[i])->rect.size.x = 100 * gap_UI - headings->rect.size.x; | ||
897 | } | ||
898 | iWidget *div = new_Widget(); { | ||
899 | setFlags_Widget(div, arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag, iTrue); | ||
900 | addChild_Widget(div, iClob(new_LabelWidget("Cancel", SDLK_ESCAPE, 0, "cancel"))); | ||
901 | addChild_Widget( | ||
902 | div, | ||
903 | iClob(new_LabelWidget( | ||
904 | uiTextAction_ColorEscape "Create Identity", SDLK_RETURN, KMOD_PRIMARY, "ident.accept"))); | ||
763 | } | 905 | } |
764 | addChild_Widget(dlg, iClob(div)); | 906 | addChild_Widget(dlg, iClob(div)); |
765 | addChild_Widget(get_Window()->root, iClob(dlg)); | 907 | addChild_Widget(get_Window()->root, iClob(dlg)); |
diff --git a/src/ui/util.h b/src/ui/util.h index 6595b94c..9ef166de 100644 --- a/src/ui/util.h +++ b/src/ui/util.h | |||
@@ -1,5 +1,28 @@ | |||
1 | /* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi> | ||
2 | |||
3 | Redistribution and use in source and binary forms, with or without | ||
4 | modification, are permitted provided that the following conditions are met: | ||
5 | |||
6 | 1. Redistributions of source code must retain the above copyright notice, this | ||
7 | list of conditions and the following disclaimer. | ||
8 | 2. Redistributions in binary form must reproduce the above copyright notice, | ||
9 | this list of conditions and the following disclaimer in the documentation | ||
10 | and/or other materials provided with the distribution. | ||
11 | |||
12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | ||
13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | ||
14 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||
15 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR | ||
16 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | ||
17 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | ||
18 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON | ||
19 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||
20 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | ||
21 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | ||
22 | |||
1 | #pragma once | 23 | #pragma once |
2 | 24 | ||
25 | #include <the_Foundation/string.h> | ||
3 | #include <the_Foundation/rect.h> | 26 | #include <the_Foundation/rect.h> |
4 | #include <the_Foundation/vec2.h> | 27 | #include <the_Foundation/vec2.h> |
5 | #include <SDL_events.h> | 28 | #include <SDL_events.h> |
@@ -117,8 +140,11 @@ void makeFilePath_Widget (iWidget *parent, const iString *initialPath | |||
117 | iWidget * makeValueInput_Widget (iWidget *parent, const iString *initialValue, const char *title, | 140 | iWidget * makeValueInput_Widget (iWidget *parent, const iString *initialValue, const char *title, |
118 | const char *prompt, const char *acceptLabel, const char *command); | 141 | const char *prompt, const char *acceptLabel, const char *command); |
119 | void updateValueInput_Widget (iWidget *, const char *title, const char *prompt); | 142 | void updateValueInput_Widget (iWidget *, const char *title, const char *prompt); |
120 | void makeMessage_Widget (const char *title, const char *msg); | 143 | iWidget * makeMessage_Widget (const char *title, const char *msg); |
121 | iWidget * makeQuestion_Widget (const char *title, const char *msg, | 144 | iWidget * makeQuestion_Widget (const char *title, const char *msg, |
122 | const char *labels[], const char *commands[], size_t count); | 145 | const char *labels[], const char *commands[], size_t count); |
123 | iWidget * makePreferences_Widget (void); | 146 | |
124 | iWidget * makeBookmarkEditor_Widget(void); | 147 | iWidget * makePreferences_Widget (void); |
148 | iWidget * makeBookmarkEditor_Widget (void); | ||
149 | iWidget * makeBookmarkCreation_Widget (const iString *url, const iString *title, iChar icon); | ||
150 | iWidget * makeIdentityCreation_Widget (void); | ||
diff --git a/src/ui/widget.c b/src/ui/widget.c index 1c19b70f..b5ea3b0f 100644 --- a/src/ui/widget.c +++ b/src/ui/widget.c | |||
@@ -1,3 +1,25 @@ | |||
1 | /* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi> | ||
2 | |||
3 | Redistribution and use in source and binary forms, with or without | ||
4 | modification, are permitted provided that the following conditions are met: | ||
5 | |||
6 | 1. Redistributions of source code must retain the above copyright notice, this | ||
7 | list of conditions and the following disclaimer. | ||
8 | 2. Redistributions in binary form must reproduce the above copyright notice, | ||
9 | this list of conditions and the following disclaimer in the documentation | ||
10 | and/or other materials provided with the distribution. | ||
11 | |||
12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | ||
13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | ||
14 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||
15 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR | ||
16 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | ||
17 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | ||
18 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON | ||
19 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||
20 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | ||
21 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | ||
22 | |||
1 | #include "widget.h" | 23 | #include "widget.h" |
2 | 24 | ||
3 | #include "app.h" | 25 | #include "app.h" |
@@ -6,6 +28,7 @@ | |||
6 | #include "util.h" | 28 | #include "util.h" |
7 | #include "window.h" | 29 | #include "window.h" |
8 | 30 | ||
31 | #include <the_Foundation/ptrarray.h> | ||
9 | #include <the_Foundation/ptrset.h> | 32 | #include <the_Foundation/ptrset.h> |
10 | #include <SDL_mouse.h> | 33 | #include <SDL_mouse.h> |
11 | #include <stdarg.h> | 34 | #include <stdarg.h> |
@@ -16,15 +39,15 @@ struct Impl_RootData { | |||
16 | iWidget *hover; | 39 | iWidget *hover; |
17 | iWidget *mouseGrab; | 40 | iWidget *mouseGrab; |
18 | iWidget *focus; | 41 | iWidget *focus; |
19 | iPtrSet *onTop; | 42 | iPtrArray *onTop; /* order is important; last one is topmost */ |
20 | iPtrSet *pendingDestruction; | 43 | iPtrSet *pendingDestruction; |
21 | }; | 44 | }; |
22 | 45 | ||
23 | static iRootData rootData_; | 46 | static iRootData rootData_; |
24 | 47 | ||
25 | iPtrSet *onTop_RootData_(void) { | 48 | iPtrArray *onTop_RootData_(void) { |
26 | if (!rootData_.onTop) { | 49 | if (!rootData_.onTop) { |
27 | rootData_.onTop = new_PtrSet(); | 50 | rootData_.onTop = new_PtrArray(); |
28 | } | 51 | } |
29 | return rootData_.onTop; | 52 | return rootData_.onTop; |
30 | } | 53 | } |
@@ -32,7 +55,7 @@ iPtrSet *onTop_RootData_(void) { | |||
32 | void destroyPending_Widget(void) { | 55 | void destroyPending_Widget(void) { |
33 | iForEach(PtrSet, i, rootData_.pendingDestruction) { | 56 | iForEach(PtrSet, i, rootData_.pendingDestruction) { |
34 | iWidget *widget = *i.value; | 57 | iWidget *widget = *i.value; |
35 | remove_PtrSet(onTop_RootData_(), widget); | 58 | removeOne_PtrArray(onTop_RootData_(), widget); |
36 | if (widget->parent) { | 59 | if (widget->parent) { |
37 | iRelease(removeChild_Widget(widget->parent, widget)); | 60 | iRelease(removeChild_Widget(widget->parent, widget)); |
38 | } | 61 | } |
@@ -54,6 +77,7 @@ void init_Widget(iWidget *d) { | |||
54 | d->children = NULL; | 77 | d->children = NULL; |
55 | d->parent = NULL; | 78 | d->parent = NULL; |
56 | d->commandHandler = NULL; | 79 | d->commandHandler = NULL; |
80 | iZap(d->padding); | ||
57 | } | 81 | } |
58 | 82 | ||
59 | void deinit_Widget(iWidget *d) { | 83 | void deinit_Widget(iWidget *d) { |
@@ -103,10 +127,10 @@ void setFlags_Widget(iWidget *d, int flags, iBool set) { | |||
103 | iChangeFlags(d->flags, flags, set); | 127 | iChangeFlags(d->flags, flags, set); |
104 | if (flags & keepOnTop_WidgetFlag) { | 128 | if (flags & keepOnTop_WidgetFlag) { |
105 | if (set) { | 129 | if (set) { |
106 | insert_PtrSet(onTop_RootData_(), d); | 130 | pushBack_PtrArray(onTop_RootData_(), d); |
107 | } | 131 | } |
108 | else { | 132 | else { |
109 | remove_PtrSet(onTop_RootData_(), d); | 133 | removeOne_PtrArray(onTop_RootData_(), d); |
110 | } | 134 | } |
111 | } | 135 | } |
112 | } | 136 | } |
@@ -120,6 +144,13 @@ void setSize_Widget(iWidget *d, iInt2 size) { | |||
120 | setFlags_Widget(d, fixedSize_WidgetFlag, iTrue); | 144 | setFlags_Widget(d, fixedSize_WidgetFlag, iTrue); |
121 | } | 145 | } |
122 | 146 | ||
147 | void setPadding_Widget(iWidget *d, int left, int top, int right, int bottom) { | ||
148 | d->padding[0] = left; | ||
149 | d->padding[1] = top; | ||
150 | d->padding[2] = right; | ||
151 | d->padding[3] = bottom; | ||
152 | } | ||
153 | |||
123 | void setBackgroundColor_Widget(iWidget *d, int bgColor) { | 154 | void setBackgroundColor_Widget(iWidget *d, int bgColor) { |
124 | d->bgColor = bgColor; | 155 | d->bgColor = bgColor; |
125 | } | 156 | } |
@@ -169,19 +200,26 @@ iLocalDef iBool isCollapsed_Widget_(const iWidget *d) { | |||
169 | (hidden_WidgetFlag | collapse_WidgetFlag); | 200 | (hidden_WidgetFlag | collapse_WidgetFlag); |
170 | } | 201 | } |
171 | 202 | ||
203 | iLocalDef iRect innerRect_Widget_(const iWidget *d) { | ||
204 | return init_Rect(d->padding[0], | ||
205 | d->padding[1], | ||
206 | width_Rect(d->rect) - d->padding[0] - d->padding[2], | ||
207 | height_Rect(d->rect) - d->padding[1] - d->padding[3]); | ||
208 | } | ||
209 | |||
172 | void arrange_Widget(iWidget *d) { | 210 | void arrange_Widget(iWidget *d) { |
173 | if (isCollapsed_Widget_(d)) { | 211 | if (isCollapsed_Widget_(d)) { |
174 | setFlags_Widget(d, wasCollapsed_WidgetFlag, iTrue); | 212 | setFlags_Widget(d, wasCollapsed_WidgetFlag, iTrue); |
175 | return; | 213 | return; |
176 | } | 214 | } |
177 | if (d->flags & moveToParentRightEdge_WidgetFlag) { | 215 | if (d->flags & moveToParentRightEdge_WidgetFlag) { |
178 | d->rect.pos.x = width_Rect(d->parent->rect) - width_Rect(d->rect); | 216 | d->rect.pos.x = width_Rect(innerRect_Widget_(d->parent)) - width_Rect(d->rect); |
179 | } | 217 | } |
180 | if (d->flags & resizeToParentWidth_WidgetFlag) { | 218 | if (d->flags & resizeToParentWidth_WidgetFlag) { |
181 | setWidth_Widget_(d, d->parent->rect.size.x); | 219 | setWidth_Widget_(d, width_Rect(innerRect_Widget_(d->parent))); |
182 | } | 220 | } |
183 | if (d->flags & resizeToParentHeight_WidgetFlag) { | 221 | if (d->flags & resizeToParentHeight_WidgetFlag) { |
184 | setHeight_Widget_(d, d->parent->rect.size.y); | 222 | setHeight_Widget_(d, height_Rect(innerRect_Widget_(d->parent))); |
185 | } | 223 | } |
186 | /* The rest of the arrangement depends on child widgets. */ | 224 | /* The rest of the arrangement depends on child widgets. */ |
187 | if (!d->children) { | 225 | if (!d->children) { |
@@ -214,7 +252,7 @@ void arrange_Widget(iWidget *d) { | |||
214 | const int expCount = numExpandingChildren_Widget_(d); | 252 | const int expCount = numExpandingChildren_Widget_(d); |
215 | /* Only resize the expanding children, not touching the others. */ | 253 | /* Only resize the expanding children, not touching the others. */ |
216 | if (expCount > 0) { | 254 | if (expCount > 0) { |
217 | iInt2 avail = d->rect.size; | 255 | iInt2 avail = innerRect_Widget_(d).size; |
218 | iConstForEach(ObjectList, i, d->children) { | 256 | iConstForEach(ObjectList, i, d->children) { |
219 | const iWidget *child = constAs_Widget(i.object); | 257 | const iWidget *child = constAs_Widget(i.object); |
220 | if (~child->flags & expand_WidgetFlag) { | 258 | if (~child->flags & expand_WidgetFlag) { |
@@ -228,27 +266,27 @@ void arrange_Widget(iWidget *d) { | |||
228 | if (child->flags & expand_WidgetFlag) { | 266 | if (child->flags & expand_WidgetFlag) { |
229 | if (d->flags & arrangeHorizontal_WidgetFlag) { | 267 | if (d->flags & arrangeHorizontal_WidgetFlag) { |
230 | if (dirs.x) setWidth_Widget_(child, avail.x); | 268 | if (dirs.x) setWidth_Widget_(child, avail.x); |
231 | if (dirs.y) setHeight_Widget_(child, d->rect.size.y); | 269 | if (dirs.y) setHeight_Widget_(child, height_Rect(innerRect_Widget_(d))); |
232 | } | 270 | } |
233 | else if (d->flags & arrangeVertical_WidgetFlag) { | 271 | else if (d->flags & arrangeVertical_WidgetFlag) { |
234 | if (dirs.x) setWidth_Widget_(child, d->rect.size.x); | 272 | if (dirs.x) setWidth_Widget_(child, width_Rect(innerRect_Widget_(d))); |
235 | if (dirs.y) setHeight_Widget_(child, avail.y); | 273 | if (dirs.y) setHeight_Widget_(child, avail.y); |
236 | } | 274 | } |
237 | } | 275 | } |
238 | else { | 276 | else { |
239 | /* Fill the off axis, though. */ | 277 | /* Fill the off axis, though. */ |
240 | if (d->flags & arrangeHorizontal_WidgetFlag) { | 278 | if (d->flags & arrangeHorizontal_WidgetFlag) { |
241 | if (dirs.y) setHeight_Widget_(child, d->rect.size.y); | 279 | if (dirs.y) setHeight_Widget_(child, height_Rect(innerRect_Widget_(d))); |
242 | } | 280 | } |
243 | else if (d->flags & arrangeVertical_WidgetFlag) { | 281 | else if (d->flags & arrangeVertical_WidgetFlag) { |
244 | if (dirs.x) setWidth_Widget_(child, d->rect.size.x); | 282 | if (dirs.x) setWidth_Widget_(child, width_Rect(innerRect_Widget_(d))); |
245 | } | 283 | } |
246 | } | 284 | } |
247 | } | 285 | } |
248 | } | 286 | } |
249 | else { | 287 | else { |
250 | /* Evenly size all children. */ | 288 | /* Evenly size all children. */ |
251 | iInt2 childSize = d->rect.size; | 289 | iInt2 childSize = innerRect_Widget_(d).size; |
252 | if (d->flags & arrangeHorizontal_WidgetFlag) { | 290 | if (d->flags & arrangeHorizontal_WidgetFlag) { |
253 | childSize.x /= childCount; | 291 | childSize.x /= childCount; |
254 | } | 292 | } |
@@ -270,7 +308,7 @@ void arrange_Widget(iWidget *d) { | |||
270 | setWidth_Widget_(as_Widget(i.object), widest); | 308 | setWidth_Widget_(as_Widget(i.object), widest); |
271 | } | 309 | } |
272 | } | 310 | } |
273 | iInt2 pos = zero_I2(); | 311 | iInt2 pos = initv_I2(d->padding); |
274 | iForEach(ObjectList, i, d->children) { | 312 | iForEach(ObjectList, i, d->children) { |
275 | iWidget *child = as_Widget(i.object); | 313 | iWidget *child = as_Widget(i.object); |
276 | arrange_Widget(child); | 314 | arrange_Widget(child); |
@@ -286,6 +324,9 @@ void arrange_Widget(iWidget *d) { | |||
286 | pos.y += child->rect.size.y; | 324 | pos.y += child->rect.size.y; |
287 | } | 325 | } |
288 | } | 326 | } |
327 | else if (d->flags & resizeChildren_WidgetFlag) { | ||
328 | child->rect.pos = pos; | ||
329 | } | ||
289 | } | 330 | } |
290 | /* Update the size of the widget according to the arrangement. */ | 331 | /* Update the size of the widget according to the arrangement. */ |
291 | if (d->flags & arrangeSize_WidgetFlag) { | 332 | if (d->flags & arrangeSize_WidgetFlag) { |
@@ -299,6 +340,7 @@ void arrange_Widget(iWidget *d) { | |||
299 | bounds = union_Rect(bounds, child->rect); | 340 | bounds = union_Rect(bounds, child->rect); |
300 | } | 341 | } |
301 | } | 342 | } |
343 | adjustEdges_Rect(&bounds, -d->padding[1], d->padding[2], d->padding[3], -d->padding[0]); | ||
302 | if (d->flags & arrangeWidth_WidgetFlag) { | 344 | if (d->flags & arrangeWidth_WidgetFlag) { |
303 | setWidth_Widget_(d, bounds.size.x); | 345 | setWidth_Widget_(d, bounds.size.x); |
304 | /* Parent size changed, must update the children.*/ | 346 | /* Parent size changed, must update the children.*/ |
@@ -381,7 +423,7 @@ iBool dispatchEvent_Widget(iWidget *d, const SDL_Event *ev) { | |||
381 | } | 423 | } |
382 | } | 424 | } |
383 | /* Root offers events first to widgets on top. */ | 425 | /* Root offers events first to widgets on top. */ |
384 | iForEach(PtrSet, i, rootData_.onTop) { | 426 | iReverseForEach(PtrArray, i, rootData_.onTop) { |
385 | iWidget *widget = *i.value; | 427 | iWidget *widget = *i.value; |
386 | if (isVisible_Widget(widget) && dispatchEvent_Widget(widget, ev)) { | 428 | if (isVisible_Widget(widget) && dispatchEvent_Widget(widget, ev)) { |
387 | return iTrue; | 429 | return iTrue; |
@@ -454,6 +496,7 @@ iBool processEvent_Widget(iWidget *d, const SDL_Event *ev) { | |||
454 | } | 496 | } |
455 | } | 497 | } |
456 | if (d->flags & mouseModal_WidgetFlag && isMouseEvent_(ev)) { | 498 | if (d->flags & mouseModal_WidgetFlag && isMouseEvent_(ev)) { |
499 | setCursor_Window(get_Window(), SDL_SYSTEM_CURSOR_ARROW); | ||
457 | return iTrue; | 500 | return iTrue; |
458 | } | 501 | } |
459 | return iFalse; | 502 | return iFalse; |
@@ -480,7 +523,7 @@ void draw_Widget(const iWidget *d) { | |||
480 | } | 523 | } |
481 | /* Root draws the on-top widgets on top of everything else. */ | 524 | /* Root draws the on-top widgets on top of everything else. */ |
482 | if (!d->parent) { | 525 | if (!d->parent) { |
483 | iConstForEach(PtrSet, i, onTop_RootData_()) { | 526 | iConstForEach(PtrArray, i, onTop_RootData_()) { |
484 | draw_Widget(*i.value); | 527 | draw_Widget(*i.value); |
485 | } | 528 | } |
486 | } | 529 | } |
@@ -594,7 +637,7 @@ iBool isHover_Widget(const iWidget *d) { | |||
594 | } | 637 | } |
595 | 638 | ||
596 | iBool isSelected_Widget(const iWidget *d) { | 639 | iBool isSelected_Widget(const iWidget *d) { |
597 | return (d->flags & selected_WidgetFlag) != 0; | 640 | return d && (d->flags & selected_WidgetFlag) != 0; |
598 | } | 641 | } |
599 | 642 | ||
600 | iBool equalWidget_Command(const char *cmd, const iWidget *widget, const char *checkCommand) { | 643 | iBool equalWidget_Command(const char *cmd, const iWidget *widget, const char *checkCommand) { |
diff --git a/src/ui/widget.h b/src/ui/widget.h index e88a10dc..3f03fc07 100644 --- a/src/ui/widget.h +++ b/src/ui/widget.h | |||
@@ -1,3 +1,25 @@ | |||
1 | /* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi> | ||
2 | |||
3 | Redistribution and use in source and binary forms, with or without | ||
4 | modification, are permitted provided that the following conditions are met: | ||
5 | |||
6 | 1. Redistributions of source code must retain the above copyright notice, this | ||
7 | list of conditions and the following disclaimer. | ||
8 | 2. Redistributions in binary form must reproduce the above copyright notice, | ||
9 | this list of conditions and the following disclaimer in the documentation | ||
10 | and/or other materials provided with the distribution. | ||
11 | |||
12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | ||
13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | ||
14 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||
15 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR | ||
16 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | ||
17 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | ||
18 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON | ||
19 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||
20 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | ||
21 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | ||
22 | |||
1 | #pragma once | 23 | #pragma once |
2 | 24 | ||
3 | /* Base class for UI widgets. */ | 25 | /* Base class for UI widgets. */ |
@@ -73,6 +95,7 @@ struct Impl_Widget { | |||
73 | iString id; | 95 | iString id; |
74 | int flags; | 96 | int flags; |
75 | iRect rect; | 97 | iRect rect; |
98 | int padding[4]; /* left, top, right, bottom */ | ||
76 | int bgColor; | 99 | int bgColor; |
77 | int frameColor; | 100 | int frameColor; |
78 | iObjectList *children; | 101 | iObjectList *children; |
@@ -107,7 +130,8 @@ void destroyPending_Widget(void); | |||
107 | 130 | ||
108 | const iString *id_Widget (const iWidget *); | 131 | const iString *id_Widget (const iWidget *); |
109 | int flags_Widget (const iWidget *); | 132 | int flags_Widget (const iWidget *); |
110 | iRect bounds_Widget (const iWidget *); | 133 | iRect bounds_Widget (const iWidget *); /* outer bounds */ |
134 | iRect innerBounds_Widget (const iWidget *); | ||
111 | iInt2 localCoord_Widget (const iWidget *, iInt2 coord); | 135 | iInt2 localCoord_Widget (const iWidget *, iInt2 coord); |
112 | iBool contains_Widget (const iWidget *, iInt2 coord); | 136 | iBool contains_Widget (const iWidget *, iInt2 coord); |
113 | iAny * findChild_Widget (const iWidget *, const char *id); | 137 | iAny * findChild_Widget (const iWidget *, const char *id); |
@@ -131,6 +155,8 @@ void setId_Widget (iWidget *, const char *id); | |||
131 | void setFlags_Widget (iWidget *, int flags, iBool set); | 155 | void setFlags_Widget (iWidget *, int flags, iBool set); |
132 | void setPos_Widget (iWidget *, iInt2 pos); | 156 | void setPos_Widget (iWidget *, iInt2 pos); |
133 | void setSize_Widget (iWidget *, iInt2 size); | 157 | void setSize_Widget (iWidget *, iInt2 size); |
158 | void setPadding_Widget (iWidget *, int left, int top, int right, int bottom); | ||
159 | iLocalDef void setPadding1_Widget (iWidget *d, int padding) { setPadding_Widget(d, padding, padding, padding, padding); } | ||
134 | void setBackgroundColor_Widget (iWidget *, int bgColor); | 160 | void setBackgroundColor_Widget (iWidget *, int bgColor); |
135 | void setFrameColor_Widget (iWidget *, int frameColor); | 161 | void setFrameColor_Widget (iWidget *, int frameColor); |
136 | void setCommandHandler_Widget (iWidget *, iBool (*handler)(iWidget *, const char *)); | 162 | void setCommandHandler_Widget (iWidget *, iBool (*handler)(iWidget *, const char *)); |
diff --git a/src/ui/window.c b/src/ui/window.c index 3d9d98d1..650bc9ee 100644 --- a/src/ui/window.c +++ b/src/ui/window.c | |||
@@ -1,17 +1,39 @@ | |||
1 | /* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi> | ||
2 | |||
3 | Redistribution and use in source and binary forms, with or without | ||
4 | modification, are permitted provided that the following conditions are met: | ||
5 | |||
6 | 1. Redistributions of source code must retain the above copyright notice, this | ||
7 | list of conditions and the following disclaimer. | ||
8 | 2. Redistributions in binary form must reproduce the above copyright notice, | ||
9 | this list of conditions and the following disclaimer in the documentation | ||
10 | and/or other materials provided with the distribution. | ||
11 | |||
12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | ||
13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | ||
14 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||
15 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR | ||
16 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | ||
17 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | ||
18 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON | ||
19 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||
20 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | ||
21 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | ||
22 | |||
1 | #include "window.h" | 23 | #include "window.h" |
2 | 24 | ||
25 | #include "labelwidget.h" | ||
26 | #include "inputwidget.h" | ||
27 | #include "documentwidget.h" | ||
28 | #include "sidebarwidget.h" | ||
3 | #include "embedded.h" | 29 | #include "embedded.h" |
4 | #include "app.h" | ||
5 | #include "command.h" | 30 | #include "command.h" |
6 | #include "paint.h" | 31 | #include "paint.h" |
7 | #include "text.h" | ||
8 | #include "util.h" | 32 | #include "util.h" |
33 | #include "../app.h" | ||
9 | #include "../visited.h" | 34 | #include "../visited.h" |
10 | #include "labelwidget.h" | 35 | #include "../gmcerts.h" |
11 | #include "inputwidget.h" | 36 | #include "../gmutil.h" |
12 | #include "documentwidget.h" | ||
13 | #include "sidebarwidget.h" | ||
14 | #include "gmutil.h" | ||
15 | #if defined (iPlatformMsys) | 37 | #if defined (iPlatformMsys) |
16 | # include "../win32.h" | 38 | # include "../win32.h" |
17 | #endif | 39 | #endif |
@@ -80,6 +102,8 @@ static const iMenuItem navMenuItems[] = { | |||
80 | { "Reset Zoom", SDLK_0, KMOD_PRIMARY, "zoom.set arg:100" }, | 102 | { "Reset Zoom", SDLK_0, KMOD_PRIMARY, "zoom.set arg:100" }, |
81 | { "---", 0, 0, NULL }, | 103 | { "---", 0, 0, NULL }, |
82 | { "Preferences...", SDLK_COMMA, KMOD_PRIMARY, "preferences" }, | 104 | { "Preferences...", SDLK_COMMA, KMOD_PRIMARY, "preferences" }, |
105 | { "Help", 0, 0, "!open url:about:help" }, | ||
106 | { "Release Notes", 0, 0, "!open url:about:version" }, | ||
83 | { "---", 0, 0, NULL }, | 107 | { "---", 0, 0, NULL }, |
84 | { "Quit Lagrange", 'q', KMOD_PRIMARY, "quit" } | 108 | { "Quit Lagrange", 'q', KMOD_PRIMARY, "quit" } |
85 | }; | 109 | }; |
@@ -94,8 +118,13 @@ static const iMenuItem fileMenuItems[] = { | |||
94 | 118 | ||
95 | static const iMenuItem editMenuItems[] = { | 119 | static const iMenuItem editMenuItems[] = { |
96 | { "Copy Source Text", SDLK_c, KMOD_PRIMARY, "copy" }, | 120 | { "Copy Source Text", SDLK_c, KMOD_PRIMARY, "copy" }, |
121 | { "Copy Link to Page", SDLK_c, KMOD_PRIMARY | KMOD_SHIFT, "document.copylink" }, | ||
97 | { "---", 0, 0, NULL }, | 122 | { "---", 0, 0, NULL }, |
98 | { "Bookmark This Page", SDLK_d, KMOD_PRIMARY, "bookmark.add" }, | 123 | { "Bookmark This Page...", SDLK_d, KMOD_PRIMARY, "bookmark.add" }, |
124 | }; | ||
125 | |||
126 | static const iMenuItem identityMenuItems[] = { | ||
127 | { "New Identity...", SDLK_n, KMOD_PRIMARY | KMOD_SHIFT, "ident.new" }, | ||
99 | }; | 128 | }; |
100 | 129 | ||
101 | static const iMenuItem viewMenuItems[] = { | 130 | static const iMenuItem viewMenuItems[] = { |
@@ -115,14 +144,43 @@ static const iMenuItem viewMenuItems[] = { | |||
115 | }; | 144 | }; |
116 | 145 | ||
117 | static const iMenuItem helpMenuItems[] = { | 146 | static const iMenuItem helpMenuItems[] = { |
118 | { "Help", 0, 0, "open url:about:help" }, | 147 | { "Help", 0, 0, "!open url:about:help" }, |
119 | { "Release Notes", 0, 0, "open url:about:version" }, | 148 | { "Release Notes", 0, 0, "!open url:about:version" }, |
120 | }; | 149 | }; |
121 | #endif | 150 | #endif |
122 | 151 | ||
152 | static const iMenuItem identityButtonMenuItems[] = { | ||
153 | { "No Active Identity", 0, 0, "ident.showactive" }, | ||
154 | { "---", 0, 0, NULL }, | ||
155 | #if !defined (iHaveNativeMenus) | ||
156 | { "New Identity...", SDLK_n, KMOD_PRIMARY | KMOD_SHIFT, "ident.new" }, | ||
157 | { "---", 0, 0, NULL }, | ||
158 | { "Show Identities", '3', KMOD_PRIMARY, "sidebar.mode arg:2 show:1" }, | ||
159 | #else | ||
160 | { "New Identity...", 0, 0, "ident.new" }, | ||
161 | { "---", 0, 0, NULL }, | ||
162 | { "Show Identities", 0, 0, "sidebar.mode arg:2 show:1" }, | ||
163 | #endif | ||
164 | }; | ||
165 | |||
123 | static const char *reloadCStr_ = "\U0001f503"; | 166 | static const char *reloadCStr_ = "\U0001f503"; |
124 | static const char *stopCStr_ = uiTextCaution_ColorEscape "\U0001f310"; | 167 | static const char *stopCStr_ = uiTextCaution_ColorEscape "\U0001f310"; |
125 | 168 | ||
169 | static void updateNavBarIdentity_(iWidget *navBar) { | ||
170 | const iGmIdentity *ident = | ||
171 | identityForUrl_GmCerts(certs_App(), url_DocumentWidget(document_App())); | ||
172 | iWidget *button = findChild_Widget(navBar, "navbar.ident"); | ||
173 | setFlags_Widget(button, selected_WidgetFlag, ident != NULL); | ||
174 | /* Update menu. */ | ||
175 | iLabelWidget *idItem = child_Widget(findChild_Widget(button, "menu"), 0); | ||
176 | setTextCStr_LabelWidget( | ||
177 | idItem, | ||
178 | ident ? format_CStr(uiTextAction_ColorEscape "%s", | ||
179 | cstrCollect_String(subject_TlsCertificate(ident->cert))) | ||
180 | : "No Active Identity"); | ||
181 | setFlags_Widget(as_Widget(idItem), disabled_WidgetFlag, !ident); | ||
182 | } | ||
183 | |||
126 | static iBool handleNavBarCommands_(iWidget *navBar, const char *cmd) { | 184 | static iBool handleNavBarCommands_(iWidget *navBar, const char *cmd) { |
127 | if (equal_Command(cmd, "window.resized")) { | 185 | if (equal_Command(cmd, "window.resized")) { |
128 | const iBool isNarrow = width_Rect(bounds_Widget(navBar)) / gap_UI < 140; | 186 | const iBool isNarrow = width_Rect(bounds_Widget(navBar)) / gap_UI < 140; |
@@ -160,6 +218,7 @@ static iBool handleNavBarCommands_(iWidget *navBar, const char *cmd) { | |||
160 | const iString *urlStr = collect_String(suffix_Command(cmd, "url")); | 218 | const iString *urlStr = collect_String(suffix_Command(cmd, "url")); |
161 | setText_InputWidget(url, urlStr); | 219 | setText_InputWidget(url, urlStr); |
162 | updateTextCStr_LabelWidget(reloadButton, reloadCStr_); | 220 | updateTextCStr_LabelWidget(reloadButton, reloadCStr_); |
221 | updateNavBarIdentity_(navBar); | ||
163 | return iFalse; | 222 | return iFalse; |
164 | } | 223 | } |
165 | else if (equal_Command(cmd, "document.request.cancelled")) { | 224 | else if (equal_Command(cmd, "document.request.cancelled")) { |
@@ -184,12 +243,13 @@ static iBool handleNavBarCommands_(iWidget *navBar, const char *cmd) { | |||
184 | setText_InputWidget(findChild_Widget(navBar, "url"), url_DocumentWidget(doc)); | 243 | setText_InputWidget(findChild_Widget(navBar, "url"), url_DocumentWidget(doc)); |
185 | updateTextCStr_LabelWidget(findChild_Widget(navBar, "reload"), | 244 | updateTextCStr_LabelWidget(findChild_Widget(navBar, "reload"), |
186 | isRequestOngoing_DocumentWidget(doc) ? stopCStr_ : reloadCStr_); | 245 | isRequestOngoing_DocumentWidget(doc) ? stopCStr_ : reloadCStr_); |
246 | updateNavBarIdentity_(navBar); | ||
187 | } | 247 | } |
188 | } | 248 | } |
189 | else if (equal_Command(cmd, "mouse.clicked")) { | 249 | else if (equal_Command(cmd, "mouse.clicked") && arg_Command(cmd)) { |
190 | iWidget *widget = pointer_Command(cmd); | 250 | iWidget *widget = pointer_Command(cmd); |
191 | iWidget *menu = findWidget_App("doctabs.menu"); | 251 | iWidget *menu = findWidget_App("doctabs.menu"); |
192 | if (isTabButton_Widget(widget)) { | 252 | if (isTabButton_Widget(widget) && !isVisible_Widget(menu)) { |
193 | iWidget *tabs = findWidget_App("doctabs"); | 253 | iWidget *tabs = findWidget_App("doctabs"); |
194 | showTabPage_Widget(tabs, | 254 | showTabPage_Widget(tabs, |
195 | tabPage_Widget(tabs, childIndex_Widget(widget->parent, widget))); | 255 | tabPage_Widget(tabs, childIndex_Widget(widget->parent, widget))); |
@@ -261,6 +321,7 @@ static void setupUserInterface_Window(iWindow *d) { | |||
261 | /* Navigation bar. */ { | 321 | /* Navigation bar. */ { |
262 | iWidget *navBar = new_Widget(); | 322 | iWidget *navBar = new_Widget(); |
263 | setId_Widget(navBar, "navbar"); | 323 | setId_Widget(navBar, "navbar"); |
324 | setPadding_Widget(navBar, gap_UI / 2, 0, gap_UI / 2, 0); | ||
264 | setFlags_Widget(navBar, | 325 | setFlags_Widget(navBar, |
265 | arrangeHeight_WidgetFlag | resizeChildren_WidgetFlag | | 326 | arrangeHeight_WidgetFlag | resizeChildren_WidgetFlag | |
266 | arrangeHorizontal_WidgetFlag, | 327 | arrangeHorizontal_WidgetFlag, |
@@ -270,7 +331,11 @@ static void setupUserInterface_Window(iWindow *d) { | |||
270 | setCommandHandler_Widget(navBar, handleNavBarCommands_); | 331 | setCommandHandler_Widget(navBar, handleNavBarCommands_); |
271 | addChild_Widget(navBar, iClob(newIcon_LabelWidget("\U0001f850", 0, 0, "navigate.back"))); | 332 | addChild_Widget(navBar, iClob(newIcon_LabelWidget("\U0001f850", 0, 0, "navigate.back"))); |
272 | addChild_Widget(navBar, iClob(newIcon_LabelWidget("\U0001f852", 0, 0, "navigate.forward"))); | 333 | addChild_Widget(navBar, iClob(newIcon_LabelWidget("\U0001f852", 0, 0, "navigate.forward"))); |
273 | addChild_Widget(navBar, iClob(newIcon_LabelWidget("\U0001f3e0", 0, 0, "navigate.home"))); | 334 | iLabelWidget *idMenu = |
335 | makeMenuButton_LabelWidget("\U0001f464", identityButtonMenuItems, iElemCount(identityButtonMenuItems)); | ||
336 | setAlignVisually_LabelWidget(idMenu, iTrue); | ||
337 | addChild_Widget(navBar, iClob(idMenu)); | ||
338 | setId_Widget(as_Widget(idMenu), "navbar.ident"); | ||
274 | iLabelWidget *lock = | 339 | iLabelWidget *lock = |
275 | addChildFlags_Widget(navBar, | 340 | addChildFlags_Widget(navBar, |
276 | iClob(newIcon_LabelWidget("\U0001f513", 0, 0, "server.showcert")), | 341 | iClob(newIcon_LabelWidget("\U0001f513", 0, 0, "server.showcert")), |
@@ -279,14 +344,16 @@ static void setupUserInterface_Window(iWindow *d) { | |||
279 | setFont_LabelWidget(lock, defaultSymbols_FontId); | 344 | setFont_LabelWidget(lock, defaultSymbols_FontId); |
280 | updateTextCStr_LabelWidget(lock, "\U0001f512"); | 345 | updateTextCStr_LabelWidget(lock, "\U0001f512"); |
281 | iInputWidget *url = new_InputWidget(0); | 346 | iInputWidget *url = new_InputWidget(0); |
347 | setSelectAllOnFocus_InputWidget(url, iTrue); | ||
282 | setId_Widget(as_Widget(url), "url"); | 348 | setId_Widget(as_Widget(url), "url"); |
283 | setTextCStr_InputWidget(url, "gemini://"); | 349 | setTextCStr_InputWidget(url, "gemini://"); |
284 | addChildFlags_Widget(navBar, iClob(url), expand_WidgetFlag); | 350 | addChildFlags_Widget(navBar, iClob(url), expand_WidgetFlag); |
285 | setId_Widget( | 351 | setId_Widget(addChild_Widget( |
286 | addChild_Widget(navBar, iClob(newIcon_LabelWidget(reloadCStr_, 0, 0, "navigate.reload"))), | 352 | navBar, iClob(newIcon_LabelWidget(reloadCStr_, 0, 0, "navigate.reload"))), |
287 | "reload"); | 353 | "reload"); |
288 | addChild_Widget(navBar, iClob(newIcon_LabelWidget("\U0001f464", 0, 0, "cert.client"))); | 354 | addChild_Widget(navBar, |
289 | 355 | iClob(newIcon_LabelWidget( | |
356 | "\U0001f3e0", SDLK_h, KMOD_PRIMARY | KMOD_SHIFT, "navigate.home"))); | ||
290 | #if !defined (iHaveNativeMenus) | 357 | #if !defined (iHaveNativeMenus) |
291 | iLabelWidget *navMenu = | 358 | iLabelWidget *navMenu = |
292 | makeMenuButton_LabelWidget("\U0001d362", navMenuItems, iElemCount(navMenuItems)); | 359 | makeMenuButton_LabelWidget("\U0001d362", navMenuItems, iElemCount(navMenuItems)); |
@@ -296,7 +363,8 @@ static void setupUserInterface_Window(iWindow *d) { | |||
296 | insertMenuItems_MacOS("File", 1, fileMenuItems, iElemCount(fileMenuItems)); | 363 | insertMenuItems_MacOS("File", 1, fileMenuItems, iElemCount(fileMenuItems)); |
297 | insertMenuItems_MacOS("Edit", 2, editMenuItems, iElemCount(editMenuItems)); | 364 | insertMenuItems_MacOS("Edit", 2, editMenuItems, iElemCount(editMenuItems)); |
298 | insertMenuItems_MacOS("View", 3, viewMenuItems, iElemCount(viewMenuItems)); | 365 | insertMenuItems_MacOS("View", 3, viewMenuItems, iElemCount(viewMenuItems)); |
299 | insertMenuItems_MacOS("Help", 5, helpMenuItems, iElemCount(helpMenuItems)); | 366 | insertMenuItems_MacOS("Identity", 4, identityMenuItems, iElemCount(identityMenuItems)); |
367 | insertMenuItems_MacOS("Help", 6, helpMenuItems, iElemCount(helpMenuItems)); | ||
300 | #endif | 368 | #endif |
301 | } | 369 | } |
302 | /* Tab bar. */ { | 370 | /* Tab bar. */ { |
@@ -380,6 +448,8 @@ static void drawBlank_Window_(iWindow *d) { | |||
380 | 448 | ||
381 | void init_Window(iWindow *d, iRect rect) { | 449 | void init_Window(iWindow *d, iRect rect) { |
382 | theWindow_ = d; | 450 | theWindow_ = d; |
451 | iZap(d->cursors); | ||
452 | d->pendingCursor = NULL; | ||
383 | d->isDrawFrozen = iTrue; | 453 | d->isDrawFrozen = iTrue; |
384 | uint32_t flags = SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI; | 454 | uint32_t flags = SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI; |
385 | #if defined (iPlatformApple) | 455 | #if defined (iPlatformApple) |
@@ -396,7 +466,7 @@ void init_Window(iWindow *d, iRect rect) { | |||
396 | if (left_Rect(rect) >= 0) { | 466 | if (left_Rect(rect) >= 0) { |
397 | SDL_SetWindowPosition(d->win, left_Rect(rect), top_Rect(rect)); | 467 | SDL_SetWindowPosition(d->win, left_Rect(rect), top_Rect(rect)); |
398 | } | 468 | } |
399 | SDL_SetWindowMinimumSize(d->win, 400, 200); | 469 | SDL_SetWindowMinimumSize(d->win, 400, 250); |
400 | SDL_SetWindowTitle(d->win, "Lagrange"); | 470 | SDL_SetWindowTitle(d->win, "Lagrange"); |
401 | /* Some info. */ { | 471 | /* Some info. */ { |
402 | SDL_RendererInfo info; | 472 | SDL_RendererInfo info; |
@@ -442,6 +512,11 @@ void deinit_Window(iWindow *d) { | |||
442 | if (theWindow_ == d) { | 512 | if (theWindow_ == d) { |
443 | theWindow_ = NULL; | 513 | theWindow_ = NULL; |
444 | } | 514 | } |
515 | iForIndices(i, d->cursors) { | ||
516 | if (d->cursors[i]) { | ||
517 | SDL_FreeCursor(d->cursors[i]); | ||
518 | } | ||
519 | } | ||
445 | iReleasePtr(&d->root); | 520 | iReleasePtr(&d->root); |
446 | deinit_Text(); | 521 | deinit_Text(); |
447 | SDL_DestroyRenderer(d->render); | 522 | SDL_DestroyRenderer(d->render); |
@@ -470,6 +545,13 @@ static iBool handleWindowEvent_Window_(iWindow *d, const SDL_WindowEvent *ev) { | |||
470 | return iFalse; | 545 | return iFalse; |
471 | } | 546 | } |
472 | 547 | ||
548 | static void applyCursor_Window_(iWindow *d) { | ||
549 | if (d->pendingCursor) { | ||
550 | SDL_SetCursor(d->pendingCursor); | ||
551 | d->pendingCursor = NULL; | ||
552 | } | ||
553 | } | ||
554 | |||
473 | iBool processEvent_Window(iWindow *d, const SDL_Event *ev) { | 555 | iBool processEvent_Window(iWindow *d, const SDL_Event *ev) { |
474 | switch (ev->type) { | 556 | switch (ev->type) { |
475 | case SDL_WINDOWEVENT: { | 557 | case SDL_WINDOWEVENT: { |
@@ -484,6 +566,7 @@ iBool processEvent_Window(iWindow *d, const SDL_Event *ev) { | |||
484 | } | 566 | } |
485 | /* Map mouse pointer coordinate to our coordinate system. */ | 567 | /* Map mouse pointer coordinate to our coordinate system. */ |
486 | if (event.type == SDL_MOUSEMOTION) { | 568 | if (event.type == SDL_MOUSEMOTION) { |
569 | setCursor_Window(d, SDL_SYSTEM_CURSOR_ARROW); /* default cursor */ | ||
487 | const iInt2 pos = coord_Window(d, event.motion.x, event.motion.y); | 570 | const iInt2 pos = coord_Window(d, event.motion.x, event.motion.y); |
488 | event.motion.x = pos.x; | 571 | event.motion.x = pos.x; |
489 | event.motion.y = pos.y; | 572 | event.motion.y = pos.y; |
@@ -506,6 +589,9 @@ iBool processEvent_Window(iWindow *d, const SDL_Event *ev) { | |||
506 | if (oldHover != hover_Widget()) { | 589 | if (oldHover != hover_Widget()) { |
507 | postRefresh_App(); | 590 | postRefresh_App(); |
508 | } | 591 | } |
592 | if (event.type == SDL_MOUSEMOTION) { | ||
593 | applyCursor_Window_(d); | ||
594 | } | ||
509 | return wasUsed; | 595 | return wasUsed; |
510 | } | 596 | } |
511 | } | 597 | } |
@@ -564,6 +650,13 @@ void setFreezeDraw_Window(iWindow *d, iBool freezeDraw) { | |||
564 | d->isDrawFrozen = freezeDraw; | 650 | d->isDrawFrozen = freezeDraw; |
565 | } | 651 | } |
566 | 652 | ||
653 | void setCursor_Window(iWindow *d, int cursor) { | ||
654 | if (!d->cursors[cursor]) { | ||
655 | d->cursors[cursor] = SDL_CreateSystemCursor(cursor); | ||
656 | } | ||
657 | d->pendingCursor = d->cursors[cursor]; | ||
658 | } | ||
659 | |||
567 | iInt2 rootSize_Window(const iWindow *d) { | 660 | iInt2 rootSize_Window(const iWindow *d) { |
568 | return d->root->rect.size; | 661 | return d->root->rect.size; |
569 | } | 662 | } |
diff --git a/src/ui/window.h b/src/ui/window.h index d6eed841..4aec2fa7 100644 --- a/src/ui/window.h +++ b/src/ui/window.h | |||
@@ -1,3 +1,25 @@ | |||
1 | /* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi> | ||
2 | |||
3 | Redistribution and use in source and binary forms, with or without | ||
4 | modification, are permitted provided that the following conditions are met: | ||
5 | |||
6 | 1. Redistributions of source code must retain the above copyright notice, this | ||
7 | list of conditions and the following disclaimer. | ||
8 | 2. Redistributions in binary form must reproduce the above copyright notice, | ||
9 | this list of conditions and the following disclaimer in the documentation | ||
10 | and/or other materials provided with the distribution. | ||
11 | |||
12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | ||
13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | ||
14 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||
15 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR | ||
16 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | ||
17 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | ||
18 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON | ||
19 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||
20 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | ||
21 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | ||
22 | |||
1 | #pragma once | 23 | #pragma once |
2 | 24 | ||
3 | #include "widget.h" | 25 | #include "widget.h" |
@@ -19,6 +41,8 @@ struct Impl_Window { | |||
19 | float uiScale; | 41 | float uiScale; |
20 | uint32_t frameTime; | 42 | uint32_t frameTime; |
21 | double presentTime; | 43 | double presentTime; |
44 | SDL_Cursor * cursors[SDL_NUM_SYSTEM_CURSORS]; | ||
45 | SDL_Cursor * pendingCursor; | ||
22 | }; | 46 | }; |
23 | 47 | ||
24 | iBool processEvent_Window (iWindow *, const SDL_Event *); | 48 | iBool processEvent_Window (iWindow *, const SDL_Event *); |
@@ -27,6 +51,7 @@ void resize_Window (iWindow *, int w, int h); | |||
27 | void setTitle_Window (iWindow *, const iString *title); | 51 | void setTitle_Window (iWindow *, const iString *title); |
28 | void setUiScale_Window (iWindow *, float uiScale); | 52 | void setUiScale_Window (iWindow *, float uiScale); |
29 | void setFreezeDraw_Window (iWindow *, iBool freezeDraw); | 53 | void setFreezeDraw_Window (iWindow *, iBool freezeDraw); |
54 | void setCursor_Window (iWindow *, int cursor); | ||
30 | 55 | ||
31 | iInt2 rootSize_Window (const iWindow *); | 56 | iInt2 rootSize_Window (const iWindow *); |
32 | float uiScale_Window (const iWindow *); | 57 | float uiScale_Window (const iWindow *); |
diff --git a/src/visited.c b/src/visited.c index af976be5..912a6318 100644 --- a/src/visited.c +++ b/src/visited.c | |||
@@ -1,8 +1,31 @@ | |||
1 | /* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi> | ||
2 | |||
3 | Redistribution and use in source and binary forms, with or without | ||
4 | modification, are permitted provided that the following conditions are met: | ||
5 | |||
6 | 1. Redistributions of source code must retain the above copyright notice, this | ||
7 | list of conditions and the following disclaimer. | ||
8 | 2. Redistributions in binary form must reproduce the above copyright notice, | ||
9 | this list of conditions and the following disclaimer in the documentation | ||
10 | and/or other materials provided with the distribution. | ||
11 | |||
12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | ||
13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | ||
14 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||
15 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR | ||
16 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | ||
17 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | ||
18 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON | ||
19 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||
20 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | ||
21 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | ||
22 | |||
1 | #include "visited.h" | 23 | #include "visited.h" |
2 | #include "app.h" | 24 | #include "app.h" |
3 | 25 | ||
4 | #include <the_Foundation/file.h> | 26 | #include <the_Foundation/file.h> |
5 | #include <the_Foundation/path.h> | 27 | #include <the_Foundation/path.h> |
28 | #include <the_Foundation/ptrarray.h> | ||
6 | #include <the_Foundation/sortedarray.h> | 29 | #include <the_Foundation/sortedarray.h> |
7 | 30 | ||
8 | static const size_t maxAgeVisited_Visited_ = 3600 * 24 * 30; /* one month */ | 31 | static const size_t maxAgeVisited_Visited_ = 3600 * 24 * 30; /* one month */ |
@@ -73,7 +96,7 @@ void load_Visited(iVisited *d, const char *dirPath) { | |||
73 | iRangecc line = iNullRange; | 96 | iRangecc line = iNullRange; |
74 | iTime now; | 97 | iTime now; |
75 | initCurrent_Time(&now); | 98 | initCurrent_Time(&now); |
76 | while (nextSplit_Rangecc(&src, "\n", &line)) { | 99 | while (nextSplit_Rangecc(src, "\n", &line)) { |
77 | int y, m, D, H, M, S; | 100 | int y, m, D, H, M, S; |
78 | sscanf(line.start, "%04d-%02d-%02dT%02d:%02d:%02d ", &y, &m, &D, &H, &M, &S); | 101 | sscanf(line.start, "%04d-%02d-%02dT%02d:%02d:%02d ", &y, &m, &D, &H, &M, &S); |
79 | if (!y) break; | 102 | if (!y) break; |
@@ -99,6 +122,16 @@ void clear_Visited(iVisited *d) { | |||
99 | clear_SortedArray(&d->visited); | 122 | clear_SortedArray(&d->visited); |
100 | } | 123 | } |
101 | 124 | ||
125 | static size_t find_Visited_(const iVisited *d, const iString *url) { | ||
126 | iVisitedUrl visit; | ||
127 | init_VisitedUrl(&visit); | ||
128 | set_String(&visit.url, url); | ||
129 | size_t pos = iInvalidPos; | ||
130 | locate_SortedArray(&d->visited, &visit, &pos); | ||
131 | deinit_VisitedUrl(&visit); | ||
132 | return pos; | ||
133 | } | ||
134 | |||
102 | void visitUrl_Visited(iVisited *d, const iString *url) { | 135 | void visitUrl_Visited(iVisited *d, const iString *url) { |
103 | iVisitedUrl visit; | 136 | iVisitedUrl visit; |
104 | init_VisitedUrl(&visit); | 137 | init_VisitedUrl(&visit); |
@@ -115,6 +148,14 @@ void visitUrl_Visited(iVisited *d, const iString *url) { | |||
115 | insert_SortedArray(&d->visited, &visit); | 148 | insert_SortedArray(&d->visited, &visit); |
116 | } | 149 | } |
117 | 150 | ||
151 | void removeUrl_Visited(iVisited *d, const iString *url) { | ||
152 | size_t pos = find_Visited_(d, url); | ||
153 | if (pos != iInvalidPos) { | ||
154 | deinit_VisitedUrl(at_SortedArray(&d->visited, pos)); | ||
155 | remove_Array(&d->visited.values, pos); | ||
156 | } | ||
157 | } | ||
158 | |||
118 | iTime urlVisitTime_Visited(const iVisited *d, const iString *url) { | 159 | iTime urlVisitTime_Visited(const iVisited *d, const iString *url) { |
119 | iVisitedUrl item; | 160 | iVisitedUrl item; |
120 | size_t pos; | 161 | size_t pos; |
@@ -126,3 +167,17 @@ iTime urlVisitTime_Visited(const iVisited *d, const iString *url) { | |||
126 | deinit_String(&item.url); | 167 | deinit_String(&item.url); |
127 | return item.when; | 168 | return item.when; |
128 | } | 169 | } |
170 | |||
171 | static int cmpWhenDescending_VisitedUrlPtr_(const void *a, const void *b) { | ||
172 | const iVisitedUrl *s = *(const void **) a, *t = *(const void **) b; | ||
173 | return -cmp_Time(&s->when, &t->when); | ||
174 | } | ||
175 | |||
176 | const iArray *list_Visited(const iVisited *d, size_t count) { | ||
177 | iPtrArray *urls = collectNew_PtrArray(); | ||
178 | iConstForEach(Array, i, &d->visited.values) { | ||
179 | pushBack_PtrArray(urls, i.value); | ||
180 | } | ||
181 | sort_Array(urls, cmpWhenDescending_VisitedUrlPtr_); | ||
182 | return urls; | ||
183 | } | ||
diff --git a/src/visited.h b/src/visited.h index 2f8382c7..5a34d008 100644 --- a/src/visited.h +++ b/src/visited.h | |||
@@ -1,8 +1,30 @@ | |||
1 | /* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi> | ||
2 | |||
3 | Redistribution and use in source and binary forms, with or without | ||
4 | modification, are permitted provided that the following conditions are met: | ||
5 | |||
6 | 1. Redistributions of source code must retain the above copyright notice, this | ||
7 | list of conditions and the following disclaimer. | ||
8 | 2. Redistributions in binary form must reproduce the above copyright notice, | ||
9 | this list of conditions and the following disclaimer in the documentation | ||
10 | and/or other materials provided with the distribution. | ||
11 | |||
12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | ||
13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | ||
14 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||
15 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR | ||
16 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | ||
17 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | ||
18 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON | ||
19 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||
20 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | ||
21 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | ||
22 | |||
1 | #pragma once | 23 | #pragma once |
2 | 24 | ||
3 | #include "gmrequest.h" | 25 | #include "gmrequest.h" |
4 | 26 | ||
5 | #include <the_Foundation/array.h> | 27 | #include <the_Foundation/ptrarray.h> |
6 | #include <the_Foundation/string.h> | 28 | #include <the_Foundation/string.h> |
7 | #include <the_Foundation/time.h> | 29 | #include <the_Foundation/time.h> |
8 | 30 | ||
@@ -23,3 +45,6 @@ void save_Visited (const iVisited *, const char *dirPath); | |||
23 | 45 | ||
24 | iTime urlVisitTime_Visited (const iVisited *, const iString *url); | 46 | iTime urlVisitTime_Visited (const iVisited *, const iString *url); |
25 | void visitUrl_Visited (iVisited *, const iString *url); /* adds URL to the visited URLs set */ | 47 | void visitUrl_Visited (iVisited *, const iString *url); /* adds URL to the visited URLs set */ |
48 | void removeUrl_Visited (iVisited *, const iString *url); | ||
49 | |||
50 | const iPtrArray * list_Visited (const iVisited *, size_t count); /* returns collected */ | ||
diff --git a/src/win32.c b/src/win32.c index 1cdcf34c..7685fcac 100644 --- a/src/win32.c +++ b/src/win32.c | |||
@@ -1,3 +1,25 @@ | |||
1 | /* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi> | ||
2 | |||
3 | Redistribution and use in source and binary forms, with or without | ||
4 | modification, are permitted provided that the following conditions are met: | ||
5 | |||
6 | 1. Redistributions of source code must retain the above copyright notice, this | ||
7 | list of conditions and the following disclaimer. | ||
8 | 2. Redistributions in binary form must reproduce the above copyright notice, | ||
9 | this list of conditions and the following disclaimer in the documentation | ||
10 | and/or other materials provided with the distribution. | ||
11 | |||
12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | ||
13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | ||
14 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||
15 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR | ||
16 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | ||
17 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | ||
18 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON | ||
19 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||
20 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | ||
21 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | ||
22 | |||
1 | #include "win32.h" | 23 | #include "win32.h" |
2 | #include <SDL_syswm.h> | 24 | #include <SDL_syswm.h> |
3 | 25 | ||
diff --git a/src/win32.h b/src/win32.h index b223ea4d..3dac677c 100644 --- a/src/win32.h +++ b/src/win32.h | |||
@@ -1,3 +1,25 @@ | |||
1 | /* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi> | ||
2 | |||
3 | Redistribution and use in source and binary forms, with or without | ||
4 | modification, are permitted provided that the following conditions are met: | ||
5 | |||
6 | 1. Redistributions of source code must retain the above copyright notice, this | ||
7 | list of conditions and the following disclaimer. | ||
8 | 2. Redistributions in binary form must reproduce the above copyright notice, | ||
9 | this list of conditions and the following disclaimer in the documentation | ||
10 | and/or other materials provided with the distribution. | ||
11 | |||
12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | ||
13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | ||
14 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||
15 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR | ||
16 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | ||
17 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | ||
18 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON | ||
19 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||
20 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | ||
21 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | ||
22 | |||
1 | #pragma once | 23 | #pragma once |
2 | #include <SDL_video.h> | 24 | #include <SDL_video.h> |
3 | 25 | ||