├── .github └── workflows │ ├── docs.yaml │ └── test.yaml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE.txt ├── README.md ├── examples ├── .icons │ ├── icon.ico │ └── icon.png ├── api │ ├── .gitignore │ ├── .prettierrc.yaml │ ├── README.md │ ├── package.json │ ├── pnpm-lock.yaml │ ├── src-tauri │ │ ├── Cargo.toml │ │ ├── build.rs │ │ ├── capabilities │ │ │ └── main.json │ │ ├── src │ │ │ ├── main.rs │ │ │ └── tray.rs │ │ └── tauri.conf.json │ ├── src │ │ ├── components │ │ │ ├── api-forms.css │ │ │ ├── api-forms.tsx │ │ │ ├── title-bar.css │ │ │ └── title-bar.tsx │ │ ├── index.html │ │ ├── main.tsx │ │ └── style.css │ ├── tsconfig.json │ └── vite.config.ts └── helloworld │ ├── .gitignore │ ├── Cargo.toml │ ├── README.md │ ├── build.rs │ ├── capabilities │ └── main.json │ ├── index.html │ ├── src │ └── main.rs │ └── tauri.conf.json ├── src ├── event_loop_ext.rs ├── invoke-system-initialization-script.js ├── lib.rs ├── runtime.rs ├── utils.rs ├── webview.rs └── window.rs └── tauri-runtime-verso-build ├── Cargo.toml ├── README.md └── src └── lib.rs /.github/workflows/docs.yaml: -------------------------------------------------------------------------------- 1 | name: Generate And Upload Documentation 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - main 8 | 9 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 10 | permissions: 11 | contents: read 12 | pages: write 13 | id-token: write 14 | 15 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 16 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 17 | concurrency: 18 | group: pages 19 | cancel-in-progress: false 20 | 21 | env: 22 | CARGO_TERM_COLOR: always 23 | 24 | jobs: 25 | generate-docs: 26 | # Using macos since it's github's fastest runner 27 | runs-on: macos-latest 28 | steps: 29 | - name: Checkout the repository 30 | uses: actions/checkout@v4 31 | 32 | - uses: Swatinem/rust-cache@v2 33 | 34 | - name: Build 35 | run: cargo doc --package tauri-runtime-verso --package tauri-runtime-verso-build --no-deps 36 | 37 | - name: Setup Pages 38 | uses: actions/configure-pages@v4 39 | - name: Upload artifact 40 | uses: actions/upload-pages-artifact@v3 41 | with: 42 | path: ./target/doc/ 43 | 44 | deploy: 45 | runs-on: ubuntu-latest 46 | needs: generate-docs 47 | # Deploy to the github-pages environment 48 | environment: 49 | name: github-pages 50 | url: ${{ steps.deployment.outputs.page_url }} 51 | steps: 52 | - name: Deploy to GitHub Pages 53 | id: deployment 54 | uses: actions/deploy-pages@v4 55 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Test Hello World Example 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.ref }} 12 | cancel-in-progress: true 13 | 14 | env: 15 | CARGO_TERM_COLOR: always 16 | 17 | jobs: 18 | build: 19 | strategy: 20 | fail-fast: false 21 | matrix: 22 | platform: 23 | - ubuntu-latest 24 | - macos-latest 25 | - windows-latest 26 | runs-on: ${{ matrix.platform }} 27 | steps: 28 | - name: Checkout the repository 29 | uses: actions/checkout@v4 30 | 31 | - name: Install Linux dependencies 32 | if: matrix.platform == 'ubuntu-latest' 33 | run: | 34 | sudo apt-get update 35 | sudo apt-get install -y libxdo-dev libayatana-appindicator3-dev 36 | 37 | - uses: Swatinem/rust-cache@v2 38 | 39 | - name: Build 40 | run: cargo build --package helloworld 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .vscode 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tauri-runtime-verso" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [workspace] 7 | members = [ 8 | "tauri-runtime-verso-build", 9 | # Examples 10 | "examples/helloworld", 11 | "examples/api/src-tauri", 12 | ] 13 | 14 | [workspace.dependencies] 15 | verso = { git = "https://github.com/tauri-apps/verso", rev = "850eef2b4a3f5f90e11ea6f375cf8ef8ea5f4feb" } 16 | versoview_build = { git = "https://github.com/tauri-apps/verso", rev = "850eef2b4a3f5f90e11ea6f375cf8ef8ea5f4feb" } 17 | 18 | [features] 19 | # Required if you use tauri's macos-private-api feature 20 | macos-private-api = ["tauri-runtime/macos-private-api"] 21 | 22 | [dependencies] 23 | verso = { workspace = true } 24 | tauri = { version = "=2.5.1", default-features = false } 25 | tauri-runtime = "=2.6.0" 26 | tauri-utils = "=2.4.0" 27 | tao = "0.33" 28 | raw-window-handle = "0.6" 29 | url = "2" 30 | http = "1" 31 | percent-encoding = "2" 32 | log = "0.4" 33 | 34 | [target."cfg(windows)".dependencies] 35 | windows = "0.61" 36 | 37 | [target."cfg(any(target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dependencies] 38 | gtk = { version = "0.18", features = ["v3_24"] } 39 | 40 | [patch."https://github.com/tauri-apps/verso"] 41 | # verso = { path = "../verso/verso" } 42 | # versoview_build = { path = "../verso/versoview_build" } 43 | 44 | [lints.clippy] 45 | needless_doctest_main = "allow" 46 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Versotile 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tauri Runtime Verso 2 | 3 | A tauri runtime to replace the backend with [Verso](https://github.com/tauri-apps/verso) 4 | 5 | ## What is Verso 6 | 7 | [Verso](https://github.com/tauri-apps/verso) is a browser built on top of the [Servo](https://servo.org/) browser engine, and in our case, we use it as a webview. The reason for this is because the Servo embedding APIs are quite low level and requires the user (embedder) to send everything about the system to drive it, and with Verso, we provide Servo with these things under the hood, then expose some higher level APIs so that you can just let it run and only control it when you need something, this is similar to the [windowed vs visual hosting mode](https://learn.microsoft.com/en-us/microsoft-edge/webview2/concepts/windowed-vs-visual-hosting) of [WebView2](https://developer.microsoft.com/en-us/microsoft-edge/webview2) except for when using Servo directly, you'll need to pass in more than just user inputs 8 | 9 | ## Usage 10 | 11 | To get started, you need to add this crate to your project, and use `default-feature = false` on `tauri` to disable the `wry` feature 12 | 13 | ```diff 14 | [build-dependencies] 15 | tauri-build = "2" 16 | + tauri-runtime-verso-build = { git = "https://github.com/versotile-org/tauri-runtime-verso.git" } 17 | 18 | [dependencies] 19 | - tauri = { version = "2", features = [] } 20 | + tauri = { version = "2", default-features = false, features = ["common-controls-v6"] } 21 | + tauri-runtime-verso = { git = "https://github.com/versotile-org/tauri-runtime-verso.git" } 22 | ``` 23 | 24 | In your build script, add the `tauri-runtime-verso-build` script, which will download the pre-built `versoview` to `versoview/versoview-{target-triple}` 25 | 26 | > Note we currently only have pre-built `versoview` for x64 Linux, Windows, MacOS and arm64 MacOS, also the download might take a bit of time if you have a slow internet connection 27 | 28 | ```diff 29 | fn main() { 30 | + tauri_runtime_verso_build::get_verso_as_external_bin().unwrap(); 31 | tauri_build::build(); 32 | } 33 | ``` 34 | 35 | Then add the downloaded executable to your tauri config file (`tauri.conf.json`) as an external binary file 36 | 37 | ```diff 38 | { 39 | + "bundle": { 40 | + "externalBin": [ 41 | + "versoview/versoview" 42 | + ] 43 | + } 44 | } 45 | ``` 46 | 47 | Finally, setup the code like this: 48 | 49 | ```diff 50 | fn main() { 51 | - tauri::Builder::new() 52 | + tauri_runtime_verso::builder() 53 | .run(tauri::generate_context!()) 54 | .unwrap(); 55 | } 56 | ``` 57 | 58 | For more, take a look at the [hello world example](examples/helloworld), or a more sophisticated [api example](examples/api) show casing how you can use [`React`](https://react.dev/), tauri plugins, and tray icons with it 59 | 60 | Also, you can checkout the [documentation](https://versotile-org.github.io/tauri-runtime-verso/tauri_runtime_verso) 61 | 62 | ### Common Problems 63 | 64 | #### No such file or directory on Linux 65 | 66 | This error means either the path you set through `set_verso_path` is wrong (this should not be a problem if you're using the `externalBin` setup from the [Usage](#usage)) or the `versoview` exectuable requires a more recent version of glibc that your system doesn't have, in this case, you'll need to either update your linux distro or build `versoview` yourself 67 | 68 | ## Tips 69 | 70 | ### Devtools 71 | 72 | Since Verso doesn't have a devtools built-in, you'll need to use the one from the Firefox, first put in this in your code 73 | 74 | ```rust 75 | // This will make the webviews created afters this open up a devtools server on this port, 76 | // setting it to 0 for a random port 77 | tauri_runtime_verso::set_verso_devtools_port(1234); 78 | ``` 79 | 80 | Then go to `about:debugging` in Firefox and connect to `localhost:1234` there 81 | 82 | ## Known limitations 83 | 84 | ### Security 85 | 86 | We currently hard coded the `Origin` header for the custom protocol IPC to work, but this means Tauri won't be able to check for if the URL is a remote URL or a local one for the capabilities, so right now, please don't use this to load arbitrary websites if you have related settings 87 | 88 | ### Menus 89 | 90 | Currently, only the app wide menus on macOS are supported, per window menus are not supported yet 91 | 92 | For more, checkout the [documentation](https://versotile-org.github.io/tauri-runtime-verso/tauri_runtime_verso) 93 | -------------------------------------------------------------------------------- /examples/.icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/versotile-org/tauri-runtime-verso/ca597dd525ca96851a0e82e4cbe12777ce44b791/examples/.icons/icon.ico -------------------------------------------------------------------------------- /examples/.icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/versotile-org/tauri-runtime-verso/ca597dd525ca96851a0e82e4cbe12777ce44b791/examples/.icons/icon.png -------------------------------------------------------------------------------- /examples/api/.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | src-tauri/gen/ 4 | src-tauri/versoview/ 5 | -------------------------------------------------------------------------------- /examples/api/.prettierrc.yaml: -------------------------------------------------------------------------------- 1 | semi: false 2 | singleQuote: true 3 | htmlWhitespaceSensitivity: ignore 4 | useTabs: true 5 | printWidth: 100 6 | experimentalOperatorPosition: start 7 | -------------------------------------------------------------------------------- /examples/api/README.md: -------------------------------------------------------------------------------- 1 | To get started with this example 2 | 3 | 1. `cd` to this directory 4 | 2. Run `pnpm install` to install dependencies 5 | 3. Run `pnpm web:dev` to start the dev vite server 6 | 4. Run `pnpm dev` to run this Tauri app (You'll need the [prerequisites](https://tauri.app/start/prerequisites/) of Tauri) 7 | -------------------------------------------------------------------------------- /examples/api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "tauri": "tauri", 4 | "dev": "tauri dev", 5 | "build": "tauri build", 6 | "web:dev": "vite", 7 | "web:build": "tsc && vite build" 8 | }, 9 | "devDependencies": { 10 | "@tauri-apps/cli": "^2.5.0", 11 | "@types/react": "^19.1.6", 12 | "@types/react-dom": "^19.1.5", 13 | "@vitejs/plugin-react": "^4.5.0", 14 | "prettier": "^3.5.3", 15 | "typescript": "~5.8.3", 16 | "vite": "^6.3.5" 17 | }, 18 | "dependencies": { 19 | "@tauri-apps/api": "^2.5.0", 20 | "@tauri-apps/plugin-log": "^2.4.0", 21 | "@tauri-apps/plugin-opener": "^2.2.7", 22 | "react": "^19.1.0", 23 | "react-dom": "^19.1.0" 24 | }, 25 | "engines": { 26 | "node": "22.x" 27 | }, 28 | "type": "module" 29 | } 30 | -------------------------------------------------------------------------------- /examples/api/pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '9.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | importers: 8 | 9 | .: 10 | dependencies: 11 | '@tauri-apps/api': 12 | specifier: ^2.5.0 13 | version: 2.5.0 14 | '@tauri-apps/plugin-log': 15 | specifier: ^2.4.0 16 | version: 2.4.0 17 | '@tauri-apps/plugin-opener': 18 | specifier: ^2.2.7 19 | version: 2.2.7 20 | react: 21 | specifier: ^19.1.0 22 | version: 19.1.0 23 | react-dom: 24 | specifier: ^19.1.0 25 | version: 19.1.0(react@19.1.0) 26 | devDependencies: 27 | '@tauri-apps/cli': 28 | specifier: ^2.5.0 29 | version: 2.5.0 30 | '@types/react': 31 | specifier: ^19.1.6 32 | version: 19.1.6 33 | '@types/react-dom': 34 | specifier: ^19.1.5 35 | version: 19.1.5(@types/react@19.1.6) 36 | '@vitejs/plugin-react': 37 | specifier: ^4.5.0 38 | version: 4.5.0(vite@6.3.5) 39 | prettier: 40 | specifier: ^3.5.3 41 | version: 3.5.3 42 | typescript: 43 | specifier: ~5.8.3 44 | version: 5.8.3 45 | vite: 46 | specifier: ^6.3.5 47 | version: 6.3.5 48 | 49 | packages: 50 | 51 | '@ampproject/remapping@2.3.0': 52 | resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} 53 | engines: {node: '>=6.0.0'} 54 | 55 | '@babel/code-frame@7.27.1': 56 | resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} 57 | engines: {node: '>=6.9.0'} 58 | 59 | '@babel/compat-data@7.27.3': 60 | resolution: {integrity: sha512-V42wFfx1ymFte+ecf6iXghnnP8kWTO+ZLXIyZq+1LAXHHvTZdVxicn4yiVYdYMGaCO3tmqub11AorKkv+iodqw==} 61 | engines: {node: '>=6.9.0'} 62 | 63 | '@babel/core@7.27.3': 64 | resolution: {integrity: sha512-hyrN8ivxfvJ4i0fIJuV4EOlV0WDMz5Ui4StRTgVaAvWeiRCilXgwVvxJKtFQ3TKtHgJscB2YiXKGNJuVwhQMtA==} 65 | engines: {node: '>=6.9.0'} 66 | 67 | '@babel/generator@7.27.3': 68 | resolution: {integrity: sha512-xnlJYj5zepml8NXtjkG0WquFUv8RskFqyFcVgTBp5k+NaA/8uw/K+OSVf8AMGw5e9HKP2ETd5xpK5MLZQD6b4Q==} 69 | engines: {node: '>=6.9.0'} 70 | 71 | '@babel/helper-compilation-targets@7.27.2': 72 | resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} 73 | engines: {node: '>=6.9.0'} 74 | 75 | '@babel/helper-module-imports@7.27.1': 76 | resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} 77 | engines: {node: '>=6.9.0'} 78 | 79 | '@babel/helper-module-transforms@7.27.3': 80 | resolution: {integrity: sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==} 81 | engines: {node: '>=6.9.0'} 82 | peerDependencies: 83 | '@babel/core': ^7.0.0 84 | 85 | '@babel/helper-plugin-utils@7.27.1': 86 | resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==} 87 | engines: {node: '>=6.9.0'} 88 | 89 | '@babel/helper-string-parser@7.27.1': 90 | resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} 91 | engines: {node: '>=6.9.0'} 92 | 93 | '@babel/helper-validator-identifier@7.27.1': 94 | resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} 95 | engines: {node: '>=6.9.0'} 96 | 97 | '@babel/helper-validator-option@7.27.1': 98 | resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} 99 | engines: {node: '>=6.9.0'} 100 | 101 | '@babel/helpers@7.27.3': 102 | resolution: {integrity: sha512-h/eKy9agOya1IGuLaZ9tEUgz+uIRXcbtOhRtUyyMf8JFmn1iT13vnl/IGVWSkdOCG/pC57U4S1jnAabAavTMwg==} 103 | engines: {node: '>=6.9.0'} 104 | 105 | '@babel/parser@7.27.3': 106 | resolution: {integrity: sha512-xyYxRj6+tLNDTWi0KCBcZ9V7yg3/lwL9DWh9Uwh/RIVlIfFidggcgxKX3GCXwCiswwcGRawBKbEg2LG/Y8eJhw==} 107 | engines: {node: '>=6.0.0'} 108 | hasBin: true 109 | 110 | '@babel/plugin-transform-react-jsx-self@7.27.1': 111 | resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} 112 | engines: {node: '>=6.9.0'} 113 | peerDependencies: 114 | '@babel/core': ^7.0.0-0 115 | 116 | '@babel/plugin-transform-react-jsx-source@7.27.1': 117 | resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==} 118 | engines: {node: '>=6.9.0'} 119 | peerDependencies: 120 | '@babel/core': ^7.0.0-0 121 | 122 | '@babel/template@7.27.2': 123 | resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} 124 | engines: {node: '>=6.9.0'} 125 | 126 | '@babel/traverse@7.27.3': 127 | resolution: {integrity: sha512-lId/IfN/Ye1CIu8xG7oKBHXd2iNb2aW1ilPszzGcJug6M8RCKfVNcYhpI5+bMvFYjK7lXIM0R+a+6r8xhHp2FQ==} 128 | engines: {node: '>=6.9.0'} 129 | 130 | '@babel/types@7.27.3': 131 | resolution: {integrity: sha512-Y1GkI4ktrtvmawoSq+4FCVHNryea6uR+qUQy0AGxLSsjCX0nVmkYQMBLHDkXZuo5hGx7eYdnIaslsdBFm7zbUw==} 132 | engines: {node: '>=6.9.0'} 133 | 134 | '@esbuild/aix-ppc64@0.25.5': 135 | resolution: {integrity: sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==} 136 | engines: {node: '>=18'} 137 | cpu: [ppc64] 138 | os: [aix] 139 | 140 | '@esbuild/android-arm64@0.25.5': 141 | resolution: {integrity: sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==} 142 | engines: {node: '>=18'} 143 | cpu: [arm64] 144 | os: [android] 145 | 146 | '@esbuild/android-arm@0.25.5': 147 | resolution: {integrity: sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==} 148 | engines: {node: '>=18'} 149 | cpu: [arm] 150 | os: [android] 151 | 152 | '@esbuild/android-x64@0.25.5': 153 | resolution: {integrity: sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==} 154 | engines: {node: '>=18'} 155 | cpu: [x64] 156 | os: [android] 157 | 158 | '@esbuild/darwin-arm64@0.25.5': 159 | resolution: {integrity: sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==} 160 | engines: {node: '>=18'} 161 | cpu: [arm64] 162 | os: [darwin] 163 | 164 | '@esbuild/darwin-x64@0.25.5': 165 | resolution: {integrity: sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==} 166 | engines: {node: '>=18'} 167 | cpu: [x64] 168 | os: [darwin] 169 | 170 | '@esbuild/freebsd-arm64@0.25.5': 171 | resolution: {integrity: sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==} 172 | engines: {node: '>=18'} 173 | cpu: [arm64] 174 | os: [freebsd] 175 | 176 | '@esbuild/freebsd-x64@0.25.5': 177 | resolution: {integrity: sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==} 178 | engines: {node: '>=18'} 179 | cpu: [x64] 180 | os: [freebsd] 181 | 182 | '@esbuild/linux-arm64@0.25.5': 183 | resolution: {integrity: sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==} 184 | engines: {node: '>=18'} 185 | cpu: [arm64] 186 | os: [linux] 187 | 188 | '@esbuild/linux-arm@0.25.5': 189 | resolution: {integrity: sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==} 190 | engines: {node: '>=18'} 191 | cpu: [arm] 192 | os: [linux] 193 | 194 | '@esbuild/linux-ia32@0.25.5': 195 | resolution: {integrity: sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==} 196 | engines: {node: '>=18'} 197 | cpu: [ia32] 198 | os: [linux] 199 | 200 | '@esbuild/linux-loong64@0.25.5': 201 | resolution: {integrity: sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==} 202 | engines: {node: '>=18'} 203 | cpu: [loong64] 204 | os: [linux] 205 | 206 | '@esbuild/linux-mips64el@0.25.5': 207 | resolution: {integrity: sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==} 208 | engines: {node: '>=18'} 209 | cpu: [mips64el] 210 | os: [linux] 211 | 212 | '@esbuild/linux-ppc64@0.25.5': 213 | resolution: {integrity: sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==} 214 | engines: {node: '>=18'} 215 | cpu: [ppc64] 216 | os: [linux] 217 | 218 | '@esbuild/linux-riscv64@0.25.5': 219 | resolution: {integrity: sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==} 220 | engines: {node: '>=18'} 221 | cpu: [riscv64] 222 | os: [linux] 223 | 224 | '@esbuild/linux-s390x@0.25.5': 225 | resolution: {integrity: sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==} 226 | engines: {node: '>=18'} 227 | cpu: [s390x] 228 | os: [linux] 229 | 230 | '@esbuild/linux-x64@0.25.5': 231 | resolution: {integrity: sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==} 232 | engines: {node: '>=18'} 233 | cpu: [x64] 234 | os: [linux] 235 | 236 | '@esbuild/netbsd-arm64@0.25.5': 237 | resolution: {integrity: sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==} 238 | engines: {node: '>=18'} 239 | cpu: [arm64] 240 | os: [netbsd] 241 | 242 | '@esbuild/netbsd-x64@0.25.5': 243 | resolution: {integrity: sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==} 244 | engines: {node: '>=18'} 245 | cpu: [x64] 246 | os: [netbsd] 247 | 248 | '@esbuild/openbsd-arm64@0.25.5': 249 | resolution: {integrity: sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==} 250 | engines: {node: '>=18'} 251 | cpu: [arm64] 252 | os: [openbsd] 253 | 254 | '@esbuild/openbsd-x64@0.25.5': 255 | resolution: {integrity: sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==} 256 | engines: {node: '>=18'} 257 | cpu: [x64] 258 | os: [openbsd] 259 | 260 | '@esbuild/sunos-x64@0.25.5': 261 | resolution: {integrity: sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==} 262 | engines: {node: '>=18'} 263 | cpu: [x64] 264 | os: [sunos] 265 | 266 | '@esbuild/win32-arm64@0.25.5': 267 | resolution: {integrity: sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==} 268 | engines: {node: '>=18'} 269 | cpu: [arm64] 270 | os: [win32] 271 | 272 | '@esbuild/win32-ia32@0.25.5': 273 | resolution: {integrity: sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==} 274 | engines: {node: '>=18'} 275 | cpu: [ia32] 276 | os: [win32] 277 | 278 | '@esbuild/win32-x64@0.25.5': 279 | resolution: {integrity: sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==} 280 | engines: {node: '>=18'} 281 | cpu: [x64] 282 | os: [win32] 283 | 284 | '@jridgewell/gen-mapping@0.3.8': 285 | resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} 286 | engines: {node: '>=6.0.0'} 287 | 288 | '@jridgewell/resolve-uri@3.1.2': 289 | resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} 290 | engines: {node: '>=6.0.0'} 291 | 292 | '@jridgewell/set-array@1.2.1': 293 | resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} 294 | engines: {node: '>=6.0.0'} 295 | 296 | '@jridgewell/sourcemap-codec@1.5.0': 297 | resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} 298 | 299 | '@jridgewell/trace-mapping@0.3.25': 300 | resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} 301 | 302 | '@rolldown/pluginutils@1.0.0-beta.9': 303 | resolution: {integrity: sha512-e9MeMtVWo186sgvFFJOPGy7/d2j2mZhLJIdVW0C/xDluuOvymEATqz6zKsP0ZmXGzQtqlyjz5sC1sYQUoJG98w==} 304 | 305 | '@rollup/rollup-android-arm-eabi@4.41.1': 306 | resolution: {integrity: sha512-NELNvyEWZ6R9QMkiytB4/L4zSEaBC03KIXEghptLGLZWJ6VPrL63ooZQCOnlx36aQPGhzuOMwDerC1Eb2VmrLw==} 307 | cpu: [arm] 308 | os: [android] 309 | 310 | '@rollup/rollup-android-arm64@4.41.1': 311 | resolution: {integrity: sha512-DXdQe1BJ6TK47ukAoZLehRHhfKnKg9BjnQYUu9gzhI8Mwa1d2fzxA1aw2JixHVl403bwp1+/o/NhhHtxWJBgEA==} 312 | cpu: [arm64] 313 | os: [android] 314 | 315 | '@rollup/rollup-darwin-arm64@4.41.1': 316 | resolution: {integrity: sha512-5afxvwszzdulsU2w8JKWwY8/sJOLPzf0e1bFuvcW5h9zsEg+RQAojdW0ux2zyYAz7R8HvvzKCjLNJhVq965U7w==} 317 | cpu: [arm64] 318 | os: [darwin] 319 | 320 | '@rollup/rollup-darwin-x64@4.41.1': 321 | resolution: {integrity: sha512-egpJACny8QOdHNNMZKf8xY0Is6gIMz+tuqXlusxquWu3F833DcMwmGM7WlvCO9sB3OsPjdC4U0wHw5FabzCGZg==} 322 | cpu: [x64] 323 | os: [darwin] 324 | 325 | '@rollup/rollup-freebsd-arm64@4.41.1': 326 | resolution: {integrity: sha512-DBVMZH5vbjgRk3r0OzgjS38z+atlupJ7xfKIDJdZZL6sM6wjfDNo64aowcLPKIx7LMQi8vybB56uh1Ftck/Atg==} 327 | cpu: [arm64] 328 | os: [freebsd] 329 | 330 | '@rollup/rollup-freebsd-x64@4.41.1': 331 | resolution: {integrity: sha512-3FkydeohozEskBxNWEIbPfOE0aqQgB6ttTkJ159uWOFn42VLyfAiyD9UK5mhu+ItWzft60DycIN1Xdgiy8o/SA==} 332 | cpu: [x64] 333 | os: [freebsd] 334 | 335 | '@rollup/rollup-linux-arm-gnueabihf@4.41.1': 336 | resolution: {integrity: sha512-wC53ZNDgt0pqx5xCAgNunkTzFE8GTgdZ9EwYGVcg+jEjJdZGtq9xPjDnFgfFozQI/Xm1mh+D9YlYtl+ueswNEg==} 337 | cpu: [arm] 338 | os: [linux] 339 | 340 | '@rollup/rollup-linux-arm-musleabihf@4.41.1': 341 | resolution: {integrity: sha512-jwKCca1gbZkZLhLRtsrka5N8sFAaxrGz/7wRJ8Wwvq3jug7toO21vWlViihG85ei7uJTpzbXZRcORotE+xyrLA==} 342 | cpu: [arm] 343 | os: [linux] 344 | 345 | '@rollup/rollup-linux-arm64-gnu@4.41.1': 346 | resolution: {integrity: sha512-g0UBcNknsmmNQ8V2d/zD2P7WWfJKU0F1nu0k5pW4rvdb+BIqMm8ToluW/eeRmxCared5dD76lS04uL4UaNgpNA==} 347 | cpu: [arm64] 348 | os: [linux] 349 | 350 | '@rollup/rollup-linux-arm64-musl@4.41.1': 351 | resolution: {integrity: sha512-XZpeGB5TKEZWzIrj7sXr+BEaSgo/ma/kCgrZgL0oo5qdB1JlTzIYQKel/RmhT6vMAvOdM2teYlAaOGJpJ9lahg==} 352 | cpu: [arm64] 353 | os: [linux] 354 | 355 | '@rollup/rollup-linux-loongarch64-gnu@4.41.1': 356 | resolution: {integrity: sha512-bkCfDJ4qzWfFRCNt5RVV4DOw6KEgFTUZi2r2RuYhGWC8WhCA8lCAJhDeAmrM/fdiAH54m0mA0Vk2FGRPyzI+tw==} 357 | cpu: [loong64] 358 | os: [linux] 359 | 360 | '@rollup/rollup-linux-powerpc64le-gnu@4.41.1': 361 | resolution: {integrity: sha512-3mr3Xm+gvMX+/8EKogIZSIEF0WUu0HL9di+YWlJpO8CQBnoLAEL/roTCxuLncEdgcfJcvA4UMOf+2dnjl4Ut1A==} 362 | cpu: [ppc64] 363 | os: [linux] 364 | 365 | '@rollup/rollup-linux-riscv64-gnu@4.41.1': 366 | resolution: {integrity: sha512-3rwCIh6MQ1LGrvKJitQjZFuQnT2wxfU+ivhNBzmxXTXPllewOF7JR1s2vMX/tWtUYFgphygxjqMl76q4aMotGw==} 367 | cpu: [riscv64] 368 | os: [linux] 369 | 370 | '@rollup/rollup-linux-riscv64-musl@4.41.1': 371 | resolution: {integrity: sha512-LdIUOb3gvfmpkgFZuccNa2uYiqtgZAz3PTzjuM5bH3nvuy9ty6RGc/Q0+HDFrHrizJGVpjnTZ1yS5TNNjFlklw==} 372 | cpu: [riscv64] 373 | os: [linux] 374 | 375 | '@rollup/rollup-linux-s390x-gnu@4.41.1': 376 | resolution: {integrity: sha512-oIE6M8WC9ma6xYqjvPhzZYk6NbobIURvP/lEbh7FWplcMO6gn7MM2yHKA1eC/GvYwzNKK/1LYgqzdkZ8YFxR8g==} 377 | cpu: [s390x] 378 | os: [linux] 379 | 380 | '@rollup/rollup-linux-x64-gnu@4.41.1': 381 | resolution: {integrity: sha512-cWBOvayNvA+SyeQMp79BHPK8ws6sHSsYnK5zDcsC3Hsxr1dgTABKjMnMslPq1DvZIp6uO7kIWhiGwaTdR4Og9A==} 382 | cpu: [x64] 383 | os: [linux] 384 | 385 | '@rollup/rollup-linux-x64-musl@4.41.1': 386 | resolution: {integrity: sha512-y5CbN44M+pUCdGDlZFzGGBSKCA4A/J2ZH4edTYSSxFg7ce1Xt3GtydbVKWLlzL+INfFIZAEg1ZV6hh9+QQf9YQ==} 387 | cpu: [x64] 388 | os: [linux] 389 | 390 | '@rollup/rollup-win32-arm64-msvc@4.41.1': 391 | resolution: {integrity: sha512-lZkCxIrjlJlMt1dLO/FbpZbzt6J/A8p4DnqzSa4PWqPEUUUnzXLeki/iyPLfV0BmHItlYgHUqJe+3KiyydmiNQ==} 392 | cpu: [arm64] 393 | os: [win32] 394 | 395 | '@rollup/rollup-win32-ia32-msvc@4.41.1': 396 | resolution: {integrity: sha512-+psFT9+pIh2iuGsxFYYa/LhS5MFKmuivRsx9iPJWNSGbh2XVEjk90fmpUEjCnILPEPJnikAU6SFDiEUyOv90Pg==} 397 | cpu: [ia32] 398 | os: [win32] 399 | 400 | '@rollup/rollup-win32-x64-msvc@4.41.1': 401 | resolution: {integrity: sha512-Wq2zpapRYLfi4aKxf2Xff0tN+7slj2d4R87WEzqw7ZLsVvO5zwYCIuEGSZYiK41+GlwUo1HiR+GdkLEJnCKTCw==} 402 | cpu: [x64] 403 | os: [win32] 404 | 405 | '@tauri-apps/api@2.5.0': 406 | resolution: {integrity: sha512-Ldux4ip+HGAcPUmuLT8EIkk6yafl5vK0P0c0byzAKzxJh7vxelVtdPONjfgTm96PbN24yjZNESY8CKo8qniluA==} 407 | 408 | '@tauri-apps/cli-darwin-arm64@2.5.0': 409 | resolution: {integrity: sha512-VuVAeTFq86dfpoBDNYAdtQVLbP0+2EKCHIIhkaxjeoPARR0sLpFHz2zs0PcFU76e+KAaxtEtAJAXGNUc8E1PzQ==} 410 | engines: {node: '>= 10'} 411 | cpu: [arm64] 412 | os: [darwin] 413 | 414 | '@tauri-apps/cli-darwin-x64@2.5.0': 415 | resolution: {integrity: sha512-hUF01sC06cZVa8+I0/VtsHOk9BbO75rd+YdtHJ48xTdcYaQ5QIwL4yZz9OR1AKBTaUYhBam8UX9Pvd5V2/4Dpw==} 416 | engines: {node: '>= 10'} 417 | cpu: [x64] 418 | os: [darwin] 419 | 420 | '@tauri-apps/cli-linux-arm-gnueabihf@2.5.0': 421 | resolution: {integrity: sha512-LQKqttsK252LlqYyX8R02MinUsfFcy3+NZiJwHFgi5Y3+ZUIAED9cSxJkyNtuY5KMnR4RlpgWyLv4P6akN1xhg==} 422 | engines: {node: '>= 10'} 423 | cpu: [arm] 424 | os: [linux] 425 | 426 | '@tauri-apps/cli-linux-arm64-gnu@2.5.0': 427 | resolution: {integrity: sha512-mTQufsPcpdHg5RW0zypazMo4L55EfeE5snTzrPqbLX4yCK2qalN7+rnP8O8GT06xhp6ElSP/Ku1M2MR297SByQ==} 428 | engines: {node: '>= 10'} 429 | cpu: [arm64] 430 | os: [linux] 431 | 432 | '@tauri-apps/cli-linux-arm64-musl@2.5.0': 433 | resolution: {integrity: sha512-rQO1HhRUQqyEaal5dUVOQruTRda/TD36s9kv1hTxZiFuSq3558lsTjAcUEnMAtBcBkps20sbyTJNMT0AwYIk8Q==} 434 | engines: {node: '>= 10'} 435 | cpu: [arm64] 436 | os: [linux] 437 | 438 | '@tauri-apps/cli-linux-riscv64-gnu@2.5.0': 439 | resolution: {integrity: sha512-7oS18FN46yDxyw1zX/AxhLAd7T3GrLj3Ai6s8hZKd9qFVzrAn36ESL7d3G05s8wEtsJf26qjXnVF4qleS3dYsA==} 440 | engines: {node: '>= 10'} 441 | cpu: [riscv64] 442 | os: [linux] 443 | 444 | '@tauri-apps/cli-linux-x64-gnu@2.5.0': 445 | resolution: {integrity: sha512-SG5sFNL7VMmDBdIg3nO3EzNRT306HsiEQ0N90ILe3ZABYAVoPDO/ttpCO37ApLInTzrq/DLN+gOlC/mgZvLw1w==} 446 | engines: {node: '>= 10'} 447 | cpu: [x64] 448 | os: [linux] 449 | 450 | '@tauri-apps/cli-linux-x64-musl@2.5.0': 451 | resolution: {integrity: sha512-QXDM8zp/6v05PNWju5ELsVwF0VH1n6b5pk2E6W/jFbbiwz80Vs1lACl9pv5kEHkrxBj+aWU/03JzGuIj2g3SkQ==} 452 | engines: {node: '>= 10'} 453 | cpu: [x64] 454 | os: [linux] 455 | 456 | '@tauri-apps/cli-win32-arm64-msvc@2.5.0': 457 | resolution: {integrity: sha512-pFSHFK6b+o9y4Un8w0gGLwVyFTZaC3P0kQ7umRt/BLDkzD5RnQ4vBM7CF8BCU5nkwmEBUCZd7Wt3TWZxe41o6Q==} 458 | engines: {node: '>= 10'} 459 | cpu: [arm64] 460 | os: [win32] 461 | 462 | '@tauri-apps/cli-win32-ia32-msvc@2.5.0': 463 | resolution: {integrity: sha512-EArv1IaRlogdLAQyGlKmEqZqm5RfHCUMhJoedWu7GtdbOMUfSAz6FMX2boE1PtEmNO4An+g188flLeVErrxEKg==} 464 | engines: {node: '>= 10'} 465 | cpu: [ia32] 466 | os: [win32] 467 | 468 | '@tauri-apps/cli-win32-x64-msvc@2.5.0': 469 | resolution: {integrity: sha512-lj43EFYbnAta8pd9JnUq87o+xRUR0odz+4rixBtTUwUgdRdwQ2V9CzFtsMu6FQKpFQ6mujRK6P1IEwhL6ADRsQ==} 470 | engines: {node: '>= 10'} 471 | cpu: [x64] 472 | os: [win32] 473 | 474 | '@tauri-apps/cli@2.5.0': 475 | resolution: {integrity: sha512-rAtHqG0Gh/IWLjN2zTf3nZqYqbo81oMbqop56rGTjrlWk9pTTAjkqOjSL9XQLIMZ3RbeVjveCqqCA0s8RnLdMg==} 476 | engines: {node: '>= 10'} 477 | hasBin: true 478 | 479 | '@tauri-apps/plugin-log@2.4.0': 480 | resolution: {integrity: sha512-j7yrDtLNmayCBOO2esl3aZv9jSXy2an8MDLry3Ys9ZXerwUg35n1Y2uD8HoCR+8Ng/EUgx215+qOUfJasjYrHw==} 481 | 482 | '@tauri-apps/plugin-opener@2.2.7': 483 | resolution: {integrity: sha512-uduEyvOdjpPOEeDRrhwlCspG/f9EQalHumWBtLBnp3fRp++fKGLqDOyUhSIn7PzX45b/rKep//ZQSAQoIxobLA==} 484 | 485 | '@types/babel__core@7.20.5': 486 | resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} 487 | 488 | '@types/babel__generator@7.27.0': 489 | resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} 490 | 491 | '@types/babel__template@7.4.4': 492 | resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} 493 | 494 | '@types/babel__traverse@7.20.7': 495 | resolution: {integrity: sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==} 496 | 497 | '@types/estree@1.0.7': 498 | resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==} 499 | 500 | '@types/react-dom@19.1.5': 501 | resolution: {integrity: sha512-CMCjrWucUBZvohgZxkjd6S9h0nZxXjzus6yDfUb+xLxYM7VvjKNH1tQrE9GWLql1XoOP4/Ds3bwFqShHUYraGg==} 502 | peerDependencies: 503 | '@types/react': ^19.0.0 504 | 505 | '@types/react@19.1.6': 506 | resolution: {integrity: sha512-JeG0rEWak0N6Itr6QUx+X60uQmN+5t3j9r/OVDtWzFXKaj6kD1BwJzOksD0FF6iWxZlbE1kB0q9vtnU2ekqa1Q==} 507 | 508 | '@vitejs/plugin-react@4.5.0': 509 | resolution: {integrity: sha512-JuLWaEqypaJmOJPLWwO335Ig6jSgC1FTONCWAxnqcQthLTK/Yc9aH6hr9z/87xciejbQcnP3GnA1FWUSWeXaeg==} 510 | engines: {node: ^14.18.0 || >=16.0.0} 511 | peerDependencies: 512 | vite: ^4.2.0 || ^5.0.0 || ^6.0.0 513 | 514 | browserslist@4.24.5: 515 | resolution: {integrity: sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==} 516 | engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} 517 | hasBin: true 518 | 519 | caniuse-lite@1.0.30001718: 520 | resolution: {integrity: sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==} 521 | 522 | convert-source-map@2.0.0: 523 | resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} 524 | 525 | csstype@3.1.3: 526 | resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} 527 | 528 | debug@4.4.1: 529 | resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} 530 | engines: {node: '>=6.0'} 531 | peerDependencies: 532 | supports-color: '*' 533 | peerDependenciesMeta: 534 | supports-color: 535 | optional: true 536 | 537 | electron-to-chromium@1.5.159: 538 | resolution: {integrity: sha512-CEvHptWAMV5p6GJ0Lq8aheyvVbfzVrv5mmidu1D3pidoVNkB3tTBsTMVtPJ+rzRK5oV229mCLz9Zj/hNvU8GBA==} 539 | 540 | esbuild@0.25.5: 541 | resolution: {integrity: sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==} 542 | engines: {node: '>=18'} 543 | hasBin: true 544 | 545 | escalade@3.2.0: 546 | resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} 547 | engines: {node: '>=6'} 548 | 549 | fdir@6.4.4: 550 | resolution: {integrity: sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==} 551 | peerDependencies: 552 | picomatch: ^3 || ^4 553 | peerDependenciesMeta: 554 | picomatch: 555 | optional: true 556 | 557 | fsevents@2.3.3: 558 | resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} 559 | engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 560 | os: [darwin] 561 | 562 | gensync@1.0.0-beta.2: 563 | resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} 564 | engines: {node: '>=6.9.0'} 565 | 566 | globals@11.12.0: 567 | resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} 568 | engines: {node: '>=4'} 569 | 570 | js-tokens@4.0.0: 571 | resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} 572 | 573 | jsesc@3.1.0: 574 | resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} 575 | engines: {node: '>=6'} 576 | hasBin: true 577 | 578 | json5@2.2.3: 579 | resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} 580 | engines: {node: '>=6'} 581 | hasBin: true 582 | 583 | lru-cache@5.1.1: 584 | resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} 585 | 586 | ms@2.1.3: 587 | resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} 588 | 589 | nanoid@3.3.11: 590 | resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} 591 | engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} 592 | hasBin: true 593 | 594 | node-releases@2.0.19: 595 | resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} 596 | 597 | picocolors@1.1.1: 598 | resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} 599 | 600 | picomatch@4.0.2: 601 | resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} 602 | engines: {node: '>=12'} 603 | 604 | postcss@8.5.3: 605 | resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==} 606 | engines: {node: ^10 || ^12 || >=14} 607 | 608 | prettier@3.5.3: 609 | resolution: {integrity: sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==} 610 | engines: {node: '>=14'} 611 | hasBin: true 612 | 613 | react-dom@19.1.0: 614 | resolution: {integrity: sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==} 615 | peerDependencies: 616 | react: ^19.1.0 617 | 618 | react-refresh@0.17.0: 619 | resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} 620 | engines: {node: '>=0.10.0'} 621 | 622 | react@19.1.0: 623 | resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==} 624 | engines: {node: '>=0.10.0'} 625 | 626 | rollup@4.41.1: 627 | resolution: {integrity: sha512-cPmwD3FnFv8rKMBc1MxWCwVQFxwf1JEmSX3iQXrRVVG15zerAIXRjMFVWnd5Q5QvgKF7Aj+5ykXFhUl+QGnyOw==} 628 | engines: {node: '>=18.0.0', npm: '>=8.0.0'} 629 | hasBin: true 630 | 631 | scheduler@0.26.0: 632 | resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==} 633 | 634 | semver@6.3.1: 635 | resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} 636 | hasBin: true 637 | 638 | source-map-js@1.2.1: 639 | resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} 640 | engines: {node: '>=0.10.0'} 641 | 642 | tinyglobby@0.2.14: 643 | resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} 644 | engines: {node: '>=12.0.0'} 645 | 646 | typescript@5.8.3: 647 | resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} 648 | engines: {node: '>=14.17'} 649 | hasBin: true 650 | 651 | update-browserslist-db@1.1.3: 652 | resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} 653 | hasBin: true 654 | peerDependencies: 655 | browserslist: '>= 4.21.0' 656 | 657 | vite@6.3.5: 658 | resolution: {integrity: sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==} 659 | engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} 660 | hasBin: true 661 | peerDependencies: 662 | '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 663 | jiti: '>=1.21.0' 664 | less: '*' 665 | lightningcss: ^1.21.0 666 | sass: '*' 667 | sass-embedded: '*' 668 | stylus: '*' 669 | sugarss: '*' 670 | terser: ^5.16.0 671 | tsx: ^4.8.1 672 | yaml: ^2.4.2 673 | peerDependenciesMeta: 674 | '@types/node': 675 | optional: true 676 | jiti: 677 | optional: true 678 | less: 679 | optional: true 680 | lightningcss: 681 | optional: true 682 | sass: 683 | optional: true 684 | sass-embedded: 685 | optional: true 686 | stylus: 687 | optional: true 688 | sugarss: 689 | optional: true 690 | terser: 691 | optional: true 692 | tsx: 693 | optional: true 694 | yaml: 695 | optional: true 696 | 697 | yallist@3.1.1: 698 | resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} 699 | 700 | snapshots: 701 | 702 | '@ampproject/remapping@2.3.0': 703 | dependencies: 704 | '@jridgewell/gen-mapping': 0.3.8 705 | '@jridgewell/trace-mapping': 0.3.25 706 | 707 | '@babel/code-frame@7.27.1': 708 | dependencies: 709 | '@babel/helper-validator-identifier': 7.27.1 710 | js-tokens: 4.0.0 711 | picocolors: 1.1.1 712 | 713 | '@babel/compat-data@7.27.3': {} 714 | 715 | '@babel/core@7.27.3': 716 | dependencies: 717 | '@ampproject/remapping': 2.3.0 718 | '@babel/code-frame': 7.27.1 719 | '@babel/generator': 7.27.3 720 | '@babel/helper-compilation-targets': 7.27.2 721 | '@babel/helper-module-transforms': 7.27.3(@babel/core@7.27.3) 722 | '@babel/helpers': 7.27.3 723 | '@babel/parser': 7.27.3 724 | '@babel/template': 7.27.2 725 | '@babel/traverse': 7.27.3 726 | '@babel/types': 7.27.3 727 | convert-source-map: 2.0.0 728 | debug: 4.4.1 729 | gensync: 1.0.0-beta.2 730 | json5: 2.2.3 731 | semver: 6.3.1 732 | transitivePeerDependencies: 733 | - supports-color 734 | 735 | '@babel/generator@7.27.3': 736 | dependencies: 737 | '@babel/parser': 7.27.3 738 | '@babel/types': 7.27.3 739 | '@jridgewell/gen-mapping': 0.3.8 740 | '@jridgewell/trace-mapping': 0.3.25 741 | jsesc: 3.1.0 742 | 743 | '@babel/helper-compilation-targets@7.27.2': 744 | dependencies: 745 | '@babel/compat-data': 7.27.3 746 | '@babel/helper-validator-option': 7.27.1 747 | browserslist: 4.24.5 748 | lru-cache: 5.1.1 749 | semver: 6.3.1 750 | 751 | '@babel/helper-module-imports@7.27.1': 752 | dependencies: 753 | '@babel/traverse': 7.27.3 754 | '@babel/types': 7.27.3 755 | transitivePeerDependencies: 756 | - supports-color 757 | 758 | '@babel/helper-module-transforms@7.27.3(@babel/core@7.27.3)': 759 | dependencies: 760 | '@babel/core': 7.27.3 761 | '@babel/helper-module-imports': 7.27.1 762 | '@babel/helper-validator-identifier': 7.27.1 763 | '@babel/traverse': 7.27.3 764 | transitivePeerDependencies: 765 | - supports-color 766 | 767 | '@babel/helper-plugin-utils@7.27.1': {} 768 | 769 | '@babel/helper-string-parser@7.27.1': {} 770 | 771 | '@babel/helper-validator-identifier@7.27.1': {} 772 | 773 | '@babel/helper-validator-option@7.27.1': {} 774 | 775 | '@babel/helpers@7.27.3': 776 | dependencies: 777 | '@babel/template': 7.27.2 778 | '@babel/types': 7.27.3 779 | 780 | '@babel/parser@7.27.3': 781 | dependencies: 782 | '@babel/types': 7.27.3 783 | 784 | '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.27.3)': 785 | dependencies: 786 | '@babel/core': 7.27.3 787 | '@babel/helper-plugin-utils': 7.27.1 788 | 789 | '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.27.3)': 790 | dependencies: 791 | '@babel/core': 7.27.3 792 | '@babel/helper-plugin-utils': 7.27.1 793 | 794 | '@babel/template@7.27.2': 795 | dependencies: 796 | '@babel/code-frame': 7.27.1 797 | '@babel/parser': 7.27.3 798 | '@babel/types': 7.27.3 799 | 800 | '@babel/traverse@7.27.3': 801 | dependencies: 802 | '@babel/code-frame': 7.27.1 803 | '@babel/generator': 7.27.3 804 | '@babel/parser': 7.27.3 805 | '@babel/template': 7.27.2 806 | '@babel/types': 7.27.3 807 | debug: 4.4.1 808 | globals: 11.12.0 809 | transitivePeerDependencies: 810 | - supports-color 811 | 812 | '@babel/types@7.27.3': 813 | dependencies: 814 | '@babel/helper-string-parser': 7.27.1 815 | '@babel/helper-validator-identifier': 7.27.1 816 | 817 | '@esbuild/aix-ppc64@0.25.5': 818 | optional: true 819 | 820 | '@esbuild/android-arm64@0.25.5': 821 | optional: true 822 | 823 | '@esbuild/android-arm@0.25.5': 824 | optional: true 825 | 826 | '@esbuild/android-x64@0.25.5': 827 | optional: true 828 | 829 | '@esbuild/darwin-arm64@0.25.5': 830 | optional: true 831 | 832 | '@esbuild/darwin-x64@0.25.5': 833 | optional: true 834 | 835 | '@esbuild/freebsd-arm64@0.25.5': 836 | optional: true 837 | 838 | '@esbuild/freebsd-x64@0.25.5': 839 | optional: true 840 | 841 | '@esbuild/linux-arm64@0.25.5': 842 | optional: true 843 | 844 | '@esbuild/linux-arm@0.25.5': 845 | optional: true 846 | 847 | '@esbuild/linux-ia32@0.25.5': 848 | optional: true 849 | 850 | '@esbuild/linux-loong64@0.25.5': 851 | optional: true 852 | 853 | '@esbuild/linux-mips64el@0.25.5': 854 | optional: true 855 | 856 | '@esbuild/linux-ppc64@0.25.5': 857 | optional: true 858 | 859 | '@esbuild/linux-riscv64@0.25.5': 860 | optional: true 861 | 862 | '@esbuild/linux-s390x@0.25.5': 863 | optional: true 864 | 865 | '@esbuild/linux-x64@0.25.5': 866 | optional: true 867 | 868 | '@esbuild/netbsd-arm64@0.25.5': 869 | optional: true 870 | 871 | '@esbuild/netbsd-x64@0.25.5': 872 | optional: true 873 | 874 | '@esbuild/openbsd-arm64@0.25.5': 875 | optional: true 876 | 877 | '@esbuild/openbsd-x64@0.25.5': 878 | optional: true 879 | 880 | '@esbuild/sunos-x64@0.25.5': 881 | optional: true 882 | 883 | '@esbuild/win32-arm64@0.25.5': 884 | optional: true 885 | 886 | '@esbuild/win32-ia32@0.25.5': 887 | optional: true 888 | 889 | '@esbuild/win32-x64@0.25.5': 890 | optional: true 891 | 892 | '@jridgewell/gen-mapping@0.3.8': 893 | dependencies: 894 | '@jridgewell/set-array': 1.2.1 895 | '@jridgewell/sourcemap-codec': 1.5.0 896 | '@jridgewell/trace-mapping': 0.3.25 897 | 898 | '@jridgewell/resolve-uri@3.1.2': {} 899 | 900 | '@jridgewell/set-array@1.2.1': {} 901 | 902 | '@jridgewell/sourcemap-codec@1.5.0': {} 903 | 904 | '@jridgewell/trace-mapping@0.3.25': 905 | dependencies: 906 | '@jridgewell/resolve-uri': 3.1.2 907 | '@jridgewell/sourcemap-codec': 1.5.0 908 | 909 | '@rolldown/pluginutils@1.0.0-beta.9': {} 910 | 911 | '@rollup/rollup-android-arm-eabi@4.41.1': 912 | optional: true 913 | 914 | '@rollup/rollup-android-arm64@4.41.1': 915 | optional: true 916 | 917 | '@rollup/rollup-darwin-arm64@4.41.1': 918 | optional: true 919 | 920 | '@rollup/rollup-darwin-x64@4.41.1': 921 | optional: true 922 | 923 | '@rollup/rollup-freebsd-arm64@4.41.1': 924 | optional: true 925 | 926 | '@rollup/rollup-freebsd-x64@4.41.1': 927 | optional: true 928 | 929 | '@rollup/rollup-linux-arm-gnueabihf@4.41.1': 930 | optional: true 931 | 932 | '@rollup/rollup-linux-arm-musleabihf@4.41.1': 933 | optional: true 934 | 935 | '@rollup/rollup-linux-arm64-gnu@4.41.1': 936 | optional: true 937 | 938 | '@rollup/rollup-linux-arm64-musl@4.41.1': 939 | optional: true 940 | 941 | '@rollup/rollup-linux-loongarch64-gnu@4.41.1': 942 | optional: true 943 | 944 | '@rollup/rollup-linux-powerpc64le-gnu@4.41.1': 945 | optional: true 946 | 947 | '@rollup/rollup-linux-riscv64-gnu@4.41.1': 948 | optional: true 949 | 950 | '@rollup/rollup-linux-riscv64-musl@4.41.1': 951 | optional: true 952 | 953 | '@rollup/rollup-linux-s390x-gnu@4.41.1': 954 | optional: true 955 | 956 | '@rollup/rollup-linux-x64-gnu@4.41.1': 957 | optional: true 958 | 959 | '@rollup/rollup-linux-x64-musl@4.41.1': 960 | optional: true 961 | 962 | '@rollup/rollup-win32-arm64-msvc@4.41.1': 963 | optional: true 964 | 965 | '@rollup/rollup-win32-ia32-msvc@4.41.1': 966 | optional: true 967 | 968 | '@rollup/rollup-win32-x64-msvc@4.41.1': 969 | optional: true 970 | 971 | '@tauri-apps/api@2.5.0': {} 972 | 973 | '@tauri-apps/cli-darwin-arm64@2.5.0': 974 | optional: true 975 | 976 | '@tauri-apps/cli-darwin-x64@2.5.0': 977 | optional: true 978 | 979 | '@tauri-apps/cli-linux-arm-gnueabihf@2.5.0': 980 | optional: true 981 | 982 | '@tauri-apps/cli-linux-arm64-gnu@2.5.0': 983 | optional: true 984 | 985 | '@tauri-apps/cli-linux-arm64-musl@2.5.0': 986 | optional: true 987 | 988 | '@tauri-apps/cli-linux-riscv64-gnu@2.5.0': 989 | optional: true 990 | 991 | '@tauri-apps/cli-linux-x64-gnu@2.5.0': 992 | optional: true 993 | 994 | '@tauri-apps/cli-linux-x64-musl@2.5.0': 995 | optional: true 996 | 997 | '@tauri-apps/cli-win32-arm64-msvc@2.5.0': 998 | optional: true 999 | 1000 | '@tauri-apps/cli-win32-ia32-msvc@2.5.0': 1001 | optional: true 1002 | 1003 | '@tauri-apps/cli-win32-x64-msvc@2.5.0': 1004 | optional: true 1005 | 1006 | '@tauri-apps/cli@2.5.0': 1007 | optionalDependencies: 1008 | '@tauri-apps/cli-darwin-arm64': 2.5.0 1009 | '@tauri-apps/cli-darwin-x64': 2.5.0 1010 | '@tauri-apps/cli-linux-arm-gnueabihf': 2.5.0 1011 | '@tauri-apps/cli-linux-arm64-gnu': 2.5.0 1012 | '@tauri-apps/cli-linux-arm64-musl': 2.5.0 1013 | '@tauri-apps/cli-linux-riscv64-gnu': 2.5.0 1014 | '@tauri-apps/cli-linux-x64-gnu': 2.5.0 1015 | '@tauri-apps/cli-linux-x64-musl': 2.5.0 1016 | '@tauri-apps/cli-win32-arm64-msvc': 2.5.0 1017 | '@tauri-apps/cli-win32-ia32-msvc': 2.5.0 1018 | '@tauri-apps/cli-win32-x64-msvc': 2.5.0 1019 | 1020 | '@tauri-apps/plugin-log@2.4.0': 1021 | dependencies: 1022 | '@tauri-apps/api': 2.5.0 1023 | 1024 | '@tauri-apps/plugin-opener@2.2.7': 1025 | dependencies: 1026 | '@tauri-apps/api': 2.5.0 1027 | 1028 | '@types/babel__core@7.20.5': 1029 | dependencies: 1030 | '@babel/parser': 7.27.3 1031 | '@babel/types': 7.27.3 1032 | '@types/babel__generator': 7.27.0 1033 | '@types/babel__template': 7.4.4 1034 | '@types/babel__traverse': 7.20.7 1035 | 1036 | '@types/babel__generator@7.27.0': 1037 | dependencies: 1038 | '@babel/types': 7.27.3 1039 | 1040 | '@types/babel__template@7.4.4': 1041 | dependencies: 1042 | '@babel/parser': 7.27.3 1043 | '@babel/types': 7.27.3 1044 | 1045 | '@types/babel__traverse@7.20.7': 1046 | dependencies: 1047 | '@babel/types': 7.27.3 1048 | 1049 | '@types/estree@1.0.7': {} 1050 | 1051 | '@types/react-dom@19.1.5(@types/react@19.1.6)': 1052 | dependencies: 1053 | '@types/react': 19.1.6 1054 | 1055 | '@types/react@19.1.6': 1056 | dependencies: 1057 | csstype: 3.1.3 1058 | 1059 | '@vitejs/plugin-react@4.5.0(vite@6.3.5)': 1060 | dependencies: 1061 | '@babel/core': 7.27.3 1062 | '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.27.3) 1063 | '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.27.3) 1064 | '@rolldown/pluginutils': 1.0.0-beta.9 1065 | '@types/babel__core': 7.20.5 1066 | react-refresh: 0.17.0 1067 | vite: 6.3.5 1068 | transitivePeerDependencies: 1069 | - supports-color 1070 | 1071 | browserslist@4.24.5: 1072 | dependencies: 1073 | caniuse-lite: 1.0.30001718 1074 | electron-to-chromium: 1.5.159 1075 | node-releases: 2.0.19 1076 | update-browserslist-db: 1.1.3(browserslist@4.24.5) 1077 | 1078 | caniuse-lite@1.0.30001718: {} 1079 | 1080 | convert-source-map@2.0.0: {} 1081 | 1082 | csstype@3.1.3: {} 1083 | 1084 | debug@4.4.1: 1085 | dependencies: 1086 | ms: 2.1.3 1087 | 1088 | electron-to-chromium@1.5.159: {} 1089 | 1090 | esbuild@0.25.5: 1091 | optionalDependencies: 1092 | '@esbuild/aix-ppc64': 0.25.5 1093 | '@esbuild/android-arm': 0.25.5 1094 | '@esbuild/android-arm64': 0.25.5 1095 | '@esbuild/android-x64': 0.25.5 1096 | '@esbuild/darwin-arm64': 0.25.5 1097 | '@esbuild/darwin-x64': 0.25.5 1098 | '@esbuild/freebsd-arm64': 0.25.5 1099 | '@esbuild/freebsd-x64': 0.25.5 1100 | '@esbuild/linux-arm': 0.25.5 1101 | '@esbuild/linux-arm64': 0.25.5 1102 | '@esbuild/linux-ia32': 0.25.5 1103 | '@esbuild/linux-loong64': 0.25.5 1104 | '@esbuild/linux-mips64el': 0.25.5 1105 | '@esbuild/linux-ppc64': 0.25.5 1106 | '@esbuild/linux-riscv64': 0.25.5 1107 | '@esbuild/linux-s390x': 0.25.5 1108 | '@esbuild/linux-x64': 0.25.5 1109 | '@esbuild/netbsd-arm64': 0.25.5 1110 | '@esbuild/netbsd-x64': 0.25.5 1111 | '@esbuild/openbsd-arm64': 0.25.5 1112 | '@esbuild/openbsd-x64': 0.25.5 1113 | '@esbuild/sunos-x64': 0.25.5 1114 | '@esbuild/win32-arm64': 0.25.5 1115 | '@esbuild/win32-ia32': 0.25.5 1116 | '@esbuild/win32-x64': 0.25.5 1117 | 1118 | escalade@3.2.0: {} 1119 | 1120 | fdir@6.4.4(picomatch@4.0.2): 1121 | optionalDependencies: 1122 | picomatch: 4.0.2 1123 | 1124 | fsevents@2.3.3: 1125 | optional: true 1126 | 1127 | gensync@1.0.0-beta.2: {} 1128 | 1129 | globals@11.12.0: {} 1130 | 1131 | js-tokens@4.0.0: {} 1132 | 1133 | jsesc@3.1.0: {} 1134 | 1135 | json5@2.2.3: {} 1136 | 1137 | lru-cache@5.1.1: 1138 | dependencies: 1139 | yallist: 3.1.1 1140 | 1141 | ms@2.1.3: {} 1142 | 1143 | nanoid@3.3.11: {} 1144 | 1145 | node-releases@2.0.19: {} 1146 | 1147 | picocolors@1.1.1: {} 1148 | 1149 | picomatch@4.0.2: {} 1150 | 1151 | postcss@8.5.3: 1152 | dependencies: 1153 | nanoid: 3.3.11 1154 | picocolors: 1.1.1 1155 | source-map-js: 1.2.1 1156 | 1157 | prettier@3.5.3: {} 1158 | 1159 | react-dom@19.1.0(react@19.1.0): 1160 | dependencies: 1161 | react: 19.1.0 1162 | scheduler: 0.26.0 1163 | 1164 | react-refresh@0.17.0: {} 1165 | 1166 | react@19.1.0: {} 1167 | 1168 | rollup@4.41.1: 1169 | dependencies: 1170 | '@types/estree': 1.0.7 1171 | optionalDependencies: 1172 | '@rollup/rollup-android-arm-eabi': 4.41.1 1173 | '@rollup/rollup-android-arm64': 4.41.1 1174 | '@rollup/rollup-darwin-arm64': 4.41.1 1175 | '@rollup/rollup-darwin-x64': 4.41.1 1176 | '@rollup/rollup-freebsd-arm64': 4.41.1 1177 | '@rollup/rollup-freebsd-x64': 4.41.1 1178 | '@rollup/rollup-linux-arm-gnueabihf': 4.41.1 1179 | '@rollup/rollup-linux-arm-musleabihf': 4.41.1 1180 | '@rollup/rollup-linux-arm64-gnu': 4.41.1 1181 | '@rollup/rollup-linux-arm64-musl': 4.41.1 1182 | '@rollup/rollup-linux-loongarch64-gnu': 4.41.1 1183 | '@rollup/rollup-linux-powerpc64le-gnu': 4.41.1 1184 | '@rollup/rollup-linux-riscv64-gnu': 4.41.1 1185 | '@rollup/rollup-linux-riscv64-musl': 4.41.1 1186 | '@rollup/rollup-linux-s390x-gnu': 4.41.1 1187 | '@rollup/rollup-linux-x64-gnu': 4.41.1 1188 | '@rollup/rollup-linux-x64-musl': 4.41.1 1189 | '@rollup/rollup-win32-arm64-msvc': 4.41.1 1190 | '@rollup/rollup-win32-ia32-msvc': 4.41.1 1191 | '@rollup/rollup-win32-x64-msvc': 4.41.1 1192 | fsevents: 2.3.3 1193 | 1194 | scheduler@0.26.0: {} 1195 | 1196 | semver@6.3.1: {} 1197 | 1198 | source-map-js@1.2.1: {} 1199 | 1200 | tinyglobby@0.2.14: 1201 | dependencies: 1202 | fdir: 6.4.4(picomatch@4.0.2) 1203 | picomatch: 4.0.2 1204 | 1205 | typescript@5.8.3: {} 1206 | 1207 | update-browserslist-db@1.1.3(browserslist@4.24.5): 1208 | dependencies: 1209 | browserslist: 4.24.5 1210 | escalade: 3.2.0 1211 | picocolors: 1.1.1 1212 | 1213 | vite@6.3.5: 1214 | dependencies: 1215 | esbuild: 0.25.5 1216 | fdir: 6.4.4(picomatch@4.0.2) 1217 | picomatch: 4.0.2 1218 | postcss: 8.5.3 1219 | rollup: 4.41.1 1220 | tinyglobby: 0.2.14 1221 | optionalDependencies: 1222 | fsevents: 2.3.3 1223 | 1224 | yallist@3.1.1: {} 1225 | -------------------------------------------------------------------------------- /examples/api/src-tauri/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "api" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [build-dependencies] 7 | tauri-runtime-verso-build = { path = "../../../tauri-runtime-verso-build" } 8 | tauri-build = { version = "2", features = [] } 9 | 10 | [dependencies] 11 | tauri = { version = "2", default-features = false, features = [ 12 | # Default features 13 | # "wry", 14 | # "compression", 15 | "common-controls-v6", 16 | # Additional features 17 | "tray-icon", 18 | ] } 19 | serde_json = "1" 20 | log = "0.4" 21 | tauri-plugin-log = "2" 22 | tauri-plugin-opener = "2" 23 | tauri-runtime-verso = { path = "../../../" } 24 | -------------------------------------------------------------------------------- /examples/api/src-tauri/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | tauri_runtime_verso_build::get_verso_as_external_bin().unwrap(); 3 | tauri_build::build(); 4 | } 5 | -------------------------------------------------------------------------------- /examples/api/src-tauri/capabilities/main.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../gen/schemas/desktop-schema.json", 3 | "identifier": "main", 4 | "windows": [ 5 | "main" 6 | ], 7 | "permissions": [ 8 | "core:default", 9 | "core:window:allow-minimize", 10 | "core:window:allow-toggle-maximize", 11 | "core:window:allow-close", 12 | "core:window:allow-start-dragging", 13 | "opener:default", 14 | { 15 | "identifier": "opener:allow-open-path", 16 | "allow": [ 17 | { 18 | "path": "$APPLOG/**" 19 | } 20 | ] 21 | }, 22 | "log:default" 23 | ] 24 | } -------------------------------------------------------------------------------- /examples/api/src-tauri/src/main.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] 2 | 3 | mod tray; 4 | 5 | use tauri::WebviewWindowBuilder; 6 | 7 | #[tauri::command] 8 | fn greet(name: &str) -> String { 9 | format!("Hello {name}, You have been greeted from Rust!") 10 | } 11 | 12 | fn main() { 13 | tauri_runtime_verso::builder() 14 | .plugin( 15 | tauri_plugin_log::Builder::default() 16 | .level(log::LevelFilter::Info) 17 | .build(), 18 | ) 19 | .plugin(tauri_plugin_opener::init()) 20 | .invoke_handler(tauri::generate_handler![greet]) 21 | .setup(|app| { 22 | WebviewWindowBuilder::new(app, "main", Default::default()) 23 | .inner_size(900., 700.) 24 | .decorations(false) 25 | .build()?; 26 | tray::create_tray(app.handle())?; 27 | Ok(()) 28 | }) 29 | .run(tauri::generate_context!()) 30 | .expect("error while running tauri application") 31 | } 32 | -------------------------------------------------------------------------------- /examples/api/src-tauri/src/tray.rs: -------------------------------------------------------------------------------- 1 | use tauri::{ 2 | AppHandle, Runtime, 3 | menu::{MenuBuilder, MenuItemBuilder}, 4 | tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent}, 5 | }; 6 | 7 | pub fn create_tray(app: &AppHandle) -> tauri::Result<()> { 8 | const CLICK_ME_ID: &str = "click-me"; 9 | let menu = MenuBuilder::new(app) 10 | .item( 11 | &MenuItemBuilder::new("Click me!") 12 | .id(CLICK_ME_ID) 13 | .build(app)?, 14 | ) 15 | .quit() 16 | .build()?; 17 | TrayIconBuilder::new() 18 | .tooltip("Tauri") 19 | .icon(app.default_window_icon().unwrap().clone()) 20 | .menu(&menu) 21 | .show_menu_on_left_click(false) 22 | .on_tray_icon_event(|_tray, event| match event { 23 | TrayIconEvent::Click { 24 | button: MouseButton::Left, 25 | button_state: MouseButtonState::Up, 26 | .. 27 | } => { 28 | dbg!("Tray icon clicked!"); 29 | } 30 | _ => {} 31 | }) 32 | .on_menu_event(|_app, event| { 33 | if event.id == CLICK_ME_ID { 34 | dbg!("Click me clicked!"); 35 | } 36 | }) 37 | .build(app)?; 38 | Ok(()) 39 | } 40 | -------------------------------------------------------------------------------- /examples/api/src-tauri/tauri.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.tauri.app/config/2", 3 | "productName": "Verso Tauri API", 4 | "version": "0.1.0", 5 | "identifier": "com.tauri.dev", 6 | "build": { 7 | "frontendDist": "../dist", 8 | "devUrl": "http://localhost:1420", 9 | "beforeBuildCommand": "pnpm web:build" 10 | }, 11 | "app": { 12 | "withGlobalTauri": true 13 | }, 14 | "bundle": { 15 | "icon": [ 16 | "../../.icons/icon.ico", 17 | "../../.icons/icon.png" 18 | ], 19 | "externalBin": [ 20 | "versoview/versoview" 21 | ] 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/api/src/components/api-forms.css: -------------------------------------------------------------------------------- 1 | .forms-group { 2 | display: flex; 3 | flex-direction: column; 4 | gap: 1.5em; 5 | } 6 | 7 | .form-and-message { 8 | background-color: var(--color-secondary-background); 9 | padding: 1.5em; 10 | 11 | display: flex; 12 | flex-direction: column; 13 | justify-content: center; 14 | align-items: center; 15 | gap: 2em; 16 | } 17 | 18 | .form-and-message > h2 { 19 | font-size: 1.4em; 20 | font-weight: 600; 21 | margin: 0; 22 | } 23 | 24 | .form-and-message > form { 25 | display: flex; 26 | justify-content: center; 27 | gap: 1em; 28 | } 29 | 30 | .form-and-message > .message { 31 | display: flex; 32 | justify-content: center; 33 | } 34 | 35 | .form-and-message > form input { 36 | padding: 0.5em 0.8em; 37 | border: 1px solid hsl(0, 0%, 60%); 38 | outline: none; 39 | min-width: 0; 40 | width: 14em; 41 | } 42 | .form-and-message > form input:focus { 43 | border-color: var(--color-primary); 44 | } 45 | 46 | .form-and-message button { 47 | appearance: none; 48 | border: none; 49 | cursor: pointer; 50 | outline: none; 51 | padding: 0.4em 1em; 52 | color: var(--color-on-primary); 53 | background-color: var(--color-primary); 54 | display: inline-flex; 55 | align-items: center; 56 | } 57 | .form-and-message button:hover, 58 | .form-and-message button:focus { 59 | background-color: var(--color-primary-hightlight); 60 | } 61 | -------------------------------------------------------------------------------- /examples/api/src/components/api-forms.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import { invoke } from '@tauri-apps/api/core' 3 | import { info } from '@tauri-apps/plugin-log' 4 | import { getName } from '@tauri-apps/api/app' 5 | import { resolve, appLogDir } from '@tauri-apps/api/path' 6 | import { openPath } from '@tauri-apps/plugin-opener' 7 | 8 | import './api-forms.css' 9 | 10 | export function FormsGroup() { 11 | return ( 12 |
13 | 14 | 15 |
16 | ) 17 | } 18 | 19 | export function HelloWorld() { 20 | const [name, setName] = useState('') 21 | const [message, setMessage] = useState('') 22 | return ( 23 |
24 |

Greet Command Example

25 |
{ 27 | ev.preventDefault() 28 | const newMessage = await invoke('greet', { name }) 29 | setMessage(newMessage) 30 | }} 31 | > 32 | setName(e.target.value)} 36 | /> 37 | 38 | 39 |
{message}
40 |
41 | ) 42 | } 43 | 44 | export function LoggingExample() { 45 | const [name, setName] = useState('') 46 | return ( 47 |
48 |

Log Plugin Command Example

49 |
{ 51 | ev.preventDefault() 52 | await info(name) 53 | }} 54 | > 55 | setName(e.target.value)} 59 | /> 60 | 61 |
62 | 67 |
68 | ) 69 | } 70 | -------------------------------------------------------------------------------- /examples/api/src/components/title-bar.css: -------------------------------------------------------------------------------- 1 | .title-bar { 2 | display: flex; 3 | justify-content: end; 4 | align-items: center; 5 | } 6 | 7 | .title-bar button { 8 | display: inline-flex; 9 | appearance: none; 10 | padding: 0; 11 | margin: 0; 12 | border: none; 13 | width: 1.5em; 14 | height: 1.5em; 15 | justify-content: center; 16 | align-items: center; 17 | background-color: transparent; 18 | font-size: 1.5em; 19 | } 20 | .title-bar button:hover { 21 | background-color: var(--color-background-10-percent); 22 | } 23 | .title-bar button.close-button:hover { 24 | background-color: var(--color-red); 25 | color: white; 26 | } 27 | -------------------------------------------------------------------------------- /examples/api/src/components/title-bar.tsx: -------------------------------------------------------------------------------- 1 | import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow' 2 | 3 | import './title-bar.css' 4 | 5 | export function TitleBar() { 6 | return ( 7 |
8 | 9 | 10 | 13 |
14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /examples/api/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Tauri Runtime Verso APIs 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/api/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import { TitleBar } from './components/title-bar' 4 | import { error } from '@tauri-apps/plugin-log' 5 | import { FormsGroup } from './components/api-forms' 6 | 7 | window.addEventListener('error', async (event) => { 8 | try { 9 | await error(event.error) 10 | } catch (error) { 11 | console.error(error) 12 | } 13 | }) 14 | window.addEventListener('unhandledrejection', async (event) => { 15 | try { 16 | await error( 17 | typeof event.reason === 'object' 18 | ? JSON.stringify(event.reason, undefined, 4) 19 | : String(event.reason), 20 | ) 21 | } catch (error) { 22 | console.error(error) 23 | } 24 | }) 25 | 26 | function App() { 27 | return ( 28 | <> 29 | 30 | 31 |

Verso (Servo) + Tauri!

32 | 33 | 34 | ) 35 | } 36 | 37 | createRoot(document.getElementById('root')!).render( 38 | 39 | 40 | , 41 | ) 42 | -------------------------------------------------------------------------------- /examples/api/src/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --color-background: white; 3 | --color-secondary-background: hsl(217, 40%, 96%); 4 | --color-on-background: hsl(90, 8%, 10%); 5 | --color-primary: hsl(205, 85%, 40%); 6 | --color-primary-hightlight: hsl(205, 85%, 35%); 7 | --color-on-primary: white; 8 | --color-red: rgb(232, 17, 35); 9 | --color-background-10-percent: rgba(0, 0, 0, 0.1); 10 | 11 | font-family: 12 | -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 13 | 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; 14 | line-height: 1.5; 15 | background-color: var(--color-background); 16 | color: var(--color-on-background); 17 | } 18 | 19 | *, 20 | ::before, 21 | ::after { 22 | box-sizing: border-box; 23 | } 24 | 25 | button, 26 | input, 27 | textarea, 28 | select { 29 | font: inherit; 30 | } 31 | 32 | body { 33 | display: flex; 34 | flex-direction: column; 35 | gap: 1em; 36 | margin: 0; 37 | min-height: 100vh; 38 | } 39 | 40 | .title { 41 | margin: 0; 42 | padding: 1em; 43 | text-align: center; 44 | } 45 | -------------------------------------------------------------------------------- /examples/api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react-jsx", 4 | "esModuleInterop": true, 5 | "skipLibCheck": true, 6 | "target": "ESNext", 7 | "allowJs": true, 8 | "resolveJsonModule": true, 9 | "moduleDetection": "force", 10 | "isolatedModules": true, 11 | "strict": true, 12 | "noUncheckedIndexedAccess": true, 13 | "moduleResolution": "Bundler", 14 | "module": "ESNext", 15 | "noEmit": true, 16 | "lib": [ 17 | "es2022", 18 | "dom", 19 | "dom.iterable", 20 | ], 21 | "types": [ 22 | "vite/client", 23 | ], 24 | }, 25 | "include": ["src"] 26 | } -------------------------------------------------------------------------------- /examples/api/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | root: 'src', 7 | plugins: [react()], 8 | server: { 9 | port: 1420, 10 | strictPort: true, 11 | }, 12 | build: { 13 | outDir: '../dist', 14 | emptyOutDir: true, 15 | target: 'esnext', 16 | modulePreload: false, 17 | }, 18 | }) 19 | -------------------------------------------------------------------------------- /examples/helloworld/.gitignore: -------------------------------------------------------------------------------- 1 | gen 2 | versoview/ 3 | -------------------------------------------------------------------------------- /examples/helloworld/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "helloworld" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [build-dependencies] 7 | tauri-runtime-verso-build = { path = "../../tauri-runtime-verso-build" } 8 | tauri-build = { version = "2", features = [] } 9 | 10 | [dependencies] 11 | tauri = { version = "2", default-features = false, features = [ 12 | # Default features 13 | # "wry", 14 | # "compression", 15 | "common-controls-v6", 16 | # Additional features 17 | # "tray-icon", 18 | ] } 19 | tauri-runtime-verso = { path = "../../" } 20 | # serde = "1" 21 | -------------------------------------------------------------------------------- /examples/helloworld/README.md: -------------------------------------------------------------------------------- 1 | To get started with this example, run `cargo run --package helloworld` (You'll need the [prerequisites](https://tauri.app/start/prerequisites/) of Tauri) 2 | -------------------------------------------------------------------------------- /examples/helloworld/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | tauri_runtime_verso_build::get_verso_as_external_bin().unwrap(); 3 | tauri_build::build(); 4 | } 5 | -------------------------------------------------------------------------------- /examples/helloworld/capabilities/main.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../gen/schemas/desktop-schema.json", 3 | "identifier": "main", 4 | "windows": [ 5 | "main" 6 | ], 7 | "permissions": [ 8 | "core:default" 9 | ] 10 | } -------------------------------------------------------------------------------- /examples/helloworld/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Welcome to Tauri! 7 | 84 | 99 | 100 | 101 |

