├── .editorconfig ├── .github └── workflows │ ├── linux.yml │ ├── macos.yml │ └── windows.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── ext ├── webview.cc └── webview.h ├── shard.yml ├── spec ├── spec_helper.cr └── webview_spec.cr └── src ├── lib.cr └── webview.cr /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.cr] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 2 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.github/workflows/linux.yml: -------------------------------------------------------------------------------- 1 | name: Linux CI 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | schedule: 9 | - cron: "0 6 * * 6" 10 | jobs: 11 | build-ubuntu: 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | include: 16 | - { os: ubuntu-latest, crystal: latest } 17 | - { os: ubuntu-latest, crystal: nightly } 18 | runs-on: ${{matrix.os}} 19 | steps: 20 | - name: Install Crystal 21 | uses: crystal-lang/install-crystal@v1 22 | with: 23 | crystal: ${{matrix.crystal}} 24 | - uses: actions/checkout@v2 25 | - name: Install dependencies 26 | run: sudo apt install libgtk-3-dev libwebkit2gtk-4.0-dev 27 | - name: Install crystal land dependencies 28 | run: shards install 29 | - name: Compile library 30 | run: make 31 | - name: Run tests 32 | run: crystal spec 33 | -------------------------------------------------------------------------------- /.github/workflows/macos.yml: -------------------------------------------------------------------------------- 1 | name: Mac OSX CI 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | schedule: 9 | - cron: "0 6 * * 6" 10 | jobs: 11 | build-macos: 12 | runs-on: macos-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Install Crystal 16 | uses: crystal-lang/install-crystal@v1 17 | - name: Install dependencies 18 | run: shards install 19 | - name: Compile library 20 | run: make 21 | - name: Run tests 22 | run: crystal spec 23 | -------------------------------------------------------------------------------- /.github/workflows/windows.yml: -------------------------------------------------------------------------------- 1 | name: Windows CI 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | schedule: 9 | - cron: "0 6 * * 6" 10 | jobs: 11 | build-windows: 12 | runs-on: windows-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Install Crystal 16 | uses: crystal-lang/install-crystal@v1 17 | - name: Enable Developer Command Prompt 18 | uses: ilammy/msvc-dev-cmd@cec98b9d092141f74527d0afa6feb2af698cfe89 # v1.12.1 19 | - name: Install Webview2 SDK 20 | run: | 21 | mkdir libs\webview2 22 | nuget.exe install Microsoft.Web.Webview2 -Version 1.0.1150.38 -OutputDirectory libs\webview2 23 | copy libs\webview2\microsoft.web.webview2.1.0.1150.38\build\native\include\*.h ext\ 24 | - name: Install crystal land dependencies 25 | run: shards install 26 | - name: Compile library 27 | shell: powershell 28 | run: | 29 | cd ext 30 | cl /D "WEBVIEW_API=__declspec(dllexport)" /std:c++17 /EHsc webview.cc /link /DLL "/OUT:..\webview.dll" 31 | cd .. 32 | copy webview.lib (crystal env CRYSTAL_LIBRARY_PATH) 33 | - name: Run tests 34 | run: crystal spec 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /docs/ 2 | /lib/ 3 | /bin/ 4 | /.shards/ 5 | *.dwarf 6 | /.vscode/ 7 | # Libraries don't need dependency lock 8 | # Dependencies will be locked in applications that use them 9 | /shard.lock 10 | .ameba.yml 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Ali Naqvi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | UNAME := $(shell uname) 2 | 3 | ifeq ($(UNAME), Darwin) 4 | CFLAGS = -DWEBVIEW_COCOA=1 -DOBJC_OLD_DISPATCH_PROTOTYPES=1 5 | endif 6 | 7 | ifeq ($(UNAME), Linux) 8 | CFLAGS = -DWEBVIEW_GTK=1 `pkg-config --cflags --libs gtk+-3.0 webkit2gtk-4.0` 9 | endif 10 | 11 | CFLAGS += -std=c++11 12 | cpp_file := ext/webview.cc 13 | obj_file := $(cpp_file:.cc=.o) 14 | 15 | .PHONY: all 16 | all: $(obj_file) 17 | 18 | ifeq ($(UNAME), Linux) 19 | ar rcs ext/libwebview.a ext/webview.o 20 | endif 21 | 22 | %.o: %.cc 23 | $(CXX) -c -o $@ $(CFLAGS) $< 24 | 25 | .PHONY: clean 26 | clean: 27 | rm -f $(obj_file) 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Linux CI](https://github.com/naqvis/webview/actions/workflows/linux.yml/badge.svg)](https://github.com/naqvis/webview/actions/workflows/linux.yml) 2 | [![MacOSX CI](https://github.com/naqvis/webview/actions/workflows/macos.yml/badge.svg)](https://github.com/naqvis/webview/actions/workflows/macos.yml) 3 | [![Windows CI](https://github.com/naqvis/webview/actions/workflows/windows.yml/badge.svg)](https://github.com/naqvis/webview/actions/workflows/windows.yml) 4 | # Crystal Webview 5 | 6 | Crystal language bindings for [zserge's Webview](https://github.com/zserge/webview) which is an excellent cross-platform single-header webview library for C/C++ using Gtk, Cocoa, or MSHTML/Edge, depending on the host OS. 7 | 8 | **Webview** relys on default rendering engine of host Operating System, thus binaries generated with this Shard will be much more leaner as compared to [Electron](https://github.com/electron/electron) which bundles Chromium with each distribution. 9 | 10 | This shard supports **two-way bindings** between Crystal and JavaScript. You can invoke JS code via `Webview::Webview#eval` and calling Crystal code from JS is done via `WebView::Webview#bind` (refer to Examples 3 & 4 for samples on how to invoke Crystal functions from JS). 11 | 12 | Webview-supported platforms and the engines you can expect to render your application content are as follows: 13 | 14 | | Operating System | Browser Engine Used | 15 | | ---------------- | ------------------- | 16 | | macOS | Cocoa, [WebKit][webkit] | 17 | | Linux | [GTK 3][gtk], [WebKitGTK][webkitgtk]| 18 | | Windows | [Windows API][win32-api], [WebView2][ms-webview2] | 19 | 20 | ## Pre-requisite 21 | If you're planning on targeting Linux or BSD you must ensure that [WebKit2GTK][webkitgtk] is already installed and available for discovery via the pkg-config command. 22 | 23 | Debian-based systems: 24 | 25 | * Packages: 26 | * Development: `apt install libgtk-3-dev libwebkit2gtk-4.0-dev` 27 | * Production: `apt install libgtk-3-0 libwebkit2gtk-4.0-37` 28 | 29 | BSD-based systems: 30 | 31 | * FreeBSD packages: `pkg install webkit2-gtk3` 32 | * Execution on BSD-based systems may require adding the `wxallowed` option (see [mount(8)](https://man.openbsd.org/mount.8)) to your fstab to bypass [W^X](https://en.wikipedia.org/wiki/W%5EX "write xor execute") memory protection for your executable. Please see if it works without disabling this security feature first. 33 | 34 | Microsoft Windows: 35 | 36 | * You should have Visual C++ Build tools already as it's a pre-requisite for crystal compiler 37 | * `git clone https://github.com/webview/webview` to get WebView sources 38 | * `webview\script\build.bat` to compile them (it will download required nuget package) 39 | * copy `webview\dll\x64\webview.lib` to `\lib` 40 | * copy `webview\dll\x64\webview.dll` to directory with your program 41 | 42 | ## Installation 43 | 44 | 1. Add the dependency to your `shard.yml`: 45 | 46 | ```yaml 47 | dependencies: 48 | webview: 49 | github: naqvis/webview 50 | ``` 51 | 52 | 2. Run `shards install` 53 | 54 | ## Usage 55 | 56 | ### Example 1: Loading URL 57 | 58 | ```crystal 59 | require "webview" 60 | 61 | wv = Webview.window(640, 480, Webview::SizeHints::NONE, "Hello WebView", "http://crystal-lang.org") 62 | wv.run 63 | wv.destroy 64 | ``` 65 | 66 | ### Example 2: Loading HTML 67 | 68 | ```crystal 69 | require "webview" 70 | 71 | html = <<-HTML 72 | 73 | 74 | Hello,World! 75 | 76 | 77 |
78 |
79 | 80 |

City Gallery

81 |
82 | 89 |
90 |

London

91 | Mountain View 92 |

London is the capital city of England. It is the most populous city in the United Kingdom, with a metropolitan area of over 13 million inhabitants.

93 |

Standing on the River Thames, London has been a major settlement for two millennia, its history going back to its founding by the Romans, who named it Londinium.