Welcome to Tauri!

102 | 103 |
104 |
105 | 106 | 107 |
108 |
109 |
110 | 111 | 112 | -------------------------------------------------------------------------------- /examples/helloworld/src/main.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] 2 | 3 | use tauri::Manager; 4 | 5 | #[tauri::command] 6 | fn greet(name: &str) -> String { 7 | format!("Hello {name}, You have been greeted from Rust!") 8 | } 9 | 10 | fn main() { 11 | // You can also set the `versoview` executable path yourself 12 | // tauri_runtime_verso::set_verso_path("../verso/target/debug/versoview"); 13 | 14 | // To use the devtools, set it like to let verso open a devtools server, 15 | // and you can connect to it through Firefox's devtools in the `about:debugging` page 16 | tauri_runtime_verso::set_verso_devtools_port(1234); 17 | 18 | tauri_runtime_verso::builder() 19 | .invoke_handler(tauri::generate_handler![greet]) 20 | .setup(|app| { 21 | dbg!(app.get_webview_window("main").unwrap().inner_size()).unwrap(); 22 | Ok(()) 23 | }) 24 | .run(tauri::generate_context!()) 25 | .expect("error while running tauri application") 26 | } 27 | -------------------------------------------------------------------------------- /examples/helloworld/tauri.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.tauri.app/config/2", 3 | "productName": "Hello World", 4 | "version": "0.1.0", 5 | "identifier": "com.tauri.dev", 6 | "build": { 7 | "frontendDist": ["index.html"] 8 | }, 9 | "app": { 10 | "withGlobalTauri": true, 11 | "windows": [ 12 | { 13 | "title": "Welcome to Tauri!", 14 | "width": 700, 15 | "height": 500 16 | } 17 | ] 18 | }, 19 | "bundle": { 20 | "icon": [ 21 | "../.icons/icon.ico", 22 | "../.icons/icon.png" 23 | ], 24 | "externalBin": [ 25 | "versoview/versoview" 26 | ] 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/event_loop_ext.rs: -------------------------------------------------------------------------------- 1 | use tao::event_loop::EventLoopWindowTarget as TaoEventLoopWindowTarget; 2 | use tauri_runtime::{Error, Result, dpi::PhysicalPosition, monitor::Monitor}; 3 | 4 | pub trait TaoEventLoopWindowTargetExt { 5 | fn tauri_primary_monitor(&self) -> Option; 6 | fn tauri_monitor_from_point(&self, x: f64, y: f64) -> Option; 7 | fn tauri_available_monitors(&self) -> Vec; 8 | fn tauri_cursor_position(&self) -> Result>; 9 | } 10 | 11 | impl TaoEventLoopWindowTargetExt for TaoEventLoopWindowTarget { 12 | fn tauri_primary_monitor(&self) -> Option { 13 | self.primary_monitor().map(tao_monitor_to_tauri_monitor) 14 | } 15 | 16 | fn tauri_monitor_from_point(&self, x: f64, y: f64) -> Option { 17 | self.monitor_from_point(x, y) 18 | .map(tao_monitor_to_tauri_monitor) 19 | } 20 | 21 | fn tauri_available_monitors(&self) -> Vec { 22 | self.available_monitors() 23 | .map(tao_monitor_to_tauri_monitor) 24 | .collect() 25 | } 26 | 27 | fn tauri_cursor_position(&self) -> Result> { 28 | let position = self 29 | .cursor_position() 30 | .map_err(|_| Error::FailedToGetCursorPosition)?; 31 | Ok(position) 32 | } 33 | } 34 | 35 | pub fn tao_monitor_to_tauri_monitor(monitor: tao::monitor::MonitorHandle) -> Monitor { 36 | Monitor { 37 | name: monitor.name(), 38 | position: monitor.position(), 39 | scale_factor: monitor.scale_factor(), 40 | size: monitor.size(), 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/invoke-system-initialization-script.js: -------------------------------------------------------------------------------- 1 | // This file is copied and modified from Tauri with a few modifications 2 | // - Changed `processIpcMessage` to always return a string so we can put it inside of http request header 3 | // - Changed custom protocol IPC to use header instead of body since we can't get the body in Servo yet 4 | // 5 | // > ipc-protocol.js: https://github.com/tauri-apps/tauri/blob/dev/crates/tauri/scripts/ipc-protocol.js 6 | // > process-ipc-message-fn.js: https://github.com/tauri-apps/tauri/blob/dev/crates/tauri/scripts/process-ipc-message-fn.js 7 | 8 | ;(function () { 9 | const processIpcMessage = function (message) { 10 | // if (message instanceof ArrayBuffer || ArrayBuffer.isView(message) || Array.isArray(message)) { 11 | // return { 12 | // contentType: 'application/octet-stream', 13 | // data: message, 14 | // } 15 | // } else { 16 | const data = JSON.stringify(message, (_k, val) => { 17 | // if this value changes, make sure to update it in: 18 | // 1. ipc.js 19 | // 2. core.ts 20 | const SERIALIZE_TO_IPC_FN = '__TAURI_TO_IPC_KEY__' 21 | 22 | if (val instanceof Map) { 23 | return Object.fromEntries(val.entries()) 24 | } else if (val instanceof Uint8Array) { 25 | return Array.from(val) 26 | } else if (val instanceof ArrayBuffer) { 27 | return Array.from(new Uint8Array(val)) 28 | } else if (typeof val === 'object' && val !== null && SERIALIZE_TO_IPC_FN in val) { 29 | return val[SERIALIZE_TO_IPC_FN]() 30 | } else { 31 | return val 32 | } 33 | }) 34 | 35 | return { 36 | contentType: 'application/json', 37 | data, 38 | } 39 | // } 40 | } 41 | 42 | /** 43 | * A runtime generated key to ensure an IPC call comes from an initialized frame. 44 | * 45 | * This is declared outside the `window.__TAURI_INVOKE__` definition to prevent 46 | * the key from being leaked by `window.__TAURI_INVOKE__.toString()`. 47 | */ 48 | const __TAURI_INVOKE_KEY__ = __INVOKE_KEY__ 49 | 50 | let customProtocolIpcFailed = false 51 | 52 | function sendIpcMessage(message) { 53 | const { cmd, callback, error, payload, options } = message 54 | 55 | if (!customProtocolIpcFailed) { 56 | const { contentType, data } = processIpcMessage(payload) 57 | 58 | // Headers can only contain ascii characters, so encoding is needed, 59 | // and since tauri already depends on percent-encoding rust crate, we use `encodeURI` here for that reason 60 | const invokeBody = encodeURI(data) 61 | 62 | const headers = new Headers((options && options.headers) || {}) 63 | headers.set('Content-Type', contentType) 64 | headers.set('Tauri-Callback', callback) 65 | headers.set('Tauri-Error', error) 66 | headers.set('Tauri-Invoke-Key', __TAURI_INVOKE_KEY__) 67 | headers.set('Tauri-VersoRuntime-Invoke-Body', invokeBody) 68 | 69 | fetch(window.__TAURI_INTERNALS__.convertFileSrc(cmd, 'ipc'), { 70 | method: 'POST', 71 | // body: data, 72 | headers, 73 | }) 74 | .then((response) => { 75 | const cb = response.headers.get('Tauri-Response') === 'ok' ? callback : error 76 | // we need to split here because on Android the content-type gets duplicated 77 | switch ((response.headers.get('content-type') || '').split(',')[0]) { 78 | case 'application/json': 79 | return response.json().then((r) => [cb, r]) 80 | case 'text/plain': 81 | return response.text().then((r) => [cb, r]) 82 | default: 83 | return response.arrayBuffer().then((r) => [cb, r]) 84 | } 85 | }) 86 | .then(([cb, data]) => { 87 | if (window[`_${cb}`]) { 88 | window[`_${cb}`](data) 89 | } else { 90 | console.warn( 91 | `[TAURI] Couldn't find callback id {cb} in window. This might happen when the app is reloaded while Rust is running an asynchronous operation.` 92 | ) 93 | } 94 | }) 95 | .catch((e) => { 96 | console.warn( 97 | 'IPC custom protocol failed, Tauri will now use the postMessage interface instead', 98 | e 99 | ) 100 | // failed to use the custom protocol IPC (either the webview blocked a custom protocol or it was a CSP error) 101 | // so we need to fallback to the postMessage interface 102 | customProtocolIpcFailed = true 103 | sendIpcMessage(message) 104 | }) 105 | } else { 106 | // otherwise use the postMessage interface 107 | const { data } = processIpcMessage({ 108 | cmd, 109 | callback, 110 | error, 111 | options: { 112 | ...options, 113 | customProtocolIpcBlocked: customProtocolIpcFailed, 114 | }, 115 | payload, 116 | __TAURI_INVOKE_KEY__, 117 | }) 118 | window.ipc.postMessage(data) 119 | } 120 | } 121 | 122 | Object.defineProperty(window.__TAURI_INTERNALS__, 'postMessage', { 123 | value: sendIpcMessage, 124 | }) 125 | })() 126 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # Tauri Runtime Verso 2 | //! 3 | //! Use Verso as the backend for Tauri 4 | //! 5 | //! ## Usage 6 | //! 7 | //! To get started, you need to add this crate to your project, and use `default-feature = false` on `tauri` to disable the `wry` feature 8 | //! 9 | //! ```diff 10 | //! [build-dependencies] 11 | //! tauri-build = "2" 12 | //! + tauri-runtime-verso-build = { git = "https://github.com/versotile-org/tauri-runtime-verso.git" } 13 | //! 14 | //! [dependencies] 15 | //! - tauri = { version = "2", features = [] } 16 | //! + tauri = { version = "2", default-features = false, features = ["common-controls-v6"] } 17 | //! + tauri-runtime-verso = { git = "https://github.com/versotile-org/tauri-runtime-verso.git" } 18 | //! ``` 19 | //! 20 | //! In your build script, add the `tauri-runtime-verso-build` script, which will download the pre-built `versoview` to `versoview/versoview-{target-triple}` 21 | //! 22 | //! > Note we currently only have pre-built `versoview` for x64 Linux, Windows, MacOS and arm64 MacOS, also the download might take a bit of time if you have a slow internet connection 23 | //! 24 | //! ```diff 25 | //! fn main() { 26 | //! + tauri_runtime_verso_build::get_verso_as_external_bin().unwrap(); 27 | //! tauri_build::build(); 28 | //! } 29 | //! ``` 30 | //! 31 | //! Then add the downloaded executable to your tauri config file (`tauri.conf.json`) as an external binary file 32 | //! 33 | //! ```diff 34 | //! { 35 | //! + "bundle": { 36 | //! + "externalBin": [ 37 | //! + "versoview/versoview" 38 | //! + ] 39 | //! + } 40 | //! } 41 | //! ``` 42 | //! 43 | //! Finally, setup the code like this: 44 | //! 45 | //! ```diff 46 | //! fn main() { 47 | //! - tauri::Builder::new() 48 | //! + tauri_runtime_verso::builder() 49 | //! .run(tauri::generate_context!()) 50 | //! .unwrap(); 51 | //! } 52 | //! ``` 53 | //! 54 | //! ## Tips 55 | //! 56 | //! ### Devtools 57 | //! 58 | //! Since Verso doesn't have a devtools built-in, you'll need to use the one from the Firefox, first put in this in your code 59 | //! 60 | //! ```rust 61 | //! // This will make the webviews created afters this open up a devtools server on this port, 62 | //! // setting it to 0 for a random port 63 | //! tauri_runtime_verso::set_verso_devtools_port(1234); 64 | //! ``` 65 | //! 66 | //! Then go to `about:debugging` in Firefox and connect to `localhost:1234` there 67 | //! 68 | //! ## Cargo features 69 | //! 70 | //! - **macos-private-api**: Matching with Tauri's macos-private-api feature, required if you use that 71 | 72 | mod event_loop_ext; 73 | mod runtime; 74 | mod webview; 75 | mod window; 76 | mod utils; 77 | 78 | pub use runtime::{EventProxy, RuntimeContext, VersoRuntime, VersoRuntimeHandle}; 79 | pub use webview::VersoWebviewDispatcher; 80 | pub use window::{VersoWindowBuilder, VersoWindowDispatcher}; 81 | 82 | use std::{ 83 | env::current_exe, 84 | path::{Path, PathBuf}, 85 | sync::{Mutex, OnceLock}, 86 | }; 87 | 88 | static VERSO_PATH: OnceLock = OnceLock::new(); 89 | 90 | /// Sets the Verso executable path to ues for the webviews, 91 | /// must be called before you create any webviews if you don't have the `externalBin` setup 92 | /// 93 | /// ### Example: 94 | /// 95 | /// ``` 96 | /// fn main() { 97 | /// tauri_runtime_verso::set_verso_path("../verso/target/debug/versoview"); 98 | /// tauri_runtime_verso::builder() 99 | /// .run(tauri::generate_context!()) 100 | /// .unwrap(); 101 | /// } 102 | /// ``` 103 | pub fn set_verso_path(path: impl Into) { 104 | VERSO_PATH 105 | .set(path.into()) 106 | .expect("Verso path is already set, you can't set it multiple times"); 107 | } 108 | 109 | fn get_verso_path() -> &'static Path { 110 | VERSO_PATH.get_or_init(|| { 111 | relative_command_path("versoview").expect( 112 | "Verso path not set! You need to call set_verso_path before creating any webviews!", 113 | ) 114 | }) 115 | } 116 | 117 | fn relative_command_path(name: &str) -> Option { 118 | let extension = if cfg!(windows) { ".exe" } else { "" }; 119 | current_exe() 120 | .ok()? 121 | .parent()? 122 | .join(format!("{name}{extension}")) 123 | .canonicalize() 124 | .ok() 125 | } 126 | 127 | static VERSO_RESOURCES_DIRECTORY: Mutex> = Mutex::new(None); 128 | 129 | /// Sets the Verso resources directory to ues for the webviews, 130 | /// note this only affects webviews created after you set this 131 | /// 132 | /// ### Example: 133 | /// 134 | /// ``` 135 | /// fn main() { 136 | /// tauri_runtime_verso::set_verso_path("../verso/target/debug/versoview"); 137 | /// tauri_runtime_verso::set_verso_resource_directory("../verso/resources"); 138 | /// tauri_runtime_verso::builder() 139 | /// .run(tauri::generate_context!()) 140 | /// .unwrap(); 141 | /// } 142 | /// ``` 143 | pub fn set_verso_resource_directory(path: impl Into) { 144 | VERSO_RESOURCES_DIRECTORY 145 | .lock() 146 | .unwrap() 147 | .replace(path.into()); 148 | } 149 | 150 | fn get_verso_resource_directory() -> Option { 151 | VERSO_RESOURCES_DIRECTORY.lock().unwrap().clone() 152 | } 153 | 154 | /// You need to set this on [`tauri::Builder::invoke_system`] for the invoke system to work, 155 | /// you can skip this if you're using [`tauri_runtime_verso::builder`](builder) 156 | /// 157 | /// ### Example: 158 | /// 159 | /// ``` 160 | /// fn main() { 161 | /// tauri_runtime_verso::set_verso_path("../verso/target/debug/versoview"); 162 | /// tauri_runtime_verso::set_verso_resource_directory("../verso/resources"); 163 | /// tauri::Builder::::new() 164 | /// .invoke_system(tauri_runtime_verso::INVOKE_SYSTEM_SCRIPTS.to_owned()) 165 | /// .run(tauri::generate_context!()) 166 | /// .unwrap(); 167 | /// } 168 | /// ``` 169 | pub const INVOKE_SYSTEM_SCRIPTS: &str = include_str!("./invoke-system-initialization-script.js"); 170 | 171 | static DEV_TOOLS_PORT: Mutex> = Mutex::new(None); 172 | 173 | /// Sets the Verso devtools port to ues for the webviews, 0 for random port, 174 | /// note this only affects webviews created after you set this 175 | /// 176 | /// Since Verso doesn't have devtools built-in, 177 | /// you need to use the one from Firefox from the `about:debugging` page, 178 | /// this setting allows you to let verso open a port for it 179 | pub fn set_verso_devtools_port(port: u16) { 180 | DEV_TOOLS_PORT.lock().unwrap().replace(port); 181 | } 182 | 183 | fn get_verso_devtools_port() -> Option { 184 | *DEV_TOOLS_PORT.lock().unwrap() 185 | } 186 | 187 | /// Creates a new [`tauri::Builder`] using the [`VersoRuntime`] 188 | /// 189 | /// ### Example: 190 | /// 191 | /// ```no_run 192 | /// fn main() { 193 | /// // instead of `tauri::Builder::new()` 194 | /// tauri_runtime_verso::builder() 195 | /// .run(tauri::generate_context!()) 196 | /// .unwrap(); 197 | /// } 198 | /// ``` 199 | pub fn builder() -> tauri::Builder { 200 | tauri::Builder::new().invoke_system(INVOKE_SYSTEM_SCRIPTS) 201 | } 202 | -------------------------------------------------------------------------------- /src/runtime.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused_variables)] 2 | 3 | use tao::{ 4 | event::{Event as TaoEvent, StartCause}, 5 | event_loop::{ 6 | ControlFlow, EventLoop, EventLoopBuilder, EventLoopProxy as TaoEventLoopProxy, 7 | EventLoopWindowTarget as TaoEventLoopWindowTarget, 8 | }, 9 | platform::run_return::EventLoopExtRunReturn, 10 | }; 11 | use tauri_runtime::{ 12 | DeviceEventFilter, Error, EventLoopProxy, ExitRequestedEventAction, Result, RunEvent, Runtime, 13 | RuntimeHandle, RuntimeInitArgs, UserEvent, WindowEventId, 14 | dpi::PhysicalPosition, 15 | monitor::Monitor, 16 | webview::{DetachedWebview, PendingWebview}, 17 | window::{ 18 | DetachedWindow, DetachedWindowWebview, PendingWindow, RawWindow, WindowBuilder, 19 | WindowEvent, WindowId, 20 | }, 21 | }; 22 | use tauri_utils::Theme; 23 | use url::Url; 24 | use verso::CustomProtocolBuilder; 25 | 26 | use std::{ 27 | borrow::Cow, 28 | collections::HashMap, 29 | fmt::{self, Debug}, 30 | ops::Deref, 31 | sync::{ 32 | Arc, Mutex, 33 | atomic::{AtomicU32, Ordering}, 34 | mpsc::channel, 35 | }, 36 | thread::{ThreadId, current as current_thread}, 37 | }; 38 | 39 | use crate::{ 40 | event_loop_ext::TaoEventLoopWindowTargetExt, 41 | get_verso_path, 42 | utils::{to_tao_theme, to_verso_theme}, 43 | webview::VersoWebviewDispatcher, 44 | window::{VersoWindowDispatcher, Window}, 45 | }; 46 | 47 | type Task = Box; 48 | type TaskWithEventLoop = Box>) + Send + 'static>; 49 | 50 | pub enum Message { 51 | Task(Task), 52 | /// Run task with the [`EventLoopWindowTarget`](TaoEventLoopWindowTarget) 53 | TaskWithEventLoop(TaskWithEventLoop), 54 | CloseWindow(WindowId), 55 | DestroyWindow(WindowId), 56 | RequestExit(i32), 57 | UserEvent(T), 58 | } 59 | 60 | impl Clone for Message { 61 | fn clone(&self) -> Self { 62 | match self { 63 | Self::UserEvent(t) => Self::UserEvent(t.clone()), 64 | _ => unimplemented!(), 65 | } 66 | } 67 | } 68 | 69 | #[derive(Clone)] 70 | pub struct DispatcherMainThreadContext { 71 | window_target: TaoEventLoopWindowTarget>, 72 | } 73 | 74 | // SAFETY: we ensure this type is only used on the main thread. 75 | #[allow(clippy::non_send_fields_in_send_ty)] 76 | unsafe impl Send for DispatcherMainThreadContext {} 77 | 78 | // SAFETY: we ensure this type is only used on the main thread. 79 | #[allow(clippy::non_send_fields_in_send_ty)] 80 | unsafe impl Sync for DispatcherMainThreadContext {} 81 | 82 | #[derive(Clone)] 83 | pub struct RuntimeContext { 84 | windows: Arc>>, 85 | prefered_theme: Arc>>, 86 | event_proxy: TaoEventLoopProxy>, 87 | // This must only be used on main thread 88 | main_thread: DispatcherMainThreadContext, 89 | main_thread_id: ThreadId, 90 | next_window_id: Arc, 91 | next_webview_id: Arc, 92 | next_window_event_id: Arc, 93 | next_webview_event_id: Arc, 94 | } 95 | 96 | impl RuntimeContext { 97 | pub fn send_message(&self, message: Message) -> Result<()> { 98 | if current_thread().id() == self.main_thread_id { 99 | match message { 100 | Message::Task(task) => { 101 | task(); 102 | return Ok(()); 103 | } 104 | Message::TaskWithEventLoop(task) => { 105 | task(&self.main_thread.window_target); 106 | return Ok(()); 107 | } 108 | _ => {} 109 | } 110 | } 111 | self.event_proxy 112 | .send_event(message) 113 | .map_err(|_| Error::FailedToSendMessage)?; 114 | Ok(()) 115 | } 116 | 117 | /// Run a task on the main thread. 118 | pub fn run_on_main_thread(&self, f: F) -> Result<()> { 119 | self.send_message(Message::Task(Box::new(f))) 120 | } 121 | 122 | /// Run a task on the main thread. 123 | pub fn run_on_main_thread_with_event_loop< 124 | X: Send + Sync + 'static, 125 | F: FnOnce(&TaoEventLoopWindowTarget>) -> X + Send + 'static, 126 | >( 127 | &self, 128 | f: F, 129 | ) -> Result { 130 | let (tx, rx) = channel(); 131 | self.send_message(Message::TaskWithEventLoop(Box::new(move |e| { 132 | let _ = tx.send(f(e)); 133 | })))?; 134 | rx.recv() 135 | .map_err(|_| tauri_runtime::Error::FailedToReceiveMessage) 136 | } 137 | 138 | pub fn next_window_id(&self) -> WindowId { 139 | self.next_window_id.fetch_add(1, Ordering::Relaxed).into() 140 | } 141 | 142 | pub fn next_webview_id(&self) -> u32 { 143 | self.next_webview_id.fetch_add(1, Ordering::Relaxed) 144 | } 145 | 146 | pub fn next_window_event_id(&self) -> WindowEventId { 147 | self.next_window_event_id.fetch_add(1, Ordering::Relaxed) 148 | } 149 | 150 | pub fn next_webview_event_id(&self) -> WindowEventId { 151 | self.next_webview_event_id.fetch_add(1, Ordering::Relaxed) 152 | } 153 | 154 | /// `after_window_creation` not supported 155 | /// 156 | /// Only creating the window with a webview is supported, 157 | /// will return [`tauri_runtime::Error::CreateWindow`] if there is no [`PendingWindow::webview`] 158 | pub fn create_window< 159 | R: Runtime< 160 | T, 161 | WindowDispatcher = VersoWindowDispatcher, 162 | WebviewDispatcher = VersoWebviewDispatcher, 163 | >, 164 | F: Fn(RawWindow<'_>) + Send + 'static, 165 | >( 166 | &self, 167 | pending: PendingWindow, 168 | _after_window_creation: Option, 169 | ) -> Result> { 170 | let label = pending.label; 171 | let Some(pending_webview) = pending.webview else { 172 | return Err(tauri_runtime::Error::CreateWindow); 173 | }; 174 | 175 | let window_id = self.next_window_id(); 176 | let webview_id = self.next_webview_id(); 177 | 178 | let mut window_builder = pending.window_builder; 179 | 180 | if window_builder.get_theme().is_none() { 181 | window_builder = window_builder.theme(*self.prefered_theme.lock().unwrap()); 182 | } 183 | 184 | let webview = window_builder 185 | .verso_builder 186 | .user_scripts( 187 | pending_webview 188 | .webview_attributes 189 | .initialization_scripts 190 | .into_iter() 191 | .map(|script| script.script), 192 | ) 193 | .custom_protocols( 194 | pending_webview 195 | .uri_scheme_protocols 196 | .keys() 197 | .map(CustomProtocolBuilder::new), 198 | ) 199 | .build(get_verso_path(), Url::parse(&pending_webview.url).unwrap()); 200 | 201 | let webview_label = label.clone(); 202 | let sender = self.event_proxy.clone(); 203 | let uri_scheme_protocols: HashMap<_, _> = pending_webview 204 | .uri_scheme_protocols 205 | .into_iter() 206 | .map(|(key, value)| (key, Arc::new(value))) 207 | .collect(); 208 | webview 209 | .on_web_resource_requested(move |mut request, response_fn| { 210 | // dbg!(&request); 211 | // TODO: Servo's EmbedderMsg::WebResourceRequested message is sent too early 212 | // that it doesn't include Origin header, so I hard coded this for now 213 | if !request.headers().contains_key("Origin") { 214 | #[cfg(windows)] 215 | let uri = { 216 | let scheme = if pending_webview.webview_attributes.use_https_scheme { 217 | "https" 218 | } else { 219 | "http" 220 | }; 221 | format!("{scheme}://tauri.localhost") 222 | }; 223 | #[cfg(not(windows))] 224 | let uri = "tauri://localhost"; 225 | request.headers_mut().insert("Origin", uri.parse().unwrap()); 226 | } 227 | for (scheme, handler) in &uri_scheme_protocols { 228 | // Since servo doesn't support body in its EmbedderMsg::WebResourceRequested yet, 229 | // we use a header instead for now 230 | if scheme == "ipc" { 231 | if let Some(data) = request 232 | .headers_mut() 233 | .remove("Tauri-VersoRuntime-Invoke-Body") 234 | { 235 | if let Ok(body) = 236 | percent_encoding::percent_decode(data.as_bytes()).decode_utf8() 237 | { 238 | *request.body_mut() = body.as_bytes().to_vec(); 239 | } else { 240 | log::error!("IPC invoke body header is not a valid UTF-8 string"); 241 | } 242 | } 243 | } 244 | #[cfg(windows)] 245 | let (uri, http_or_https) = ( 246 | request.uri().to_string(), 247 | if pending_webview.webview_attributes.use_https_scheme { 248 | "https" 249 | } else { 250 | "http" 251 | }, 252 | ); 253 | #[cfg(windows)] 254 | let is_custom_protocol_uri = is_work_around_uri(&uri, http_or_https, scheme); 255 | #[cfg(not(windows))] 256 | let is_custom_protocol_uri = request.uri().scheme_str() == Some(scheme); 257 | if is_custom_protocol_uri { 258 | #[cfg(windows)] 259 | { 260 | if let Ok(reverted) = 261 | revert_custom_protocol_work_around(&uri, http_or_https, scheme) 262 | { 263 | *request.uri_mut() = reverted 264 | } else { 265 | log::error!("Can't revert the URI work around on: {uri}") 266 | }; 267 | } 268 | // Run the handler on main thread, this is needed because Tauri expects this 269 | let handler = handler.clone(); 270 | let webview_label = webview_label.clone(); 271 | let _ = sender.send_event(Message::Task(Box::new(move || { 272 | handler( 273 | &webview_label, 274 | request, 275 | Box::new(move |response| { 276 | response_fn(Some(response.map(Cow::into_owned))); 277 | }), 278 | ); 279 | }))); 280 | return; 281 | } 282 | } 283 | response_fn(None); 284 | }) 285 | .map_err(|_| tauri_runtime::Error::CreateWindow)?; 286 | 287 | if let Some(navigation_handler) = pending_webview.navigation_handler { 288 | if let Err(error) = webview.on_navigation_starting(move |url| navigation_handler(&url)) 289 | { 290 | log::error!( 291 | "Register `on_navigation_starting` failed with {error}, `navigation_handler` will not get called for this window ({label})!" 292 | ); 293 | } 294 | } 295 | 296 | let sender = self.event_proxy.clone(); 297 | webview 298 | .on_close_requested(move || { 299 | let _ = sender.send_event(Message::CloseWindow(window_id)); 300 | }) 301 | .map_err(|_| tauri_runtime::Error::CreateWindow)?; 302 | 303 | let on_window_event_listeners = Arc::new(Mutex::new(HashMap::new())); 304 | 305 | let webview = Arc::new(Mutex::new(webview)); 306 | let window = Window { 307 | label: label.clone(), 308 | webview: webview.clone(), 309 | on_window_event_listeners: on_window_event_listeners.clone(), 310 | }; 311 | 312 | self.windows.lock().unwrap().insert(window_id, window); 313 | 314 | Ok(DetachedWindow { 315 | id: window_id, 316 | label: label.clone(), 317 | dispatcher: VersoWindowDispatcher { 318 | id: window_id, 319 | context: self.clone(), 320 | webview: webview.clone(), 321 | on_window_event_listeners, 322 | }, 323 | webview: Some(DetachedWindowWebview { 324 | webview: DetachedWebview { 325 | label, 326 | dispatcher: VersoWebviewDispatcher { 327 | id: webview_id, 328 | context: self.clone(), 329 | webview, 330 | }, 331 | }, 332 | use_https_scheme: false, 333 | }), 334 | }) 335 | } 336 | 337 | /// Handles the close window request by sending the [`WindowEvent::CloseRequested`] event 338 | /// if the request doesn't request a forced close 339 | /// and if not prevented, send [`WindowEvent::Destroyed`] 340 | /// then checks if there're windows left, if not, send [`RunEvent::ExitRequested`] 341 | /// returns if we should exit the event loop 342 | pub fn handle_close_window_request) + 'static>( 343 | &self, 344 | callback: &mut F, 345 | id: WindowId, 346 | force: bool, 347 | ) -> bool { 348 | let mut windows = self.windows.lock().unwrap(); 349 | let Some(window) = windows.get(&id) else { 350 | return false; 351 | }; 352 | let label = window.label.clone(); 353 | let on_window_event_listeners = window.on_window_event_listeners.clone(); 354 | 355 | if !force { 356 | let (tx, rx) = channel(); 357 | let window_event = WindowEvent::CloseRequested { 358 | signal_tx: tx.clone(), 359 | }; 360 | for handler in on_window_event_listeners.lock().unwrap().values() { 361 | handler(&window_event); 362 | } 363 | callback(RunEvent::WindowEvent { 364 | label: label.clone(), 365 | event: WindowEvent::CloseRequested { signal_tx: tx }, 366 | }); 367 | 368 | let should_prevent = matches!(rx.try_recv(), Ok(true)); 369 | if should_prevent { 370 | return false; 371 | } 372 | } 373 | 374 | let webview_weak = std::sync::Arc::downgrade(&window.webview); 375 | 376 | windows.remove(&id); 377 | callback(RunEvent::WindowEvent { 378 | label, 379 | event: WindowEvent::Destroyed, 380 | }); 381 | 382 | // This is required becuase tauri puts in a clone of the window in to WindowEventHandler closure, 383 | // and we need to clear it for the window to drop or else it will stay there forever 384 | on_window_event_listeners.lock().unwrap().clear(); 385 | 386 | if let Some(webview) = webview_weak.upgrade() { 387 | log::warn!( 388 | "The versoview controller reference count is not 0 on window close, \ 389 | there're leaks happening, shutting down this versoview instance regardless" 390 | ); 391 | if let Err(error) = webview.lock().unwrap().exit() { 392 | log::error!("Failed to exit the webview: {error}"); 393 | } 394 | } 395 | 396 | let is_empty = windows.is_empty(); 397 | if !is_empty { 398 | return false; 399 | } 400 | 401 | let (tx, rx) = channel(); 402 | callback(RunEvent::ExitRequested { code: None, tx }); 403 | 404 | let recv = rx.try_recv(); 405 | let should_prevent = matches!(recv, Ok(ExitRequestedEventAction::Prevent)); 406 | 407 | !should_prevent 408 | } 409 | } 410 | 411 | // Copied from wry 412 | /// WebView2 supports non-standard protocols only on Windows 10+, so we have to use a workaround, 413 | /// conveting `{protocol}://localhost/abc` to `{http_or_https}://{protocol}.localhost/abc`, 414 | /// and this function tests if the URI starts with `{http_or_https}://{protocol}.` 415 | /// 416 | /// See https://github.com/MicrosoftEdge/WebView2Feedback/issues/73 417 | #[cfg(windows)] 418 | fn is_work_around_uri(uri: &str, http_or_https: &str, protocol: &str) -> bool { 419 | uri.strip_prefix(http_or_https) 420 | .and_then(|rest| rest.strip_prefix("://")) 421 | .and_then(|rest| rest.strip_prefix(protocol)) 422 | .and_then(|rest| rest.strip_prefix(".")) 423 | .is_some() 424 | } 425 | 426 | // This is a work around wry did for old version of webview2, and tauri also expects it... 427 | // On Windows, the custom protocol looks like `http://.` while other platforms, it looks like `://` 428 | // And we need to revert this here to align with the wry behavior... 429 | #[cfg(windows)] 430 | fn revert_custom_protocol_work_around( 431 | uri: &str, 432 | http_or_https: &'static str, 433 | protocol: &str, 434 | ) -> std::result::Result { 435 | uri.replace( 436 | &work_around_uri_prefix(http_or_https, protocol), 437 | &format!("{protocol}://"), 438 | ) 439 | .parse() 440 | } 441 | 442 | #[cfg(windows)] 443 | fn work_around_uri_prefix(http_or_https: &str, protocol: &str) -> String { 444 | format!("{http_or_https}://{protocol}.") 445 | } 446 | 447 | impl Debug for RuntimeContext { 448 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 449 | f.debug_struct("RuntimeContext").finish() 450 | } 451 | } 452 | 453 | /// A handle to the [`VersoRuntime`] runtime. 454 | #[derive(Debug, Clone)] 455 | pub struct VersoRuntimeHandle { 456 | context: RuntimeContext, 457 | } 458 | 459 | impl RuntimeHandle for VersoRuntimeHandle { 460 | type Runtime = VersoRuntime; 461 | 462 | fn create_proxy(&self) -> EventProxy { 463 | EventProxy(self.context.event_proxy.clone()) 464 | } 465 | 466 | /// Unsupported, has no effect 467 | #[cfg(target_os = "macos")] 468 | #[cfg_attr(docsrs, doc(cfg(target_os = "macos")))] 469 | fn set_activation_policy( 470 | &self, 471 | activation_policy: tauri_runtime::ActivationPolicy, 472 | ) -> Result<()> { 473 | Ok(()) 474 | } 475 | 476 | /// Unsupported, has no effect 477 | #[cfg(target_os = "macos")] 478 | #[cfg_attr(docsrs, doc(cfg(target_os = "macos")))] 479 | fn set_dock_visibility(&self, visible: bool) -> Result<()> { 480 | Ok(()) 481 | } 482 | 483 | fn request_exit(&self, code: i32) -> Result<()> { 484 | self.context.send_message(Message::RequestExit(code)) 485 | } 486 | 487 | /// `after_window_creation` not supported 488 | /// 489 | /// Only creating the window with a webview is supported, 490 | /// will return [`tauri_runtime::Error::CreateWindow`] if there is no [`PendingWindow::webview`] 491 | fn create_window) + Send + 'static>( 492 | &self, 493 | pending: PendingWindow, 494 | after_window_creation: Option, 495 | ) -> Result> { 496 | self.context.create_window(pending, after_window_creation) 497 | } 498 | 499 | /// Unsupported, always fail with [`tauri_runtime::Error::CreateWindow`] 500 | fn create_webview( 501 | &self, 502 | window_id: WindowId, 503 | pending: PendingWebview, 504 | ) -> Result> { 505 | Err(tauri_runtime::Error::CreateWindow) 506 | } 507 | 508 | /// Run a task on the main thread. 509 | fn run_on_main_thread(&self, f: F) -> Result<()> { 510 | self.context.run_on_main_thread(f) 511 | } 512 | 513 | fn primary_monitor(&self) -> Option { 514 | self.context 515 | .run_on_main_thread_with_event_loop(|e| e.tauri_primary_monitor()) 516 | .ok() 517 | .flatten() 518 | } 519 | 520 | fn monitor_from_point(&self, x: f64, y: f64) -> Option { 521 | self.context 522 | .run_on_main_thread_with_event_loop(move |e| e.tauri_monitor_from_point(x, y)) 523 | .ok() 524 | .flatten() 525 | } 526 | 527 | fn available_monitors(&self) -> Vec { 528 | self.context 529 | .run_on_main_thread_with_event_loop(|e| e.tauri_available_monitors()) 530 | .unwrap() 531 | } 532 | 533 | fn cursor_position(&self) -> Result> { 534 | self.context 535 | .run_on_main_thread_with_event_loop(|e| e.tauri_cursor_position())? 536 | } 537 | 538 | fn set_theme(&self, theme: Option) { 539 | *self.context.prefered_theme.lock().unwrap() = theme; 540 | for window in self.context.windows.lock().unwrap().values() { 541 | if let Err(error) = window 542 | .webview 543 | .lock() 544 | .unwrap() 545 | .set_theme(theme.map(to_verso_theme)) 546 | { 547 | log::error!("Failed to set the theme for webview: {error}"); 548 | } 549 | } 550 | let _ = self 551 | .context 552 | .run_on_main_thread_with_event_loop(move |e| e.set_theme(theme.map(to_tao_theme))); 553 | } 554 | 555 | /// Unsupported, has no effect 556 | #[cfg(target_os = "macos")] 557 | fn show(&self) -> Result<()> { 558 | Ok(()) 559 | } 560 | 561 | /// Unsupported, has no effect 562 | #[cfg(target_os = "macos")] 563 | fn hide(&self) -> Result<()> { 564 | Ok(()) 565 | } 566 | 567 | /// Unsupported, will always return an error 568 | fn display_handle( 569 | &self, 570 | ) -> std::result::Result { 571 | Err(raw_window_handle::HandleError::NotSupported) 572 | } 573 | 574 | /// Unsupported, has no effect, the callback will not be called 575 | #[cfg(any(target_os = "macos", target_os = "ios"))] 576 | #[cfg_attr(docsrs, doc(cfg(any(target_os = "macos", target_os = "ios"))))] 577 | fn fetch_data_store_identifiers) + Send + 'static>( 578 | &self, 579 | cb: F, 580 | ) -> Result<()> { 581 | Ok(()) 582 | } 583 | 584 | /// Unsupported, has no effect, the callback will not be called 585 | #[cfg(any(target_os = "macos", target_os = "ios"))] 586 | #[cfg_attr(docsrs, doc(cfg(any(target_os = "macos", target_os = "ios"))))] 587 | fn remove_data_store) + Send + 'static>( 588 | &self, 589 | uuid: [u8; 16], 590 | cb: F, 591 | ) -> Result<()> { 592 | Ok(()) 593 | } 594 | } 595 | 596 | #[derive(Debug, Clone)] 597 | pub struct EventProxy(TaoEventLoopProxy>); 598 | 599 | impl EventLoopProxy for EventProxy { 600 | fn send_event(&self, event: T) -> Result<()> { 601 | self.0 602 | .send_event(Message::UserEvent(event)) 603 | .map_err(|_| Error::FailedToSendMessage) 604 | } 605 | } 606 | 607 | /// A Tauri Runtime wrapper around Verso. 608 | #[derive(Debug)] 609 | pub struct VersoRuntime { 610 | pub context: RuntimeContext, 611 | event_loop: EventLoop>, 612 | } 613 | 614 | impl VersoRuntime { 615 | fn init(event_loop: EventLoop>) -> Self { 616 | let context = RuntimeContext { 617 | windows: Default::default(), 618 | prefered_theme: Arc::default(), 619 | event_proxy: event_loop.create_proxy(), 620 | main_thread: DispatcherMainThreadContext { 621 | window_target: event_loop.deref().clone(), 622 | }, 623 | main_thread_id: current_thread().id(), 624 | next_window_id: Default::default(), 625 | next_webview_id: Default::default(), 626 | next_window_event_id: Default::default(), 627 | next_webview_event_id: Default::default(), 628 | }; 629 | Self { 630 | context, 631 | event_loop, 632 | } 633 | } 634 | 635 | fn init_with_builder( 636 | mut event_loop_builder: EventLoopBuilder>, 637 | args: RuntimeInitArgs, 638 | ) -> Self { 639 | #[cfg(windows)] 640 | if let Some(hook) = args.msg_hook { 641 | use tao::platform::windows::EventLoopBuilderExtWindows; 642 | event_loop_builder.with_msg_hook(hook); 643 | } 644 | 645 | #[cfg(any( 646 | target_os = "linux", 647 | target_os = "dragonfly", 648 | target_os = "freebsd", 649 | target_os = "netbsd", 650 | target_os = "openbsd" 651 | ))] 652 | if let Some(app_id) = args.app_id { 653 | use tao::platform::unix::EventLoopBuilderExtUnix; 654 | event_loop_builder.with_app_id(app_id); 655 | } 656 | Self::init(event_loop_builder.build()) 657 | } 658 | } 659 | 660 | impl Runtime for VersoRuntime { 661 | type WindowDispatcher = VersoWindowDispatcher; 662 | type WebviewDispatcher = VersoWebviewDispatcher; 663 | type Handle = VersoRuntimeHandle; 664 | type EventLoopProxy = EventProxy; 665 | 666 | /// `args.msg_hook` hooks on the event loop of this process, 667 | /// this doesn't work for the event loop of versoview instances 668 | fn new(args: RuntimeInitArgs) -> Result { 669 | let event_loop_builder = EventLoopBuilder::>::with_user_event(); 670 | Ok(Self::init_with_builder(event_loop_builder, args)) 671 | } 672 | 673 | /// `args.msg_hook` hooks on the event loop of this process, 674 | /// this doesn't work for the event loop of versoview instances 675 | #[cfg(any(windows, target_os = "linux"))] 676 | fn new_any_thread(args: RuntimeInitArgs) -> Result { 677 | let mut event_loop_builder = EventLoopBuilder::>::with_user_event(); 678 | #[cfg(target_os = "linux")] 679 | use tao::platform::unix::EventLoopBuilderExtUnix; 680 | #[cfg(windows)] 681 | use tao::platform::windows::EventLoopBuilderExtWindows; 682 | event_loop_builder.with_any_thread(true); 683 | Ok(Self::init_with_builder(event_loop_builder, args)) 684 | } 685 | 686 | fn create_proxy(&self) -> EventProxy { 687 | EventProxy(self.event_loop.create_proxy()) 688 | } 689 | 690 | fn handle(&self) -> Self::Handle { 691 | VersoRuntimeHandle { 692 | context: self.context.clone(), 693 | } 694 | } 695 | 696 | /// `after_window_creation` not supported 697 | /// 698 | /// Only creating the window with a webview is supported, 699 | /// will return [`tauri_runtime::Error::CreateWindow`] if there is no [`PendingWindow::webview`] 700 | fn create_window) + Send + 'static>( 701 | &self, 702 | pending: PendingWindow, 703 | after_window_creation: Option, 704 | ) -> Result> { 705 | self.context.create_window(pending, after_window_creation) 706 | } 707 | 708 | /// Unsupported, always fail with [`tauri_runtime::Error::CreateWindow`] 709 | fn create_webview( 710 | &self, 711 | window_id: WindowId, 712 | pending: PendingWebview, 713 | ) -> Result> { 714 | Err(tauri_runtime::Error::CreateWindow) 715 | } 716 | 717 | fn primary_monitor(&self) -> Option { 718 | self.event_loop.tauri_primary_monitor() 719 | } 720 | 721 | fn monitor_from_point(&self, x: f64, y: f64) -> Option { 722 | self.event_loop.tauri_monitor_from_point(x, y) 723 | } 724 | 725 | fn available_monitors(&self) -> Vec { 726 | self.event_loop.tauri_available_monitors() 727 | } 728 | 729 | fn cursor_position(&self) -> Result> { 730 | self.event_loop.tauri_cursor_position() 731 | } 732 | 733 | fn set_theme(&self, theme: Option) { 734 | *self.context.prefered_theme.lock().unwrap() = theme; 735 | for window in self.context.windows.lock().unwrap().values() { 736 | if let Err(error) = window 737 | .webview 738 | .lock() 739 | .unwrap() 740 | .set_theme(theme.map(to_verso_theme)) 741 | { 742 | log::error!("Failed to set the theme for webview: {error}"); 743 | } 744 | } 745 | self.event_loop.set_theme(theme.map(to_tao_theme)); 746 | } 747 | 748 | /// Unsupported, has no effect when called 749 | #[cfg(target_os = "macos")] 750 | #[cfg_attr(docsrs, doc(cfg(target_os = "macos")))] 751 | fn set_activation_policy(&mut self, activation_policy: tauri_runtime::ActivationPolicy) {} 752 | 753 | /// Unsupported, has no effect when called 754 | #[cfg(target_os = "macos")] 755 | #[cfg_attr(docsrs, doc(cfg(target_os = "macos")))] 756 | fn show(&self) {} 757 | 758 | /// Unsupported, has no effect when called 759 | #[cfg(target_os = "macos")] 760 | #[cfg_attr(docsrs, doc(cfg(target_os = "macos")))] 761 | fn hide(&self) {} 762 | 763 | /// Unsupported, has no effect 764 | #[cfg(target_os = "macos")] 765 | #[cfg_attr(docsrs, doc(cfg(target_os = "macos")))] 766 | fn set_dock_visibility(&mut self, visible: bool) {} 767 | 768 | /// Unsupported, has no effect when called 769 | fn set_device_event_filter(&mut self, filter: DeviceEventFilter) {} 770 | 771 | /// Unsupported, has no effect when called 772 | fn run_iteration)>(&mut self, callback: F) {} 773 | 774 | fn run) + 'static>(self, callback: F) { 775 | let exit_code = self.run_return(callback); 776 | // std::process::exit(exit_code); 777 | } 778 | 779 | fn run_return) + 'static>(mut self, mut callback: F) -> i32 { 780 | self.event_loop 781 | .run_return(|event, event_loop, control_flow| { 782 | if *control_flow != ControlFlow::Exit { 783 | *control_flow = ControlFlow::Wait; 784 | } 785 | 786 | match event { 787 | TaoEvent::NewEvents(StartCause::Init) => { 788 | callback(RunEvent::Ready); 789 | } 790 | TaoEvent::NewEvents(StartCause::Poll) => { 791 | callback(RunEvent::Resumed); 792 | } 793 | TaoEvent::MainEventsCleared => { 794 | callback(RunEvent::MainEventsCleared); 795 | } 796 | TaoEvent::LoopDestroyed => { 797 | callback(RunEvent::Exit); 798 | } 799 | TaoEvent::UserEvent(user_event) => match user_event { 800 | Message::Task(p) => p(), 801 | Message::TaskWithEventLoop(p) => p(event_loop), 802 | Message::CloseWindow(id) => { 803 | let should_exit = 804 | self.context 805 | .handle_close_window_request(&mut callback, id, false); 806 | if should_exit { 807 | *control_flow = ControlFlow::Exit; 808 | } 809 | } 810 | Message::DestroyWindow(id) => { 811 | let should_exit = 812 | self.context 813 | .handle_close_window_request(&mut callback, id, true); 814 | if should_exit { 815 | *control_flow = ControlFlow::Exit; 816 | } 817 | } 818 | Message::RequestExit(code) => { 819 | let (tx, rx) = channel(); 820 | callback(RunEvent::ExitRequested { 821 | code: Some(code), 822 | tx, 823 | }); 824 | 825 | let recv = rx.try_recv(); 826 | let should_prevent = 827 | matches!(recv, Ok(ExitRequestedEventAction::Prevent)); 828 | 829 | if !should_prevent { 830 | *control_flow = ControlFlow::Exit; 831 | } 832 | } 833 | Message::UserEvent(user_event) => callback(RunEvent::UserEvent(user_event)), 834 | }, 835 | _ => {} 836 | } 837 | }) 838 | } 839 | } 840 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | pub fn to_verso_theme(theme: tauri_utils::Theme) -> verso::Theme { 2 | match theme { 3 | tauri_utils::Theme::Dark => verso::Theme::Dark, 4 | _ => verso::Theme::Light, 5 | } 6 | } 7 | 8 | pub fn from_verso_theme(theme: verso::Theme) -> tauri_utils::Theme { 9 | match theme { 10 | verso::Theme::Dark => tauri_utils::Theme::Dark, 11 | verso::Theme::Light => tauri_utils::Theme::Light, 12 | } 13 | } 14 | 15 | pub fn to_tao_theme(theme: tauri_utils::Theme) -> tao::window::Theme { 16 | match theme { 17 | tauri_utils::Theme::Dark => tao::window::Theme::Dark, 18 | _ => tao::window::Theme::Light, 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/webview.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused_variables)] 2 | 3 | use tauri_runtime::{ 4 | Error, Result, UserEvent, WebviewDispatch, WebviewEventId, 5 | dpi::{PhysicalPosition, PhysicalSize, Position, Size}, 6 | window::{WebviewEvent, WindowId}, 7 | }; 8 | use url::Url; 9 | use verso::VersoviewController; 10 | 11 | use std::{ 12 | fmt::{self, Debug}, 13 | sync::{Arc, Mutex}, 14 | }; 15 | 16 | use crate::{RuntimeContext, VersoRuntime}; 17 | 18 | /// The Tauri [`WebviewDispatch`] for [`VersoRuntime`]. 19 | #[derive(Clone)] 20 | pub struct VersoWebviewDispatcher { 21 | pub(crate) id: u32, 22 | pub(crate) context: RuntimeContext, 23 | pub(crate) webview: Arc>, 24 | } 25 | 26 | impl Debug for VersoWebviewDispatcher { 27 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 28 | f.debug_struct("VersoWebviewDispatcher") 29 | .field("id", &self.id) 30 | .field("context", &self.context) 31 | .field("webview", &"VersoviewController") 32 | .finish() 33 | } 34 | } 35 | 36 | impl WebviewDispatch for VersoWebviewDispatcher { 37 | type Runtime = VersoRuntime; 38 | 39 | fn run_on_main_thread(&self, f: F) -> Result<()> { 40 | self.context.run_on_main_thread(f) 41 | } 42 | 43 | /// Unsupported, has no effect when called, the callback will not be called 44 | fn on_webview_event(&self, f: F) -> WebviewEventId { 45 | self.context.next_window_event_id() 46 | } 47 | 48 | /// Unsupported, has no effect when called, the callback will not be called 49 | fn with_webview) + Send + 'static>(&self, f: F) -> Result<()> { 50 | Ok(()) 51 | } 52 | 53 | /// Unsupported, has no effect when called 54 | fn set_zoom(&self, scale_factor: f64) -> Result<()> { 55 | Ok(()) 56 | } 57 | 58 | fn eval_script>(&self, script: S) -> Result<()> { 59 | self.webview 60 | .lock() 61 | .unwrap() 62 | .execute_script(script.into()) 63 | .map_err(|_| Error::FailedToSendMessage)?; 64 | Ok(()) 65 | } 66 | 67 | fn url(&self) -> Result { 68 | Ok(self 69 | .webview 70 | .lock() 71 | .unwrap() 72 | .get_current_url() 73 | .map_err(|_| Error::FailedToSendMessage)? 74 | .to_string()) 75 | } 76 | 77 | fn bounds(&self) -> Result { 78 | Ok(tauri_runtime::Rect { 79 | position: self.position()?.into(), 80 | size: self.size()?.into(), 81 | }) 82 | } 83 | 84 | fn position(&self) -> Result> { 85 | Ok(PhysicalPosition { x: 0, y: 0 }) 86 | } 87 | 88 | fn size(&self) -> Result> { 89 | let size = self 90 | .webview 91 | .lock() 92 | .unwrap() 93 | .get_inner_size() 94 | .map_err(|_| Error::FailedToSendMessage)?; 95 | Ok(size) 96 | } 97 | 98 | fn navigate(&self, url: Url) -> Result<()> { 99 | self.webview 100 | .lock() 101 | .unwrap() 102 | .navigate(url) 103 | .map_err(|_| Error::FailedToSendMessage)?; 104 | Ok(()) 105 | } 106 | 107 | /// Unsupported, has no effect when called 108 | fn print(&self) -> Result<()> { 109 | Ok(()) 110 | } 111 | 112 | /// Unsupported, has no effect when called, 113 | /// the versoview controls both the webview and the window 114 | /// use the method from the parent window instead 115 | fn close(&self) -> Result<()> { 116 | Ok(()) 117 | } 118 | 119 | /// Unsupported, has no effect when called, 120 | /// the versoview controls both the webview and the window 121 | /// use the method from the parent window instead 122 | fn set_bounds(&self, bounds: tauri_runtime::Rect) -> Result<()> { 123 | Ok(()) 124 | } 125 | 126 | /// Unsupported, has no effect when called, 127 | /// the versoview controls both the webview and the window 128 | /// use the method from the parent window instead 129 | fn set_size(&self, _size: Size) -> Result<()> { 130 | Ok(()) 131 | } 132 | 133 | /// Unsupported, has no effect when called, 134 | /// the versoview controls both the webview and the window 135 | /// use the method from the parent window instead 136 | fn set_position(&self, _position: Position) -> Result<()> { 137 | Ok(()) 138 | } 139 | 140 | /// Unsupported, has no effect when called, 141 | /// the versoview controls both the webview and the window 142 | /// use the method from the parent window instead 143 | fn set_focus(&self) -> Result<()> { 144 | Ok(()) 145 | } 146 | 147 | /// Unsupported, has no effect when called 148 | fn reparent(&self, window_id: WindowId) -> Result<()> { 149 | Ok(()) 150 | } 151 | 152 | /// Unsupported, has no effect when called 153 | fn set_auto_resize(&self, auto_resize: bool) -> Result<()> { 154 | Ok(()) 155 | } 156 | 157 | /// Unsupported, has no effect when called 158 | fn clear_all_browsing_data(&self) -> Result<()> { 159 | Ok(()) 160 | } 161 | 162 | /// Unsupported, has no effect when called, 163 | /// the versoview controls both the webview and the window 164 | /// use the method from the parent window instead 165 | fn hide(&self) -> Result<()> { 166 | Ok(()) 167 | } 168 | 169 | /// Unsupported, has no effect when called, 170 | /// the versoview controls both the webview and the window 171 | /// use the method from the parent window instead 172 | fn show(&self) -> Result<()> { 173 | Ok(()) 174 | } 175 | 176 | /// Unsupported, has no effect when called 177 | fn set_background_color(&self, color: Option) -> Result<()> { 178 | Ok(()) 179 | } 180 | 181 | /// Unsupported, has no effect when called 182 | #[cfg(debug_assertions)] 183 | fn open_devtools(&self) {} 184 | 185 | /// Unsupported, has no effect when called 186 | #[cfg(debug_assertions)] 187 | fn close_devtools(&self) {} 188 | 189 | /// Always false since we don't have devtools built-in 190 | #[cfg(debug_assertions)] 191 | fn is_devtools_open(&self) -> Result { 192 | Ok(false) 193 | } 194 | 195 | fn reload(&self) -> Result<()> { 196 | self.webview 197 | .lock() 198 | .unwrap() 199 | .reload() 200 | .map_err(|_| Error::FailedToSendMessage)?; 201 | Ok(()) 202 | } 203 | 204 | /// Unsupported, always returns an empty vector 205 | fn cookies_for_url(&self, url: Url) -> Result>> { 206 | Ok(Vec::new()) 207 | } 208 | 209 | /// Unsupported, always returns an empty vector 210 | fn cookies(&self) -> Result>> { 211 | Ok(Vec::new()) 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /src/window.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused_variables)] 2 | 3 | use tauri::{LogicalPosition, LogicalSize}; 4 | use tauri_runtime::{ 5 | Error, Icon, ProgressBarState, Result, UserAttentionType, UserEvent, WindowDispatch, 6 | WindowEventId, 7 | dpi::{PhysicalPosition, PhysicalSize, Position, Size}, 8 | monitor::Monitor, 9 | webview::{DetachedWebview, PendingWebview}, 10 | window::{ 11 | CursorIcon, DetachedWindow, PendingWindow, RawWindow, WindowBuilder, WindowBuilderBase, 12 | WindowEvent, WindowId, 13 | }, 14 | }; 15 | use tauri_utils::{Theme, config::WindowConfig}; 16 | use verso::{VersoBuilder, VersoviewController}; 17 | #[cfg(windows)] 18 | use windows::Win32::Foundation::HWND; 19 | 20 | use std::{ 21 | collections::HashMap, 22 | fmt::{self, Debug}, 23 | sync::{Arc, Mutex}, 24 | }; 25 | 26 | use crate::{ 27 | RuntimeContext, VersoRuntime, 28 | event_loop_ext::TaoEventLoopWindowTargetExt, 29 | get_verso_devtools_port, get_verso_resource_directory, 30 | runtime::Message, 31 | utils::{from_verso_theme, to_verso_theme}, 32 | }; 33 | 34 | pub(crate) struct Window { 35 | pub(crate) label: String, 36 | pub(crate) webview: Arc>, 37 | pub(crate) on_window_event_listeners: WindowEventListeners, 38 | } 39 | 40 | #[derive(Debug, Clone)] 41 | pub struct VersoWindowBuilder { 42 | pub verso_builder: VersoBuilder, 43 | pub has_icon: bool, 44 | pub theme: Option, 45 | } 46 | 47 | impl Default for VersoWindowBuilder { 48 | fn default() -> Self { 49 | let mut verso_builder = VersoBuilder::new(); 50 | if let Some(resource_directory) = get_verso_resource_directory() { 51 | verso_builder = verso_builder.resources_directory(resource_directory); 52 | } 53 | if let Some(devtools_port) = get_verso_devtools_port() { 54 | verso_builder = verso_builder.devtools_port(devtools_port); 55 | } 56 | // Default `decorated` to `true` to align with the wry runtime 57 | verso_builder = verso_builder.decorated(true); 58 | // Default `transparent` to `false` to align with the wry runtime 59 | verso_builder = verso_builder.transparent(false); 60 | Self { 61 | verso_builder, 62 | has_icon: false, 63 | theme: None, 64 | } 65 | } 66 | } 67 | 68 | impl WindowBuilderBase for VersoWindowBuilder {} 69 | 70 | impl WindowBuilder for VersoWindowBuilder { 71 | fn new() -> Self { 72 | Self::default() 73 | } 74 | 75 | fn with_config(config: &WindowConfig) -> Self { 76 | let builder = Self::default(); 77 | let mut verso_builder = builder.verso_builder; 78 | verso_builder = verso_builder 79 | .focused(config.focus) 80 | .fullscreen(config.fullscreen) 81 | .maximized(config.maximized) 82 | .visible(config.visible) 83 | .inner_size(LogicalSize::new(config.width, config.height)) 84 | .title(config.title.clone()) 85 | .decorated(config.decorations) 86 | .transparent(config.transparent); 87 | 88 | if let (Some(x), Some(y)) = (config.x, config.y) { 89 | verso_builder = verso_builder.position(LogicalPosition::new(x, y)); 90 | }; 91 | 92 | if let Some(theme) = config.theme { 93 | verso_builder = verso_builder.theme(to_verso_theme(theme)); 94 | } 95 | 96 | Self { 97 | verso_builder, 98 | has_icon: false, 99 | theme: None, 100 | } 101 | } 102 | 103 | /// Unsupported, has no effect 104 | fn center(self) -> Self { 105 | self 106 | } 107 | 108 | /// Note: x and y are in logical unit 109 | fn position(mut self, x: f64, y: f64) -> Self { 110 | self.verso_builder = self.verso_builder.position(LogicalPosition::new(x, y)); 111 | self 112 | } 113 | 114 | /// Note: width and height are in logical unit 115 | fn inner_size(mut self, width: f64, height: f64) -> Self { 116 | self.verso_builder = self 117 | .verso_builder 118 | .inner_size(LogicalSize::new(width, height)); 119 | self 120 | } 121 | 122 | /// Unsupported, has no effect 123 | fn min_inner_size(self, min_width: f64, min_height: f64) -> Self { 124 | self 125 | } 126 | 127 | /// Unsupported, has no effect 128 | fn max_inner_size(self, max_width: f64, max_height: f64) -> Self { 129 | self 130 | } 131 | 132 | /// Unsupported, has no effect 133 | fn inner_size_constraints( 134 | self, 135 | constraints: tauri_runtime::window::WindowSizeConstraints, 136 | ) -> Self { 137 | self 138 | } 139 | 140 | /// Unsupported, has no effect 141 | fn resizable(self, resizable: bool) -> Self { 142 | self 143 | } 144 | 145 | /// Unsupported, has no effect 146 | fn maximizable(self, resizable: bool) -> Self { 147 | self 148 | } 149 | 150 | /// Unsupported, has no effect 151 | fn minimizable(self, resizable: bool) -> Self { 152 | self 153 | } 154 | 155 | /// Unsupported, has no effect 156 | fn closable(self, resizable: bool) -> Self { 157 | self 158 | } 159 | 160 | fn title>(mut self, title: S) -> Self { 161 | self.verso_builder = self.verso_builder.title(title); 162 | self 163 | } 164 | 165 | fn fullscreen(mut self, fullscreen: bool) -> Self { 166 | self.verso_builder = self.verso_builder.fullscreen(fullscreen); 167 | self 168 | } 169 | 170 | fn focused(mut self, focused: bool) -> Self { 171 | self.verso_builder = self.verso_builder.focused(focused); 172 | self 173 | } 174 | 175 | fn maximized(mut self, maximized: bool) -> Self { 176 | self.verso_builder = self.verso_builder.maximized(maximized); 177 | self 178 | } 179 | 180 | fn visible(mut self, visible: bool) -> Self { 181 | self.verso_builder = self.verso_builder.visible(visible); 182 | self 183 | } 184 | 185 | fn decorations(mut self, decorations: bool) -> Self { 186 | self.verso_builder = self.verso_builder.decorated(decorations); 187 | self 188 | } 189 | 190 | fn always_on_bottom(mut self, always_on_bottom: bool) -> Self { 191 | self.verso_builder = self.verso_builder.window_level(if always_on_bottom { 192 | verso::WindowLevel::AlwaysOnTop 193 | } else { 194 | verso::WindowLevel::Normal 195 | }); 196 | self 197 | } 198 | 199 | fn always_on_top(mut self, always_on_top: bool) -> Self { 200 | self.verso_builder = self.verso_builder.window_level(if always_on_top { 201 | verso::WindowLevel::AlwaysOnTop 202 | } else { 203 | verso::WindowLevel::Normal 204 | }); 205 | self 206 | } 207 | 208 | /// Unsupported, has no effect 209 | fn visible_on_all_workspaces(self, visible_on_all_workspaces: bool) -> Self { 210 | self 211 | } 212 | 213 | /// Unsupported, has no effect 214 | fn content_protected(self, protected: bool) -> Self { 215 | self 216 | } 217 | 218 | fn icon(mut self, icon: Icon<'_>) -> Result { 219 | self.verso_builder = self.verso_builder.icon(verso::Icon { 220 | rgba: icon.rgba.to_vec(), 221 | width: icon.width, 222 | height: icon.height, 223 | }); 224 | self.has_icon = true; 225 | Ok(self) 226 | } 227 | 228 | /// Unsupported, has no effect 229 | fn skip_taskbar(self, skip: bool) -> Self { 230 | self 231 | } 232 | 233 | /// Unsupported, has no effect 234 | fn window_classname>(self, classname: S) -> Self { 235 | self 236 | } 237 | 238 | /// Unsupported, has no effect 239 | fn shadow(self, enable: bool) -> Self { 240 | self 241 | } 242 | 243 | /// Unsupported, has no effect 244 | #[cfg(target_os = "macos")] 245 | fn parent(self, parent: *mut std::ffi::c_void) -> Self { 246 | self 247 | } 248 | 249 | /// Unsupported, has no effect 250 | #[cfg(any( 251 | target_os = "linux", 252 | target_os = "dragonfly", 253 | target_os = "freebsd", 254 | target_os = "netbsd", 255 | target_os = "openbsd" 256 | ))] 257 | fn transient_for(self, parent: &impl gtk::glib::IsA) -> Self { 258 | self 259 | } 260 | 261 | /// Unsupported, has no effect 262 | #[cfg(windows)] 263 | fn drag_and_drop(self, enabled: bool) -> Self { 264 | self 265 | } 266 | 267 | /// Unsupported, has no effect 268 | #[cfg(target_os = "macos")] 269 | fn title_bar_style(self, style: tauri_utils::TitleBarStyle) -> Self { 270 | self 271 | } 272 | 273 | /// Unsupported, has no effect 274 | #[cfg(target_os = "macos")] 275 | fn hidden_title(self, transparent: bool) -> Self { 276 | self 277 | } 278 | 279 | /// Unsupported, has no effect 280 | #[cfg(target_os = "macos")] 281 | fn tabbing_identifier(self, identifier: &str) -> Self { 282 | self 283 | } 284 | 285 | /// Unsupported, has no effect 286 | #[cfg(target_os = "macos")] 287 | fn traffic_light_position>(self, position: P) -> Self { 288 | self 289 | } 290 | 291 | fn theme(mut self, theme: Option) -> Self { 292 | if let Some(theme) = theme { 293 | self.verso_builder = self.verso_builder.theme(to_verso_theme(theme)); 294 | self.theme = Some(theme); 295 | } 296 | self 297 | } 298 | 299 | fn has_icon(&self) -> bool { 300 | self.has_icon 301 | } 302 | 303 | fn get_theme(&self) -> Option { 304 | self.theme 305 | } 306 | 307 | /// Unsupported, has no effect 308 | fn background_color(self, _color: tauri_utils::config::Color) -> Self { 309 | self 310 | } 311 | 312 | /// Unsupported, has no effect 313 | #[cfg(windows)] 314 | fn owner(self, owner: HWND) -> Self { 315 | self 316 | } 317 | 318 | /// Unsupported, has no effect 319 | #[cfg(windows)] 320 | fn parent(self, parent: HWND) -> Self { 321 | self 322 | } 323 | 324 | #[cfg(any(not(target_os = "macos"), feature = "macos-private-api"))] 325 | #[cfg_attr( 326 | docsrs, 327 | doc(cfg(any(not(target_os = "macos"), feature = "macos-private-api"))) 328 | )] 329 | fn transparent(mut self, transparent: bool) -> Self { 330 | self.verso_builder = self.verso_builder.transparent(transparent); 331 | self 332 | } 333 | 334 | /// Unsupported, has no effect 335 | fn prevent_overflow(self) -> Self { 336 | self 337 | } 338 | 339 | /// Unsupported, has no effect 340 | fn prevent_overflow_with_margin(self, margin: tauri_runtime::dpi::Size) -> Self { 341 | self 342 | } 343 | } 344 | 345 | pub type WindowEventHandler = Box; 346 | pub type WindowEventListeners = Arc>>; 347 | 348 | /// The Tauri [`WindowDispatch`] for [`VersoRuntime`]. 349 | #[derive(Clone)] 350 | pub struct VersoWindowDispatcher { 351 | pub(crate) id: WindowId, 352 | pub(crate) context: RuntimeContext, 353 | pub(crate) webview: Arc>, 354 | pub(crate) on_window_event_listeners: WindowEventListeners, 355 | } 356 | 357 | impl Debug for VersoWindowDispatcher { 358 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 359 | f.debug_struct("VersoWebviewDispatcher") 360 | .field("id", &self.id) 361 | .field("context", &self.context) 362 | .field("webview", &"VersoviewController") 363 | .finish() 364 | } 365 | } 366 | 367 | impl WindowDispatch for VersoWindowDispatcher { 368 | type Runtime = VersoRuntime; 369 | 370 | type WindowBuilder = VersoWindowBuilder; 371 | 372 | fn run_on_main_thread(&self, f: F) -> Result<()> { 373 | self.context.run_on_main_thread(f) 374 | } 375 | 376 | /// Currently only [`WindowEvent::CloseRequested`] will be emitted 377 | fn on_window_event(&self, f: F) -> WindowEventId { 378 | let id = self.context.next_window_event_id(); 379 | self.on_window_event_listeners 380 | .lock() 381 | .unwrap() 382 | .insert(id, Box::new(f)); 383 | id 384 | } 385 | 386 | fn scale_factor(&self) -> Result { 387 | self.webview 388 | .lock() 389 | .unwrap() 390 | .get_scale_factor() 391 | .map_err(|_| Error::FailedToSendMessage) 392 | } 393 | 394 | /// Returns the position of the top-left hand corner of the window's client area relative to the top-left hand corner of the desktop. 395 | /// 396 | /// ## Platform-specific 397 | /// 398 | /// **Wayland**: always return `PhysicalPosition { x: 0, y: 0 }` 399 | fn inner_position(&self) -> Result> { 400 | Ok(self 401 | .webview 402 | .lock() 403 | .unwrap() 404 | .get_inner_position() 405 | .map_err(|_| Error::FailedToSendMessage)? 406 | .unwrap_or_default()) 407 | } 408 | 409 | /// Returns the position of the top-left hand corner of the window relative to the top-left hand corner of the desktop. 410 | /// 411 | /// ## Platform-specific 412 | /// 413 | /// **Wayland**: always return `PhysicalPosition { x: 0, y: 0 }` 414 | fn outer_position(&self) -> Result> { 415 | Ok(self 416 | .webview 417 | .lock() 418 | .unwrap() 419 | .get_outer_position() 420 | .map_err(|_| Error::FailedToSendMessage)? 421 | .unwrap_or_default()) 422 | } 423 | 424 | fn inner_size(&self) -> Result> { 425 | self.webview 426 | .lock() 427 | .unwrap() 428 | .get_inner_size() 429 | .map_err(|_| Error::FailedToSendMessage) 430 | } 431 | 432 | fn outer_size(&self) -> Result> { 433 | self.webview 434 | .lock() 435 | .unwrap() 436 | .get_outer_size() 437 | .map_err(|_| Error::FailedToSendMessage) 438 | } 439 | 440 | fn is_fullscreen(&self) -> Result { 441 | self.webview 442 | .lock() 443 | .unwrap() 444 | .is_fullscreen() 445 | .map_err(|_| Error::FailedToSendMessage) 446 | } 447 | 448 | fn is_minimized(&self) -> Result { 449 | self.webview 450 | .lock() 451 | .unwrap() 452 | .is_minimized() 453 | .map_err(|_| Error::FailedToSendMessage) 454 | } 455 | 456 | fn is_maximized(&self) -> Result { 457 | self.webview 458 | .lock() 459 | .unwrap() 460 | .is_maximized() 461 | .map_err(|_| Error::FailedToSendMessage) 462 | } 463 | 464 | /// Unsupported, always returns false 465 | fn is_focused(&self) -> Result { 466 | Ok(false) 467 | } 468 | 469 | /// Unsupported, always returns false 470 | fn is_decorated(&self) -> Result { 471 | Ok(false) 472 | } 473 | 474 | /// Unsupported, always returns true 475 | fn is_resizable(&self) -> Result { 476 | Ok(true) 477 | } 478 | 479 | /// Unsupported, always returns true 480 | fn is_maximizable(&self) -> Result { 481 | Ok(true) 482 | } 483 | 484 | /// Unsupported, always returns true 485 | fn is_minimizable(&self) -> Result { 486 | Ok(true) 487 | } 488 | 489 | /// Unsupported, always returns true 490 | fn is_closable(&self) -> Result { 491 | Ok(true) 492 | } 493 | 494 | fn is_visible(&self) -> Result { 495 | self.webview 496 | .lock() 497 | .unwrap() 498 | .is_visible() 499 | .map_err(|_| Error::FailedToSendMessage) 500 | } 501 | 502 | fn title(&self) -> Result { 503 | Ok(self 504 | .webview 505 | .lock() 506 | .unwrap() 507 | .get_title() 508 | .map_err(|_| Error::FailedToSendMessage)?) 509 | } 510 | 511 | /// Unsupported, always returns [`None`] 512 | fn current_monitor(&self) -> Result> { 513 | Ok(None) 514 | } 515 | 516 | fn primary_monitor(&self) -> Result> { 517 | self.context 518 | .run_on_main_thread_with_event_loop(|e| e.tauri_primary_monitor()) 519 | } 520 | 521 | fn monitor_from_point(&self, x: f64, y: f64) -> Result> { 522 | self.context 523 | .run_on_main_thread_with_event_loop(move |e| e.tauri_monitor_from_point(x, y)) 524 | } 525 | 526 | fn available_monitors(&self) -> Result> { 527 | self.context 528 | .run_on_main_thread_with_event_loop(|e| e.tauri_available_monitors()) 529 | } 530 | 531 | fn theme(&self) -> Result { 532 | let theme = self 533 | .webview 534 | .lock() 535 | .unwrap() 536 | .get_theme() 537 | .map_err(|_| Error::FailedToSendMessage)?; 538 | Ok(from_verso_theme(theme)) 539 | } 540 | 541 | /// Unsupported, panics when called 542 | #[cfg(any( 543 | target_os = "linux", 544 | target_os = "dragonfly", 545 | target_os = "freebsd", 546 | target_os = "netbsd", 547 | target_os = "openbsd" 548 | ))] 549 | fn gtk_window(&self) -> Result { 550 | unimplemented!() 551 | } 552 | 553 | /// Unsupported, panics when called 554 | #[cfg(any( 555 | target_os = "linux", 556 | target_os = "dragonfly", 557 | target_os = "freebsd", 558 | target_os = "netbsd", 559 | target_os = "openbsd" 560 | ))] 561 | fn default_vbox(&self) -> Result { 562 | unimplemented!() 563 | } 564 | 565 | /// Unsupported, has no effect when called 566 | fn center(&self) -> Result<()> { 567 | Ok(()) 568 | } 569 | 570 | /// Unsupported, has no effect when called 571 | fn request_user_attention(&self, request_type: Option) -> Result<()> { 572 | Ok(()) 573 | } 574 | 575 | /// `after_window_creation` not supported 576 | /// 577 | /// Only creating the window with a webview is supported, 578 | /// will return [`tauri_runtime::Error::CreateWindow`] if there is no [`PendingWindow::webview`] 579 | fn create_window) + Send + 'static>( 580 | &mut self, 581 | pending: PendingWindow, 582 | after_window_creation: Option, 583 | ) -> Result> { 584 | self.context.create_window(pending, after_window_creation) 585 | } 586 | 587 | /// Unsupported, always fail with [`tauri_runtime::Error::CreateWindow`] 588 | fn create_webview( 589 | &mut self, 590 | pending: PendingWebview, 591 | ) -> Result> { 592 | Err(tauri_runtime::Error::CreateWindow) 593 | } 594 | 595 | /// Unsupported, has no effect when called 596 | fn set_resizable(&self, resizable: bool) -> Result<()> { 597 | Ok(()) 598 | } 599 | 600 | /// Unsupported, has no effect when called 601 | fn set_maximizable(&self, maximizable: bool) -> Result<()> { 602 | Ok(()) 603 | } 604 | 605 | /// Unsupported, has no effect when called 606 | fn set_minimizable(&self, minimizable: bool) -> Result<()> { 607 | Ok(()) 608 | } 609 | 610 | /// Unsupported, has no effect when called 611 | fn set_closable(&self, closable: bool) -> Result<()> { 612 | Ok(()) 613 | } 614 | 615 | fn set_title>(&self, title: S) -> Result<()> { 616 | self.webview 617 | .lock() 618 | .unwrap() 619 | .set_title(title) 620 | .map_err(|_| Error::FailedToSendMessage)?; 621 | Ok(()) 622 | } 623 | 624 | fn maximize(&self) -> Result<()> { 625 | self.webview 626 | .lock() 627 | .unwrap() 628 | .set_maximized(true) 629 | .map_err(|_| Error::FailedToSendMessage)?; 630 | Ok(()) 631 | } 632 | 633 | fn unmaximize(&self) -> Result<()> { 634 | self.webview 635 | .lock() 636 | .unwrap() 637 | .set_maximized(false) 638 | .map_err(|_| Error::FailedToSendMessage)?; 639 | Ok(()) 640 | } 641 | 642 | fn minimize(&self) -> Result<()> { 643 | self.webview 644 | .lock() 645 | .unwrap() 646 | .set_minimized(true) 647 | .map_err(|_| Error::FailedToSendMessage)?; 648 | Ok(()) 649 | } 650 | 651 | fn unminimize(&self) -> Result<()> { 652 | self.webview 653 | .lock() 654 | .unwrap() 655 | .set_minimized(false) 656 | .map_err(|_| Error::FailedToSendMessage)?; 657 | Ok(()) 658 | } 659 | 660 | fn show(&self) -> Result<()> { 661 | self.webview 662 | .lock() 663 | .unwrap() 664 | .set_visible(true) 665 | .map_err(|_| Error::FailedToSendMessage)?; 666 | Ok(()) 667 | } 668 | 669 | fn hide(&self) -> Result<()> { 670 | self.webview 671 | .lock() 672 | .unwrap() 673 | .set_visible(false) 674 | .map_err(|_| Error::FailedToSendMessage)?; 675 | Ok(()) 676 | } 677 | 678 | fn close(&self) -> Result<()> { 679 | self.context.send_message(Message::CloseWindow(self.id))?; 680 | Ok(()) 681 | } 682 | 683 | fn destroy(&self) -> Result<()> { 684 | self.context.send_message(Message::DestroyWindow(self.id))?; 685 | Ok(()) 686 | } 687 | 688 | /// Unsupported, has no effect when called 689 | fn set_decorations(&self, decorations: bool) -> Result<()> { 690 | Ok(()) 691 | } 692 | 693 | /// Unsupported, has no effect when called 694 | fn set_shadow(&self, shadow: bool) -> Result<()> { 695 | Ok(()) 696 | } 697 | 698 | fn set_always_on_bottom(&self, always_on_bottom: bool) -> Result<()> { 699 | self.webview 700 | .lock() 701 | .unwrap() 702 | .set_window_level(if always_on_bottom { 703 | verso::WindowLevel::AlwaysOnBottom 704 | } else { 705 | verso::WindowLevel::Normal 706 | }) 707 | .map_err(|_| Error::FailedToSendMessage)?; 708 | Ok(()) 709 | } 710 | 711 | fn set_always_on_top(&self, always_on_top: bool) -> Result<()> { 712 | self.webview 713 | .lock() 714 | .unwrap() 715 | .set_window_level(if always_on_top { 716 | verso::WindowLevel::AlwaysOnTop 717 | } else { 718 | verso::WindowLevel::Normal 719 | }) 720 | .map_err(|_| Error::FailedToSendMessage)?; 721 | Ok(()) 722 | } 723 | 724 | /// Unsupported, has no effect when called 725 | fn set_visible_on_all_workspaces(&self, visible_on_all_workspaces: bool) -> Result<()> { 726 | Ok(()) 727 | } 728 | 729 | /// Unsupported, has no effect when called 730 | fn set_content_protected(&self, protected: bool) -> Result<()> { 731 | Ok(()) 732 | } 733 | 734 | fn set_size(&self, size: Size) -> Result<()> { 735 | self.webview 736 | .lock() 737 | .unwrap() 738 | .set_size(size) 739 | .map_err(|_| Error::FailedToSendMessage)?; 740 | Ok(()) 741 | } 742 | 743 | /// Unsupported, has no effect when called 744 | fn set_min_size(&self, size: Option) -> Result<()> { 745 | Ok(()) 746 | } 747 | 748 | /// Unsupported, has no effect when called 749 | fn set_max_size(&self, size: Option) -> Result<()> { 750 | Ok(()) 751 | } 752 | 753 | fn set_position(&self, position: Position) -> Result<()> { 754 | self.webview 755 | .lock() 756 | .unwrap() 757 | .set_position(position) 758 | .map_err(|_| Error::FailedToSendMessage)?; 759 | Ok(()) 760 | } 761 | 762 | fn set_fullscreen(&self, fullscreen: bool) -> Result<()> { 763 | self.webview 764 | .lock() 765 | .unwrap() 766 | .set_fullscreen(fullscreen) 767 | .map_err(|_| Error::FailedToSendMessage)?; 768 | Ok(()) 769 | } 770 | 771 | fn set_focus(&self) -> Result<()> { 772 | self.webview 773 | .lock() 774 | .unwrap() 775 | .focus() 776 | .map_err(|_| Error::FailedToSendMessage)?; 777 | Ok(()) 778 | } 779 | 780 | /// Unsupported, has no effect when called 781 | fn set_icon(&self, icon: Icon<'_>) -> Result<()> { 782 | Ok(()) 783 | } 784 | 785 | /// Unsupported, has no effect when called 786 | fn set_skip_taskbar(&self, skip: bool) -> Result<()> { 787 | Ok(()) 788 | } 789 | 790 | /// Unsupported, has no effect when called 791 | fn set_cursor_grab(&self, grab: bool) -> Result<()> { 792 | Ok(()) 793 | } 794 | 795 | /// Unsupported, has no effect when called 796 | fn set_cursor_visible(&self, visible: bool) -> Result<()> { 797 | Ok(()) 798 | } 799 | 800 | /// Unsupported, has no effect when called 801 | fn set_cursor_icon(&self, icon: CursorIcon) -> Result<()> { 802 | Ok(()) 803 | } 804 | 805 | /// Unsupported, has no effect when called 806 | fn set_cursor_position>(&self, position: Pos) -> Result<()> { 807 | Ok(()) 808 | } 809 | 810 | /// Unsupported, has no effect when called 811 | fn set_ignore_cursor_events(&self, ignore: bool) -> Result<()> { 812 | Ok(()) 813 | } 814 | 815 | fn start_dragging(&self) -> Result<()> { 816 | self.webview 817 | .lock() 818 | .unwrap() 819 | .start_dragging() 820 | .map_err(|_| Error::FailedToSendMessage)?; 821 | Ok(()) 822 | } 823 | 824 | /// Unsupported, has no effect when called 825 | fn start_resize_dragging(&self, direction: tauri_runtime::ResizeDirection) -> Result<()> { 826 | Ok(()) 827 | } 828 | 829 | /// Unsupported, has no effect when called 830 | fn set_progress_bar(&self, progress_state: ProgressBarState) -> Result<()> { 831 | Ok(()) 832 | } 833 | 834 | /// Unsupported, has no effect when called 835 | fn set_badge_count(&self, count: Option, desktop_filename: Option) -> Result<()> { 836 | Ok(()) 837 | } 838 | 839 | /// Unsupported, has no effect when called 840 | fn set_badge_label(&self, label: Option) -> Result<()> { 841 | Ok(()) 842 | } 843 | 844 | /// Unsupported, has no effect when called 845 | fn set_overlay_icon(&self, icon: Option>) -> Result<()> { 846 | Ok(()) 847 | } 848 | 849 | /// Unsupported, has no effect when called 850 | fn set_title_bar_style(&self, style: tauri_utils::TitleBarStyle) -> Result<()> { 851 | Ok(()) 852 | } 853 | 854 | /// Unsupported, has no effect when called 855 | fn set_size_constraints( 856 | &self, 857 | constraints: tauri_runtime::window::WindowSizeConstraints, 858 | ) -> Result<()> { 859 | Ok(()) 860 | } 861 | 862 | fn set_theme(&self, theme: Option) -> Result<()> { 863 | self.webview 864 | .lock() 865 | .unwrap() 866 | .set_theme(theme.map(to_verso_theme)) 867 | .map_err(|_| Error::FailedToSendMessage)?; 868 | Ok(()) 869 | } 870 | 871 | /// Unsupported, has no effect when called 872 | fn set_enabled(&self, enabled: bool) -> Result<()> { 873 | Ok(()) 874 | } 875 | 876 | /// Unsupported, always returns true 877 | fn is_enabled(&self) -> Result { 878 | Ok(true) 879 | } 880 | 881 | /// Unsupported, has no effect when called 882 | fn set_background_color(&self, color: Option) -> Result<()> { 883 | Ok(()) 884 | } 885 | 886 | /// Unsupported, will always return an error 887 | fn window_handle( 888 | &self, 889 | ) -> std::result::Result, raw_window_handle::HandleError> 890 | { 891 | Err(raw_window_handle::HandleError::NotSupported) 892 | } 893 | 894 | /// Unsupported, always returns false 895 | fn is_always_on_top(&self) -> Result { 896 | Ok(false) 897 | } 898 | 899 | /// Unsupported, has no effect when called 900 | fn set_traffic_light_position(&self, position: Position) -> Result<()> { 901 | Ok(()) 902 | } 903 | } 904 | -------------------------------------------------------------------------------- /tauri-runtime-verso-build/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tauri-runtime-verso-build" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | versoview_build = { workspace = true } 8 | -------------------------------------------------------------------------------- /tauri-runtime-verso-build/README.md: -------------------------------------------------------------------------------- 1 | # Tauri Runtime Verso Build 2 | 3 | This crate re-exports `versoview-build` and provides additional methods for easier Tauri integration, also this crate syncs the `versoview-build` git revision with `verso` in the workspace so the user can align the versions easier 4 | -------------------------------------------------------------------------------- /tauri-runtime-verso-build/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # Tauri Runtime Verso Build 2 | //! 3 | //! This is a crate to help with getting started with using verso as a webview without building it yourself 4 | //! 5 | //! ## Example 6 | //! 7 | //! To use it, first add it to your build dependency, and in your build script: 8 | //! 9 | //! ```no_run 10 | //! fn main() { 11 | //! tauri_runtime_verso_build::get_verso_as_external_bin().unwrap(); 12 | //! tauri_build::build(); 13 | //! } 14 | //! ``` 15 | //! 16 | //! and in your tauri config file (`tauri.conf.json`), add the following 17 | //! 18 | //! ```json 19 | //! { 20 | //! "bundle": { 21 | //! "externalBin": [ 22 | //! "versoview/versoview" 23 | //! ] 24 | //! } 25 | //! } 26 | //! ``` 27 | //! 28 | 29 | use std::{io, path::PathBuf}; 30 | 31 | pub use versoview_build; 32 | 33 | /// Downloads and extracts the pre-built versoview executable 34 | /// to `./versoview/versoview(.exe)` relative to the directory containing your `Cargo.toml` file 35 | pub fn get_verso_as_external_bin() -> io::Result<()> { 36 | let target_triple = std::env::var("TARGET").unwrap(); 37 | let project_directory = std::env::var("CARGO_MANIFEST_DIR").unwrap(); 38 | let output_directory = PathBuf::from(project_directory).join("versoview"); 39 | 40 | let extension = if cfg!(windows) { ".exe" } else { "" }; 41 | let output_executable = output_directory.join(format!("versoview-{target_triple}{extension}")); 42 | let output_version = output_directory.join("versoview-version.txt"); 43 | 44 | if std::fs::exists(&output_executable)? 45 | && std::fs::read_to_string(&output_version).unwrap_or_default() 46 | == versoview_build::VERSO_VERSION 47 | { 48 | return Ok(()); 49 | } 50 | 51 | versoview_build::download_and_extract_verso(&output_directory)?; 52 | 53 | let extracted_versoview_path = output_directory.join(format!("versoview{extension}")); 54 | std::fs::rename(extracted_versoview_path, &output_executable)?; 55 | std::fs::write(&output_version, versoview_build::VERSO_VERSION)?; 56 | 57 | println!("cargo:rerun-if-changed={}", output_executable.display()); 58 | println!("cargo:rerun-if-changed={}", output_version.display()); 59 | 60 | Ok(()) 61 | } 62 | --------------------------------------------------------------------------------