94 |
95 |
Copyright © W3Schools.com
96 |
97 | 98 | 99 | HTML 100 | 101 | wv = Webview.window(640, 480, Webview::SizeHints::NONE, "Hello WebView") 102 | wv.html = html 103 | wv.run 104 | wv.destroy 105 | ``` 106 | 107 | ### Example 3: Calling Crystal code from JavaScript 108 | ```crystal 109 | require "webview" 110 | 111 | html = <<-HTML 112 | 113 | 114 | hello 115 | 126 | 127 | HTML 128 | 129 | wv = Webview.window(640, 480, Webview::SizeHints::NONE, "Hello WebView", true) 130 | wv.html = html 131 | wv.bind("noop", Webview::JSProc.new { |a| 132 | pp "Noop called with arguments: #{a}" 133 | JSON::Any.new("noop") 134 | }) 135 | 136 | wv.bind("add", Webview::JSProc.new { |a| 137 | pp "add called with arguments: #{a}" 138 | ret = 0_i64 139 | a.each do |v| 140 | ret += v.as_i64 141 | end 142 | JSON::Any.new(ret) 143 | }) 144 | 145 | 146 | wv.run 147 | wv.destroy 148 | ``` 149 | 150 | ### Example 4: Calling Crystal code from JavaScript and executing JavaScript from Crystal 151 | 152 | ```crystal 153 | require "webview" 154 | 155 | html = <<-HTML 156 | 157 | 158 | Hello,World! 159 | 160 | 161 | 162 | 163 | 164 | HTML 165 | 166 | 167 | inject = <<-JS 168 | elem = document.createElement('div'); 169 | elem.innerHTML = "hello webview %s"; 170 | document.body.appendChild(elem); 171 | JS 172 | 173 | wv = Webview.window(640, 480, Webview::SizeHints::NONE, "Hello WebView", true) 174 | wv.html = html 175 | 176 | wv.bind("add", Webview::JSProc.new { |n| 177 | wv.eval(sprintf(inject, n)) 178 | JSON::Any.new(nil) 179 | }) 180 | 181 | wv.run 182 | wv.destroy 183 | ``` 184 | 185 | ### Example 5: Running your web app in another thread 186 | 187 | ```crystal 188 | Thread.new do 189 | get "/" do 190 | "hello from kemal" 191 | end 192 | Kemal.run 193 | end 194 | 195 | wv = Webview.window(640, 480, Webview::SizeHints::NONE, "WebView with local webapp!", "http://localhost:3000") 196 | wv.run 197 | wv.destroy 198 | ``` 199 | 200 | ## App Distribution 201 | 202 | Distribution of your app is outside the scope of this library but we can give some pointers for you to explore. 203 | 204 | ### macOS Application Bundle 205 | 206 | On macOS you would typically create a bundle for your app with an icon and proper metadata. 207 | 208 | A minimalistic bundle typically has the following directory structure: 209 | 210 | ``` 211 | example.app bundle 212 | └── Contents 213 | ├── Info.plist information property list 214 | ├── MacOS 215 | | └── example executable 216 | └── Resources 217 | └── example.icns icon 218 | ``` 219 | 220 | Read more about the [structure of bundles][macos-app-bundle] at the Apple Developer site. 221 | 222 | > Tip: The `png2icns` tool can create icns files from PNG files. See the `icnsutils` package for Debian-based systems. 223 | 224 | ### Windows Apps 225 | 226 | You would typically create a resource script file (`*.rc`) with information about the app as well as an icon. Since you should have MinGW-w64 readily available then you can compile the file using `windres` and link it into your program. If you instead use Visual C++ then look into the [Windows Resource Compiler][win32-rc]. 227 | 228 | The directory structure could look like this: 229 | 230 | ``` 231 | my-project/ 232 | ├── icons/ 233 | | ├── application.ico 234 | | └── window.ico 235 | ├── basic.cc 236 | └── resources.rc 237 | ``` 238 | 239 | `resources.rc`: 240 | ``` 241 | 100 ICON "icons\\application.ico" 242 | 32512 ICON "icons\\window.ico" 243 | ``` 244 | 245 | > **Note:** The ID of the icon resource to be used for the window must be `32512` (`IDI_APPLICATION`). 246 | 247 | ## Limitations 248 | 249 | ### Browser Features 250 | 251 | Since a browser engine is not a full web browser it may not support every feature you may expect from a browser. If you find that a feature does not work as expected then please consult with the browser engine's documentation and [open an issue on webview library][issues-new] if you think that the library should support it. 252 | 253 | For example, the `webview` library does not attempt to support user interaction features like `alert()`, `confirm()` and `prompt()` and other non-essential features like `console.log()`. 254 | ## Contributing 255 | 256 | 1. Fork it () 257 | 2. Create your feature branch (`git checkout -b my-new-feature`) 258 | 3. Commit your changes (`git commit -am 'Add some feature'`) 259 | 4. Push to the branch (`git push origin my-new-feature`) 260 | 5. Create a new Pull Request 261 | 262 | ## Contributors 263 | 264 | - [Ali Naqvi](https://github.com/naqvis) - creator and maintainer 265 | 266 | 267 | [macos-app-bundle]: https://developer.apple.com/library/archive/documentation/CoreFoundation/Conceptual/CFBundles/BundleTypes/BundleTypes.html 268 | [gtk]: https://docs.gtk.org/gtk3/ 269 | [issues-new]: https://github.com/webview/webview/issues/new 270 | [webkit]: https://webkit.org/ 271 | [webkitgtk]: https://webkitgtk.org/ 272 | [ms-webview2]: https://developer.microsoft.com/en-us/microsoft-edge/webview2/ 273 | [ms-webview2-sdk]: https://www.nuget.org/packages/Microsoft.Web.WebView2 274 | [ms-webview2-rt]: https://developer.microsoft.com/en-us/microsoft-edge/webview2/ 275 | [win32-api]: https://docs.microsoft.com/en-us/windows/win32/apiindex/windows-api-list 276 | [win32-rc]: https://docs.microsoft.com/en-us/windows/win32/menurc/resource-compiler -------------------------------------------------------------------------------- /ext/webview.cc: -------------------------------------------------------------------------------- 1 | #define WEBVIEW_IMPLEMENTATION 2 | #include "webview.h" 3 | -------------------------------------------------------------------------------- /ext/webview.h: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2017 Serge Zaitsev 5 | * Copyright (c) 2022 Steffen André Langnes 6 | * 7 | * Permission is hereby granted, free of charge, to any person obtaining a copy 8 | * of this software and associated documentation files (the "Software"), to deal 9 | * in the Software without restriction, including without limitation the rights 10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | * copies of the Software, and to permit persons to whom the Software is 12 | * furnished to do so, subject to the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be included in 15 | * all copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | * SOFTWARE. 24 | */ 25 | #ifndef WEBVIEW_H 26 | #define WEBVIEW_H 27 | 28 | #ifndef WEBVIEW_API 29 | #define WEBVIEW_API extern 30 | #endif 31 | 32 | #ifndef WEBVIEW_VERSION_MAJOR 33 | // The current library major version. 34 | #define WEBVIEW_VERSION_MAJOR 0 35 | #endif 36 | 37 | #ifndef WEBVIEW_VERSION_MINOR 38 | // The current library minor version. 39 | #define WEBVIEW_VERSION_MINOR 10 40 | #endif 41 | 42 | #ifndef WEBVIEW_VERSION_PATCH 43 | // The current library patch version. 44 | #define WEBVIEW_VERSION_PATCH 0 45 | #endif 46 | 47 | #ifndef WEBVIEW_VERSION_PRE_RELEASE 48 | // SemVer 2.0.0 pre-release labels prefixed with "-". 49 | #define WEBVIEW_VERSION_PRE_RELEASE "" 50 | #endif 51 | 52 | #ifndef WEBVIEW_VERSION_BUILD_METADATA 53 | // SemVer 2.0.0 build metadata prefixed with "+". 54 | #define WEBVIEW_VERSION_BUILD_METADATA "" 55 | #endif 56 | 57 | // Utility macro for stringifying a macro argument. 58 | #define WEBVIEW_STRINGIFY(x) #x 59 | 60 | // Utility macro for stringifying the result of a macro argument expansion. 61 | #define WEBVIEW_EXPAND_AND_STRINGIFY(x) WEBVIEW_STRINGIFY(x) 62 | 63 | // SemVer 2.0.0 version number in MAJOR.MINOR.PATCH format. 64 | #define WEBVIEW_VERSION_NUMBER \ 65 | WEBVIEW_EXPAND_AND_STRINGIFY(WEBVIEW_VERSION_MAJOR) \ 66 | "." WEBVIEW_EXPAND_AND_STRINGIFY( \ 67 | WEBVIEW_VERSION_MINOR) "." WEBVIEW_EXPAND_AND_STRINGIFY(WEBVIEW_VERSION_PATCH) 68 | 69 | // Holds the elements of a MAJOR.MINOR.PATCH version number. 70 | typedef struct { 71 | // Major version. 72 | unsigned int major; 73 | // Minor version. 74 | unsigned int minor; 75 | // Patch version. 76 | unsigned int patch; 77 | } webview_version_t; 78 | 79 | // Holds the library's version information. 80 | typedef struct { 81 | // The elements of the version number. 82 | webview_version_t version; 83 | // SemVer 2.0.0 version number in MAJOR.MINOR.PATCH format. 84 | char version_number[32]; 85 | // SemVer 2.0.0 pre-release labels prefixed with "-" if specified, otherwise 86 | // an empty string. 87 | char pre_release[48]; 88 | // SemVer 2.0.0 build metadata prefixed with "+", otherwise an empty string. 89 | char build_metadata[48]; 90 | } webview_version_info_t; 91 | 92 | #ifdef __cplusplus 93 | extern "C" { 94 | #endif 95 | 96 | typedef void *webview_t; 97 | 98 | // Creates a new webview instance. If debug is non-zero - developer tools will 99 | // be enabled (if the platform supports them). Window parameter can be a 100 | // pointer to the native window handle. If it's non-null - then child WebView 101 | // is embedded into the given parent window. Otherwise a new window is created. 102 | // Depending on the platform, a GtkWindow, NSWindow or HWND pointer can be 103 | // passed here. Returns null on failure. Creation can fail for various reasons 104 | // such as when required runtime dependencies are missing or when window creation 105 | // fails. 106 | WEBVIEW_API webview_t webview_create(int debug, void *window); 107 | 108 | // Destroys a webview and closes the native window. 109 | WEBVIEW_API void webview_destroy(webview_t w); 110 | 111 | // Runs the main loop until it's terminated. After this function exits - you 112 | // must destroy the webview. 113 | WEBVIEW_API void webview_run(webview_t w); 114 | 115 | // Stops the main loop. It is safe to call this function from another other 116 | // background thread. 117 | WEBVIEW_API void webview_terminate(webview_t w); 118 | 119 | // Posts a function to be executed on the main thread. You normally do not need 120 | // to call this function, unless you want to tweak the native window. 121 | WEBVIEW_API void 122 | webview_dispatch(webview_t w, void (*fn)(webview_t w, void *arg), void *arg); 123 | 124 | // Returns a native window handle pointer. When using GTK backend the pointer 125 | // is GtkWindow pointer, when using Cocoa backend the pointer is NSWindow 126 | // pointer, when using Win32 backend the pointer is HWND pointer. 127 | WEBVIEW_API void *webview_get_window(webview_t w); 128 | 129 | // Updates the title of the native window. Must be called from the UI thread. 130 | WEBVIEW_API void webview_set_title(webview_t w, const char *title); 131 | 132 | // Window size hints 133 | #define WEBVIEW_HINT_NONE 0 // Width and height are default size 134 | #define WEBVIEW_HINT_MIN 1 // Width and height are minimum bounds 135 | #define WEBVIEW_HINT_MAX 2 // Width and height are maximum bounds 136 | #define WEBVIEW_HINT_FIXED 3 // Window size can not be changed by a user 137 | // Updates native window size. See WEBVIEW_HINT constants. 138 | WEBVIEW_API void webview_set_size(webview_t w, int width, int height, 139 | int hints); 140 | 141 | // Navigates webview to the given URL. URL may be a properly encoded data URI. 142 | // Examples: 143 | // webview_navigate(w, "https://github.com/webview/webview"); 144 | // webview_navigate(w, "data:text/html,%3Ch1%3EHello%3C%2Fh1%3E"); 145 | // webview_navigate(w, "data:text/html;base64,PGgxPkhlbGxvPC9oMT4="); 146 | WEBVIEW_API void webview_navigate(webview_t w, const char *url); 147 | 148 | // Set webview HTML directly. 149 | // Example: webview_set_html(w, "

Hello

"); 150 | WEBVIEW_API void webview_set_html(webview_t w, const char *html); 151 | 152 | // Injects JavaScript code at the initialization of the new page. Every time 153 | // the webview will open a the new page - this initialization code will be 154 | // executed. It is guaranteed that code is executed before window.onload. 155 | WEBVIEW_API void webview_init(webview_t w, const char *js); 156 | 157 | // Evaluates arbitrary JavaScript code. Evaluation happens asynchronously, also 158 | // the result of the expression is ignored. Use RPC bindings if you want to 159 | // receive notifications about the results of the evaluation. 160 | WEBVIEW_API void webview_eval(webview_t w, const char *js); 161 | 162 | // Binds a native C callback so that it will appear under the given name as a 163 | // global JavaScript function. Internally it uses webview_init(). Callback 164 | // receives a request string and a user-provided argument pointer. Request 165 | // string is a JSON array of all the arguments passed to the JavaScript 166 | // function. 167 | WEBVIEW_API void webview_bind(webview_t w, const char *name, 168 | void (*fn)(const char *seq, const char *req, 169 | void *arg), 170 | void *arg); 171 | 172 | // Removes a native C callback that was previously set by webview_bind. 173 | WEBVIEW_API void webview_unbind(webview_t w, const char *name); 174 | 175 | // Allows to return a value from the native binding. Original request pointer 176 | // must be provided to help internal RPC engine match requests with responses. 177 | // If status is zero - result is expected to be a valid JSON result value. 178 | // If status is not zero - result is an error JSON object. 179 | WEBVIEW_API void webview_return(webview_t w, const char *seq, int status, 180 | const char *result); 181 | 182 | // Get the library's version information. 183 | // @since 0.10 184 | WEBVIEW_API const webview_version_info_t *webview_version(); 185 | 186 | #ifdef __cplusplus 187 | } 188 | 189 | #ifndef WEBVIEW_HEADER 190 | 191 | #if !defined(WEBVIEW_GTK) && !defined(WEBVIEW_COCOA) && !defined(WEBVIEW_EDGE) 192 | #if defined(__APPLE__) 193 | #define WEBVIEW_COCOA 194 | #elif defined(__unix__) 195 | #define WEBVIEW_GTK 196 | #elif defined(_WIN32) 197 | #define WEBVIEW_EDGE 198 | #else 199 | #error "please, specify webview backend" 200 | #endif 201 | #endif 202 | 203 | #ifndef WEBVIEW_DEPRECATED 204 | #if __cplusplus >= 201402L 205 | #define WEBVIEW_DEPRECATED(reason) [[deprecated(reason)]] 206 | #elif defined(_MSC_VER) 207 | #define WEBVIEW_DEPRECATED(reason) __declspec(deprecated(reason)) 208 | #else 209 | #define WEBVIEW_DEPRECATED(reason) __attribute__((deprecated(reason))) 210 | #endif 211 | #endif 212 | 213 | #ifndef WEBVIEW_DEPRECATED_PRIVATE 214 | #define WEBVIEW_DEPRECATED_PRIVATE \ 215 | WEBVIEW_DEPRECATED("Private API should not be used") 216 | #endif 217 | 218 | #include 219 | #include 220 | #include 221 | #include 222 | #include 223 | #include 224 | #include 225 | #include 226 | 227 | #include 228 | 229 | namespace webview { 230 | 231 | using dispatch_fn_t = std::function; 232 | 233 | namespace detail { 234 | 235 | // The library's version information. 236 | constexpr const webview_version_info_t library_version_info{ 237 | {WEBVIEW_VERSION_MAJOR, WEBVIEW_VERSION_MINOR, WEBVIEW_VERSION_PATCH}, 238 | WEBVIEW_VERSION_NUMBER, 239 | WEBVIEW_VERSION_PRE_RELEASE, 240 | WEBVIEW_VERSION_BUILD_METADATA}; 241 | 242 | inline int json_parse_c(const char *s, size_t sz, const char *key, size_t keysz, 243 | const char **value, size_t *valuesz) { 244 | enum { 245 | JSON_STATE_VALUE, 246 | JSON_STATE_LITERAL, 247 | JSON_STATE_STRING, 248 | JSON_STATE_ESCAPE, 249 | JSON_STATE_UTF8 250 | } state = JSON_STATE_VALUE; 251 | const char *k = nullptr; 252 | int index = 1; 253 | int depth = 0; 254 | int utf8_bytes = 0; 255 | 256 | *value = nullptr; 257 | *valuesz = 0; 258 | 259 | if (key == nullptr) { 260 | index = static_cast(keysz); 261 | if (index < 0) { 262 | return -1; 263 | } 264 | keysz = 0; 265 | } 266 | 267 | for (; sz > 0; s++, sz--) { 268 | enum { 269 | JSON_ACTION_NONE, 270 | JSON_ACTION_START, 271 | JSON_ACTION_END, 272 | JSON_ACTION_START_STRUCT, 273 | JSON_ACTION_END_STRUCT 274 | } action = JSON_ACTION_NONE; 275 | auto c = static_cast(*s); 276 | switch (state) { 277 | case JSON_STATE_VALUE: 278 | if (c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == ',' || 279 | c == ':') { 280 | continue; 281 | } else if (c == '"') { 282 | action = JSON_ACTION_START; 283 | state = JSON_STATE_STRING; 284 | } else if (c == '{' || c == '[') { 285 | action = JSON_ACTION_START_STRUCT; 286 | } else if (c == '}' || c == ']') { 287 | action = JSON_ACTION_END_STRUCT; 288 | } else if (c == 't' || c == 'f' || c == 'n' || c == '-' || 289 | (c >= '0' && c <= '9')) { 290 | action = JSON_ACTION_START; 291 | state = JSON_STATE_LITERAL; 292 | } else { 293 | return -1; 294 | } 295 | break; 296 | case JSON_STATE_LITERAL: 297 | if (c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == ',' || 298 | c == ']' || c == '}' || c == ':') { 299 | state = JSON_STATE_VALUE; 300 | s--; 301 | sz++; 302 | action = JSON_ACTION_END; 303 | } else if (c < 32 || c > 126) { 304 | return -1; 305 | } // fallthrough 306 | case JSON_STATE_STRING: 307 | if (c < 32 || (c > 126 && c < 192)) { 308 | return -1; 309 | } else if (c == '"') { 310 | action = JSON_ACTION_END; 311 | state = JSON_STATE_VALUE; 312 | } else if (c == '\\') { 313 | state = JSON_STATE_ESCAPE; 314 | } else if (c >= 192 && c < 224) { 315 | utf8_bytes = 1; 316 | state = JSON_STATE_UTF8; 317 | } else if (c >= 224 && c < 240) { 318 | utf8_bytes = 2; 319 | state = JSON_STATE_UTF8; 320 | } else if (c >= 240 && c < 247) { 321 | utf8_bytes = 3; 322 | state = JSON_STATE_UTF8; 323 | } else if (c >= 128 && c < 192) { 324 | return -1; 325 | } 326 | break; 327 | case JSON_STATE_ESCAPE: 328 | if (c == '"' || c == '\\' || c == '/' || c == 'b' || c == 'f' || 329 | c == 'n' || c == 'r' || c == 't' || c == 'u') { 330 | state = JSON_STATE_STRING; 331 | } else { 332 | return -1; 333 | } 334 | break; 335 | case JSON_STATE_UTF8: 336 | if (c < 128 || c > 191) { 337 | return -1; 338 | } 339 | utf8_bytes--; 340 | if (utf8_bytes == 0) { 341 | state = JSON_STATE_STRING; 342 | } 343 | break; 344 | default: 345 | return -1; 346 | } 347 | 348 | if (action == JSON_ACTION_END_STRUCT) { 349 | depth--; 350 | } 351 | 352 | if (depth == 1) { 353 | if (action == JSON_ACTION_START || action == JSON_ACTION_START_STRUCT) { 354 | if (index == 0) { 355 | *value = s; 356 | } else if (keysz > 0 && index == 1) { 357 | k = s; 358 | } else { 359 | index--; 360 | } 361 | } else if (action == JSON_ACTION_END || 362 | action == JSON_ACTION_END_STRUCT) { 363 | if (*value != nullptr && index == 0) { 364 | *valuesz = (size_t)(s + 1 - *value); 365 | return 0; 366 | } else if (keysz > 0 && k != nullptr) { 367 | if (keysz == (size_t)(s - k - 1) && memcmp(key, k + 1, keysz) == 0) { 368 | index = 0; 369 | } else { 370 | index = 2; 371 | } 372 | k = nullptr; 373 | } 374 | } 375 | } 376 | 377 | if (action == JSON_ACTION_START_STRUCT) { 378 | depth++; 379 | } 380 | } 381 | return -1; 382 | } 383 | 384 | inline std::string json_escape(const std::string &s) { 385 | // TODO: implement 386 | return '"' + s + '"'; 387 | } 388 | 389 | inline int json_unescape(const char *s, size_t n, char *out) { 390 | int r = 0; 391 | if (*s++ != '"') { 392 | return -1; 393 | } 394 | while (n > 2) { 395 | char c = *s; 396 | if (c == '\\') { 397 | s++; 398 | n--; 399 | switch (*s) { 400 | case 'b': 401 | c = '\b'; 402 | break; 403 | case 'f': 404 | c = '\f'; 405 | break; 406 | case 'n': 407 | c = '\n'; 408 | break; 409 | case 'r': 410 | c = '\r'; 411 | break; 412 | case 't': 413 | c = '\t'; 414 | break; 415 | case '\\': 416 | c = '\\'; 417 | break; 418 | case '/': 419 | c = '/'; 420 | break; 421 | case '\"': 422 | c = '\"'; 423 | break; 424 | default: // TODO: support unicode decoding 425 | return -1; 426 | } 427 | } 428 | if (out != nullptr) { 429 | *out++ = c; 430 | } 431 | s++; 432 | n--; 433 | r++; 434 | } 435 | if (*s != '"') { 436 | return -1; 437 | } 438 | if (out != nullptr) { 439 | *out = '\0'; 440 | } 441 | return r; 442 | } 443 | 444 | inline std::string json_parse(const std::string &s, const std::string &key, 445 | const int index) { 446 | const char *value; 447 | size_t value_sz; 448 | if (key.empty()) { 449 | json_parse_c(s.c_str(), s.length(), nullptr, index, &value, &value_sz); 450 | } else { 451 | json_parse_c(s.c_str(), s.length(), key.c_str(), key.length(), &value, 452 | &value_sz); 453 | } 454 | if (value != nullptr) { 455 | if (value[0] != '"') { 456 | return {value, value_sz}; 457 | } 458 | int n = json_unescape(value, value_sz, nullptr); 459 | if (n > 0) { 460 | char *decoded = new char[n + 1]; 461 | json_unescape(value, value_sz, decoded); 462 | std::string result(decoded, n); 463 | delete[] decoded; 464 | return result; 465 | } 466 | } 467 | return ""; 468 | } 469 | 470 | } // namespace detail 471 | 472 | WEBVIEW_DEPRECATED_PRIVATE 473 | inline int json_parse_c(const char *s, size_t sz, const char *key, size_t keysz, 474 | const char **value, size_t *valuesz) { 475 | return detail::json_parse_c(s, sz, key, keysz, value, valuesz); 476 | } 477 | 478 | WEBVIEW_DEPRECATED_PRIVATE 479 | inline std::string json_escape(const std::string &s) { 480 | return detail::json_escape(s); 481 | } 482 | 483 | WEBVIEW_DEPRECATED_PRIVATE 484 | inline int json_unescape(const char *s, size_t n, char *out) { 485 | return detail::json_unescape(s, n, out); 486 | } 487 | 488 | WEBVIEW_DEPRECATED_PRIVATE 489 | inline std::string json_parse(const std::string &s, const std::string &key, 490 | const int index) { 491 | return detail::json_parse(s, key, index); 492 | } 493 | 494 | } // namespace webview 495 | 496 | #if defined(WEBVIEW_GTK) 497 | // 498 | // ==================================================================== 499 | // 500 | // This implementation uses webkit2gtk backend. It requires gtk+3.0 and 501 | // webkit2gtk-4.0 libraries. Proper compiler flags can be retrieved via: 502 | // 503 | // pkg-config --cflags --libs gtk+-3.0 webkit2gtk-4.0 504 | // 505 | // ==================================================================== 506 | // 507 | #include 508 | #include 509 | #include 510 | 511 | namespace webview { 512 | namespace detail { 513 | 514 | class gtk_webkit_engine { 515 | public: 516 | gtk_webkit_engine(bool debug, void *window) 517 | : m_window(static_cast(window)) { 518 | if (gtk_init_check(nullptr, nullptr) == FALSE) { 519 | return; 520 | } 521 | m_window = static_cast(window); 522 | if (m_window == nullptr) { 523 | m_window = gtk_window_new(GTK_WINDOW_TOPLEVEL); 524 | } 525 | g_signal_connect(G_OBJECT(m_window), "destroy", 526 | G_CALLBACK(+[](GtkWidget *, gpointer arg) { 527 | static_cast(arg)->terminate(); 528 | }), 529 | this); 530 | // Initialize webview widget 531 | m_webview = webkit_web_view_new(); 532 | WebKitUserContentManager *manager = 533 | webkit_web_view_get_user_content_manager(WEBKIT_WEB_VIEW(m_webview)); 534 | g_signal_connect(manager, "script-message-received::external", 535 | G_CALLBACK(+[](WebKitUserContentManager *, 536 | WebKitJavascriptResult *r, gpointer arg) { 537 | auto *w = static_cast(arg); 538 | char *s = get_string_from_js_result(r); 539 | w->on_message(s); 540 | g_free(s); 541 | }), 542 | this); 543 | webkit_user_content_manager_register_script_message_handler(manager, 544 | "external"); 545 | init("window.external={invoke:function(s){window.webkit.messageHandlers." 546 | "external.postMessage(s);}}"); 547 | 548 | gtk_container_add(GTK_CONTAINER(m_window), GTK_WIDGET(m_webview)); 549 | gtk_widget_grab_focus(GTK_WIDGET(m_webview)); 550 | 551 | WebKitSettings *settings = 552 | webkit_web_view_get_settings(WEBKIT_WEB_VIEW(m_webview)); 553 | webkit_settings_set_javascript_can_access_clipboard(settings, true); 554 | if (debug) { 555 | webkit_settings_set_enable_write_console_messages_to_stdout(settings, 556 | true); 557 | webkit_settings_set_enable_developer_extras(settings, true); 558 | } 559 | 560 | gtk_widget_show_all(m_window); 561 | } 562 | virtual ~gtk_webkit_engine() = default; 563 | void *window() { return (void *)m_window; } 564 | void run() { gtk_main(); } 565 | void terminate() { gtk_main_quit(); } 566 | void dispatch(std::function f) { 567 | g_idle_add_full(G_PRIORITY_HIGH_IDLE, (GSourceFunc)([](void *f) -> int { 568 | (*static_cast(f))(); 569 | return G_SOURCE_REMOVE; 570 | }), 571 | new std::function(f), 572 | [](void *f) { delete static_cast(f); }); 573 | } 574 | 575 | void set_title(const std::string &title) { 576 | gtk_window_set_title(GTK_WINDOW(m_window), title.c_str()); 577 | } 578 | 579 | void set_size(int width, int height, int hints) { 580 | gtk_window_set_resizable(GTK_WINDOW(m_window), hints != WEBVIEW_HINT_FIXED); 581 | if (hints == WEBVIEW_HINT_NONE) { 582 | gtk_window_resize(GTK_WINDOW(m_window), width, height); 583 | } else if (hints == WEBVIEW_HINT_FIXED) { 584 | gtk_widget_set_size_request(m_window, width, height); 585 | } else { 586 | GdkGeometry g; 587 | g.min_width = g.max_width = width; 588 | g.min_height = g.max_height = height; 589 | GdkWindowHints h = 590 | (hints == WEBVIEW_HINT_MIN ? GDK_HINT_MIN_SIZE : GDK_HINT_MAX_SIZE); 591 | // This defines either MIN_SIZE, or MAX_SIZE, but not both: 592 | gtk_window_set_geometry_hints(GTK_WINDOW(m_window), nullptr, &g, h); 593 | } 594 | } 595 | 596 | void navigate(const std::string &url) { 597 | webkit_web_view_load_uri(WEBKIT_WEB_VIEW(m_webview), url.c_str()); 598 | } 599 | 600 | void set_html(const std::string &html) { 601 | webkit_web_view_load_html(WEBKIT_WEB_VIEW(m_webview), html.c_str(), 602 | nullptr); 603 | } 604 | 605 | void init(const std::string &js) { 606 | WebKitUserContentManager *manager = 607 | webkit_web_view_get_user_content_manager(WEBKIT_WEB_VIEW(m_webview)); 608 | webkit_user_content_manager_add_script( 609 | manager, 610 | webkit_user_script_new(js.c_str(), WEBKIT_USER_CONTENT_INJECT_TOP_FRAME, 611 | WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, 612 | nullptr, nullptr)); 613 | } 614 | 615 | void eval(const std::string &js) { 616 | webkit_web_view_run_javascript(WEBKIT_WEB_VIEW(m_webview), js.c_str(), 617 | nullptr, nullptr, nullptr); 618 | } 619 | 620 | private: 621 | virtual void on_message(const std::string &msg) = 0; 622 | 623 | static char *get_string_from_js_result(WebKitJavascriptResult *r) { 624 | char *s; 625 | #if WEBKIT_MAJOR_VERSION >= 2 && WEBKIT_MINOR_VERSION >= 22 626 | JSCValue *value = webkit_javascript_result_get_js_value(r); 627 | s = jsc_value_to_string(value); 628 | #else 629 | JSGlobalContextRef ctx = webkit_javascript_result_get_global_context(r); 630 | JSValueRef value = webkit_javascript_result_get_value(r); 631 | JSStringRef js = JSValueToStringCopy(ctx, value, nullptr); 632 | size_t n = JSStringGetMaximumUTF8CStringSize(js); 633 | s = g_new(char, n); 634 | JSStringGetUTF8CString(js, s, n); 635 | JSStringRelease(js); 636 | #endif 637 | return s; 638 | } 639 | 640 | GtkWidget *m_window; 641 | GtkWidget *m_webview; 642 | }; 643 | 644 | } // namespace detail 645 | 646 | using browser_engine = detail::gtk_webkit_engine; 647 | 648 | } // namespace webview 649 | 650 | #elif defined(WEBVIEW_COCOA) 651 | 652 | // 653 | // ==================================================================== 654 | // 655 | // This implementation uses Cocoa WKWebView backend on macOS. It is 656 | // written using ObjC runtime and uses WKWebView class as a browser runtime. 657 | // You should pass "-framework Webkit" flag to the compiler. 658 | // 659 | // ==================================================================== 660 | // 661 | 662 | #include 663 | #include 664 | #include 665 | 666 | namespace webview { 667 | namespace detail { 668 | namespace objc { 669 | 670 | // A convenient template function for unconditionally casting the specified 671 | // C-like function into a function that can be called with the given return 672 | // type and arguments. Caller takes full responsibility for ensuring that 673 | // the function call is valid. It is assumed that the function will not 674 | // throw exceptions. 675 | template 676 | Result invoke(Callable callable, Args... args) noexcept { 677 | return reinterpret_cast(callable)(args...); 678 | } 679 | 680 | // Calls objc_msgSend. 681 | template 682 | Result msg_send(Args... args) noexcept { 683 | return invoke(objc_msgSend, args...); 684 | } 685 | 686 | } // namespace objc 687 | 688 | enum NSBackingStoreType : NSUInteger { NSBackingStoreBuffered = 2 }; 689 | 690 | enum NSWindowStyleMask : NSUInteger { 691 | NSWindowStyleMaskTitled = 1, 692 | NSWindowStyleMaskClosable = 2, 693 | NSWindowStyleMaskMiniaturizable = 4, 694 | NSWindowStyleMaskResizable = 8 695 | }; 696 | 697 | enum NSApplicationActivationPolicy : NSInteger { 698 | NSApplicationActivationPolicyRegular = 0 699 | }; 700 | 701 | enum WKUserScriptInjectionTime : NSInteger { 702 | WKUserScriptInjectionTimeAtDocumentStart = 0 703 | }; 704 | 705 | enum NSModalResponse : NSInteger { NSModalResponseOK = 1 }; 706 | 707 | // Convenient conversion of string literals. 708 | inline id operator"" _cls(const char *s, std::size_t) { 709 | return (id)objc_getClass(s); 710 | } 711 | inline SEL operator"" _sel(const char *s, std::size_t) { 712 | return sel_registerName(s); 713 | } 714 | inline id operator"" _str(const char *s, std::size_t) { 715 | return objc::msg_send("NSString"_cls, "stringWithUTF8String:"_sel, s); 716 | } 717 | 718 | class cocoa_wkwebview_engine { 719 | public: 720 | cocoa_wkwebview_engine(bool debug, void *window) 721 | : m_debug{debug}, m_parent_window{window} { 722 | auto app = get_shared_application(); 723 | auto delegate = create_app_delegate(); 724 | objc_setAssociatedObject(delegate, "webview", (id)this, 725 | OBJC_ASSOCIATION_ASSIGN); 726 | objc::msg_send(app, "setDelegate:"_sel, delegate); 727 | 728 | // See comments related to application lifecycle in create_app_delegate(). 729 | if (window) { 730 | on_application_did_finish_launching(delegate, app); 731 | } else { 732 | // Start the main run loop so that the app delegate gets the 733 | // NSApplicationDidFinishLaunchingNotification notification after the run 734 | // loop has started in order to perform further initialization. 735 | // We need to return from this constructor so this run loop is only 736 | // temporary. 737 | objc::msg_send(app, "run"_sel); 738 | } 739 | } 740 | virtual ~cocoa_wkwebview_engine() = default; 741 | void *window() { return (void *)m_window; } 742 | void terminate() { 743 | auto app = get_shared_application(); 744 | objc::msg_send(app, "terminate:"_sel, nullptr); 745 | } 746 | void run() { 747 | auto app = get_shared_application(); 748 | objc::msg_send(app, "run"_sel); 749 | } 750 | void dispatch(std::function f) { 751 | dispatch_async_f(dispatch_get_main_queue(), new dispatch_fn_t(f), 752 | (dispatch_function_t)([](void *arg) { 753 | auto f = static_cast(arg); 754 | (*f)(); 755 | delete f; 756 | })); 757 | } 758 | void set_title(const std::string &title) { 759 | objc::msg_send(m_window, "setTitle:"_sel, 760 | objc::msg_send("NSString"_cls, 761 | "stringWithUTF8String:"_sel, 762 | title.c_str())); 763 | } 764 | void set_size(int width, int height, int hints) { 765 | auto style = static_cast( 766 | NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | 767 | NSWindowStyleMaskMiniaturizable); 768 | if (hints != WEBVIEW_HINT_FIXED) { 769 | style = 770 | static_cast(style | NSWindowStyleMaskResizable); 771 | } 772 | objc::msg_send(m_window, "setStyleMask:"_sel, style); 773 | 774 | if (hints == WEBVIEW_HINT_MIN) { 775 | objc::msg_send(m_window, "setContentMinSize:"_sel, 776 | CGSizeMake(width, height)); 777 | } else if (hints == WEBVIEW_HINT_MAX) { 778 | objc::msg_send(m_window, "setContentMaxSize:"_sel, 779 | CGSizeMake(width, height)); 780 | } else { 781 | objc::msg_send(m_window, "setFrame:display:animate:"_sel, 782 | CGRectMake(0, 0, width, height), YES, NO); 783 | } 784 | objc::msg_send(m_window, "center"_sel); 785 | } 786 | void navigate(const std::string &url) { 787 | auto nsurl = objc::msg_send( 788 | "NSURL"_cls, "URLWithString:"_sel, 789 | objc::msg_send("NSString"_cls, "stringWithUTF8String:"_sel, 790 | url.c_str())); 791 | 792 | objc::msg_send( 793 | m_webview, "loadRequest:"_sel, 794 | objc::msg_send("NSURLRequest"_cls, "requestWithURL:"_sel, nsurl)); 795 | } 796 | void set_html(const std::string &html) { 797 | objc::msg_send(m_webview, "loadHTMLString:baseURL:"_sel, 798 | objc::msg_send("NSString"_cls, 799 | "stringWithUTF8String:"_sel, 800 | html.c_str()), 801 | nullptr); 802 | } 803 | void init(const std::string &js) { 804 | // Equivalent Obj-C: 805 | // [m_manager addUserScript:[[WKUserScript alloc] initWithSource:[NSString stringWithUTF8String:js.c_str()] injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:YES]] 806 | objc::msg_send( 807 | m_manager, "addUserScript:"_sel, 808 | objc::msg_send(objc::msg_send("WKUserScript"_cls, "alloc"_sel), 809 | "initWithSource:injectionTime:forMainFrameOnly:"_sel, 810 | objc::msg_send("NSString"_cls, 811 | "stringWithUTF8String:"_sel, 812 | js.c_str()), 813 | WKUserScriptInjectionTimeAtDocumentStart, YES)); 814 | } 815 | void eval(const std::string &js) { 816 | objc::msg_send(m_webview, "evaluateJavaScript:completionHandler:"_sel, 817 | objc::msg_send("NSString"_cls, 818 | "stringWithUTF8String:"_sel, 819 | js.c_str()), 820 | nullptr); 821 | } 822 | 823 | private: 824 | virtual void on_message(const std::string &msg) = 0; 825 | id create_app_delegate() { 826 | // Note: Avoid registering the class name "AppDelegate" as it is the 827 | // default name in projects created with Xcode, and using the same name 828 | // causes objc_registerClassPair to crash. 829 | auto cls = objc_allocateClassPair((Class) "NSResponder"_cls, 830 | "WebviewAppDelegate", 0); 831 | class_addProtocol(cls, objc_getProtocol("NSTouchBarProvider")); 832 | class_addMethod(cls, "applicationShouldTerminateAfterLastWindowClosed:"_sel, 833 | (IMP)(+[](id, SEL, id) -> BOOL { return 1; }), "c@:@"); 834 | // If the library was not initialized with an existing window then the user 835 | // is likely managing the application lifecycle and we would not get the 836 | // "applicationDidFinishLaunching:" message and therefore do not need to 837 | // add this method. 838 | if (!m_parent_window) { 839 | class_addMethod(cls, "applicationDidFinishLaunching:"_sel, 840 | (IMP)(+[](id self, SEL, id notification) { 841 | auto app = 842 | objc::msg_send(notification, "object"_sel); 843 | auto w = get_associated_webview(self); 844 | w->on_application_did_finish_launching(self, app); 845 | }), 846 | "v@:@"); 847 | } 848 | objc_registerClassPair(cls); 849 | return objc::msg_send((id)cls, "new"_sel); 850 | } 851 | id create_script_message_handler() { 852 | auto cls = objc_allocateClassPair((Class) "NSResponder"_cls, 853 | "WebkitScriptMessageHandler", 0); 854 | class_addProtocol(cls, objc_getProtocol("WKScriptMessageHandler")); 855 | class_addMethod( 856 | cls, "userContentController:didReceiveScriptMessage:"_sel, 857 | (IMP)(+[](id self, SEL, id, id msg) { 858 | auto w = get_associated_webview(self); 859 | w->on_message(objc::msg_send( 860 | objc::msg_send(msg, "body"_sel), "UTF8String"_sel)); 861 | }), 862 | "v@:@@"); 863 | objc_registerClassPair(cls); 864 | auto instance = objc::msg_send((id)cls, "new"_sel); 865 | objc_setAssociatedObject(instance, "webview", (id)this, 866 | OBJC_ASSOCIATION_ASSIGN); 867 | return instance; 868 | } 869 | static id create_webkit_ui_delegate() { 870 | auto cls = 871 | objc_allocateClassPair((Class) "NSObject"_cls, "WebkitUIDelegate", 0); 872 | class_addProtocol(cls, objc_getProtocol("WKUIDelegate")); 873 | class_addMethod( 874 | cls, 875 | "webView:runOpenPanelWithParameters:initiatedByFrame:completionHandler:"_sel, 876 | (IMP)(+[](id, SEL, id, id parameters, id, id completion_handler) { 877 | auto allows_multiple_selection = 878 | objc::msg_send(parameters, "allowsMultipleSelection"_sel); 879 | auto allows_directories = 880 | objc::msg_send(parameters, "allowsDirectories"_sel); 881 | 882 | // Show a panel for selecting files. 883 | auto panel = objc::msg_send("NSOpenPanel"_cls, "openPanel"_sel); 884 | objc::msg_send(panel, "setCanChooseFiles:"_sel, YES); 885 | objc::msg_send(panel, "setCanChooseDirectories:"_sel, 886 | allows_directories); 887 | objc::msg_send(panel, "setAllowsMultipleSelection:"_sel, 888 | allows_multiple_selection); 889 | auto modal_response = 890 | objc::msg_send(panel, "runModal"_sel); 891 | 892 | // Get the URLs for the selected files. If the modal was canceled 893 | // then we pass null to the completion handler to signify 894 | // cancellation. 895 | id urls = modal_response == NSModalResponseOK 896 | ? objc::msg_send(panel, "URLs"_sel) 897 | : nullptr; 898 | 899 | // Invoke the completion handler block. 900 | auto sig = objc::msg_send("NSMethodSignature"_cls, 901 | "signatureWithObjCTypes:"_sel, "v@?@"); 902 | auto invocation = objc::msg_send( 903 | "NSInvocation"_cls, "invocationWithMethodSignature:"_sel, sig); 904 | objc::msg_send(invocation, "setTarget:"_sel, 905 | completion_handler); 906 | objc::msg_send(invocation, "setArgument:atIndex:"_sel, &urls, 907 | 1); 908 | objc::msg_send(invocation, "invoke"_sel); 909 | }), 910 | "v@:@@@@"); 911 | objc_registerClassPair(cls); 912 | return objc::msg_send((id)cls, "new"_sel); 913 | } 914 | static id get_shared_application() { 915 | return objc::msg_send("NSApplication"_cls, "sharedApplication"_sel); 916 | } 917 | static cocoa_wkwebview_engine *get_associated_webview(id object) { 918 | auto w = 919 | (cocoa_wkwebview_engine *)objc_getAssociatedObject(object, "webview"); 920 | assert(w); 921 | return w; 922 | } 923 | static id get_main_bundle() noexcept { 924 | return objc::msg_send("NSBundle"_cls, "mainBundle"_sel); 925 | } 926 | static bool is_app_bundled() noexcept { 927 | auto bundle = get_main_bundle(); 928 | if (!bundle) { 929 | return false; 930 | } 931 | auto bundle_path = objc::msg_send(bundle, "bundlePath"_sel); 932 | auto bundled = 933 | objc::msg_send(bundle_path, "hasSuffix:"_sel, ".app"_str); 934 | return !!bundled; 935 | } 936 | void on_application_did_finish_launching(id /*delegate*/, id app) { 937 | // See comments related to application lifecycle in create_app_delegate(). 938 | if (!m_parent_window) { 939 | // Stop the main run loop so that we can return 940 | // from the constructor. 941 | objc::msg_send(app, "stop:"_sel, nullptr); 942 | } 943 | 944 | // Activate the app if it is not bundled. 945 | // Bundled apps launched from Finder are activated automatically but 946 | // otherwise not. Activating the app even when it has been launched from 947 | // Finder does not seem to be harmful but calling this function is rarely 948 | // needed as proper activation is normally taken care of for us. 949 | // Bundled apps have a default activation policy of 950 | // NSApplicationActivationPolicyRegular while non-bundled apps have a 951 | // default activation policy of NSApplicationActivationPolicyProhibited. 952 | if (!is_app_bundled()) { 953 | // "setActivationPolicy:" must be invoked before 954 | // "activateIgnoringOtherApps:" for activation to work. 955 | objc::msg_send(app, "setActivationPolicy:"_sel, 956 | NSApplicationActivationPolicyRegular); 957 | // Activate the app regardless of other active apps. 958 | // This can be obtrusive so we only do it when necessary. 959 | objc::msg_send(app, "activateIgnoringOtherApps:"_sel, YES); 960 | } 961 | 962 | // Main window 963 | if (!m_parent_window) { 964 | m_window = objc::msg_send("NSWindow"_cls, "alloc"_sel); 965 | auto style = NSWindowStyleMaskTitled; 966 | m_window = objc::msg_send( 967 | m_window, "initWithContentRect:styleMask:backing:defer:"_sel, 968 | CGRectMake(0, 0, 0, 0), style, NSBackingStoreBuffered, NO); 969 | } else { 970 | m_window = (id)m_parent_window; 971 | } 972 | 973 | // Webview 974 | auto config = objc::msg_send("WKWebViewConfiguration"_cls, "new"_sel); 975 | m_manager = objc::msg_send(config, "userContentController"_sel); 976 | m_webview = objc::msg_send("WKWebView"_cls, "alloc"_sel); 977 | 978 | if (m_debug) { 979 | // Equivalent Obj-C: 980 | // [[config preferences] setValue:@YES forKey:@"developerExtrasEnabled"]; 981 | objc::msg_send( 982 | objc::msg_send(config, "preferences"_sel), "setValue:forKey:"_sel, 983 | objc::msg_send("NSNumber"_cls, "numberWithBool:"_sel, YES), 984 | "developerExtrasEnabled"_str); 985 | } 986 | 987 | // Equivalent Obj-C: 988 | // [[config preferences] setValue:@YES forKey:@"fullScreenEnabled"]; 989 | objc::msg_send( 990 | objc::msg_send(config, "preferences"_sel), "setValue:forKey:"_sel, 991 | objc::msg_send("NSNumber"_cls, "numberWithBool:"_sel, YES), 992 | "fullScreenEnabled"_str); 993 | 994 | // Equivalent Obj-C: 995 | // [[config preferences] setValue:@YES forKey:@"javaScriptCanAccessClipboard"]; 996 | objc::msg_send( 997 | objc::msg_send(config, "preferences"_sel), "setValue:forKey:"_sel, 998 | objc::msg_send("NSNumber"_cls, "numberWithBool:"_sel, YES), 999 | "javaScriptCanAccessClipboard"_str); 1000 | 1001 | // Equivalent Obj-C: 1002 | // [[config preferences] setValue:@YES forKey:@"DOMPasteAllowed"]; 1003 | objc::msg_send( 1004 | objc::msg_send(config, "preferences"_sel), "setValue:forKey:"_sel, 1005 | objc::msg_send("NSNumber"_cls, "numberWithBool:"_sel, YES), 1006 | "DOMPasteAllowed"_str); 1007 | 1008 | auto ui_delegate = create_webkit_ui_delegate(); 1009 | objc::msg_send(m_webview, "initWithFrame:configuration:"_sel, 1010 | CGRectMake(0, 0, 0, 0), config); 1011 | objc::msg_send(m_webview, "setUIDelegate:"_sel, ui_delegate); 1012 | auto script_message_handler = create_script_message_handler(); 1013 | objc::msg_send(m_manager, "addScriptMessageHandler:name:"_sel, 1014 | script_message_handler, "external"_str); 1015 | 1016 | init(R""( 1017 | window.external = { 1018 | invoke: function(s) { 1019 | window.webkit.messageHandlers.external.postMessage(s); 1020 | }, 1021 | }; 1022 | )""); 1023 | objc::msg_send(m_window, "setContentView:"_sel, m_webview); 1024 | objc::msg_send(m_window, "makeKeyAndOrderFront:"_sel, nullptr); 1025 | } 1026 | bool m_debug; 1027 | void *m_parent_window; 1028 | id m_window; 1029 | id m_webview; 1030 | id m_manager; 1031 | }; 1032 | 1033 | } // namespace detail 1034 | 1035 | using browser_engine = detail::cocoa_wkwebview_engine; 1036 | 1037 | } // namespace webview 1038 | 1039 | #elif defined(WEBVIEW_EDGE) 1040 | 1041 | // 1042 | // ==================================================================== 1043 | // 1044 | // This implementation uses Win32 API to create a native window. It 1045 | // uses Edge/Chromium webview2 backend as a browser engine. 1046 | // 1047 | // ==================================================================== 1048 | // 1049 | 1050 | #define WIN32_LEAN_AND_MEAN 1051 | #include 1052 | #include 1053 | #include 1054 | #include 1055 | 1056 | #include "WebView2.h" 1057 | 1058 | #ifdef _MSC_VER 1059 | #pragma comment(lib, "advapi32.lib") 1060 | #pragma comment(lib, "ole32.lib") 1061 | #pragma comment(lib, "shell32.lib") 1062 | #pragma comment(lib, "shlwapi.lib") 1063 | #pragma comment(lib, "user32.lib") 1064 | #pragma comment(lib, "version.lib") 1065 | #endif 1066 | 1067 | namespace webview { 1068 | namespace detail { 1069 | 1070 | using msg_cb_t = std::function; 1071 | 1072 | // Converts a narrow (UTF-8-encoded) string into a wide (UTF-16-encoded) string. 1073 | inline std::wstring widen_string(const std::string &input) { 1074 | if (input.empty()) { 1075 | return std::wstring(); 1076 | } 1077 | UINT cp = CP_UTF8; 1078 | DWORD flags = MB_ERR_INVALID_CHARS; 1079 | auto input_c = input.c_str(); 1080 | auto input_length = static_cast(input.size()); 1081 | auto required_length = 1082 | MultiByteToWideChar(cp, flags, input_c, input_length, nullptr, 0); 1083 | if (required_length > 0) { 1084 | std::wstring output(static_cast(required_length), L'\0'); 1085 | if (MultiByteToWideChar(cp, flags, input_c, input_length, &output[0], 1086 | required_length) > 0) { 1087 | return output; 1088 | } 1089 | } 1090 | // Failed to convert string from UTF-8 to UTF-16 1091 | return std::wstring(); 1092 | } 1093 | 1094 | // Converts a wide (UTF-16-encoded) string into a narrow (UTF-8-encoded) string. 1095 | inline std::string narrow_string(const std::wstring &input) { 1096 | if (input.empty()) { 1097 | return std::string(); 1098 | } 1099 | UINT cp = CP_UTF8; 1100 | DWORD flags = WC_ERR_INVALID_CHARS; 1101 | auto input_c = input.c_str(); 1102 | auto input_length = static_cast(input.size()); 1103 | auto required_length = WideCharToMultiByte(cp, flags, input_c, input_length, 1104 | nullptr, 0, nullptr, nullptr); 1105 | if (required_length > 0) { 1106 | std::string output(static_cast(required_length), '\0'); 1107 | if (WideCharToMultiByte(cp, flags, input_c, input_length, &output[0], 1108 | required_length, nullptr, nullptr) > 0) { 1109 | return output; 1110 | } 1111 | } 1112 | // Failed to convert string from UTF-16 to UTF-8 1113 | return std::string(); 1114 | } 1115 | 1116 | // Parses a version string with 1-4 integral components, e.g. "1.2.3.4". 1117 | // Missing or invalid components default to 0, and excess components are ignored. 1118 | template 1119 | std::array 1120 | parse_version(const std::basic_string &version) noexcept { 1121 | auto parse_component = [](auto sb, auto se) -> unsigned int { 1122 | try { 1123 | auto n = std::stol(std::basic_string(sb, se)); 1124 | return n < 0 ? 0 : n; 1125 | } catch (std::exception &) { 1126 | return 0; 1127 | } 1128 | }; 1129 | auto end = version.end(); 1130 | auto sb = version.begin(); // subrange begin 1131 | auto se = sb; // subrange end 1132 | unsigned int ci = 0; // component index 1133 | std::array components{}; 1134 | while (sb != end && se != end && ci < components.size()) { 1135 | if (*se == static_cast('.')) { 1136 | components[ci++] = parse_component(sb, se); 1137 | sb = ++se; 1138 | continue; 1139 | } 1140 | ++se; 1141 | } 1142 | if (sb < se && ci < components.size()) { 1143 | components[ci] = parse_component(sb, se); 1144 | } 1145 | return components; 1146 | } 1147 | 1148 | template 1149 | auto parse_version(const T (&version)[Length]) noexcept { 1150 | return parse_version(std::basic_string(version, Length)); 1151 | } 1152 | 1153 | std::wstring get_file_version_string(const std::wstring &file_path) noexcept { 1154 | DWORD dummy_handle; // Unused 1155 | DWORD info_buffer_length = 1156 | GetFileVersionInfoSizeW(file_path.c_str(), &dummy_handle); 1157 | if (info_buffer_length == 0) { 1158 | return std::wstring(); 1159 | } 1160 | std::vector info_buffer; 1161 | info_buffer.reserve(info_buffer_length); 1162 | if (!GetFileVersionInfoW(file_path.c_str(), 0, info_buffer_length, 1163 | info_buffer.data())) { 1164 | return std::wstring(); 1165 | } 1166 | auto sub_block = L"\\StringFileInfo\\040904B0\\ProductVersion"; 1167 | LPWSTR version = nullptr; 1168 | unsigned int version_length = 0; 1169 | if (!VerQueryValueW(info_buffer.data(), sub_block, 1170 | reinterpret_cast(&version), &version_length)) { 1171 | return std::wstring(); 1172 | } 1173 | if (!version || version_length == 0) { 1174 | return std::wstring(); 1175 | } 1176 | return std::wstring(version, version_length); 1177 | } 1178 | 1179 | // A wrapper around COM library initialization. Calls CoInitializeEx in the 1180 | // constructor and CoUninitialize in the destructor. 1181 | class com_init_wrapper { 1182 | public: 1183 | com_init_wrapper(DWORD dwCoInit) { 1184 | // We can safely continue as long as COM was either successfully 1185 | // initialized or already initialized. 1186 | // RPC_E_CHANGED_MODE means that CoInitializeEx was already called with 1187 | // a different concurrency model. 1188 | switch (CoInitializeEx(nullptr, dwCoInit)) { 1189 | case S_OK: 1190 | case S_FALSE: 1191 | m_initialized = true; 1192 | break; 1193 | } 1194 | } 1195 | 1196 | ~com_init_wrapper() { 1197 | if (m_initialized) { 1198 | CoUninitialize(); 1199 | m_initialized = false; 1200 | } 1201 | } 1202 | 1203 | com_init_wrapper(const com_init_wrapper &other) = delete; 1204 | com_init_wrapper &operator=(const com_init_wrapper &other) = delete; 1205 | com_init_wrapper(com_init_wrapper &&other) = delete; 1206 | com_init_wrapper &operator=(com_init_wrapper &&other) = delete; 1207 | 1208 | bool is_initialized() const { return m_initialized; } 1209 | 1210 | private: 1211 | bool m_initialized = false; 1212 | }; 1213 | 1214 | // Holds a symbol name and associated type for code clarity. 1215 | template class library_symbol { 1216 | public: 1217 | using type = T; 1218 | 1219 | constexpr explicit library_symbol(const char *name) : m_name(name) {} 1220 | constexpr const char *get_name() const { return m_name; } 1221 | 1222 | private: 1223 | const char *m_name; 1224 | }; 1225 | 1226 | // Loads a native shared library and allows one to get addresses for those 1227 | // symbols. 1228 | class native_library { 1229 | public: 1230 | explicit native_library(const wchar_t *name) : m_handle(LoadLibraryW(name)) {} 1231 | 1232 | ~native_library() { 1233 | if (m_handle) { 1234 | FreeLibrary(m_handle); 1235 | m_handle = nullptr; 1236 | } 1237 | } 1238 | 1239 | native_library(const native_library &other) = delete; 1240 | native_library &operator=(const native_library &other) = delete; 1241 | native_library(native_library &&other) = default; 1242 | native_library &operator=(native_library &&other) = default; 1243 | 1244 | // Returns true if the library is currently loaded; otherwise false. 1245 | operator bool() const { return is_loaded(); } 1246 | 1247 | // Get the address for the specified symbol or nullptr if not found. 1248 | template 1249 | typename Symbol::type get(const Symbol &symbol) const { 1250 | if (is_loaded()) { 1251 | return reinterpret_cast( 1252 | GetProcAddress(m_handle, symbol.get_name())); 1253 | } 1254 | return nullptr; 1255 | } 1256 | 1257 | // Returns true if the library is currently loaded; otherwise false. 1258 | bool is_loaded() const { return !!m_handle; } 1259 | 1260 | void detach() { m_handle = nullptr; } 1261 | 1262 | private: 1263 | HMODULE m_handle = nullptr; 1264 | }; 1265 | 1266 | struct user32_symbols { 1267 | using DPI_AWARENESS_CONTEXT = HANDLE; 1268 | using SetProcessDpiAwarenessContext_t = BOOL(WINAPI *)(DPI_AWARENESS_CONTEXT); 1269 | using SetProcessDPIAware_t = BOOL(WINAPI *)(); 1270 | 1271 | static constexpr auto SetProcessDpiAwarenessContext = 1272 | library_symbol( 1273 | "SetProcessDpiAwarenessContext"); 1274 | static constexpr auto SetProcessDPIAware = 1275 | library_symbol("SetProcessDPIAware"); 1276 | }; 1277 | 1278 | struct shcore_symbols { 1279 | typedef enum { PROCESS_PER_MONITOR_DPI_AWARE = 2 } PROCESS_DPI_AWARENESS; 1280 | using SetProcessDpiAwareness_t = HRESULT(WINAPI *)(PROCESS_DPI_AWARENESS); 1281 | 1282 | static constexpr auto SetProcessDpiAwareness = 1283 | library_symbol("SetProcessDpiAwareness"); 1284 | }; 1285 | 1286 | class reg_key { 1287 | public: 1288 | explicit reg_key(HKEY root_key, const wchar_t *sub_key, DWORD options, 1289 | REGSAM sam_desired) { 1290 | HKEY handle; 1291 | auto status = 1292 | RegOpenKeyExW(root_key, sub_key, options, sam_desired, &handle); 1293 | if (status == ERROR_SUCCESS) { 1294 | m_handle = handle; 1295 | } 1296 | } 1297 | 1298 | explicit reg_key(HKEY root_key, const std::wstring &sub_key, DWORD options, 1299 | REGSAM sam_desired) 1300 | : reg_key(root_key, sub_key.c_str(), options, sam_desired) {} 1301 | 1302 | virtual ~reg_key() { 1303 | if (m_handle) { 1304 | RegCloseKey(m_handle); 1305 | m_handle = nullptr; 1306 | } 1307 | } 1308 | 1309 | reg_key(const reg_key &other) = delete; 1310 | reg_key &operator=(const reg_key &other) = delete; 1311 | reg_key(reg_key &&other) = delete; 1312 | reg_key &operator=(reg_key &&other) = delete; 1313 | 1314 | bool is_open() const { return !!m_handle; } 1315 | bool get_handle() const { return m_handle; } 1316 | 1317 | std::wstring query_string(const wchar_t *name) const { 1318 | DWORD buf_length = 0; 1319 | // Get the size of the data in bytes. 1320 | auto status = RegQueryValueExW(m_handle, name, nullptr, nullptr, nullptr, 1321 | &buf_length); 1322 | if (status != ERROR_SUCCESS && status != ERROR_MORE_DATA) { 1323 | return std::wstring(); 1324 | } 1325 | // Read the data. 1326 | std::wstring result(buf_length / sizeof(wchar_t), 0); 1327 | auto buf = reinterpret_cast(&result[0]); 1328 | status = 1329 | RegQueryValueExW(m_handle, name, nullptr, nullptr, buf, &buf_length); 1330 | if (status != ERROR_SUCCESS) { 1331 | return std::wstring(); 1332 | } 1333 | // Remove trailing null-characters. 1334 | for (std::size_t length = result.size(); length > 0; --length) { 1335 | if (result[length - 1] != 0) { 1336 | result.resize(length); 1337 | break; 1338 | } 1339 | } 1340 | return result; 1341 | } 1342 | 1343 | private: 1344 | HKEY m_handle = nullptr; 1345 | }; 1346 | 1347 | inline bool enable_dpi_awareness() { 1348 | auto user32 = native_library(L"user32.dll"); 1349 | if (auto fn = user32.get(user32_symbols::SetProcessDpiAwarenessContext)) { 1350 | if (fn(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE)) { 1351 | return true; 1352 | } 1353 | return GetLastError() == ERROR_ACCESS_DENIED; 1354 | } 1355 | if (auto shcore = native_library(L"shcore.dll")) { 1356 | if (auto fn = shcore.get(shcore_symbols::SetProcessDpiAwareness)) { 1357 | auto result = fn(shcore_symbols::PROCESS_PER_MONITOR_DPI_AWARE); 1358 | return result == S_OK || result == E_ACCESSDENIED; 1359 | } 1360 | } 1361 | if (auto fn = user32.get(user32_symbols::SetProcessDPIAware)) { 1362 | return !!fn(); 1363 | } 1364 | return true; 1365 | } 1366 | 1367 | // Enable built-in WebView2Loader implementation by default. 1368 | #ifndef WEBVIEW_MSWEBVIEW2_BUILTIN_IMPL 1369 | #define WEBVIEW_MSWEBVIEW2_BUILTIN_IMPL 1 1370 | #endif 1371 | 1372 | // Link WebView2Loader.dll explicitly by default only if the built-in 1373 | // implementation is enabled. 1374 | #ifndef WEBVIEW_MSWEBVIEW2_EXPLICIT_LINK 1375 | #define WEBVIEW_MSWEBVIEW2_EXPLICIT_LINK WEBVIEW_MSWEBVIEW2_BUILTIN_IMPL 1376 | #endif 1377 | 1378 | // Explicit linking of WebView2Loader.dll should be used along with 1379 | // the built-in implementation. 1380 | #if WEBVIEW_MSWEBVIEW2_BUILTIN_IMPL == 1 && \ 1381 | WEBVIEW_MSWEBVIEW2_EXPLICIT_LINK != 1 1382 | #undef WEBVIEW_MSWEBVIEW2_EXPLICIT_LINK 1383 | #error Please set WEBVIEW_MSWEBVIEW2_EXPLICIT_LINK=1. 1384 | #endif 1385 | 1386 | #if WEBVIEW_MSWEBVIEW2_BUILTIN_IMPL == 1 1387 | // Gets the last component of a Windows native file path. 1388 | // For example, if the path is "C:\a\b" then the result is "b". 1389 | template 1390 | std::basic_string 1391 | get_last_native_path_component(const std::basic_string &path) { 1392 | if (auto pos = path.find_last_of(static_cast('\\')); 1393 | pos != std::basic_string::npos) { 1394 | return path.substr(pos + 1); 1395 | } 1396 | return std::basic_string(); 1397 | } 1398 | #endif /* WEBVIEW_MSWEBVIEW2_BUILTIN_IMPL */ 1399 | 1400 | template struct cast_info_t { 1401 | using type = T; 1402 | IID iid; 1403 | }; 1404 | 1405 | namespace mswebview2 { 1406 | static constexpr IID 1407 | IID_ICoreWebView2CreateCoreWebView2ControllerCompletedHandler{ 1408 | 0x6C4819F3, 0xC9B7, 0x4260, 0x81, 0x27, 0xC9, 1409 | 0xF5, 0xBD, 0xE7, 0xF6, 0x8C}; 1410 | static constexpr IID 1411 | IID_ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler{ 1412 | 0x4E8A3389, 0xC9D8, 0x4BD2, 0xB6, 0xB5, 0x12, 1413 | 0x4F, 0xEE, 0x6C, 0xC1, 0x4D}; 1414 | static constexpr IID IID_ICoreWebView2PermissionRequestedEventHandler{ 1415 | 0x15E1C6A3, 0xC72A, 0x4DF3, 0x91, 0xD7, 0xD0, 0x97, 0xFB, 0xEC, 0x6B, 0xFD}; 1416 | static constexpr IID IID_ICoreWebView2WebMessageReceivedEventHandler{ 1417 | 0x57213F19, 0x00E6, 0x49FA, 0x8E, 0x07, 0x89, 0x8E, 0xA0, 0x1E, 0xCB, 0xD2}; 1418 | 1419 | #if WEBVIEW_MSWEBVIEW2_BUILTIN_IMPL == 1 1420 | enum class webview2_runtime_type { installed = 0, embedded = 1 }; 1421 | 1422 | namespace webview2_symbols { 1423 | using CreateWebViewEnvironmentWithOptionsInternal_t = 1424 | HRESULT(STDMETHODCALLTYPE *)( 1425 | bool, webview2_runtime_type, PCWSTR, IUnknown *, 1426 | ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler *); 1427 | using DllCanUnloadNow_t = HRESULT(STDMETHODCALLTYPE *)(); 1428 | 1429 | static constexpr auto CreateWebViewEnvironmentWithOptionsInternal = 1430 | library_symbol( 1431 | "CreateWebViewEnvironmentWithOptionsInternal"); 1432 | static constexpr auto DllCanUnloadNow = 1433 | library_symbol("DllCanUnloadNow"); 1434 | } // namespace webview2_symbols 1435 | #endif /* WEBVIEW_MSWEBVIEW2_BUILTIN_IMPL */ 1436 | 1437 | #if WEBVIEW_MSWEBVIEW2_EXPLICIT_LINK == 1 1438 | namespace webview2_symbols { 1439 | using CreateCoreWebView2EnvironmentWithOptions_t = HRESULT(STDMETHODCALLTYPE *)( 1440 | PCWSTR, PCWSTR, ICoreWebView2EnvironmentOptions *, 1441 | ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler *); 1442 | using GetAvailableCoreWebView2BrowserVersionString_t = 1443 | HRESULT(STDMETHODCALLTYPE *)(PCWSTR, LPWSTR *); 1444 | 1445 | static constexpr auto CreateCoreWebView2EnvironmentWithOptions = 1446 | library_symbol( 1447 | "CreateCoreWebView2EnvironmentWithOptions"); 1448 | static constexpr auto GetAvailableCoreWebView2BrowserVersionString = 1449 | library_symbol( 1450 | "GetAvailableCoreWebView2BrowserVersionString"); 1451 | } // namespace webview2_symbols 1452 | #endif /* WEBVIEW_MSWEBVIEW2_EXPLICIT_LINK */ 1453 | 1454 | class loader { 1455 | public: 1456 | HRESULT create_environment_with_options( 1457 | PCWSTR browser_dir, PCWSTR user_data_dir, 1458 | ICoreWebView2EnvironmentOptions *env_options, 1459 | ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler 1460 | *created_handler) const { 1461 | #if WEBVIEW_MSWEBVIEW2_EXPLICIT_LINK == 1 1462 | if (m_lib.is_loaded()) { 1463 | if (auto fn = m_lib.get( 1464 | webview2_symbols::CreateCoreWebView2EnvironmentWithOptions)) { 1465 | return fn(browser_dir, user_data_dir, env_options, created_handler); 1466 | } 1467 | } 1468 | #if WEBVIEW_MSWEBVIEW2_BUILTIN_IMPL == 1 1469 | return create_environment_with_options_impl(browser_dir, user_data_dir, 1470 | env_options, created_handler); 1471 | #else 1472 | return S_FALSE; 1473 | #endif 1474 | #else 1475 | return ::CreateCoreWebView2EnvironmentWithOptions( 1476 | browser_dir, user_data_dir, env_options, created_handler); 1477 | #endif /* WEBVIEW_MSWEBVIEW2_EXPLICIT_LINK */ 1478 | } 1479 | 1480 | HRESULT 1481 | get_available_browser_version_string(PCWSTR browser_dir, 1482 | LPWSTR *version) const { 1483 | #if WEBVIEW_MSWEBVIEW2_EXPLICIT_LINK == 1 1484 | if (m_lib.is_loaded()) { 1485 | if (auto fn = m_lib.get( 1486 | webview2_symbols::GetAvailableCoreWebView2BrowserVersionString)) { 1487 | return fn(browser_dir, version); 1488 | } 1489 | } 1490 | #if WEBVIEW_MSWEBVIEW2_BUILTIN_IMPL == 1 1491 | return get_available_browser_version_string_impl(browser_dir, version); 1492 | #else 1493 | return S_FALSE; 1494 | #endif 1495 | #else 1496 | return ::GetAvailableCoreWebView2BrowserVersionString(browser_dir, version); 1497 | #endif /* WEBVIEW_MSWEBVIEW2_EXPLICIT_LINK */ 1498 | } 1499 | 1500 | private: 1501 | #if WEBVIEW_MSWEBVIEW2_BUILTIN_IMPL == 1 1502 | struct client_info_t { 1503 | bool found = false; 1504 | std::wstring dll_path; 1505 | std::wstring version; 1506 | webview2_runtime_type runtime_type; 1507 | }; 1508 | 1509 | HRESULT create_environment_with_options_impl( 1510 | PCWSTR browser_dir, PCWSTR user_data_dir, 1511 | ICoreWebView2EnvironmentOptions *env_options, 1512 | ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler 1513 | *created_handler) const { 1514 | auto found_client = find_available_client(browser_dir); 1515 | if (!found_client.found) { 1516 | return -1; 1517 | } 1518 | auto client_dll = native_library(found_client.dll_path.c_str()); 1519 | if (auto fn = client_dll.get( 1520 | webview2_symbols::CreateWebViewEnvironmentWithOptionsInternal)) { 1521 | return fn(true, found_client.runtime_type, user_data_dir, env_options, 1522 | created_handler); 1523 | } 1524 | if (auto fn = client_dll.get(webview2_symbols::DllCanUnloadNow)) { 1525 | if (!fn()) { 1526 | client_dll.detach(); 1527 | } 1528 | } 1529 | return ERROR_SUCCESS; 1530 | } 1531 | 1532 | HRESULT 1533 | get_available_browser_version_string_impl(PCWSTR browser_dir, 1534 | LPWSTR *version) const { 1535 | if (!version) { 1536 | return -1; 1537 | } 1538 | auto found_client = find_available_client(browser_dir); 1539 | if (!found_client.found) { 1540 | return -1; 1541 | } 1542 | auto info_length_bytes = 1543 | found_client.version.size() * sizeof(found_client.version[0]); 1544 | auto info = static_cast(CoTaskMemAlloc(info_length_bytes)); 1545 | if (!info) { 1546 | return -1; 1547 | } 1548 | CopyMemory(info, found_client.version.c_str(), info_length_bytes); 1549 | *version = info; 1550 | return 0; 1551 | } 1552 | 1553 | client_info_t find_available_client(PCWSTR browser_dir) const { 1554 | if (browser_dir) { 1555 | return find_embedded_client(api_version, browser_dir); 1556 | } 1557 | auto found_client = 1558 | find_installed_client(api_version, true, default_release_channel_guid); 1559 | if (!found_client.found) { 1560 | found_client = find_installed_client(api_version, false, 1561 | default_release_channel_guid); 1562 | } 1563 | return found_client; 1564 | } 1565 | 1566 | std::wstring make_client_dll_path(const std::wstring &dir) const { 1567 | auto dll_path = dir; 1568 | if (!dll_path.empty()) { 1569 | auto last_char = dir[dir.size() - 1]; 1570 | if (last_char != L'\\' && last_char != L'/') { 1571 | dll_path += L'\\'; 1572 | } 1573 | } 1574 | dll_path += L"EBWebView\\"; 1575 | #if defined(_M_X64) || defined(__x86_64__) 1576 | dll_path += L"x64"; 1577 | #elif defined(_M_IX86) || defined(__i386__) 1578 | dll_path += L"x86"; 1579 | #elif defined(_M_ARM64) || defined(__aarch64__) 1580 | dll_path += L"arm64"; 1581 | #else 1582 | #error WebView2 integration for this platform is not yet supported. 1583 | #endif 1584 | dll_path += L"\\EmbeddedBrowserWebView.dll"; 1585 | return dll_path; 1586 | } 1587 | 1588 | client_info_t 1589 | find_installed_client(unsigned int min_api_version, bool system, 1590 | const std::wstring &release_channel) const { 1591 | std::wstring sub_key = client_state_reg_sub_key; 1592 | sub_key += release_channel; 1593 | auto root_key = system ? HKEY_LOCAL_MACHINE : HKEY_CURRENT_USER; 1594 | reg_key key(root_key, sub_key, 0, KEY_READ | KEY_WOW64_32KEY); 1595 | if (!key.is_open()) { 1596 | return {}; 1597 | } 1598 | auto ebwebview_value = key.query_string(L"EBWebView"); 1599 | 1600 | auto client_version_string = 1601 | get_last_native_path_component(ebwebview_value); 1602 | auto client_version = parse_version(client_version_string); 1603 | if (client_version[2] < min_api_version) { 1604 | // Our API version is greater than the runtime API version. 1605 | return {}; 1606 | } 1607 | 1608 | auto client_dll_path = make_client_dll_path(ebwebview_value); 1609 | return {true, client_dll_path, client_version_string, 1610 | webview2_runtime_type::installed}; 1611 | } 1612 | 1613 | client_info_t find_embedded_client(unsigned int min_api_version, 1614 | const std::wstring &dir) const { 1615 | auto client_dll_path = make_client_dll_path(dir); 1616 | 1617 | auto client_version_string = get_file_version_string(client_dll_path); 1618 | auto client_version = parse_version(client_version_string); 1619 | if (client_version[2] < min_api_version) { 1620 | // Our API version is greater than the runtime API version. 1621 | return {}; 1622 | } 1623 | 1624 | return {true, client_dll_path, client_version_string, 1625 | webview2_runtime_type::embedded}; 1626 | } 1627 | 1628 | // The minimum WebView2 API version we need regardless of the SDK release 1629 | // actually used. The number comes from the SDK release version, 1630 | // e.g. 1.0.1150.38. To be safe the SDK should have a number that is greater 1631 | // than or equal to this number. The Edge browser webview client must 1632 | // have a number greater than or equal to this number. 1633 | static constexpr unsigned int api_version = 1150; 1634 | 1635 | static constexpr auto client_state_reg_sub_key = 1636 | L"SOFTWARE\\Microsoft\\EdgeUpdate\\ClientState\\"; 1637 | 1638 | // GUID for the stable release channel. 1639 | static constexpr auto stable_release_guid = 1640 | L"{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}"; 1641 | 1642 | static constexpr auto default_release_channel_guid = stable_release_guid; 1643 | #endif /* WEBVIEW_MSWEBVIEW2_BUILTIN_IMPL */ 1644 | 1645 | #if WEBVIEW_MSWEBVIEW2_EXPLICIT_LINK == 1 1646 | native_library m_lib{L"WebView2Loader.dll"}; 1647 | #endif 1648 | }; 1649 | 1650 | namespace cast_info { 1651 | static constexpr auto controller_completed = 1652 | cast_info_t{ 1653 | IID_ICoreWebView2CreateCoreWebView2ControllerCompletedHandler}; 1654 | 1655 | static constexpr auto environment_completed = 1656 | cast_info_t{ 1657 | IID_ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler}; 1658 | 1659 | static constexpr auto message_received = 1660 | cast_info_t{ 1661 | IID_ICoreWebView2WebMessageReceivedEventHandler}; 1662 | 1663 | static constexpr auto permission_requested = 1664 | cast_info_t{ 1665 | IID_ICoreWebView2PermissionRequestedEventHandler}; 1666 | } // namespace cast_info 1667 | } // namespace mswebview2 1668 | 1669 | class webview2_com_handler 1670 | : public ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler, 1671 | public ICoreWebView2CreateCoreWebView2ControllerCompletedHandler, 1672 | public ICoreWebView2WebMessageReceivedEventHandler, 1673 | public ICoreWebView2PermissionRequestedEventHandler { 1674 | using webview2_com_handler_cb_t = 1675 | std::function; 1676 | 1677 | public: 1678 | webview2_com_handler(HWND hwnd, msg_cb_t msgCb, webview2_com_handler_cb_t cb) 1679 | : m_window(hwnd), m_msgCb(msgCb), m_cb(cb) {} 1680 | 1681 | virtual ~webview2_com_handler() = default; 1682 | webview2_com_handler(const webview2_com_handler &other) = delete; 1683 | webview2_com_handler &operator=(const webview2_com_handler &other) = delete; 1684 | webview2_com_handler(webview2_com_handler &&other) = delete; 1685 | webview2_com_handler &operator=(webview2_com_handler &&other) = delete; 1686 | 1687 | ULONG STDMETHODCALLTYPE AddRef() { return ++m_ref_count; } 1688 | ULONG STDMETHODCALLTYPE Release() { 1689 | if (m_ref_count > 1) { 1690 | return --m_ref_count; 1691 | } 1692 | delete this; 1693 | return 0; 1694 | } 1695 | HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, LPVOID *ppv) { 1696 | using namespace mswebview2::cast_info; 1697 | 1698 | if (!ppv) { 1699 | return E_POINTER; 1700 | } 1701 | 1702 | // All of the COM interfaces we implement should be added here regardless 1703 | // of whether they are required. 1704 | // This is just to be on the safe side in case the WebView2 Runtime ever 1705 | // requests a pointer to an interface we implement. 1706 | // The WebView2 Runtime must at the very least be able to get a pointer to 1707 | // ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler when we use 1708 | // our custom WebView2 loader implementation, and observations have shown 1709 | // that it is the only interface requested in this case. None have been 1710 | // observed to be requested when using the official WebView2 loader. 1711 | 1712 | if (cast_if_equal_iid(riid, controller_completed, ppv) || 1713 | cast_if_equal_iid(riid, environment_completed, ppv) || 1714 | cast_if_equal_iid(riid, message_received, ppv) || 1715 | cast_if_equal_iid(riid, permission_requested, ppv)) { 1716 | return S_OK; 1717 | } 1718 | 1719 | return E_NOINTERFACE; 1720 | } 1721 | HRESULT STDMETHODCALLTYPE Invoke(HRESULT res, ICoreWebView2Environment *env) { 1722 | if (SUCCEEDED(res)) { 1723 | res = env->CreateCoreWebView2Controller(m_window, this); 1724 | if (SUCCEEDED(res)) { 1725 | return S_OK; 1726 | } 1727 | } 1728 | try_create_environment(); 1729 | return S_OK; 1730 | } 1731 | HRESULT STDMETHODCALLTYPE Invoke(HRESULT res, 1732 | ICoreWebView2Controller *controller) { 1733 | if (FAILED(res)) { 1734 | // See try_create_environment() regarding 1735 | // HRESULT_FROM_WIN32(ERROR_INVALID_STATE). 1736 | // The result is E_ABORT if the parent window has been destroyed already. 1737 | switch (res) { 1738 | case HRESULT_FROM_WIN32(ERROR_INVALID_STATE): 1739 | case E_ABORT: 1740 | return S_OK; 1741 | } 1742 | try_create_environment(); 1743 | return S_OK; 1744 | } 1745 | 1746 | ICoreWebView2 *webview; 1747 | ::EventRegistrationToken token; 1748 | controller->get_CoreWebView2(&webview); 1749 | webview->add_WebMessageReceived(this, &token); 1750 | webview->add_PermissionRequested(this, &token); 1751 | 1752 | m_cb(controller, webview); 1753 | return S_OK; 1754 | } 1755 | HRESULT STDMETHODCALLTYPE Invoke( 1756 | ICoreWebView2 *sender, ICoreWebView2WebMessageReceivedEventArgs *args) { 1757 | LPWSTR message; 1758 | args->TryGetWebMessageAsString(&message); 1759 | m_msgCb(narrow_string(message)); 1760 | sender->PostWebMessageAsString(message); 1761 | 1762 | CoTaskMemFree(message); 1763 | return S_OK; 1764 | } 1765 | HRESULT STDMETHODCALLTYPE Invoke( 1766 | ICoreWebView2 *sender, ICoreWebView2PermissionRequestedEventArgs *args) { 1767 | COREWEBVIEW2_PERMISSION_KIND kind; 1768 | args->get_PermissionKind(&kind); 1769 | if (kind == COREWEBVIEW2_PERMISSION_KIND_CLIPBOARD_READ) { 1770 | args->put_State(COREWEBVIEW2_PERMISSION_STATE_ALLOW); 1771 | } 1772 | return S_OK; 1773 | } 1774 | 1775 | // Checks whether the specified IID equals the IID of the specified type and 1776 | // if so casts the "this" pointer to T and returns it. Returns nullptr on 1777 | // mismatching IIDs. 1778 | // If ppv is specified then the pointer will also be assigned to *ppv. 1779 | template 1780 | T *cast_if_equal_iid(REFIID riid, const cast_info_t &info, 1781 | LPVOID *ppv = nullptr) noexcept { 1782 | T *ptr = nullptr; 1783 | if (IsEqualIID(riid, info.iid)) { 1784 | ptr = static_cast(this); 1785 | ptr->AddRef(); 1786 | } 1787 | if (ppv) { 1788 | *ppv = ptr; 1789 | } 1790 | return ptr; 1791 | } 1792 | 1793 | // Set the function that will perform the initiating logic for creating 1794 | // the WebView2 environment. 1795 | void set_attempt_handler(std::function attempt_handler) noexcept { 1796 | m_attempt_handler = attempt_handler; 1797 | } 1798 | 1799 | // Retry creating a WebView2 environment. 1800 | // The initiating logic for creating the environment is defined by the 1801 | // caller of set_attempt_handler(). 1802 | void try_create_environment() noexcept { 1803 | // WebView creation fails with HRESULT_FROM_WIN32(ERROR_INVALID_STATE) if 1804 | // a running instance using the same user data folder exists, and the 1805 | // Environment objects have different EnvironmentOptions. 1806 | // Source: https://docs.microsoft.com/en-us/microsoft-edge/webview2/reference/win32/icorewebview2environment?view=webview2-1.0.1150.38 1807 | if (m_attempts < m_max_attempts) { 1808 | ++m_attempts; 1809 | auto res = m_attempt_handler(); 1810 | if (SUCCEEDED(res)) { 1811 | return; 1812 | } 1813 | // Not entirely sure if this error code only applies to 1814 | // CreateCoreWebView2Controller so we check here as well. 1815 | if (res == HRESULT_FROM_WIN32(ERROR_INVALID_STATE)) { 1816 | return; 1817 | } 1818 | try_create_environment(); 1819 | return; 1820 | } 1821 | // Give up. 1822 | m_cb(nullptr, nullptr); 1823 | } 1824 | 1825 | private: 1826 | HWND m_window; 1827 | msg_cb_t m_msgCb; 1828 | webview2_com_handler_cb_t m_cb; 1829 | std::atomic m_ref_count{1}; 1830 | std::function m_attempt_handler; 1831 | unsigned int m_max_attempts = 5; 1832 | unsigned int m_attempts = 0; 1833 | }; 1834 | 1835 | class win32_edge_engine { 1836 | public: 1837 | win32_edge_engine(bool debug, void *window) { 1838 | if (!is_webview2_available()) { 1839 | return; 1840 | } 1841 | if (!m_com_init.is_initialized()) { 1842 | return; 1843 | } 1844 | enable_dpi_awareness(); 1845 | if (window == nullptr) { 1846 | HINSTANCE hInstance = GetModuleHandle(nullptr); 1847 | HICON icon = (HICON)LoadImage( 1848 | hInstance, IDI_APPLICATION, IMAGE_ICON, GetSystemMetrics(SM_CXICON), 1849 | GetSystemMetrics(SM_CYICON), LR_DEFAULTCOLOR); 1850 | 1851 | WNDCLASSEXW wc; 1852 | ZeroMemory(&wc, sizeof(WNDCLASSEX)); 1853 | wc.cbSize = sizeof(WNDCLASSEX); 1854 | wc.hInstance = hInstance; 1855 | wc.lpszClassName = L"webview"; 1856 | wc.hIcon = icon; 1857 | wc.lpfnWndProc = 1858 | (WNDPROC)(+[](HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) -> LRESULT { 1859 | auto w = (win32_edge_engine *)GetWindowLongPtr(hwnd, GWLP_USERDATA); 1860 | switch (msg) { 1861 | case WM_SIZE: 1862 | w->resize(hwnd); 1863 | break; 1864 | case WM_CLOSE: 1865 | DestroyWindow(hwnd); 1866 | break; 1867 | case WM_DESTROY: 1868 | w->terminate(); 1869 | break; 1870 | case WM_GETMINMAXINFO: { 1871 | auto lpmmi = (LPMINMAXINFO)lp; 1872 | if (w == nullptr) { 1873 | return 0; 1874 | } 1875 | if (w->m_maxsz.x > 0 && w->m_maxsz.y > 0) { 1876 | lpmmi->ptMaxSize = w->m_maxsz; 1877 | lpmmi->ptMaxTrackSize = w->m_maxsz; 1878 | } 1879 | if (w->m_minsz.x > 0 && w->m_minsz.y > 0) { 1880 | lpmmi->ptMinTrackSize = w->m_minsz; 1881 | } 1882 | } break; 1883 | default: 1884 | return DefWindowProcW(hwnd, msg, wp, lp); 1885 | } 1886 | return 0; 1887 | }); 1888 | RegisterClassExW(&wc); 1889 | m_window = CreateWindowW(L"webview", L"", WS_OVERLAPPEDWINDOW, 1890 | CW_USEDEFAULT, CW_USEDEFAULT, 640, 480, nullptr, 1891 | nullptr, hInstance, nullptr); 1892 | if (m_window == nullptr) { 1893 | return; 1894 | } 1895 | SetWindowLongPtr(m_window, GWLP_USERDATA, (LONG_PTR)this); 1896 | } else { 1897 | m_window = *(static_cast(window)); 1898 | } 1899 | 1900 | ShowWindow(m_window, SW_SHOW); 1901 | UpdateWindow(m_window); 1902 | SetFocus(m_window); 1903 | 1904 | auto cb = 1905 | std::bind(&win32_edge_engine::on_message, this, std::placeholders::_1); 1906 | 1907 | embed(m_window, debug, cb); 1908 | resize(m_window); 1909 | m_controller->MoveFocus(COREWEBVIEW2_MOVE_FOCUS_REASON_PROGRAMMATIC); 1910 | } 1911 | 1912 | virtual ~win32_edge_engine() { 1913 | if (m_com_handler) { 1914 | m_com_handler->Release(); 1915 | m_com_handler = nullptr; 1916 | } 1917 | if (m_webview) { 1918 | m_webview->Release(); 1919 | m_webview = nullptr; 1920 | } 1921 | if (m_controller) { 1922 | m_controller->Release(); 1923 | m_controller = nullptr; 1924 | } 1925 | } 1926 | 1927 | win32_edge_engine(const win32_edge_engine &other) = delete; 1928 | win32_edge_engine &operator=(const win32_edge_engine &other) = delete; 1929 | win32_edge_engine(win32_edge_engine &&other) = delete; 1930 | win32_edge_engine &operator=(win32_edge_engine &&other) = delete; 1931 | 1932 | void run() { 1933 | MSG msg; 1934 | BOOL res; 1935 | while ((res = GetMessage(&msg, nullptr, 0, 0)) != -1) { 1936 | if (msg.hwnd) { 1937 | TranslateMessage(&msg); 1938 | DispatchMessage(&msg); 1939 | continue; 1940 | } 1941 | if (msg.message == WM_APP) { 1942 | auto f = (dispatch_fn_t *)(msg.lParam); 1943 | (*f)(); 1944 | delete f; 1945 | } else if (msg.message == WM_QUIT) { 1946 | return; 1947 | } 1948 | } 1949 | } 1950 | void *window() { return (void *)m_window; } 1951 | void terminate() { PostQuitMessage(0); } 1952 | void dispatch(dispatch_fn_t f) { 1953 | PostThreadMessage(m_main_thread, WM_APP, 0, (LPARAM) new dispatch_fn_t(f)); 1954 | } 1955 | 1956 | void set_title(const std::string &title) { 1957 | SetWindowTextW(m_window, widen_string(title).c_str()); 1958 | } 1959 | 1960 | void set_size(int width, int height, int hints) { 1961 | auto style = GetWindowLong(m_window, GWL_STYLE); 1962 | if (hints == WEBVIEW_HINT_FIXED) { 1963 | style &= ~(WS_THICKFRAME | WS_MAXIMIZEBOX); 1964 | } else { 1965 | style |= (WS_THICKFRAME | WS_MAXIMIZEBOX); 1966 | } 1967 | SetWindowLong(m_window, GWL_STYLE, style); 1968 | 1969 | if (hints == WEBVIEW_HINT_MAX) { 1970 | m_maxsz.x = width; 1971 | m_maxsz.y = height; 1972 | } else if (hints == WEBVIEW_HINT_MIN) { 1973 | m_minsz.x = width; 1974 | m_minsz.y = height; 1975 | } else { 1976 | RECT r; 1977 | r.left = r.top = 0; 1978 | r.right = width; 1979 | r.bottom = height; 1980 | AdjustWindowRect(&r, WS_OVERLAPPEDWINDOW, 0); 1981 | SetWindowPos( 1982 | m_window, nullptr, r.left, r.top, r.right - r.left, r.bottom - r.top, 1983 | SWP_NOZORDER | SWP_NOACTIVATE | SWP_NOMOVE | SWP_FRAMECHANGED); 1984 | resize(m_window); 1985 | } 1986 | } 1987 | 1988 | void navigate(const std::string &url) { 1989 | auto wurl = widen_string(url); 1990 | m_webview->Navigate(wurl.c_str()); 1991 | } 1992 | 1993 | void init(const std::string &js) { 1994 | auto wjs = widen_string(js); 1995 | m_webview->AddScriptToExecuteOnDocumentCreated(wjs.c_str(), nullptr); 1996 | } 1997 | 1998 | void eval(const std::string &js) { 1999 | auto wjs = widen_string(js); 2000 | m_webview->ExecuteScript(wjs.c_str(), nullptr); 2001 | } 2002 | 2003 | void set_html(const std::string &html) { 2004 | m_webview->NavigateToString(widen_string(html).c_str()); 2005 | } 2006 | 2007 | private: 2008 | bool embed(HWND wnd, bool debug, msg_cb_t cb) { 2009 | std::atomic_flag flag = ATOMIC_FLAG_INIT; 2010 | flag.test_and_set(); 2011 | 2012 | wchar_t currentExePath[MAX_PATH]; 2013 | GetModuleFileNameW(nullptr, currentExePath, MAX_PATH); 2014 | wchar_t *currentExeName = PathFindFileNameW(currentExePath); 2015 | 2016 | wchar_t dataPath[MAX_PATH]; 2017 | if (!SUCCEEDED( 2018 | SHGetFolderPathW(nullptr, CSIDL_APPDATA, nullptr, 0, dataPath))) { 2019 | return false; 2020 | } 2021 | wchar_t userDataFolder[MAX_PATH]; 2022 | PathCombineW(userDataFolder, dataPath, currentExeName); 2023 | 2024 | m_com_handler = new webview2_com_handler( 2025 | wnd, cb, 2026 | [&](ICoreWebView2Controller *controller, ICoreWebView2 *webview) { 2027 | if (!controller || !webview) { 2028 | flag.clear(); 2029 | return; 2030 | } 2031 | controller->AddRef(); 2032 | webview->AddRef(); 2033 | m_controller = controller; 2034 | m_webview = webview; 2035 | flag.clear(); 2036 | }); 2037 | 2038 | m_com_handler->set_attempt_handler([&] { 2039 | return m_webview2_loader.create_environment_with_options( 2040 | nullptr, userDataFolder, nullptr, m_com_handler); 2041 | }); 2042 | m_com_handler->try_create_environment(); 2043 | 2044 | MSG msg = {}; 2045 | while (flag.test_and_set() && GetMessage(&msg, nullptr, 0, 0)) { 2046 | TranslateMessage(&msg); 2047 | DispatchMessage(&msg); 2048 | } 2049 | if (!m_controller || !m_webview) { 2050 | return false; 2051 | } 2052 | ICoreWebView2Settings *settings = nullptr; 2053 | auto res = m_webview->get_Settings(&settings); 2054 | if (res != S_OK) { 2055 | return false; 2056 | } 2057 | res = settings->put_AreDevToolsEnabled(debug ? TRUE : FALSE); 2058 | if (res != S_OK) { 2059 | return false; 2060 | } 2061 | init("window.external={invoke:s=>window.chrome.webview.postMessage(s)}"); 2062 | return true; 2063 | } 2064 | 2065 | void resize(HWND wnd) { 2066 | if (m_controller == nullptr) { 2067 | return; 2068 | } 2069 | RECT bounds; 2070 | GetClientRect(wnd, &bounds); 2071 | m_controller->put_Bounds(bounds); 2072 | } 2073 | 2074 | bool is_webview2_available() const noexcept { 2075 | LPWSTR version_info = nullptr; 2076 | auto res = m_webview2_loader.get_available_browser_version_string( 2077 | nullptr, &version_info); 2078 | // The result will be equal to HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND) 2079 | // if the WebView2 runtime is not installed. 2080 | auto ok = SUCCEEDED(res) && version_info; 2081 | if (version_info) { 2082 | CoTaskMemFree(version_info); 2083 | } 2084 | return ok; 2085 | } 2086 | 2087 | virtual void on_message(const std::string &msg) = 0; 2088 | 2089 | // The app is expected to call CoInitializeEx before 2090 | // CreateCoreWebView2EnvironmentWithOptions. 2091 | // Source: https://docs.microsoft.com/en-us/microsoft-edge/webview2/reference/win32/webview2-idl#createcorewebview2environmentwithoptions 2092 | com_init_wrapper m_com_init{COINIT_APARTMENTTHREADED}; 2093 | HWND m_window = nullptr; 2094 | POINT m_minsz = POINT{0, 0}; 2095 | POINT m_maxsz = POINT{0, 0}; 2096 | DWORD m_main_thread = GetCurrentThreadId(); 2097 | ICoreWebView2 *m_webview = nullptr; 2098 | ICoreWebView2Controller *m_controller = nullptr; 2099 | webview2_com_handler *m_com_handler = nullptr; 2100 | mswebview2::loader m_webview2_loader; 2101 | }; 2102 | 2103 | } // namespace detail 2104 | 2105 | using browser_engine = detail::win32_edge_engine; 2106 | 2107 | } // namespace webview 2108 | 2109 | #endif /* WEBVIEW_GTK, WEBVIEW_COCOA, WEBVIEW_EDGE */ 2110 | 2111 | namespace webview { 2112 | 2113 | class webview : public browser_engine { 2114 | public: 2115 | webview(bool debug = false, void *wnd = nullptr) 2116 | : browser_engine(debug, wnd) {} 2117 | 2118 | void navigate(const std::string &url) { 2119 | if (url.empty()) { 2120 | browser_engine::navigate("about:blank"); 2121 | return; 2122 | } 2123 | browser_engine::navigate(url); 2124 | } 2125 | 2126 | using binding_t = std::function; 2127 | class binding_ctx_t { 2128 | public: 2129 | binding_ctx_t(binding_t callback, void *arg) 2130 | : callback(callback), arg(arg) {} 2131 | // This function is called upon execution of the bound JS function 2132 | binding_t callback; 2133 | // This user-supplied argument is passed to the callback 2134 | void *arg; 2135 | }; 2136 | 2137 | using sync_binding_t = std::function; 2138 | 2139 | // Synchronous bind 2140 | void bind(const std::string &name, sync_binding_t fn) { 2141 | auto wrapper = [this, fn](const std::string &seq, const std::string &req, 2142 | void * /*arg*/) { resolve(seq, 0, fn(req)); }; 2143 | bind(name, wrapper, nullptr); 2144 | } 2145 | 2146 | // Asynchronous bind 2147 | void bind(const std::string &name, binding_t fn, void *arg) { 2148 | if (bindings.count(name) > 0) { 2149 | return; 2150 | } 2151 | bindings.emplace(name, binding_ctx_t(fn, arg)); 2152 | auto js = "(function() { var name = '" + name + "';" + R""( 2153 | var RPC = window._rpc = (window._rpc || {nextSeq: 1}); 2154 | window[name] = function() { 2155 | var seq = RPC.nextSeq++; 2156 | var promise = new Promise(function(resolve, reject) { 2157 | RPC[seq] = { 2158 | resolve: resolve, 2159 | reject: reject, 2160 | }; 2161 | }); 2162 | window.external.invoke(JSON.stringify({ 2163 | id: seq, 2164 | method: name, 2165 | params: Array.prototype.slice.call(arguments), 2166 | })); 2167 | return promise; 2168 | } 2169 | })())""; 2170 | init(js); 2171 | eval(js); 2172 | } 2173 | 2174 | void unbind(const std::string &name) { 2175 | auto found = bindings.find(name); 2176 | if (found != bindings.end()) { 2177 | auto js = "delete window['" + name + "'];"; 2178 | init(js); 2179 | eval(js); 2180 | bindings.erase(found); 2181 | } 2182 | } 2183 | 2184 | void resolve(const std::string &seq, int status, const std::string &result) { 2185 | dispatch([seq, status, result, this]() { 2186 | if (status == 0) { 2187 | eval("window._rpc[" + seq + "].resolve(" + result + 2188 | "); delete window._rpc[" + seq + "]"); 2189 | } else { 2190 | eval("window._rpc[" + seq + "].reject(" + result + 2191 | "); delete window._rpc[" + seq + "]"); 2192 | } 2193 | }); 2194 | } 2195 | 2196 | private: 2197 | void on_message(const std::string &msg) { 2198 | auto seq = detail::json_parse(msg, "id", 0); 2199 | auto name = detail::json_parse(msg, "method", 0); 2200 | auto args = detail::json_parse(msg, "params", 0); 2201 | auto found = bindings.find(name); 2202 | if (found == bindings.end()) { 2203 | return; 2204 | } 2205 | const auto &context = found->second; 2206 | context.callback(seq, args, context.arg); 2207 | } 2208 | 2209 | std::map bindings; 2210 | }; 2211 | } // namespace webview 2212 | 2213 | WEBVIEW_API webview_t webview_create(int debug, void *wnd) { 2214 | auto w = new webview::webview(debug, wnd); 2215 | if (!w->window()) { 2216 | delete w; 2217 | return nullptr; 2218 | } 2219 | return w; 2220 | } 2221 | 2222 | WEBVIEW_API void webview_destroy(webview_t w) { 2223 | delete static_cast(w); 2224 | } 2225 | 2226 | WEBVIEW_API void webview_run(webview_t w) { 2227 | static_cast(w)->run(); 2228 | } 2229 | 2230 | WEBVIEW_API void webview_terminate(webview_t w) { 2231 | static_cast(w)->terminate(); 2232 | } 2233 | 2234 | WEBVIEW_API void webview_dispatch(webview_t w, void (*fn)(webview_t, void *), 2235 | void *arg) { 2236 | static_cast(w)->dispatch([=]() { fn(w, arg); }); 2237 | } 2238 | 2239 | WEBVIEW_API void *webview_get_window(webview_t w) { 2240 | return static_cast(w)->window(); 2241 | } 2242 | 2243 | WEBVIEW_API void webview_set_title(webview_t w, const char *title) { 2244 | static_cast(w)->set_title(title); 2245 | } 2246 | 2247 | WEBVIEW_API void webview_set_size(webview_t w, int width, int height, 2248 | int hints) { 2249 | static_cast(w)->set_size(width, height, hints); 2250 | } 2251 | 2252 | WEBVIEW_API void webview_navigate(webview_t w, const char *url) { 2253 | static_cast(w)->navigate(url); 2254 | } 2255 | 2256 | WEBVIEW_API void webview_set_html(webview_t w, const char *html) { 2257 | static_cast(w)->set_html(html); 2258 | } 2259 | 2260 | WEBVIEW_API void webview_init(webview_t w, const char *js) { 2261 | static_cast(w)->init(js); 2262 | } 2263 | 2264 | WEBVIEW_API void webview_eval(webview_t w, const char *js) { 2265 | static_cast(w)->eval(js); 2266 | } 2267 | 2268 | WEBVIEW_API void webview_bind(webview_t w, const char *name, 2269 | void (*fn)(const char *seq, const char *req, 2270 | void *arg), 2271 | void *arg) { 2272 | static_cast(w)->bind( 2273 | name, 2274 | [=](const std::string &seq, const std::string &req, void *arg) { 2275 | fn(seq.c_str(), req.c_str(), arg); 2276 | }, 2277 | arg); 2278 | } 2279 | 2280 | WEBVIEW_API void webview_unbind(webview_t w, const char *name) { 2281 | static_cast(w)->unbind(name); 2282 | } 2283 | 2284 | WEBVIEW_API void webview_return(webview_t w, const char *seq, int status, 2285 | const char *result) { 2286 | static_cast(w)->resolve(seq, status, result); 2287 | } 2288 | 2289 | WEBVIEW_API const webview_version_info_t *webview_version() { 2290 | return &webview::detail::library_version_info; 2291 | } 2292 | 2293 | #endif /* WEBVIEW_HEADER */ 2294 | #endif /* __cplusplus */ 2295 | #endif /* WEBVIEW_H */ 2296 | -------------------------------------------------------------------------------- /shard.yml: -------------------------------------------------------------------------------- 1 | name: webview 2 | version: 0.2.3 3 | 4 | authors: 5 | - Ali Naqvi 6 | 7 | description: | 8 | Crystal bindings to the zserge's tiny cross-platform Webview library. 9 | 10 | scripts: 11 | postinstall: make 12 | 13 | crystal: ~> 1.0 14 | 15 | license: MIT 16 | -------------------------------------------------------------------------------- /spec/spec_helper.cr: -------------------------------------------------------------------------------- 1 | require "spec" 2 | require "../src/webview" 3 | -------------------------------------------------------------------------------- /spec/webview_spec.cr: -------------------------------------------------------------------------------- 1 | require "./spec_helper" 2 | 3 | describe Webview do 4 | end 5 | -------------------------------------------------------------------------------- /src/lib.cr: -------------------------------------------------------------------------------- 1 | module Webview 2 | {% if flag?(:darwin) %} 3 | @[Link(framework: "WebKit")] 4 | @[Link(ldflags: "-L#{__DIR__}/../ext -lwebview.o -lc++")] 5 | {% elsif flag?(:linux) %} 6 | @[Link(ldflags: "`command -v pkg-config > /dev/null && pkg-config --cflags --libs gtk+-3.0 webkit2gtk-4.0`")] 7 | @[Link(ldflags: "#{__DIR__}/../ext/libwebview.a -lstdc++")] 8 | {% elsif flag?(:windows) %} 9 | @[Link("webview")] 10 | # TODO - Windows requires special linker flags for GUI apps, but this doesn't work with crystal stdlib (tried with Crystal 1.6.2). 11 | # @[Link(ldflags: "/subsystem:windows")] 12 | {% else %} 13 | raise "Platform not supported" 14 | {% end %} 15 | lib LibWebView 16 | alias T = Void* 17 | 18 | # Creates a new webview instance. If debug is non-zero - developer tools will 19 | # be enabled (if the platform supports them). Window parameter can be a 20 | # pointer to the native window handle. If it's non-null - then child WebView 21 | # is embedded into the given parent window. Otherwise a new window is created. 22 | # Depending on the platform, a GtkWindow, NSWindow or HWND pointer can be 23 | # passed here. 24 | fun create = webview_create(debug : LibC::Int, window : Void*) : T 25 | # Destroys a webview and closes the native window. 26 | fun destroy = webview_destroy(w : T) 27 | # Runs the main loop until it's terminated. After this function exits - you 28 | # must destroy the webview. 29 | fun run = webview_run(w : T) 30 | # Stops the main loop. It is safe to call this function from another other 31 | # background thread. 32 | fun terminate = webview_terminate(w : T) 33 | # Posts a function to be executed on the main thread. You normally do not need 34 | # to call this function, unless you want to tweak the native window. 35 | fun dispatch = webview_dispatch(w : T, fn : (T, Void* -> Void), arg : Void*) 36 | # Returns a native window handle pointer. When using GTK backend the pointer 37 | # is GtkWindow pointer, when using Cocoa backend the pointer is NSWindow 38 | # pointer, when using Win32 backend the pointer is HWND pointer. 39 | fun get_window = webview_get_window(w : T) : Void* 40 | # Updates the title of the native window. Must be called from the UI thread. 41 | fun set_title = webview_set_title(w : T, title : LibC::Char*) 42 | # Updates native window size. See WEBVIEW_HINT constants. 43 | fun set_size = webview_set_size(w : T, width : LibC::Int, height : LibC::Int, hints : LibC::Int) 44 | # Navigates webview to the given URL. URL may be a data URI, i.e. 45 | # "data:text/text,...". It is often ok not to url-encode it 46 | # properly, webview will re-encode it for you. 47 | fun navigate = webview_navigate(w : T, url : LibC::Char*) 48 | # Set webview HTML directly. 49 | # Example: webview_set_html(w, "

Hello

"); 50 | fun set_html = webview_set_html(w : T, html : LibC::Char*) 51 | # Injects JavaScript code at the initialization of the new page. Every time 52 | # the webview will open a the new page - this initialization code will be 53 | # executed. It is guaranteed that code is executed before window.onload. 54 | fun init = webview_init(w : T, js : LibC::Char*) 55 | # Evaluates arbitrary JavaScript code. Evaluation happens asynchronously, also 56 | # the result of the expression is ignored. Use RPC bindings if you want to 57 | # receive notifications about the results of the evaluation. 58 | fun eval = webview_eval(w : T, js : LibC::Char*) 59 | # Binds a native C callback so that it will appear under the given name as a 60 | # global JavaScript function. Internally it uses webview_init(). Callback 61 | # receives a request string and a user-provided argument pointer. Request 62 | # string is a JSON array of all the arguments passed to the JavaScript 63 | # function. 64 | fun bind = webview_bind(w : T, name : LibC::Char*, fn : (LibC::Char*, LibC::Char*, Void* -> Void), arg : Void*) 65 | # Removes a native C callback that was previously set by webview_bind. 66 | fun unbind = webview_unbind(w : T, name : LibC::Char*) 67 | # Allows to return a value from the native binding. Original request pointer 68 | # must be provided to help internal RPC engine match requests with responses. 69 | # If status is zero - result is expected to be a valid JSON result value. 70 | # If status is not zero - result is an error JSON object. 71 | fun webview_return(w : T, seq : LibC::Char*, status : LibC::Int, result : LibC::Char*) 72 | # Get the library's version information. 73 | # @since 0.10 74 | fun version = webview_version : WebviewVersionInfo* 75 | 76 | # Holds the library's version information. 77 | struct WebviewVersionInfo 78 | # The elements of the version number. 79 | version : WebviewVersion 80 | # SemVer 2.0.0 version number in MAJOR.MINOR.PATCH format. 81 | version_number : LibC::Char[32] 82 | # SemVer 2.0.0 pre-release labels prefixed with "-" if specified, otherwise 83 | # an empty string. 84 | pre_release : LibC::Char[48] 85 | # SemVer 2.0.0 build metadata prefixed with "+", otherwise an empty string. 86 | build_metadata : LibC::Char[48] 87 | end 88 | 89 | # Holds the elements of a MAJOR.MINOR.PATCH version number. 90 | struct WebviewVersion 91 | major : LibC::UInt 92 | minor : LibC::UInt 93 | patch : LibC::UInt 94 | end 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /src/webview.cr: -------------------------------------------------------------------------------- 1 | require "json" 2 | 3 | # Crystal bindings for [zserge's Webview](https://github.com/zserge/webview) which is an excellent cross-platform single header webview library for C/C++ using Gtk, Cocoa or MSHTML repectively. 4 | module Webview 5 | VERSION = {{ `shards version "#{__DIR__}"`.chomp.stringify }} 6 | 7 | # Window size hints 8 | enum SizeHints 9 | NONE = 0 # Width and height are default size 10 | MIN = 1 # Width and height are minimum bounds 11 | MAX = 2 # Width and height are maximum bounds 12 | FIXED = 3 # Window size can not be changed by user 13 | end 14 | 15 | record VersionInfo, major : UInt32, minor : UInt32, patch : UInt32, version_number : String, 16 | pre_release : String, build_metadata : String do 17 | def self.new(ptr : Pointer(LibWebView::WebviewVersionInfo)) 18 | rec = ptr.value 19 | new(rec.version.major, rec.version.minor, rec.version.patch, 20 | String.new(rec.version_number.to_unsafe), 21 | String.new(rec.pre_release.to_unsafe), String.new(rec.build_metadata.to_unsafe)) 22 | end 23 | end 24 | 25 | alias JSProc = Array(JSON::Any) -> JSON::Any 26 | 27 | class Webview 28 | private record BindContext, w : LibWebView::T, cb : JSProc 29 | 30 | @@dispatchs = Hash(Proc(Nil), Pointer(Void)?).new 31 | @@bindings = Hash(JSProc, Pointer(Void)?).new 32 | 33 | def initialize(debug, title) 34 | @w = LibWebView.create(debug ? 1 : 0, nil) 35 | LibWebView.set_title(@w, title) 36 | end 37 | 38 | # destroys a WebView and closes the native window. 39 | def destroy 40 | LibWebView.destroy(@w) 41 | end 42 | 43 | # Terminate stops the main loop. It is safe to call this function from 44 | # a background thread. 45 | def terminate 46 | LibWebView.terminate(@w) 47 | end 48 | 49 | # runs the main loop until it's terminated. After this function exists 50 | # you must destroy the WebView 51 | def run 52 | LibWebView.run(@w) 53 | end 54 | 55 | # returns a native window handle pointer. When using GTK backend the 56 | # pointer is GtkWindow pointer, when using Cocoa backend the pointer is 57 | # NSWindow pointer, when using Win32 backend the pointer is HWND pointer. 58 | def window 59 | LibWebView.get_window(@w) 60 | end 61 | 62 | def title=(val) 63 | LibWebView.set_title(@w, val) 64 | end 65 | 66 | def size(width, height, hint : SizeHints) 67 | LibWebView.set_size(@w, width, height, hint.value) 68 | end 69 | 70 | # navigates WebView to the given URL. URL may be a data URI, i.e. 71 | # "data:text/text,..". It is often ok not to url-encode it 72 | # properlty, WebView will re-encode it for you. 73 | def navigate(url) 74 | LibWebView.navigate(@w, url) 75 | end 76 | 77 | # Set WebView HTML directly 78 | def html=(html : String) 79 | LibWebView.set_html(@w, html) 80 | end 81 | 82 | # posts a function to be executed on the main thread. You normally do no need 83 | # to call this function, unless you want to tweak the native window. 84 | def dispatch(&f : ->) 85 | boxed = Box.box(f) 86 | @@dispatchs[f] = boxed 87 | 88 | LibWebView.dispatch(@w, ->(_w, data) { 89 | cb = Box(typeof(f)).unbox(data) 90 | cb.call 91 | @@dispatchs.delete(cb) 92 | }, boxed) 93 | end 94 | 95 | # injects Javascript code at the initialization of the new page. Every 96 | # time the WebView will open the new page - this initialization code will 97 | # be executed. It is guaranteed that code is executed before window.onload. 98 | def init(js : String) 99 | LibWebView.init(@w, js) 100 | end 101 | 102 | # evaluates arbitrary Javascript code. Evaluation happens asynchronously, 103 | # also the result of the expression is ignored. Use RPC bindings if you want 104 | # to receive notifications about the result of the evaluation. 105 | def eval(js : String) 106 | LibWebView.eval(@w, js) 107 | end 108 | 109 | # binds a callback function so that it will appear under the given name 110 | # as a global Javascript function. 111 | def bind(name : String, fn : JSProc) 112 | ctx = BindContext.new(@w, fn) 113 | boxed = Box.box(ctx) 114 | @@bindings[fn] = boxed 115 | 116 | LibWebView.bind(@w, name, ->(id, req, data) { 117 | raw = JSON.parse(String.new(req)) 118 | cb_ctx = Box(BindContext).unbox(data) 119 | res = cb_ctx.cb.call(raw.as_a) 120 | LibWebView.webview_return(cb_ctx.w, id, 0, res.to_json) 121 | }, boxed) 122 | end 123 | 124 | # Removes a native Crystal callback that was previously set by `bind`. 125 | def unbind(name : String) 126 | LibWebView.unbind(@w, name) 127 | @@bindings.delete(name) 128 | end 129 | end 130 | 131 | def self.window(width : Int32, height : Int32, hint : SizeHints, title : String, debug = false) 132 | wv = Webview.new(debug, title) 133 | wv.size(width, height, hint) 134 | wv.title = title 135 | wv 136 | end 137 | 138 | def self.window(width : Int32, height : Int32, hint : SizeHints, title : String, url : String, debug = false) 139 | wv = Webview.new(debug, title) 140 | wv.size(width, height, hint) 141 | wv.title = title 142 | wv.navigate(url) 143 | wv 144 | end 145 | 146 | # Get the library's version information. 147 | def self.version 148 | VersionInfo.new(LibWebView.version) 149 | end 150 | end 151 | 152 | require "./*" 153 | --------------------------------------------------------------------------------