├── .cargo └── audit.toml ├── .changes ├── config.json └── readme.md ├── .github └── workflows │ ├── audit.yml │ ├── check.yml │ ├── covector-status.yml │ └── covector-version-or-publish.yml ├── .gitignore ├── Cargo.toml ├── LICENSE.spdx ├── LICENSE_APACHE-2.0 ├── LICENSE_MIT ├── README.md ├── SECURITY.md ├── crates ├── drag │ ├── CHANGELOG.md │ ├── Cargo.toml │ ├── LICENSE_APACHE-2.0 │ ├── LICENSE_MIT │ └── src │ │ ├── lib.rs │ │ └── platform_impl │ │ ├── gtk │ │ └── mod.rs │ │ ├── macos │ │ └── mod.rs │ │ ├── mod.rs │ │ └── windows │ │ ├── image.rs │ │ └── mod.rs ├── tauri-plugin-drag-as-window │ ├── CHANGELOG.md │ ├── Cargo.toml │ ├── LICENSE_APACHE-2.0 │ ├── LICENSE_MIT │ ├── build.rs │ ├── permissions │ │ ├── autogenerated │ │ │ ├── commands │ │ │ │ ├── drag_back.toml │ │ │ │ ├── drag_new_window.toml │ │ │ │ └── on_drop.toml │ │ │ └── reference.md │ │ ├── default.toml │ │ └── schemas │ │ │ └── schema.json │ └── src │ │ ├── api-iife.js │ │ ├── commands.rs │ │ └── lib.rs └── tauri-plugin-drag │ ├── CHANGELOG.md │ ├── Cargo.toml │ ├── LICENSE_APACHE-2.0 │ ├── LICENSE_MIT │ ├── build.rs │ ├── permissions │ ├── autogenerated │ │ ├── commands │ │ │ └── start_drag.toml │ │ └── reference.md │ ├── default.toml │ └── schemas │ │ └── schema.json │ └── src │ ├── api-iife.js │ ├── commands.rs │ └── lib.rs ├── deny.toml ├── examples ├── icon.bmp ├── icon.ico ├── icon.png ├── tao │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── tauri-dragout │ ├── Cargo.toml │ ├── build.rs │ ├── capabilities │ │ └── default.json │ ├── index.html │ ├── new_window.html │ ├── src │ │ ├── lib.rs │ │ └── main.rs │ └── tauri.conf.json ├── tauri │ ├── Cargo.toml │ ├── build.rs │ ├── capabilities │ │ └── default.json │ ├── index.html │ ├── src │ │ ├── lib.rs │ │ └── main.rs │ └── tauri.conf.json ├── winit │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── wry-dragout │ ├── Cargo.toml │ ├── dummy │ │ ├── drag-1 │ │ ├── drag-2 │ │ └── drag-3 │ └── src │ │ └── main.rs └── wry │ ├── Cargo.toml │ └── src │ └── main.rs ├── html2canvas.min.js └── packages ├── tauri-plugin-drag-api ├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── guest-js │ ├── .gitignore │ └── index.ts ├── package.json ├── rollup.config.js ├── tsconfig.json └── yarn.lock └── tauri-plugin-drag-as-window-api ├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── guest-js ├── .gitignore └── index.ts ├── package.json ├── rollup.config.js ├── tsconfig.json └── yarn.lock /.cargo/audit.toml: -------------------------------------------------------------------------------- 1 | [advisories] 2 | ignore = [ 3 | "RUSTSEC-2023-0019", # `kuchiki` unmaintained 4 | ] 5 | -------------------------------------------------------------------------------- /.changes/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "gitSiteUrl": "https://github.com/crabnebula-dev/drag-rs/", 3 | "pkgManagers": { 4 | "javascript": { 5 | "version": true, 6 | "getPublishedVersion": { 7 | "use": "fetch:check", 8 | "options": { 9 | "url": "https://registry.npmjs.com/${ pkg.pkg }/${ pkg.pkgFile.version }" 10 | } 11 | }, 12 | "publish": [ 13 | "yarn install", 14 | "yarn build", 15 | "yarn publish --access public" 16 | ] 17 | }, 18 | "rust": { 19 | "version": true, 20 | "getPublishedVersion": { 21 | "use": "fetch:check", 22 | "options": { 23 | "url": "https://crates.io/api/v1/crates/${ pkg.pkgFile.pkg.package.name }/${ pkg.pkgFile.version }" 24 | } 25 | }, 26 | "prepublish": [ 27 | "sudo apt-get update", 28 | "sudo apt-get install -y webkit2gtk-4.1" 29 | ], 30 | "publish": [ 31 | "cargo publish" 32 | ] 33 | } 34 | }, 35 | "packages": { 36 | "@crabnebula/tauri-plugin-drag": { 37 | "path": "./packages/tauri-plugin-drag-api", 38 | "manager": "javascript" 39 | }, 40 | "@crabnebula/tauri-plugin-drag-as-window": { 41 | "path": "./packages/tauri-plugin-drag-as-window-api", 42 | "manager": "javascript" 43 | }, 44 | "drag": { 45 | "path": "./crates/drag", 46 | "manager": "rust" 47 | }, 48 | "tauri-plugin-drag": { 49 | "path": "./crates/tauri-plugin-drag", 50 | "manager": "rust", 51 | "dependencies": [ 52 | "drag" 53 | ] 54 | }, 55 | "tauri-plugin-drag-as-window": { 56 | "path": "./crates/tauri-plugin-drag-as-window", 57 | "manager": "rust", 58 | "dependencies": [ 59 | "drag" 60 | ] 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /.changes/readme.md: -------------------------------------------------------------------------------- 1 | # Changes 2 | 3 | ##### via https://github.com/jbolda/covector 4 | 5 | As you create PRs and make changes that require a version bump, please add a new markdown file in this folder. You do not note the version _number_, but rather the type of bump that you expect: major, minor, or patch. The filename is not important, as long as it is a `.md`, but we recommend that it represents the overall change for organizational purposes. 6 | 7 | When you select the version bump required, you do _not_ need to consider dependencies. Only note the package with the actual change, and any packages that depend on that package will be bumped automatically in the process. 8 | 9 | Use the following format: 10 | 11 | ```md 12 | --- 13 | "package-a": patch 14 | "package-b": minor 15 | --- 16 | 17 | Change summary goes here 18 | 19 | ``` 20 | 21 | Summaries do not have a specific character limit, but are text only. These summaries are used within the (future implementation of) changelogs. They will give context to the change and also point back to the original PR if more details and context are needed. 22 | 23 | Changes will be designated as a `major`, `minor` or `patch` as further described in [semver](https://semver.org/). 24 | 25 | Given a version number MAJOR.MINOR.PATCH, increment the: 26 | 27 | - MAJOR version when you make incompatible API changes, 28 | - MINOR version when you add functionality in a backwards compatible manner, and 29 | - PATCH version when you make backwards compatible bug fixes. 30 | 31 | Additional labels for pre-release and build metadata are available as extensions to the MAJOR.MINOR.PATCH format, but will be discussed prior to usage (as extra steps will be necessary in consideration of merging and publishing). 32 | -------------------------------------------------------------------------------- /.github/workflows/audit.yml: -------------------------------------------------------------------------------- 1 | name: Audit Rust 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: "0 0 * * *" 7 | push: 8 | branches: 9 | - v2 10 | paths: 11 | - ".github/workflows/audit.yml" 12 | - "**/Cargo.lock" 13 | - "**/Cargo.toml" 14 | pull_request: 15 | branches: 16 | - v2 17 | paths: 18 | - ".github/workflows/audit.yml" 19 | - "**/Cargo.lock" 20 | - "**/Cargo.toml" 21 | 22 | concurrency: 23 | group: ${{ github.workflow }}-${{ github.ref }} 24 | cancel-in-progress: true 25 | 26 | jobs: 27 | audit: 28 | runs-on: ubuntu-latest 29 | steps: 30 | - uses: actions/checkout@v4 31 | - uses: rustsec/audit-check@v1 32 | with: 33 | token: ${{ secrets.GITHUB_TOKEN }} 34 | -------------------------------------------------------------------------------- /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | name: Check 2 | 3 | on: 4 | push: 5 | branches: 6 | - v2 7 | paths: 8 | - ".github/workflows/check.yml" 9 | - "**/*.rs" 10 | - "**/Cargo.toml" 11 | pull_request: 12 | branches: 13 | - v2 14 | paths: 15 | - ".github/workflows/check.yml" 16 | - "**/*.rs" 17 | - "**/Cargo.toml" 18 | 19 | concurrency: 20 | group: ${{ github.workflow }}-${{ github.ref }} 21 | cancel-in-progress: true 22 | 23 | jobs: 24 | rustfmt: 25 | if: ${{ !startsWith(github.head_ref, 'renovate/') }} 26 | runs-on: ubuntu-latest 27 | 28 | steps: 29 | - uses: actions/checkout@v4 30 | - uses: dtolnay/rust-toolchain@stable 31 | with: 32 | components: rustfmt 33 | - run: cargo fmt --all -- --check 34 | 35 | clippy: 36 | if: ${{ !startsWith(github.head_ref, 'renovate/') }} 37 | 38 | strategy: 39 | fail-fast: false 40 | matrix: 41 | platform: [ubuntu-latest, macos-latest, windows-latest] 42 | 43 | runs-on: ${{ matrix.platform }} 44 | 45 | steps: 46 | - uses: actions/checkout@v4 47 | - uses: dtolnay/rust-toolchain@stable 48 | with: 49 | components: clippy 50 | - name: install webkit2gtk 51 | if: matrix.platform == 'ubuntu-latest' 52 | run: | 53 | sudo apt-get update 54 | sudo apt-get install -y webkit2gtk-4.1 55 | - uses: Swatinem/rust-cache@v2 56 | - run: cargo clippy --workspace --all-targets --all-features -- -D warnings 57 | 58 | rust-test: 59 | strategy: 60 | fail-fast: false 61 | matrix: 62 | platform: [ubuntu-latest, macos-latest, windows-latest] 63 | 64 | runs-on: ${{ matrix.platform }} 65 | 66 | steps: 67 | - uses: actions/checkout@v4 68 | - name: install webkit2gtk 69 | if: matrix.platform == 'ubuntu-latest' 70 | run: | 71 | sudo apt-get update 72 | sudo apt-get install -y webkit2gtk-4.1 73 | - uses: dtolnay/rust-toolchain@stable 74 | - uses: Swatinem/rust-cache@v2 75 | - run: cargo test --workspace --lib --bins --tests --benches --all-features --no-fail-fast 76 | 77 | deny: 78 | runs-on: ubuntu-latest 79 | steps: 80 | - uses: actions/checkout@v4 81 | - uses: EmbarkStudios/cargo-deny-action@v1 82 | -------------------------------------------------------------------------------- /.github/workflows/covector-status.yml: -------------------------------------------------------------------------------- 1 | name: covector status 2 | on: [pull_request] 3 | 4 | jobs: 5 | covector: 6 | runs-on: ubuntu-latest 7 | 8 | steps: 9 | - uses: actions/checkout@v4 10 | with: 11 | fetch-depth: 0 # required for use of git history 12 | - name: covector status 13 | uses: jbolda/covector/packages/action@covector-v0 14 | id: covector 15 | with: 16 | command: 'status' 17 | -------------------------------------------------------------------------------- /.github/workflows/covector-version-or-publish.yml: -------------------------------------------------------------------------------- 1 | name: version or publish 2 | 3 | on: 4 | push: 5 | branches: 6 | - v2 7 | 8 | jobs: 9 | version-or-publish: 10 | runs-on: ubuntu-latest 11 | timeout-minutes: 65 12 | outputs: 13 | change: ${{ steps.covector.outputs.change }} 14 | commandRan: ${{ steps.covector.outputs.commandRan }} 15 | successfulPublish: ${{ steps.covector.outputs.successfulPublish }} 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | with: 20 | fetch-depth: 0 # required for use of git history 21 | - uses: actions/setup-node@v4 22 | with: 23 | node-version: 18 24 | registry-url: 'https://registry.npmjs.org' 25 | - name: cargo login 26 | run: cargo login ${{ secrets.CRATES_IO_TOKEN }} 27 | - name: git config 28 | run: | 29 | git config --global user.name "${{ github.event.pusher.name }}" 30 | git config --global user.email "${{ github.event.pusher.email }}" 31 | - name: covector version or publish (publish when no change files present) 32 | uses: jbolda/covector/packages/action@covector-v0 33 | id: covector 34 | env: 35 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 36 | CARGO_AUDIT_OPTIONS: ${{ secrets.CARGO_AUDIT_OPTIONS }} 37 | with: 38 | token: ${{ secrets.GITHUB_TOKEN }} 39 | command: 'version-or-publish' 40 | createRelease: true 41 | recognizeContributors: true 42 | - name: Create Pull Request With Versions Bumped 43 | id: cpr 44 | uses: peter-evans/create-pull-request@v3 45 | if: steps.covector.outputs.commandRan == 'version' 46 | with: 47 | title: "Publish New Versions" 48 | commit-message: "publish new versions" 49 | labels: "version updates" 50 | branch: "release" 51 | body: ${{ steps.covector.outputs.change }} 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | /Cargo.lock 3 | gen/ -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = ["crates/*", "examples/*"] 4 | 5 | [workspace.package] 6 | edition = "2021" 7 | authors = ["CrabNebula Ltd."] 8 | license = "Apache-2.0 OR MIT" 9 | 10 | [workspace.dependencies] 11 | tauri = "2" 12 | serde = "1" 13 | serde_json = "1" 14 | thiserror = "1" 15 | base64 = "0.21" 16 | tao = "0.30.3" 17 | winit = "0.30.5" 18 | wry = "0.46.1" 19 | dunce = "1.0.5" 20 | tauri-build = "2.0.1" 21 | tauri-plugin = { version = "2.0.1", features = ["build"] } 22 | drag = { path = "./crates/drag", features = [ "serde" ] } 23 | tauri-plugin-drag = { path = "./crates/tauri-plugin-drag" } 24 | tauri-plugin-drag-as-window = { path = "./crates/tauri-plugin-drag-as-window" } 25 | -------------------------------------------------------------------------------- /LICENSE.spdx: -------------------------------------------------------------------------------- 1 | SPDXVersion: SPDX-2.1 2 | DataLicense: CC0-1.0 3 | PackageName: drag-rs 4 | DataFormat: SPDXRef-1 5 | PackageSupplier: Organization: CrabNebula Ltd. 6 | PackageHomePage: https://github.com/crabnebula-dev/drag-rs 7 | PackageLicenseDeclared: Apache-2.0 8 | PackageLicenseDeclared: MIT 9 | PackageCopyrightText: 2023-2023, CrabNebula Ltd. 10 | PackageSummary: Start a drag operation out of a window on macOS, Windows and Linux (via GTK). 11 | 12 | PackageComment: The package includes the following libraries; see 13 | Relationship information. 14 | 15 | Created: 2023-11-28T09:00:00Z 16 | PackageDownloadLocation: git://github.com/crabnebula-dev/drag-rs 17 | PackageDownloadLocation: git+https://github.com/crabnebula-dev/drag-rs.git 18 | PackageDownloadLocation: git+ssh://github.com/crabnebula-dev/drag-rs.git 19 | -------------------------------------------------------------------------------- /LICENSE_APACHE-2.0: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | -------------------------------------------------------------------------------- /LICENSE_MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 - Present CrabNebula Ltd. 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 | # drag-rs 2 | 3 | Start a drag operation out of a window on macOS, Windows and Linux (via GTK). 4 | 5 | Tested for [tao](https://github.com/tauri-apps/tao) (latest), [winit](https://github.com/rust-windowing/winit) (latest), [wry](https://github.com/tauri-apps/wry) (v0.46) and [tauri](https://github.com/tauri-apps/tauri) (v2) windows. 6 | Due to the GTK-based implementation, winit currently cannot leverage this crate on Linux yet. 7 | 8 | This project also includes a Tauri plugin for simplified usage on Tauri apps. 9 | 10 | ## Setup 11 | 12 | There's two ways to consume this crate API: from Rust code via the `drag` crate or from Tauri's frontend via `tauri-plugin-drag` or `tauri-plugin-drag-as-window`. 13 | 14 | ### Rust 15 | 16 | - Add the `drag` dependency: 17 | 18 | `$ cargo add drag` 19 | 20 | - Define the drag item and preview icon: 21 | 22 | ```rust 23 | let item = drag::DragItem::Files(vec![std::fs::canonicalize("./examples/icon.png").unwrap()]); 24 | let preview_icon = drag::Image::Raw(include_bytes!("../../icon.png").to_vec()); 25 | ``` 26 | 27 | - Use the `drag::start_drag` function. It takes a `&T: raw_window_handle::HasWindowHandle` type on macOS and Windows, and a `>k::ApplicationWindow` on Linux: 28 | 29 | - tao: 30 | ```rust 31 | let event_loop = tao::event_loop::EventLoop::new(); 32 | let window = tao::window::WindowBuilder::new().build(&event_loop).unwrap(); 33 | 34 | drag::start_drag( 35 | #[cfg(target_os = "linux")] 36 | { 37 | use tao::platform::unix::WindowExtUnix; 38 | window.gtk_window() 39 | }, 40 | #[cfg(not(target_os = "linux"))] 41 | &window, 42 | item, 43 | preview_icon, 44 | ); 45 | ``` 46 | 47 | - wry: 48 | ```rust 49 | let event_loop = tao::event_loop::EventLoop::new(); 50 | let window = tao::window::WindowBuilder::new().build(&event_loop).unwrap(); 51 | let webview = wry::WebViewBuilder::new().build(&window).unwrap(); 52 | 53 | drag::start_drag( 54 | #[cfg(target_os = "linux")] 55 | { 56 | use tao::platform::unix::WindowExtUnix; 57 | window.gtk_window() 58 | }, 59 | #[cfg(not(target_os = "linux"))] 60 | &window, 61 | item, 62 | preview_icon, 63 | ); 64 | ``` 65 | 66 | - winit: 67 | ```rust 68 | let event_loop = winit::event_loop::EventLoop::new().unwrap(); 69 | let window = winit::window::WindowBuilder::new().build(&event_loop).unwrap(); 70 | let _ = drag::start_drag(&window, item, preview_icon); 71 | ``` 72 | 73 | - tauri: 74 | ```rust 75 | tauri::Builder::default() 76 | .setup(|app| { 77 | let window = app.get_window("main").unwrap(); 78 | 79 | drag::start_drag( 80 | #[cfg(target_os = "linux")] 81 | &window.gtk_window()?, 82 | #[cfg(not(target_os = "linux"))] 83 | &window, 84 | item, 85 | preview_icon 86 | ); 87 | 88 | Ok(()) 89 | }) 90 | ``` 91 | 92 | ### Tauri Plugin 93 | 94 | #### tauri-plugin-drag 95 | 96 | - Add the `tauri-plugin-drag` dependency: 97 | 98 | `$ cargo add tauri-plugin-drag` 99 | 100 | - Install the `@crabnebula/tauri-plugin-drag` NPM package containing the API bindings: 101 | 102 | ```sh 103 | pnpm add @crabnebula/tauri-plugin-drag 104 | # or 105 | npm add @crabnebula/tauri-plugin-drag 106 | # or 107 | yarn add @crabnebula/tauri-plugin-drag 108 | ``` 109 | 110 | - Register the core plugin with Tauri: 111 | 112 | `src-tauri/src/main.rs` 113 | 114 | ```rust 115 | fn main() { 116 | tauri::Builder::default() 117 | .plugin(tauri_plugin_drag::init()) 118 | .run(tauri::generate_context!()) 119 | .expect("error while running tauri application"); 120 | } 121 | ``` 122 | 123 | - Add permissions 124 | 125 | `capabilities/default.json` 126 | 127 | ```json 128 | { 129 | ... 130 | "permissions": [ 131 | ... 132 | "drag:default" 133 | ] 134 | } 135 | ``` 136 | 137 | - Afterwards all the plugin's APIs are available through the JavaScript guest bindings: 138 | 139 | ```javascript 140 | import { startDrag } from "@crabnebula/tauri-plugin-drag"; 141 | startDrag({ item: ['/path/to/drag/file'], icon: '/path/to/icon/image' }) 142 | ``` 143 | 144 | #### tauri-plugin-drag-as-window 145 | 146 | - Add the `tauri-plugin-drag-as-window` dependency: 147 | 148 | `$ cargo add tauri-plugin-drag-as-window` 149 | 150 | - Install the `@crabnebula/tauri-plugin-drag-as-window` NPM package containing the API bindings: 151 | 152 | ```sh 153 | pnpm add @crabnebula/tauri-plugin-drag-as-window 154 | # or 155 | npm add @crabnebula/tauri-plugin-drag-as-window 156 | # or 157 | yarn add @crabnebula/tauri-plugin-drag-as-window 158 | ``` 159 | 160 | - Register the core plugin with Tauri: 161 | 162 | `src-tauri/src/main.rs` 163 | 164 | ```rust 165 | fn main() { 166 | tauri::Builder::default() 167 | .plugin(tauri_plugin_drag_as_window::init()) 168 | .run(tauri::generate_context!()) 169 | .expect("error while running tauri application"); 170 | } 171 | ``` 172 | 173 | - Add permissions 174 | 175 | `capabilities/default.json` 176 | 177 | ```json 178 | { 179 | ... 180 | "permissions": [ 181 | ... 182 | "drag-as-window:default" 183 | ] 184 | } 185 | ``` 186 | 187 | - Afterwards all the plugin's APIs are available through the JavaScript guest bindings: 188 | 189 | ```javascript 190 | import { dragAsWindow, dragBack } from "@crabnebula/tauri-plugin-drag-as-window"; 191 | import { getCurrentWebviewWindow, WebviewWindow } from "@tauri-apps/api/webviewWindow"; 192 | // alternatively you can pass a DOM element instead of its selector 193 | dragAsWindow('#my-drag-element', (payload) => { 194 | console.log('dropped!') 195 | // create the window with the content from the current element (that's is up to you!) 196 | new WebviewWindow('label', { 197 | x: payload.cursorPos.x, 198 | y: payload.cursorPos.y, 199 | }) 200 | }) 201 | 202 | const el = document.querySelector('#my-drag-element') 203 | el.ondragstart = (event) => { 204 | event.preventDefault() 205 | 206 | dragBack(event.target, { data: 'some data' }, (payload) => { 207 | getCurrentWebviewWindow().close() 208 | }) 209 | } 210 | ``` 211 | 212 | ## Examples 213 | 214 | Running the examples: 215 | 216 | ```sh 217 | cargo run --bin [tauri-app|winit-app|tao-app|wry-app] 218 | ``` 219 | 220 | Additional drag as window examples are available for tauri and wry: 221 | 222 | ```sh 223 | cargo run --bin [tauri-app-dragout|wry-app-dragout] 224 | ``` 225 | 226 | ## Licenses 227 | 228 | MIT or MIT/Apache 2.0 where applicable. 229 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | **Do not report security vulnerabilities through public GitHub issues.** 4 | 5 | **Please use the [Private Vulnerability Disclosure](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability#privately-reporting-a-security-vulnerability) feature of GitHub.** 6 | 7 | Alternatively, you can also send them by email to security@crabnebula.dev. 8 | You can encrypt your mail using GnuPG if you want. 9 | 10 | See the [security.txt](https://crabnebula.dev/.well-known/security.txt) from CrabNebula 11 | 12 | ``` 13 | Contact: mailto:security@crabnebula.dev 14 | Expires: 2025-01-30T06:30:00.000Z 15 | Encryption: https://crabnebula.dev/.well-known/pgp.txt 16 | Preferred-Languages: en,de,fr 17 | Canonical: https://crabnebula.dev/.well-known/security.txt 18 | ``` 19 | 20 | Include as much of the following information: 21 | 22 | - Type of issue (e.g. buffer overflow, privilege escalation, etc.) 23 | - The location of the affected source code (tag/branch/commit or direct URL) 24 | - Any special configuration required to reproduce the issue 25 | - The distribution affected or used for reproduction. 26 | - Step-by-step instructions to reproduce the issue 27 | - Impact of the issue, including how an attacker might exploit the issue 28 | - Preferred Languages 29 | 30 | We prefer to receive reports in English. If necessary, we also understand French and German. 31 | -------------------------------------------------------------------------------- /crates/drag/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## \[2.1.0] 4 | 5 | - [`229aa3e`](https://github.com/crabnebula-dev/drag-rs/commit/229aa3e26c85c31074abd3b4a4538b2ec65eb094) Added `mode` option to `drag::Options` to either copy or move a file. 6 | - [`1e7adfd`](https://github.com/crabnebula-dev/drag-rs/commit/1e7adfd9662bc1be6d369f438ee749e908dee02a) Fix crash on macOS systems running Intel or older macOS releases. 7 | 8 | ## \[2.0.0] 9 | 10 | - [`244887f`](https://github.com/crabnebula-dev/drag-rs/commit/244887fa36b12ac615919b9d2d149edca3d1f1c7) Update to tauri v2. 11 | 12 | ## \[0.4.0] 13 | 14 | - [`639e0fd`](https://github.com/crabnebula-dev/drag-rs/commit/639e0fd801109d88007d0aeafe04367cdc251eb7) Added the cursor position of the drop event as the `start_drag` callback closure second argument. 15 | - [`639e0fd`](https://github.com/crabnebula-dev/drag-rs/commit/639e0fd801109d88007d0aeafe04367cdc251eb7) Added `Options` as the last argument of the `start_drag` function. 16 | 17 | ## \[0.3.0] 18 | 19 | - [`f58ed78`](https://github.com/crabnebula-dev/drag-rs/commit/f58ed7838abe1fe5b23c4e3aa92df28e77564345) Added `DragItem::Drag` variant (supported on macOS) to drag a buffer (e.g. Final Cut Pro XMLs). 20 | - [`1449076`](https://github.com/crabnebula-dev/drag-rs/commit/14490764de8ff50969a3f2299d204e44e091752e) The `start_drag` function now takes a closure for the operation result (either `DragResult::Dropped` or `DragResult::Cancelled`). 21 | 22 | ## \[0.2.0] 23 | 24 | - [`644cfa2`](https://github.com/crabnebula-dev/drag-rs/commit/644cfa28b09bee9c3de396bdcc1dc801a26d65bc) Initial release. 25 | -------------------------------------------------------------------------------- /crates/drag/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "drag" 3 | version = "2.1.0" 4 | description = "Start a drag operation out of a window" 5 | authors = ["CrabNebula Ltd."] 6 | edition = { workspace = true } 7 | license = { workspace = true } 8 | 9 | [dependencies] 10 | raw-window-handle = "0.6.2" 11 | thiserror.workspace = true 12 | serde = { workspace = true, optional = true } 13 | log = "0.4" 14 | 15 | [dev-dependencies] 16 | tao.workspace = true 17 | winit.workspace = true 18 | wry.workspace = true 19 | tauri.workspace = true 20 | 21 | [target."cfg(target_os = \"macos\")".dependencies] 22 | cocoa = "0.26.0" 23 | objc = "0.2.7" 24 | core-graphics = "0.24.0" 25 | 26 | [target."cfg(any(target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dependencies] 27 | gtk = { version = "0.18.1" } 28 | gdk = { version = "0.18.0" } 29 | gdkx11 = "0.18.0" 30 | 31 | [target."cfg(target_os = \"windows\")".dependencies] 32 | dunce.workspace = true 33 | windows-core = "0.58" 34 | windows = { version = "0.52", features = [ 35 | "implement", 36 | "Win32_Foundation", 37 | "Win32_Graphics_Gdi", 38 | "Win32_System_Com", 39 | "Win32_System_Com_StructuredStorage", 40 | "Win32_System_Ole", 41 | "Win32_System_Memory", 42 | "Win32_System_SystemServices", 43 | "Win32_UI_Shell", 44 | "Win32_UI_Shell_Common", 45 | "Win32_UI_WindowsAndMessaging", 46 | "Win32_Graphics_Imaging", 47 | "Win32_Graphics_Gdi", 48 | ] } 49 | 50 | [features] 51 | serde = ["dep:serde"] 52 | -------------------------------------------------------------------------------- /crates/drag/LICENSE_APACHE-2.0: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | -------------------------------------------------------------------------------- /crates/drag/LICENSE_MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 - Present CrabNebula Ltd. 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 | -------------------------------------------------------------------------------- /crates/drag/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2023 CrabNebula Ltd. 2 | // SPDX-License-Identifier: Apache-2.0 3 | // SPDX-License-Identifier: MIT 4 | 5 | //!Start a drag operation out of a window on macOS, Windows and Linux (via GTK). 6 | //! 7 | //! Tested for [tao](https://github.com/tauri-apps/tao) (latest), 8 | //! [winit](https://github.com/rust-windowing/winit) (latest), 9 | //! [wry](https://github.com/tauri-apps/wry) (v0.24) and 10 | //! [tauri](https://github.com/tauri-apps/tauri) (v1) windows. 11 | //! 12 | //! Due to the GTK-based implementation, winit currently cannot leverage this crate on Linux yet. 13 | //! 14 | //! - Add the `drag` dependency: 15 | //! 16 | //! `$ cargo add drag` 17 | //! 18 | //! - Use the `drag::start_drag` function. It takes a `&T: raw_window_handle::HasWindowHandle` type on macOS and Windows, and a `>k::ApplicationWindow` on Linux: 19 | //! 20 | //! - tao: 21 | //! ```rust,no_run 22 | //! let event_loop = tao::event_loop::EventLoop::new(); 23 | //! let window = tao::window::WindowBuilder::new().build(&event_loop).unwrap(); 24 | //! 25 | //! let item = drag::DragItem::Files(vec![std::fs::canonicalize("./examples/icon.png").unwrap()]); 26 | //! let preview_icon = drag::Image::File("./examples/icon.png".into()); 27 | //! 28 | //! drag::start_drag( 29 | //! #[cfg(target_os = "linux")] 30 | //! { 31 | //! use tao::platform::unix::WindowExtUnix; 32 | //! window.gtk_window() 33 | //! }, 34 | //! #[cfg(not(target_os = "linux"))] 35 | //! &window, 36 | //! item, 37 | //! preview_icon, 38 | //! |result, cursor_position| { 39 | //! println!("drag result: {result:?}"); 40 | //! }, 41 | //! drag::Options::default(), 42 | //! ); 43 | //! ``` 44 | //! 45 | //! - wry: 46 | //! ```rust,no_run 47 | //! let event_loop = tao::event_loop::EventLoop::new(); 48 | //! let window = tao::window::WindowBuilder::new().build(&event_loop).unwrap(); 49 | //! let webview = wry::WebViewBuilder::new().build(&window).unwrap(); 50 | //! 51 | //! let item = drag::DragItem::Files(vec![std::fs::canonicalize("./examples/icon.png").unwrap()]); 52 | //! let preview_icon = drag::Image::File("./examples/icon.png".into()); 53 | //! 54 | //! drag::start_drag( 55 | //! #[cfg(target_os = "linux")] 56 | //! { 57 | //! use tao::platform::unix::WindowExtUnix; 58 | //! window.gtk_window() 59 | //! }, 60 | //! #[cfg(not(target_os = "linux"))] 61 | //! &window, 62 | //! item, 63 | //! preview_icon, 64 | //! |result, cursor_position| { 65 | //! println!("drag result: {result:?}"); 66 | //! }, 67 | //! drag::Options::default(), 68 | //! ); 69 | //! ``` 70 | //! 71 | //! - winit: 72 | //! ```rust,ignore 73 | //! let window = ...winit window; 74 | //! 75 | //! let item = drag::DragItem::Files(vec![std::fs::canonicalize("./examples/icon.png").unwrap()]); 76 | //! let preview_icon = drag::Image::File("./examples/icon.png".into()); 77 | //! 78 | //! # #[cfg(not(target_os = "linux"))] 79 | //! let _ = drag::start_drag(&window, item, preview_icon, |result, cursor_position| { 80 | //! println!("drag result: {result:?}"); 81 | //! }, Default::default()); 82 | //! ``` 83 | 84 | #[cfg(target_os = "macos")] 85 | #[macro_use] 86 | extern crate objc; 87 | 88 | use std::path::PathBuf; 89 | 90 | mod platform_impl; 91 | pub use platform_impl::start_drag; 92 | 93 | pub type Result = std::result::Result; 94 | 95 | #[derive(Debug, thiserror::Error)] 96 | pub enum Error { 97 | #[cfg(windows)] 98 | #[error("{0}")] 99 | WindowsError(#[from] windows::core::Error), 100 | #[error(transparent)] 101 | Io(#[from] std::io::Error), 102 | #[error("unsupported window handle")] 103 | UnsupportedWindowHandle, 104 | #[error("failed to start drag")] 105 | FailedToStartDrag, 106 | #[error("drag image not found")] 107 | ImageNotFound, 108 | #[cfg(target_os = "linux")] 109 | #[error("empty drag target list")] 110 | EmptyTargetList, 111 | #[error("failed to drop items")] 112 | FailedToDrop, 113 | #[error("failed to get cursor position")] 114 | FailedToGetCursorPosition, 115 | } 116 | 117 | #[derive(Debug, Clone, Copy)] 118 | #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] 119 | pub enum DragResult { 120 | Dropped, 121 | Cancel, 122 | } 123 | 124 | pub type DataProvider = Box Option>>; 125 | 126 | /// Item to be dragged. 127 | pub enum DragItem { 128 | /// A list of files to be dragged. 129 | /// 130 | /// The paths must be absolute. 131 | Files(Vec), 132 | /// Data to share with another app. 133 | /// 134 | /// - **Windows**: Not supported. Will result in a dummy drag operation of current folder that will be cancelled upon dropping. 135 | /// - **Linux (gtk)**: Not supported. Will result in a dummy drag operation that contains nothing to drop. 136 | Data { 137 | provider: DataProvider, 138 | types: Vec, 139 | }, 140 | } 141 | 142 | #[derive(Debug, Clone, Copy)] 143 | #[repr(u64)] 144 | pub enum DragMode { 145 | Copy = 1, // NSDragOperationCopy 146 | Move = 16, // NSDragOperationMove 147 | } 148 | 149 | impl Default for DragMode { 150 | fn default() -> Self { 151 | DragMode::Copy 152 | } 153 | } 154 | 155 | #[cfg(target_os = "macos")] 156 | unsafe impl objc::Encode for DragMode { 157 | fn encode() -> objc::Encoding { 158 | unsafe { objc::Encoding::from_str("Q") } // unsigned long long 159 | } 160 | } 161 | 162 | #[derive(Default)] 163 | pub struct Options { 164 | pub skip_animatation_on_cancel_or_failure: bool, 165 | pub mode: DragMode, 166 | } 167 | 168 | /// An image definition. 169 | #[derive(Debug)] 170 | #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] 171 | #[cfg_attr(feature = "serde", serde(untagged))] 172 | pub enum Image { 173 | /// A path to a image. 174 | File(PathBuf), 175 | /// Raw bytes of the image. 176 | Raw(Vec), 177 | } 178 | 179 | /// Logical position of the cursor. 180 | /// 181 | /// - **Windows**: Currently the win32 API for logical position reports physical position as well, due to the complicated nature of potential multiple monitor with different scaling there's no trivial solution to be incorporated. 182 | #[derive(Debug, Clone)] 183 | #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] 184 | pub struct CursorPosition { 185 | pub x: i32, 186 | pub y: i32, 187 | } 188 | -------------------------------------------------------------------------------- /crates/drag/src/platform_impl/gtk/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2023 CrabNebula Ltd. 2 | // SPDX-License-Identifier: Apache-2.0 3 | // SPDX-License-Identifier: MIT 4 | 5 | use crate::{CursorPosition, DragItem, DragMode, DragResult, Error, Image, Options}; 6 | use gdkx11::{ 7 | gdk, 8 | glib::{ObjectExt, Propagation, SignalHandlerId}, 9 | }; 10 | use gtk::{ 11 | gdk_pixbuf, 12 | prelude::{ 13 | DeviceExt, DragContextExtManual, PixbufLoaderExt, SeatExt, WidgetExt, WidgetExtManual, 14 | }, 15 | }; 16 | use std::{ 17 | rc::Rc, 18 | sync::{Arc, Mutex}, 19 | }; 20 | 21 | pub fn start_drag( 22 | window: >k::ApplicationWindow, 23 | item: DragItem, 24 | image: Image, 25 | on_drop_callback: F, 26 | options: Options, 27 | ) -> crate::Result<()> { 28 | log::debug!("Starting drag operation with mode: {:?}", options.mode); 29 | let handler_ids: Arc>> = Arc::new(Mutex::new(vec![])); 30 | let drag_action = match options.mode { 31 | DragMode::Copy => gdk::DragAction::COPY, 32 | DragMode::Move => gdk::DragAction::MOVE, 33 | }; 34 | 35 | log::debug!("Setting drag source with action: {:?}", drag_action); 36 | window.drag_source_set(gdk::ModifierType::BUTTON1_MASK, &[], drag_action); 37 | 38 | match item { 39 | DragItem::Files(paths) => { 40 | log::debug!("Setting up file drag with {} paths", paths.len()); 41 | window.drag_source_add_uri_targets(); 42 | handler_ids 43 | .lock() 44 | .unwrap() 45 | .push(window.connect_drag_data_get(move |_, _, data, _, _| { 46 | log::debug!("Preparing URIs for drag data"); 47 | let uris: Vec = paths 48 | .iter() 49 | .map(|path| format!("file://{}", path.display())) 50 | .collect(); 51 | let uris: Vec<&str> = uris.iter().map(|s| s.as_str()).collect(); 52 | log::debug!("Setting URIs: {:?}", uris); 53 | data.set_uris(&uris); 54 | })); 55 | } 56 | DragItem::Data { .. } => { 57 | // Currently leaving it as is as we can utilize it as a dummy dragging feature 58 | // on_drop_callback(DragResult::Cancel, get_cursor_position(window).unwrap()); 59 | // return Ok(()); 60 | } 61 | } 62 | 63 | if let Some(target_list) = &window.drag_source_get_target_list() { 64 | log::debug!("Got target list, initiating drag"); 65 | if let Some(drag_context) = window.drag_begin_with_coordinates( 66 | target_list, 67 | drag_action, 68 | gdk::ffi::GDK_BUTTON1_MASK as i32, 69 | None, 70 | -1, 71 | -1, 72 | ) { 73 | log::debug!("Drag context created successfully"); 74 | let callback = Rc::new(on_drop_callback); 75 | on_drop_failed(callback.clone(), window, &handler_ids, &options); 76 | on_drop_performed(callback.clone(), window, &handler_ids, &drag_context); 77 | 78 | log::debug!("Setting up drag icon"); 79 | let icon_pixbuf: Option = match &image { 80 | Image::Raw(data) => image_binary_to_pixbuf(data), 81 | Image::File(path) => match std::fs::read(path) { 82 | Ok(bytes) => image_binary_to_pixbuf(&bytes), 83 | Err(_) => None, 84 | }, 85 | }; 86 | if let Some(icon) = icon_pixbuf { 87 | drag_context.drag_set_icon_pixbuf(&icon, 0, 0); 88 | } 89 | 90 | Ok(()) 91 | } else { 92 | Err(crate::Error::FailedToStartDrag) 93 | } 94 | } else { 95 | Err(crate::Error::EmptyTargetList) 96 | } 97 | } 98 | 99 | fn image_binary_to_pixbuf(data: &[u8]) -> Option { 100 | let loader = gdk_pixbuf::PixbufLoader::new(); 101 | loader 102 | .write(data) 103 | .and_then(|_| loader.close()) 104 | .map_err(|_| ()) 105 | .and_then(|_| loader.pixbuf().ok_or(())) 106 | .ok() 107 | } 108 | 109 | fn clear_signal_handlers(window: >k::ApplicationWindow, handler_ids: &mut Vec) { 110 | for handler_id in handler_ids.drain(..) { 111 | window.disconnect(handler_id); 112 | } 113 | } 114 | 115 | fn on_drop_failed( 116 | callback: Rc, 117 | window: >k::ApplicationWindow, 118 | handler_ids: &Arc>>, 119 | options: &Options, 120 | ) { 121 | log::debug!("Setting up drop failed handler"); 122 | let window_clone = window.clone(); 123 | let handler_ids_clone = handler_ids.clone(); 124 | 125 | let skip_animatation_on_cancel_or_failure = options.skip_animatation_on_cancel_or_failure; 126 | 127 | handler_ids 128 | .lock() 129 | .unwrap() 130 | .push(window.connect_drag_failed(move |_, _, _drag_result| { 131 | log::debug!("Drag failed or cancelled"); 132 | callback( 133 | DragResult::Cancel, 134 | get_cursor_position(&window_clone).unwrap(), 135 | ); 136 | 137 | cleanup_signal_handlers(&handler_ids_clone, &window_clone); 138 | if skip_animatation_on_cancel_or_failure { 139 | Propagation::Stop 140 | } else { 141 | Propagation::Proceed 142 | } 143 | })); 144 | } 145 | 146 | fn cleanup_signal_handlers( 147 | handler_ids: &Arc>>, 148 | window: >k::ApplicationWindow, 149 | ) { 150 | log::debug!("Cleaning up signal handlers"); 151 | let handler_ids = &mut handler_ids.lock().unwrap(); 152 | clear_signal_handlers(window, handler_ids); 153 | window.drag_source_unset(); 154 | log::debug!("Signal handlers cleaned up"); 155 | } 156 | 157 | fn on_drop_performed( 158 | callback: Rc, 159 | window: >k::ApplicationWindow, 160 | handler_ids: &Arc>>, 161 | drag_context: &gdk::DragContext, 162 | ) { 163 | log::debug!("Setting up drop performed handler"); 164 | let window = window.clone(); 165 | let handler_ids = handler_ids.clone(); 166 | 167 | drag_context.connect_drop_performed(move |context, _| { 168 | log::debug!("Drop performed successfully"); 169 | log::trace!("Selected action: {:?}", context.selected_action()); 170 | log::trace!("Suggested action: {:?}", context.suggested_action()); 171 | cleanup_signal_handlers(&handler_ids, &window); 172 | callback(DragResult::Dropped, get_cursor_position(&window).unwrap()); 173 | }); 174 | } 175 | 176 | fn get_cursor_position(window: >k::ApplicationWindow) -> Result { 177 | if let Some(cursor) = window 178 | .display() 179 | .default_seat() 180 | .and_then(|seat| seat.pointer()) 181 | { 182 | let (_, x, y) = cursor.position(); 183 | Ok(CursorPosition { x, y }) 184 | } else { 185 | Err(Error::FailedToGetCursorPosition) 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /crates/drag/src/platform_impl/macos/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2023 CrabNebula Ltd. 2 | // SPDX-License-Identifier: Apache-2.0 3 | // SPDX-License-Identifier: MIT 4 | 5 | use std::{ 6 | ffi::{c_char, c_void}, 7 | sync::atomic::{AtomicBool, Ordering}, 8 | }; 9 | 10 | use cocoa::{ 11 | appkit::{NSApp, NSEvent, NSEventModifierFlags, NSEventType, NSImage}, 12 | base::{id, nil}, 13 | foundation::{NSArray, NSData, NSPoint, NSRect, NSSize, NSUInteger}, 14 | }; 15 | use core_graphics::display::CGDisplay; 16 | use objc::{ 17 | declare::ClassDecl, 18 | runtime::{Class, Object, Protocol, Sel, BOOL, NO, YES}, 19 | }; 20 | use raw_window_handle::{HasWindowHandle, RawWindowHandle}; 21 | 22 | use crate::{CursorPosition, DragItem, DragMode, DragResult, Image, Options}; 23 | 24 | const UTF8_ENCODING: usize = 4; 25 | 26 | struct NSString(id); 27 | 28 | impl NSString { 29 | fn new(s: &str) -> Self { 30 | // Safety: objc runtime calls are unsafe 31 | NSString(unsafe { 32 | let ns_string: id = msg_send![class!(NSString), alloc]; 33 | let ns_string: id = msg_send![ns_string, 34 | initWithBytes:s.as_ptr() 35 | length:s.len() 36 | encoding:UTF8_ENCODING]; 37 | 38 | // The thing is allocated in rust, the thing must be set to autorelease in rust to relinquish control 39 | // or it can not be released correctly in OC runtime 40 | let _: () = msg_send![ns_string, autorelease]; 41 | 42 | ns_string 43 | }) 44 | } 45 | 46 | fn to_str(&self) -> &str { 47 | unsafe { 48 | let bytes: *const c_char = msg_send![self.0, UTF8String]; 49 | let len = msg_send![self.0, lengthOfBytesUsingEncoding: UTF8_ENCODING]; 50 | let bytes = std::slice::from_raw_parts(bytes as *const u8, len); 51 | std::str::from_utf8_unchecked(bytes) 52 | } 53 | } 54 | } 55 | 56 | pub fn start_drag( 57 | handle: &W, 58 | item: DragItem, 59 | image: Image, 60 | on_drop_callback: F, 61 | options: Options, 62 | ) -> crate::Result<()> { 63 | if let Ok(RawWindowHandle::AppKit(w)) = handle.window_handle().map(|h| h.as_raw()) { 64 | unsafe { 65 | let window: id = msg_send![w.ns_view.as_ptr() as id, window]; 66 | // wry replaces the ns_view so we don't really use AppKitWindowHandle::ns_view 67 | let ns_view: id = msg_send![window, contentView]; 68 | 69 | let current_position: NSPoint = msg_send![window, mouseLocationOutsideOfEventStream]; 70 | 71 | let img: id = msg_send![class!(NSImage), alloc]; 72 | let img: id = match image { 73 | Image::File(path) => { 74 | if !path.exists() { 75 | return Err(crate::Error::ImageNotFound); 76 | } 77 | NSImage::initByReferencingFile_(img, NSString::new(&path.to_string_lossy()).0) 78 | } 79 | Image::Raw(bytes) => { 80 | let data = NSData::dataWithBytes_length_( 81 | nil, 82 | bytes.as_ptr() as *const std::os::raw::c_void, 83 | bytes.len() as u64, 84 | ); 85 | NSImage::initWithData_(NSImage::alloc(nil), data) 86 | } 87 | }; 88 | let image_size: NSSize = img.size(); 89 | let image_rect = NSRect::new( 90 | NSPoint::new( 91 | current_position.x - image_size.width / 2., 92 | current_position.y - image_size.height / 2., 93 | ), 94 | image_size, 95 | ); 96 | 97 | let dragging_items: id = msg_send![class!(NSMutableArray), array]; 98 | 99 | match item { 100 | DragItem::Files(files) => { 101 | for path in files { 102 | let nsurl: id = msg_send![class!(NSURL), fileURLWithPath: NSString::new(&path.display().to_string()) isDirectory: false]; 103 | let drag_item: id = msg_send![class!(NSDraggingItem), alloc]; 104 | let item: id = msg_send![drag_item, initWithPasteboardWriter: nsurl]; 105 | 106 | let _: () = msg_send![item, setDraggingFrame: image_rect contents: img]; 107 | 108 | let _: () = msg_send![dragging_items, addObject: item]; 109 | } 110 | } 111 | DragItem::Data { provider, types } => { 112 | let cls = ClassDecl::new("DragRsDataProvider", class!(NSObject)); 113 | let cls = match cls { 114 | Some(mut cls) => { 115 | cls.add_ivar::<*mut c_void>("provider_ptr"); 116 | cls.add_protocol( 117 | Protocol::get("NSPasteboardItemDataProvider").unwrap(), 118 | ); 119 | cls.add_method( 120 | sel!(pasteboard:item:provideDataForType:), 121 | provide_data as extern "C" fn(&Object, Sel, id, id, id), 122 | ); 123 | cls.add_method( 124 | sel!(pasteboardFinishedWithDataProvider:), 125 | pasteboard_finished as extern "C" fn(&Object, Sel, id), 126 | ); 127 | 128 | extern "C" fn pasteboard_finished( 129 | this: &Object, 130 | _: Sel, 131 | _pasteboard: id, 132 | ) { 133 | unsafe { 134 | let provider = this.get_ivar::<*mut c_void>("provider_ptr"); 135 | drop(Box::from_raw( 136 | *provider as *mut Box Option>>, 137 | )); 138 | } 139 | } 140 | 141 | extern "C" fn provide_data( 142 | this: &Object, 143 | _: Sel, 144 | _pasteboard: id, 145 | item: id, 146 | data_type: id, 147 | ) { 148 | unsafe { 149 | let provider = this.get_ivar::<*mut c_void>("provider_ptr"); 150 | 151 | let provider = 152 | &*(*provider as *mut Box Option>>); 153 | 154 | if let Some(data) = provider(NSString(data_type).to_str()) { 155 | let bytes = data.as_ptr() as *mut c_void; 156 | let length = data.len(); 157 | let data: id = msg_send![class!(NSData), alloc]; 158 | let data: id = msg_send![data, initWithBytesNoCopy:bytes length:length freeWhenDone: if length == 0 { NO } else { YES }]; 159 | 160 | let _: () = 161 | msg_send![item, setData: data forType: data_type]; 162 | } 163 | } 164 | } 165 | 166 | cls.register() 167 | } 168 | None => Class::get("DragRsDataProvider") 169 | .expect("Failed to get the class definition"), 170 | }; 171 | 172 | let data_provider: id = msg_send![cls, alloc]; 173 | let data_provider: id = msg_send![data_provider, init]; 174 | 175 | let provider_ptr = Box::into_raw(Box::new(provider)); 176 | (*data_provider) 177 | .set_ivar("provider_ptr", provider_ptr as *mut _ as *mut c_void); 178 | 179 | let item: id = msg_send![class!(NSPasteboardItem), alloc]; 180 | let item: id = msg_send![item, init]; 181 | let types = types 182 | .into_iter() 183 | .map(|t| NSString::new(&t).0) 184 | .collect::>(); 185 | let _: () = msg_send![item, setDataProvider: data_provider forTypes: NSArray::arrayWithObjects(nil, &types)]; 186 | 187 | let drag_item: id = msg_send![class!(NSDraggingItem), alloc]; 188 | let item: id = msg_send![drag_item, initWithPasteboardWriter: item]; 189 | 190 | let _: () = msg_send![item, setDraggingFrame: image_rect contents: img]; 191 | 192 | let _: () = msg_send![dragging_items, addObject: item]; 193 | } 194 | } 195 | 196 | let drag_event: id = msg_send![class!(NSEvent), alloc]; 197 | let current_event: id = msg_send![NSApp(), currentEvent]; 198 | let drag_event: id = NSEvent::mouseEventWithType_location_modifierFlags_timestamp_windowNumber_context_eventNumber_clickCount_pressure_( 199 | drag_event, 200 | NSEventType::NSLeftMouseDragged, 201 | current_position, 202 | NSEventModifierFlags::empty(), 203 | msg_send![current_event, timestamp], 204 | msg_send![window, windowNumber], 205 | nil, 206 | 0, 207 | 1, 208 | 1.0 209 | ); 210 | 211 | let cls = ClassDecl::new("DragRsSource", class!(NSObject)); 212 | let cls = match cls { 213 | Some(mut cls) => { 214 | cls.add_ivar::<*mut c_void>("on_drop_ptr"); 215 | cls.add_ivar::("animate_on_cancel_or_failure"); 216 | cls.add_ivar::("drag_mode"); 217 | cls.add_method( 218 | sel!(draggingSession:sourceOperationMaskForDraggingContext:), 219 | dragging_session 220 | as extern "C" fn(&Object, Sel, id, NSUInteger) -> NSUInteger, 221 | ); 222 | cls.add_method( 223 | sel!(draggingSession:endedAtPoint:operation:), 224 | dragging_session_end 225 | as extern "C" fn(&Object, Sel, id, NSPoint, NSUInteger), 226 | ); 227 | 228 | extern "C" fn dragging_session( 229 | this: &Object, 230 | _: Sel, 231 | dragging_session: id, 232 | _context: NSUInteger, 233 | ) -> NSUInteger { 234 | unsafe { 235 | let animates = this.get_ivar::("animate_on_cancel_or_failure"); 236 | let mode = *this.get_ivar::("drag_mode"); 237 | let () = msg_send![dragging_session, setAnimatesToStartingPositionsOnCancelOrFail: *animates]; 238 | 239 | match mode { 240 | DragMode::Copy => 1, // NSDragOperationCopy 241 | DragMode::Move => 16, // NSDragOperationMove 242 | } 243 | } 244 | } 245 | 246 | extern "C" fn dragging_session_end( 247 | this: &Object, 248 | _: Sel, 249 | _dragging_session: id, 250 | ended_at_point: NSPoint, 251 | operation: NSUInteger, 252 | ) { 253 | unsafe { 254 | let callback = this.get_ivar::<*mut c_void>("on_drop_ptr"); 255 | 256 | let mouse_location = CursorPosition { 257 | x: ended_at_point.x as i32, 258 | y: CGDisplay::main().pixels_high() as i32 - ended_at_point.y as i32, 259 | }; 260 | 261 | let callback_closure = 262 | &*(*callback as *mut Box); 263 | 264 | if operation == 0 { 265 | // NSDragOperationNone 266 | callback_closure(DragResult::Cancel, mouse_location); 267 | } else { 268 | callback_closure(DragResult::Dropped, mouse_location); 269 | } 270 | 271 | drop(Box::from_raw(*callback as *mut Box)); 272 | } 273 | } 274 | 275 | cls.register() 276 | } 277 | None => Class::get("DragRsSource").expect("Failed to get the class definition"), 278 | }; 279 | 280 | let source: id = msg_send![cls, alloc]; 281 | let source: id = msg_send![source, init]; 282 | 283 | let on_drop_callback = 284 | Box::new(on_drop_callback) as Box; 285 | let callback_ptr = Box::into_raw(Box::new(on_drop_callback)); 286 | (*source).set_ivar("on_drop_ptr", callback_ptr as *mut _ as *mut c_void); 287 | (*source).set_ivar( 288 | "animate_on_cancel_or_failure", 289 | if options.skip_animatation_on_cancel_or_failure { 290 | YES 291 | } else { 292 | NO 293 | }, 294 | ); 295 | (*source).set_ivar("drag_mode", options.mode); 296 | 297 | let _: () = msg_send![ns_view, beginDraggingSessionWithItems: dragging_items event: drag_event source: source]; 298 | 299 | Ok(()) 300 | } 301 | } else { 302 | Err(crate::Error::UnsupportedWindowHandle) 303 | } 304 | } 305 | -------------------------------------------------------------------------------- /crates/drag/src/platform_impl/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2023 CrabNebula Ltd. 2 | // SPDX-License-Identifier: Apache-2.0 3 | // SPDX-License-Identifier: MIT 4 | 5 | #[cfg(target_os = "windows")] 6 | #[path = "windows/mod.rs"] 7 | mod platform; 8 | #[cfg(target_os = "linux")] 9 | #[path = "gtk/mod.rs"] 10 | mod platform; 11 | #[cfg(target_os = "macos")] 12 | #[path = "macos/mod.rs"] 13 | mod platform; 14 | pub use platform::start_drag; 15 | -------------------------------------------------------------------------------- /crates/drag/src/platform_impl/windows/image.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2023 CrabNebula Ltd. 2 | // SPDX-License-Identifier: Apache-2.0 3 | // SPDX-License-Identifier: MIT 4 | 5 | use std::os::windows::ffi::OsStrExt; 6 | use std::{ffi::c_void, iter::once, path::Path}; 7 | use windows::core::PCWSTR; 8 | use windows::Win32::Foundation::*; 9 | use windows::Win32::{ 10 | Graphics::{ 11 | Gdi::{CreateBitmap, HBITMAP}, 12 | Imaging::{ 13 | CLSID_WICImagingFactory, GUID_WICPixelFormat32bppPBGRA, IWICBitmapDecoder, 14 | IWICImagingFactory, WICConvertBitmapSource, WICDecodeMetadataCacheOnDemand, 15 | }, 16 | }, 17 | System::Com::{CoCreateInstance, CLSCTX_INPROC_SERVER}, 18 | }; 19 | 20 | use crate::Result; 21 | 22 | pub(crate) fn read_bytes_to_hbitmap(bytes: &[u8]) -> Result { 23 | unsafe { 24 | let factory: IWICImagingFactory = 25 | CoCreateInstance(&CLSID_WICImagingFactory, None, CLSCTX_INPROC_SERVER)?; 26 | 27 | let stream = factory.CreateStream()?; 28 | stream.InitializeFromMemory(bytes)?; 29 | 30 | let decoder = factory.CreateDecoderFromStream( 31 | &stream, 32 | std::ptr::null(), 33 | WICDecodeMetadataCacheOnDemand, 34 | )?; 35 | 36 | decoder_to_hbitmap(decoder) 37 | } 38 | } 39 | 40 | pub(crate) fn read_path_to_hbitmap(path: &Path) -> Result { 41 | unsafe { 42 | let factory: IWICImagingFactory = 43 | CoCreateInstance(&CLSID_WICImagingFactory, None, CLSCTX_INPROC_SERVER)?; 44 | 45 | let path = dunce::canonicalize(path)?; 46 | let wide_path: Vec = path.as_os_str().encode_wide().chain(once(0)).collect(); 47 | 48 | let decoder = factory.CreateDecoderFromFilename( 49 | PCWSTR::from_raw(wide_path.as_ptr()), 50 | None, 51 | GENERIC_READ, 52 | WICDecodeMetadataCacheOnDemand, 53 | )?; 54 | 55 | decoder_to_hbitmap(decoder) 56 | } 57 | } 58 | 59 | fn decoder_to_hbitmap(decoder: IWICBitmapDecoder) -> Result { 60 | unsafe { 61 | let frame = decoder.GetFrame(0)?; 62 | 63 | let mut width: u32 = 0; 64 | let mut height: u32 = 0; 65 | frame.GetSize(&mut width, &mut height)?; 66 | 67 | let mut pixel_buf: Vec = vec![0; (width * height * 4) as usize]; 68 | let pixel_format = frame.GetPixelFormat()?; 69 | if pixel_format != GUID_WICPixelFormat32bppPBGRA { 70 | let bitmap_source = WICConvertBitmapSource(&GUID_WICPixelFormat32bppPBGRA, &frame)?; 71 | bitmap_source.CopyPixels(std::ptr::null(), width * 4, &mut pixel_buf)?; 72 | } else { 73 | frame.CopyPixels(std::ptr::null(), width * 4, &mut pixel_buf)?; 74 | } 75 | 76 | Ok(CreateBitmap( 77 | width as i32, 78 | height as i32, 79 | 1, 80 | 32, 81 | Some(pixel_buf.as_ptr() as *const c_void), 82 | )) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /crates/drag/src/platform_impl/windows/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2023 CrabNebula Ltd. 2 | // SPDX-License-Identifier: Apache-2.0 3 | // SPDX-License-Identifier: MIT 4 | 5 | use raw_window_handle::{HasWindowHandle, RawWindowHandle}; 6 | 7 | use crate::{CursorPosition, DragItem, DragMode, DragResult, Image, Options}; 8 | 9 | use std::{ 10 | ffi::c_void, 11 | iter::once, 12 | os::windows::ffi::OsStrExt, 13 | path::{Path, PathBuf}, 14 | sync::Once, 15 | }; 16 | use windows::{ 17 | core::*, 18 | Win32::{ 19 | Foundation::*, 20 | Graphics::Gdi::{GetObjectW, BITMAP}, 21 | System::Com::*, 22 | System::Memory::*, 23 | System::Ole::{DoDragDrop, OleInitialize}, 24 | System::Ole::{ 25 | IDropSource, IDropSource_Impl, CF_HDROP, DROPEFFECT, DROPEFFECT_COPY, DROPEFFECT_MOVE, 26 | }, 27 | System::SystemServices::{MK_LBUTTON, MODIFIERKEYS_FLAGS}, 28 | UI::{ 29 | Shell::{ 30 | BHID_DataObject, CLSID_DragDropHelper, Common, IDragSourceHelper, IShellItemArray, 31 | SHCreateDataObject, SHCreateShellItemArrayFromIDLists, DROPFILES, SHDRAGIMAGE, 32 | }, 33 | WindowsAndMessaging::GetCursorPos, 34 | }, 35 | }, 36 | }; 37 | 38 | mod image; 39 | 40 | static mut OLE_RESULT: Result<()> = Ok(()); 41 | static OLE_UNINITIALIZE: Once = Once::new(); 42 | fn init_ole() { 43 | OLE_UNINITIALIZE.call_once(|| { 44 | unsafe { 45 | OLE_RESULT = OleInitialize(Some(std::ptr::null_mut())); 46 | } 47 | // I guess we never deinitialize for now? 48 | // OleUninitialize 49 | }); 50 | } 51 | 52 | #[implement(IDataObject)] 53 | struct DataObject { 54 | files: Vec, 55 | inner_shell_obj: IDataObject, 56 | } 57 | 58 | #[implement(IDropSource)] 59 | struct DropSource(()); 60 | 61 | #[implement(IDropSource)] 62 | struct DummyDropSource(()); 63 | 64 | impl DropSource { 65 | fn new() -> Self { 66 | Self(()) 67 | } 68 | } 69 | 70 | #[allow(non_snake_case)] 71 | impl IDropSource_Impl for DropSource { 72 | fn QueryContinueDrag(&self, fescapepressed: BOOL, grfkeystate: MODIFIERKEYS_FLAGS) -> HRESULT { 73 | if fescapepressed.as_bool() { 74 | DRAGDROP_S_CANCEL 75 | } else if (grfkeystate & MK_LBUTTON) == MODIFIERKEYS_FLAGS(0) { 76 | DRAGDROP_S_DROP 77 | } else { 78 | S_OK 79 | } 80 | } 81 | 82 | fn GiveFeedback(&self, _dweffect: DROPEFFECT) -> HRESULT { 83 | DRAGDROP_S_USEDEFAULTCURSORS 84 | } 85 | } 86 | 87 | impl DummyDropSource { 88 | fn new() -> Self { 89 | Self(()) 90 | } 91 | } 92 | 93 | #[allow(non_snake_case)] 94 | impl IDropSource_Impl for DummyDropSource { 95 | fn QueryContinueDrag(&self, fescapepressed: BOOL, grfkeystate: MODIFIERKEYS_FLAGS) -> HRESULT { 96 | if fescapepressed.as_bool() || (grfkeystate & MK_LBUTTON) == MODIFIERKEYS_FLAGS(0) { 97 | DRAGDROP_S_CANCEL 98 | } else { 99 | S_OK 100 | } 101 | } 102 | 103 | fn GiveFeedback(&self, _dweffect: DROPEFFECT) -> HRESULT { 104 | DRAGDROP_S_USEDEFAULTCURSORS 105 | } 106 | } 107 | 108 | impl DataObject { 109 | // This will be used for sharing text between applications 110 | #[allow(dead_code)] 111 | fn new(files: Vec) -> Self { 112 | unsafe { 113 | Self { 114 | files, 115 | inner_shell_obj: SHCreateDataObject(None, None, None).unwrap(), 116 | } 117 | } 118 | } 119 | 120 | fn is_supported_format(pformatetc: *const FORMATETC) -> bool { 121 | if let Some(format_etc) = unsafe { pformatetc.as_ref() } { 122 | !(format_etc.tymed as i32 != TYMED_HGLOBAL.0 123 | || format_etc.cfFormat != CF_HDROP.0 124 | || format_etc.dwAspect != DVASPECT_CONTENT.0) 125 | } else { 126 | false 127 | } 128 | } 129 | 130 | fn clone_drop_hglobal(&self) -> Result { 131 | let mut buffer = Vec::new(); 132 | for path in &self.files { 133 | let wide_path: Vec = path.as_os_str().encode_wide().chain(once(0)).collect(); 134 | buffer.extend(wide_path); 135 | } 136 | buffer.push(0); 137 | let size = std::mem::size_of::() + buffer.len() * 2; 138 | let handle = get_hglobal(size, buffer)?; 139 | Ok(handle) 140 | } 141 | } 142 | 143 | #[allow(non_snake_case)] 144 | impl IDataObject_Impl for DataObject { 145 | fn GetData(&self, pformatetc: *const FORMATETC) -> Result { 146 | unsafe { 147 | if Self::is_supported_format(pformatetc) { 148 | Ok(STGMEDIUM { 149 | tymed: TYMED_HGLOBAL.0 as u32, 150 | u: STGMEDIUM_0 { 151 | hGlobal: self.clone_drop_hglobal()?, 152 | }, 153 | pUnkForRelease: std::mem::ManuallyDrop::new(None), 154 | }) 155 | } else { 156 | self.inner_shell_obj.GetData(pformatetc) 157 | } 158 | } 159 | } 160 | 161 | fn GetDataHere(&self, _pformatetc: *const FORMATETC, _pmedium: *mut STGMEDIUM) -> Result<()> { 162 | Err(Error::new(DV_E_FORMATETC, HSTRING::new())) 163 | } 164 | 165 | fn QueryGetData(&self, pformatetc: *const FORMATETC) -> HRESULT { 166 | unsafe { 167 | if Self::is_supported_format(pformatetc) { 168 | S_OK 169 | } else { 170 | self.inner_shell_obj.QueryGetData(pformatetc) 171 | } 172 | } 173 | } 174 | 175 | fn GetCanonicalFormatEtc( 176 | &self, 177 | _pformatectin: *const FORMATETC, 178 | pformatetcout: *mut FORMATETC, 179 | ) -> HRESULT { 180 | unsafe { (*pformatetcout).ptd = std::ptr::null_mut() }; 181 | E_NOTIMPL 182 | } 183 | 184 | fn SetData( 185 | &self, 186 | pformatetc: *const FORMATETC, 187 | pmedium: *const STGMEDIUM, 188 | frelease: BOOL, 189 | ) -> Result<()> { 190 | unsafe { self.inner_shell_obj.SetData(pformatetc, pmedium, frelease) } 191 | } 192 | 193 | fn EnumFormatEtc(&self, _dwdirection: u32) -> Result { 194 | Err(Error::new(E_NOTIMPL, HSTRING::new())) 195 | } 196 | 197 | fn DAdvise( 198 | &self, 199 | _pformatetc: *const FORMATETC, 200 | _advf: u32, 201 | _padvsink: Option<&IAdviseSink>, 202 | ) -> Result { 203 | Err(Error::new(OLE_E_ADVISENOTSUPPORTED, HSTRING::new())) 204 | } 205 | 206 | fn DUnadvise(&self, _dwconnection: u32) -> Result<()> { 207 | Err(Error::new(OLE_E_ADVISENOTSUPPORTED, HSTRING::new())) 208 | } 209 | 210 | fn EnumDAdvise(&self) -> Result { 211 | Err(Error::new(OLE_E_ADVISENOTSUPPORTED, HSTRING::new())) 212 | } 213 | } 214 | 215 | pub fn start_drag( 216 | handle: &W, 217 | item: DragItem, 218 | image: Image, 219 | on_drop_callback: F, 220 | options: Options, 221 | ) -> crate::Result<()> { 222 | if let Ok(RawWindowHandle::Win32(_w)) = handle.window_handle().map(|h| h.as_raw()) { 223 | match item { 224 | DragItem::Files(files) => { 225 | init_ole(); 226 | unsafe { 227 | #[allow(static_mut_refs)] 228 | if let Err(e) = &OLE_RESULT { 229 | return Err(e.clone().into()); 230 | } 231 | } 232 | 233 | let mut paths = Vec::new(); 234 | for f in files { 235 | paths.push(dunce::canonicalize(f)?); 236 | } 237 | 238 | let data_object: IDataObject = get_file_data_object(&paths).unwrap(); 239 | let drop_source: IDropSource = DropSource::new().into(); 240 | 241 | unsafe { 242 | if let Some(drag_image) = get_drag_image(image) { 243 | if let Ok(helper) = 244 | create_instance::(&CLSID_DragDropHelper) 245 | { 246 | let _ = helper.InitializeFromBitmap(&drag_image, &data_object); 247 | } 248 | } 249 | 250 | let mut out_dropeffect = DROPEFFECT::default(); 251 | let effect = match options.mode { 252 | DragMode::Copy => DROPEFFECT_COPY, 253 | DragMode::Move => DROPEFFECT_MOVE, 254 | }; 255 | 256 | let drop_result = 257 | DoDragDrop(&data_object, &drop_source, effect, &mut out_dropeffect); 258 | let mut pt = POINT { x: 0, y: 0 }; 259 | GetCursorPos(&mut pt)?; 260 | if drop_result == DRAGDROP_S_DROP { 261 | on_drop_callback(DragResult::Dropped, CursorPosition { x: pt.x, y: pt.y }); 262 | } else { 263 | // DRAGDROP_S_CANCEL 264 | on_drop_callback(DragResult::Cancel, CursorPosition { x: pt.x, y: pt.y }); 265 | } 266 | } 267 | } 268 | DragItem::Data { .. } => { 269 | init_ole(); 270 | unsafe { 271 | #[allow(static_mut_refs)] 272 | if let Err(e) = &OLE_RESULT { 273 | return Err(e.clone().into()); 274 | } 275 | } 276 | 277 | let paths = vec![dunce::canonicalize("./")?]; 278 | 279 | let data_object: IDataObject = get_file_data_object(&paths).unwrap(); 280 | let drop_source: IDropSource = DummyDropSource::new().into(); 281 | 282 | unsafe { 283 | if let Some(drag_image) = get_drag_image(image) { 284 | if let Ok(helper) = 285 | create_instance::(&CLSID_DragDropHelper) 286 | { 287 | let _ = helper.InitializeFromBitmap(&drag_image, &data_object); 288 | } 289 | } 290 | 291 | let mut out_dropeffect = DROPEFFECT::default(); 292 | let drop_result = DoDragDrop( 293 | &data_object, 294 | &drop_source, 295 | DROPEFFECT_COPY, 296 | &mut out_dropeffect, 297 | ); 298 | let mut pt = POINT { x: 0, y: 0 }; 299 | GetCursorPos(&mut pt)?; 300 | if drop_result == DRAGDROP_S_DROP { 301 | on_drop_callback(DragResult::Dropped, CursorPosition { x: pt.x, y: pt.y }); 302 | } else { 303 | // DRAGDROP_S_CANCEL 304 | on_drop_callback(DragResult::Cancel, CursorPosition { x: pt.x, y: pt.y }); 305 | } 306 | } 307 | } 308 | } 309 | Ok(()) 310 | } else { 311 | Err(crate::Error::UnsupportedWindowHandle) 312 | } 313 | } 314 | 315 | fn get_drag_image(image: Image) -> Option { 316 | let hbitmap = match image { 317 | Image::Raw(bytes) => image::read_bytes_to_hbitmap(&bytes).ok(), 318 | Image::File(path) => image::read_path_to_hbitmap(&path).ok(), 319 | }; 320 | hbitmap.map(|hbitmap| unsafe { 321 | // get image size 322 | let mut bitmap: BITMAP = BITMAP::default(); 323 | let (width, height) = if 0 324 | == GetObjectW( 325 | hbitmap, 326 | std::mem::size_of::() as i32, 327 | Some(&mut bitmap as *mut BITMAP as *mut c_void), 328 | ) { 329 | (128, 128) 330 | } else { 331 | (bitmap.bmWidth, bitmap.bmHeight) 332 | }; 333 | 334 | SHDRAGIMAGE { 335 | sizeDragImage: SIZE { 336 | cx: width, 337 | cy: height, 338 | }, 339 | ptOffset: POINT { x: 0, y: 0 }, 340 | hbmpDragImage: hbitmap, 341 | crColorKey: COLORREF(0x00000000), 342 | } 343 | }) 344 | } 345 | 346 | fn get_hglobal(size: usize, buffer: Vec) -> Result { 347 | let handle = unsafe { GlobalAlloc(GMEM_FIXED, size).unwrap() }; 348 | let ptr = unsafe { GlobalLock(handle) }; 349 | 350 | let header = ptr as *mut DROPFILES; 351 | unsafe { 352 | (*header).pFiles = std::mem::size_of::() as u32; 353 | (*header).fWide = BOOL(1); 354 | std::ptr::copy( 355 | buffer.as_ptr() as *const c_void, 356 | ptr.add(std::mem::size_of::()), 357 | buffer.len() * 2, 358 | ); 359 | GlobalUnlock(handle) 360 | }?; 361 | Ok(handle) 362 | } 363 | 364 | pub fn create_instance(clsid: &GUID) -> Result { 365 | unsafe { CoCreateInstance(clsid, None, CLSCTX_ALL) } 366 | } 367 | 368 | fn get_file_data_object(paths: &[PathBuf]) -> Option { 369 | unsafe { 370 | let shell_item_array = get_shell_item_array(paths).unwrap(); 371 | shell_item_array.BindToHandler(None, &BHID_DataObject).ok() 372 | } 373 | } 374 | 375 | fn get_shell_item_array(paths: &[PathBuf]) -> Option { 376 | unsafe { 377 | let list: Vec<*const Common::ITEMIDLIST> = paths 378 | .iter() 379 | .map(|path| get_file_item_id(path).cast_const()) 380 | .collect(); 381 | SHCreateShellItemArrayFromIDLists(&list).ok() 382 | } 383 | } 384 | 385 | fn get_file_item_id(path: &Path) -> *mut Common::ITEMIDLIST { 386 | unsafe { 387 | let wide_path: Vec = path.as_os_str().encode_wide().chain(once(0)).collect(); 388 | windows::Win32::UI::Shell::ILCreateFromPathW(PCWSTR::from_raw(wide_path.as_ptr())) 389 | } 390 | } 391 | -------------------------------------------------------------------------------- /crates/tauri-plugin-drag-as-window/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## \[2.1.0] 4 | 5 | - [`229aa3e`](https://github.com/crabnebula-dev/drag-rs/commit/229aa3e26c85c31074abd3b4a4538b2ec65eb094) Added `mode` drag option to either copy or move a file. 6 | 7 | ### Dependencies 8 | 9 | - Upgraded to `drag@2.1.0` 10 | 11 | ## \[2.0.0] 12 | 13 | - [`244887f`](https://github.com/crabnebula-dev/drag-rs/commit/244887fa36b12ac615919b9d2d149edca3d1f1c7) Update to tauri v2. 14 | 15 | ### Dependencies 16 | 17 | - Upgraded to `drag@2.0.0` 18 | 19 | ## \[0.1.1] 20 | 21 | - [`6698e65`](https://github.com/crabnebula-dev/drag-rs/commit/6698e655215e649d8a40d4c8d6d328ca595ce2d8) Target ES2019 for macOS 10.14 compatibility. 22 | 23 | ## \[0.1.0] 24 | 25 | - [`f2d2631`](https://github.com/crabnebula-dev/drag-rs/commit/f2d26319d59dc9d92b4fc2b244d74f7b75f724b3) Initial release. 26 | 27 | ### Dependencies 28 | 29 | - Upgraded to `drag@0.4.0` 30 | -------------------------------------------------------------------------------- /crates/tauri-plugin-drag-as-window/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tauri-plugin-drag-as-window" 3 | version = "2.1.0" 4 | authors = [ "CrabNebula Ltd." ] 5 | description = "Start a drag operation from a DOM element to its own window" 6 | edition = "2021" 7 | license = { workspace = true } 8 | links = "tauri-plugin-drag-as-window" 9 | 10 | [dependencies] 11 | tauri.workspace = true 12 | serde.workspace = true 13 | serde_json.workspace = true 14 | thiserror.workspace = true 15 | drag = { path = "../drag", version = "2.1.0", features = [ "serde" ] } 16 | base64.workspace = true 17 | tempfile = "3" 18 | 19 | [build-dependencies] 20 | tauri-plugin.workspace = true 21 | -------------------------------------------------------------------------------- /crates/tauri-plugin-drag-as-window/LICENSE_APACHE-2.0: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | -------------------------------------------------------------------------------- /crates/tauri-plugin-drag-as-window/LICENSE_MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 - Present CrabNebula Ltd. 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 | -------------------------------------------------------------------------------- /crates/tauri-plugin-drag-as-window/build.rs: -------------------------------------------------------------------------------- 1 | const COMMANDS: &[&str] = &["drag_new_window", "drag_back", "on_drop"]; 2 | 3 | fn main() { 4 | tauri_plugin::Builder::new(COMMANDS) 5 | .global_api_script_path("./src/api-iife.js") 6 | .build(); 7 | } 8 | -------------------------------------------------------------------------------- /crates/tauri-plugin-drag-as-window/permissions/autogenerated/commands/drag_back.toml: -------------------------------------------------------------------------------- 1 | # Automatically generated - DO NOT EDIT! 2 | 3 | "$schema" = "../../schemas/schema.json" 4 | 5 | [[permission]] 6 | identifier = "allow-drag-back" 7 | description = "Enables the drag_back command without any pre-configured scope." 8 | commands.allow = ["drag_back"] 9 | 10 | [[permission]] 11 | identifier = "deny-drag-back" 12 | description = "Denies the drag_back command without any pre-configured scope." 13 | commands.deny = ["drag_back"] 14 | -------------------------------------------------------------------------------- /crates/tauri-plugin-drag-as-window/permissions/autogenerated/commands/drag_new_window.toml: -------------------------------------------------------------------------------- 1 | # Automatically generated - DO NOT EDIT! 2 | 3 | "$schema" = "../../schemas/schema.json" 4 | 5 | [[permission]] 6 | identifier = "allow-drag-new-window" 7 | description = "Enables the drag_new_window command without any pre-configured scope." 8 | commands.allow = ["drag_new_window"] 9 | 10 | [[permission]] 11 | identifier = "deny-drag-new-window" 12 | description = "Denies the drag_new_window command without any pre-configured scope." 13 | commands.deny = ["drag_new_window"] 14 | -------------------------------------------------------------------------------- /crates/tauri-plugin-drag-as-window/permissions/autogenerated/commands/on_drop.toml: -------------------------------------------------------------------------------- 1 | # Automatically generated - DO NOT EDIT! 2 | 3 | "$schema" = "../../schemas/schema.json" 4 | 5 | [[permission]] 6 | identifier = "allow-on-drop" 7 | description = "Enables the on_drop command without any pre-configured scope." 8 | commands.allow = ["on_drop"] 9 | 10 | [[permission]] 11 | identifier = "deny-on-drop" 12 | description = "Denies the on_drop command without any pre-configured scope." 13 | commands.deny = ["on_drop"] 14 | -------------------------------------------------------------------------------- /crates/tauri-plugin-drag-as-window/permissions/autogenerated/reference.md: -------------------------------------------------------------------------------- 1 | ## Default Permission 2 | 3 | Default permissions for the plugin 4 | 5 | - `allow-drag-new-window` 6 | - `allow-drag-back` 7 | - `allow-on-drop` 8 | 9 | ## Permission Table 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 24 | 29 | 30 | 31 | 32 | 37 | 42 | 43 | 44 | 45 | 50 | 55 | 56 | 57 | 58 | 63 | 68 | 69 | 70 | 71 | 76 | 81 | 82 | 83 | 84 | 89 | 94 | 95 |
IdentifierDescription
20 | 21 | `drag-as-window:allow-drag-back` 22 | 23 | 25 | 26 | Enables the drag_back command without any pre-configured scope. 27 | 28 |
33 | 34 | `drag-as-window:deny-drag-back` 35 | 36 | 38 | 39 | Denies the drag_back command without any pre-configured scope. 40 | 41 |
46 | 47 | `drag-as-window:allow-drag-new-window` 48 | 49 | 51 | 52 | Enables the drag_new_window command without any pre-configured scope. 53 | 54 |
59 | 60 | `drag-as-window:deny-drag-new-window` 61 | 62 | 64 | 65 | Denies the drag_new_window command without any pre-configured scope. 66 | 67 |
72 | 73 | `drag-as-window:allow-on-drop` 74 | 75 | 77 | 78 | Enables the on_drop command without any pre-configured scope. 79 | 80 |
85 | 86 | `drag-as-window:deny-on-drop` 87 | 88 | 90 | 91 | Denies the on_drop command without any pre-configured scope. 92 | 93 |
96 | -------------------------------------------------------------------------------- /crates/tauri-plugin-drag-as-window/permissions/default.toml: -------------------------------------------------------------------------------- 1 | "$schema" = "schemas/schema.json" 2 | 3 | [default] 4 | description = "Default permissions for the plugin" 5 | permissions = ["allow-drag-new-window", "allow-drag-back", "allow-on-drop"] 6 | -------------------------------------------------------------------------------- /crates/tauri-plugin-drag-as-window/permissions/schemas/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "title": "PermissionFile", 4 | "description": "Permission file that can define a default permission, a set of permissions or a list of inlined permissions.", 5 | "type": "object", 6 | "properties": { 7 | "default": { 8 | "description": "The default permission set for the plugin", 9 | "anyOf": [ 10 | { 11 | "$ref": "#/definitions/DefaultPermission" 12 | }, 13 | { 14 | "type": "null" 15 | } 16 | ] 17 | }, 18 | "set": { 19 | "description": "A list of permissions sets defined", 20 | "type": "array", 21 | "items": { 22 | "$ref": "#/definitions/PermissionSet" 23 | } 24 | }, 25 | "permission": { 26 | "description": "A list of inlined permissions", 27 | "default": [], 28 | "type": "array", 29 | "items": { 30 | "$ref": "#/definitions/Permission" 31 | } 32 | } 33 | }, 34 | "definitions": { 35 | "DefaultPermission": { 36 | "description": "The default permission set of the plugin.\n\nWorks similarly to a permission with the \"default\" identifier.", 37 | "type": "object", 38 | "required": [ 39 | "permissions" 40 | ], 41 | "properties": { 42 | "version": { 43 | "description": "The version of the permission.", 44 | "type": [ 45 | "integer", 46 | "null" 47 | ], 48 | "format": "uint64", 49 | "minimum": 1.0 50 | }, 51 | "description": { 52 | "description": "Human-readable description of what the permission does. Tauri convention is to use

headings in markdown content for Tauri documentation generation purposes.", 53 | "type": [ 54 | "string", 55 | "null" 56 | ] 57 | }, 58 | "permissions": { 59 | "description": "All permissions this set contains.", 60 | "type": "array", 61 | "items": { 62 | "type": "string" 63 | } 64 | } 65 | } 66 | }, 67 | "PermissionSet": { 68 | "description": "A set of direct permissions grouped together under a new name.", 69 | "type": "object", 70 | "required": [ 71 | "description", 72 | "identifier", 73 | "permissions" 74 | ], 75 | "properties": { 76 | "identifier": { 77 | "description": "A unique identifier for the permission.", 78 | "type": "string" 79 | }, 80 | "description": { 81 | "description": "Human-readable description of what the permission does.", 82 | "type": "string" 83 | }, 84 | "permissions": { 85 | "description": "All permissions this set contains.", 86 | "type": "array", 87 | "items": { 88 | "$ref": "#/definitions/PermissionKind" 89 | } 90 | } 91 | } 92 | }, 93 | "Permission": { 94 | "description": "Descriptions of explicit privileges of commands.\n\nIt can enable commands to be accessible in the frontend of the application.\n\nIf the scope is defined it can be used to fine grain control the access of individual or multiple commands.", 95 | "type": "object", 96 | "required": [ 97 | "identifier" 98 | ], 99 | "properties": { 100 | "version": { 101 | "description": "The version of the permission.", 102 | "type": [ 103 | "integer", 104 | "null" 105 | ], 106 | "format": "uint64", 107 | "minimum": 1.0 108 | }, 109 | "identifier": { 110 | "description": "A unique identifier for the permission.", 111 | "type": "string" 112 | }, 113 | "description": { 114 | "description": "Human-readable description of what the permission does. Tauri internal convention is to use

headings in markdown content for Tauri documentation generation purposes.", 115 | "type": [ 116 | "string", 117 | "null" 118 | ] 119 | }, 120 | "commands": { 121 | "description": "Allowed or denied commands when using this permission.", 122 | "default": { 123 | "allow": [], 124 | "deny": [] 125 | }, 126 | "allOf": [ 127 | { 128 | "$ref": "#/definitions/Commands" 129 | } 130 | ] 131 | }, 132 | "scope": { 133 | "description": "Allowed or denied scoped when using this permission.", 134 | "allOf": [ 135 | { 136 | "$ref": "#/definitions/Scopes" 137 | } 138 | ] 139 | }, 140 | "platforms": { 141 | "description": "Target platforms this permission applies. By default all platforms are affected by this permission.", 142 | "type": [ 143 | "array", 144 | "null" 145 | ], 146 | "items": { 147 | "$ref": "#/definitions/Target" 148 | } 149 | } 150 | } 151 | }, 152 | "Commands": { 153 | "description": "Allowed and denied commands inside a permission.\n\nIf two commands clash inside of `allow` and `deny`, it should be denied by default.", 154 | "type": "object", 155 | "properties": { 156 | "allow": { 157 | "description": "Allowed command.", 158 | "default": [], 159 | "type": "array", 160 | "items": { 161 | "type": "string" 162 | } 163 | }, 164 | "deny": { 165 | "description": "Denied command, which takes priority.", 166 | "default": [], 167 | "type": "array", 168 | "items": { 169 | "type": "string" 170 | } 171 | } 172 | } 173 | }, 174 | "Scopes": { 175 | "description": "An argument for fine grained behavior control of Tauri commands.\n\nIt can be of any serde serializable type and is used to allow or prevent certain actions inside a Tauri command. The configured scope is passed to the command and will be enforced by the command implementation.\n\n## Example\n\n```json { \"allow\": [{ \"path\": \"$HOME/**\" }], \"deny\": [{ \"path\": \"$HOME/secret.txt\" }] } ```", 176 | "type": "object", 177 | "properties": { 178 | "allow": { 179 | "description": "Data that defines what is allowed by the scope.", 180 | "type": [ 181 | "array", 182 | "null" 183 | ], 184 | "items": { 185 | "$ref": "#/definitions/Value" 186 | } 187 | }, 188 | "deny": { 189 | "description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.", 190 | "type": [ 191 | "array", 192 | "null" 193 | ], 194 | "items": { 195 | "$ref": "#/definitions/Value" 196 | } 197 | } 198 | } 199 | }, 200 | "Value": { 201 | "description": "All supported ACL values.", 202 | "anyOf": [ 203 | { 204 | "description": "Represents a null JSON value.", 205 | "type": "null" 206 | }, 207 | { 208 | "description": "Represents a [`bool`].", 209 | "type": "boolean" 210 | }, 211 | { 212 | "description": "Represents a valid ACL [`Number`].", 213 | "allOf": [ 214 | { 215 | "$ref": "#/definitions/Number" 216 | } 217 | ] 218 | }, 219 | { 220 | "description": "Represents a [`String`].", 221 | "type": "string" 222 | }, 223 | { 224 | "description": "Represents a list of other [`Value`]s.", 225 | "type": "array", 226 | "items": { 227 | "$ref": "#/definitions/Value" 228 | } 229 | }, 230 | { 231 | "description": "Represents a map of [`String`] keys to [`Value`]s.", 232 | "type": "object", 233 | "additionalProperties": { 234 | "$ref": "#/definitions/Value" 235 | } 236 | } 237 | ] 238 | }, 239 | "Number": { 240 | "description": "A valid ACL number.", 241 | "anyOf": [ 242 | { 243 | "description": "Represents an [`i64`].", 244 | "type": "integer", 245 | "format": "int64" 246 | }, 247 | { 248 | "description": "Represents a [`f64`].", 249 | "type": "number", 250 | "format": "double" 251 | } 252 | ] 253 | }, 254 | "Target": { 255 | "description": "Platform target.", 256 | "oneOf": [ 257 | { 258 | "description": "MacOS.", 259 | "type": "string", 260 | "enum": [ 261 | "macOS" 262 | ] 263 | }, 264 | { 265 | "description": "Windows.", 266 | "type": "string", 267 | "enum": [ 268 | "windows" 269 | ] 270 | }, 271 | { 272 | "description": "Linux.", 273 | "type": "string", 274 | "enum": [ 275 | "linux" 276 | ] 277 | }, 278 | { 279 | "description": "Android.", 280 | "type": "string", 281 | "enum": [ 282 | "android" 283 | ] 284 | }, 285 | { 286 | "description": "iOS.", 287 | "type": "string", 288 | "enum": [ 289 | "iOS" 290 | ] 291 | } 292 | ] 293 | }, 294 | "PermissionKind": { 295 | "type": "string", 296 | "oneOf": [ 297 | { 298 | "description": "Enables the drag_back command without any pre-configured scope.", 299 | "type": "string", 300 | "const": "allow-drag-back" 301 | }, 302 | { 303 | "description": "Denies the drag_back command without any pre-configured scope.", 304 | "type": "string", 305 | "const": "deny-drag-back" 306 | }, 307 | { 308 | "description": "Enables the drag_new_window command without any pre-configured scope.", 309 | "type": "string", 310 | "const": "allow-drag-new-window" 311 | }, 312 | { 313 | "description": "Denies the drag_new_window command without any pre-configured scope.", 314 | "type": "string", 315 | "const": "deny-drag-new-window" 316 | }, 317 | { 318 | "description": "Enables the on_drop command without any pre-configured scope.", 319 | "type": "string", 320 | "const": "allow-on-drop" 321 | }, 322 | { 323 | "description": "Denies the on_drop command without any pre-configured scope.", 324 | "type": "string", 325 | "const": "deny-on-drop" 326 | }, 327 | { 328 | "description": "Default permissions for the plugin", 329 | "type": "string", 330 | "const": "default" 331 | } 332 | ] 333 | } 334 | } 335 | } -------------------------------------------------------------------------------- /crates/tauri-plugin-drag-as-window/src/commands.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fs::read, 3 | io::Write, 4 | path::PathBuf, 5 | sync::{mpsc::channel, Arc, Mutex}, 6 | }; 7 | 8 | use base64::Engine; 9 | use serde::{ser::Serializer, Deserialize, Serialize}; 10 | use tauri::{ 11 | command, ipc::Channel, AppHandle, DragDropEvent, Manager, Runtime, WebviewWindow, WindowEvent, 12 | }; 13 | 14 | type Result = std::result::Result; 15 | 16 | const FILE_PREFIX: &str = "qow3ciuh"; 17 | 18 | #[derive(Debug, thiserror::Error)] 19 | pub enum Error { 20 | #[error(transparent)] 21 | Drag(#[from] drag::Error), 22 | #[error(transparent)] 23 | Tauri(#[from] tauri::Error), 24 | #[error(transparent)] 25 | Base64(#[from] base64::DecodeError), 26 | #[error(transparent)] 27 | Json(#[from] serde_json::Error), 28 | #[error(transparent)] 29 | Io(#[from] std::io::Error), 30 | #[error("invalid base64, expected image/png format")] 31 | InvalidBase64, 32 | } 33 | 34 | impl Serialize for Error { 35 | fn serialize(&self, serializer: S) -> std::result::Result 36 | where 37 | S: Serializer, 38 | { 39 | serializer.serialize_str(self.to_string().as_ref()) 40 | } 41 | } 42 | 43 | #[derive(Clone, Serialize)] 44 | pub struct CallbackResult { 45 | result: drag::DragResult, 46 | #[serde(rename = "cursorPos")] 47 | cursor_pos: drag::CursorPosition, 48 | } 49 | 50 | #[derive(Deserialize)] 51 | #[serde(rename_all = "camelCase")] 52 | pub enum DragMode { 53 | Copy, 54 | Move, 55 | } 56 | 57 | impl Default for DragMode { 58 | fn default() -> Self { 59 | Self::Copy 60 | } 61 | } 62 | 63 | impl From for drag::DragMode { 64 | fn from(value: DragMode) -> Self { 65 | match value { 66 | DragMode::Copy => Self::Copy, 67 | DragMode::Move => Self::Move, 68 | } 69 | } 70 | } 71 | 72 | #[derive(Default, Deserialize)] 73 | pub struct DragOptions { 74 | #[serde(default)] 75 | mode: DragMode, 76 | } 77 | 78 | impl From for drag::Options { 79 | fn from(options: DragOptions) -> Self { 80 | Self { 81 | skip_animatation_on_cancel_or_failure: true, 82 | mode: options.mode.into(), 83 | } 84 | } 85 | } 86 | 87 | #[command] 88 | pub async fn on_drop( 89 | window: WebviewWindow, 90 | handler: Channel, 91 | ) -> Result<()> { 92 | window.on_window_event(move |event| { 93 | if let WindowEvent::DragDrop(DragDropEvent::Drop { paths, position: _ }) = event { 94 | let path = paths.first().unwrap(); 95 | if path 96 | .file_name() 97 | .and_then(|f| f.to_str()) 98 | .map(|f| f.starts_with(FILE_PREFIX)) 99 | .unwrap_or_default() 100 | { 101 | if let Some(data) = read(path) 102 | .ok() 103 | .and_then(|bytes| serde_json::from_slice::(&bytes).ok()) 104 | { 105 | let _ = handler.send(data); 106 | } else { 107 | eprintln!("failed to read {}", path.display()); 108 | } 109 | } 110 | } 111 | }); 112 | Ok(()) 113 | } 114 | 115 | #[command] 116 | pub async fn drag_new_window( 117 | app: AppHandle, 118 | window: WebviewWindow, 119 | image_base64: String, 120 | on_event: Channel, 121 | options: Option, 122 | ) -> Result<()> { 123 | perform_drag( 124 | app, 125 | window, 126 | DragData::Data, 127 | image_base64, 128 | options.unwrap_or_default(), 129 | on_event, 130 | || {}, 131 | ) 132 | } 133 | 134 | #[command] 135 | pub async fn drag_back( 136 | app: AppHandle, 137 | window: WebviewWindow, 138 | data: serde_json::Value, 139 | image_base64: String, 140 | on_event: Channel, 141 | options: Option, 142 | ) -> Result<()> { 143 | let data = serde_json::to_vec(&data)?; 144 | 145 | let mut file = tempfile::Builder::new().prefix(FILE_PREFIX).tempfile()?; 146 | file.write_all(&data)?; 147 | file.flush()?; 148 | let path = file.path().to_path_buf(); 149 | 150 | let file = Arc::new(Mutex::new(Some(file))); 151 | 152 | perform_drag( 153 | app, 154 | window, 155 | DragData::Path(path), 156 | image_base64, 157 | options.unwrap_or_default(), 158 | on_event, 159 | move || { 160 | let file_ = file.clone(); 161 | // wait a litle to delete the file 162 | std::thread::spawn(move || { 163 | std::thread::sleep(std::time::Duration::from_secs(3)); 164 | file_.lock().unwrap().take(); 165 | }); 166 | }, 167 | ) 168 | } 169 | 170 | enum DragData { 171 | Path(PathBuf), 172 | Data, 173 | } 174 | 175 | fn perform_drag( 176 | app: AppHandle, 177 | window: WebviewWindow, 178 | data: DragData, 179 | image_base64: String, 180 | drag_options: DragOptions, 181 | on_event: Channel, 182 | handler: F, 183 | ) -> Result<()> { 184 | let (tx, rx) = channel(); 185 | 186 | let image = drag::Image::Raw( 187 | base64::engine::general_purpose::STANDARD.decode( 188 | image_base64 189 | .strip_prefix("data:image/png;base64,") 190 | .ok_or(Error::InvalidBase64)?, 191 | )?, 192 | ); 193 | 194 | app.run_on_main_thread(move || { 195 | #[cfg(target_os = "linux")] 196 | let raw_window = window.gtk_window(); 197 | #[cfg(not(target_os = "linux"))] 198 | let raw_window = tauri::Result::Ok(window.clone()); 199 | 200 | let r = match raw_window { 201 | Ok(w) => drag::start_drag( 202 | &w, 203 | match data { 204 | DragData::Path(p) => drag::DragItem::Files(vec![p]), 205 | DragData::Data => drag::DragItem::Data { 206 | provider: Box::new(|_type| Some(Vec::new())), 207 | types: vec![window.config().identifier.clone()], 208 | }, 209 | }, 210 | image, 211 | move |result, cursor_pos| { 212 | let callback_result = CallbackResult { result, cursor_pos }; 213 | let _ = on_event.send(callback_result); 214 | 215 | handler(); 216 | }, 217 | drag::Options { 218 | skip_animatation_on_cancel_or_failure: true, 219 | ..drag_options.into() 220 | }, 221 | ) 222 | .map_err(Into::into), 223 | Err(e) => Err(e.into()), 224 | }; 225 | tx.send(r).unwrap(); 226 | })?; 227 | 228 | rx.recv().unwrap() 229 | } 230 | -------------------------------------------------------------------------------- /crates/tauri-plugin-drag-as-window/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2023 CrabNebula Ltd. 2 | // SPDX-License-Identifier: Apache-2.0 3 | // SPDX-License-Identifier: MIT 4 | 5 | use tauri::{ 6 | plugin::{Builder, TauriPlugin}, 7 | Runtime, 8 | }; 9 | 10 | mod commands; 11 | 12 | /// Initializes the plugin. 13 | pub fn init() -> TauriPlugin { 14 | Builder::new("drag-as-window") 15 | .invoke_handler(tauri::generate_handler![ 16 | commands::drag_new_window, 17 | commands::drag_back, 18 | commands::on_drop 19 | ]) 20 | .build() 21 | } 22 | -------------------------------------------------------------------------------- /crates/tauri-plugin-drag/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## \[2.1.0] 4 | 5 | - [`229aa3e`](https://github.com/crabnebula-dev/drag-rs/commit/229aa3e26c85c31074abd3b4a4538b2ec65eb094) Added `mode` drag option to either copy or move a file. 6 | 7 | ### Dependencies 8 | 9 | - Upgraded to `drag@2.1.0` 10 | 11 | ## \[2.0.0] 12 | 13 | - [`244887f`](https://github.com/crabnebula-dev/drag-rs/commit/244887fa36b12ac615919b9d2d149edca3d1f1c7) Update to tauri v2. 14 | 15 | ### Dependencies 16 | 17 | - Upgraded to `drag@2.0.0` 18 | 19 | ## \[0.3.1] 20 | 21 | - [`6698e65`](https://github.com/crabnebula-dev/drag-rs/commit/6698e655215e649d8a40d4c8d6d328ca595ce2d8) Target ES2019 for macOS 10.14 compatibility. 22 | 23 | ## \[0.3.0] 24 | 25 | - [`dd3f087`](https://github.com/crabnebula-dev/drag-rs/commit/dd3f0873ae2406968d412d9dfdc1c79a5ed5533e)([#25](https://github.com/crabnebula-dev/drag-rs/pull/25)) Changed the onEvent callback payload from `DragResult` to `{ result: DragResult, cursorPos: CursorPosition }`. 26 | 27 | ### Dependencies 28 | 29 | - Upgraded to `drag@0.4.0` 30 | 31 | ## \[0.2.0] 32 | 33 | - [`1449076`](https://github.com/crabnebula-dev/drag-rs/commit/14490764de8ff50969a3f2299d204e44e091752e) The `startDrag` function now takes an argument for a callback function for the operation result (either `Dragged` or `Cancelled`). 34 | - [`f58ed78`](https://github.com/crabnebula-dev/drag-rs/commit/f58ed7838abe1fe5b23c4e3aa92df28e77564345) The `startDrag` function can now be used to drag arbitrary data strings on macOS (e.g. Final Cut Pro XMLs). 35 | 36 | ### Dependencies 37 | 38 | - Upgraded to `drag@0.3.0` 39 | 40 | ## \[0.1.0] 41 | 42 | - [`644cfa2`](https://github.com/crabnebula-dev/drag-rs/commit/644cfa28b09bee9c3de396bdcc1dc801a26d65bc) Initial release. 43 | -------------------------------------------------------------------------------- /crates/tauri-plugin-drag/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tauri-plugin-drag" 3 | version = "2.1.0" 4 | authors = [ "CrabNebula Ltd." ] 5 | description = "Start a drag operation out of a Tauri window" 6 | edition = "2021" 7 | license = { workspace = true } 8 | links = "tauri-plugin-drag" 9 | 10 | [dependencies] 11 | tauri.workspace = true 12 | serde.workspace = true 13 | serde_json.workspace = true 14 | thiserror.workspace = true 15 | drag = { path = "../drag", version = "2.1.0", features = [ "serde" ] } 16 | base64.workspace = true 17 | 18 | [build-dependencies] 19 | tauri-plugin.workspace = true 20 | -------------------------------------------------------------------------------- /crates/tauri-plugin-drag/LICENSE_APACHE-2.0: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | -------------------------------------------------------------------------------- /crates/tauri-plugin-drag/LICENSE_MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 - Present CrabNebula Ltd. 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 | -------------------------------------------------------------------------------- /crates/tauri-plugin-drag/build.rs: -------------------------------------------------------------------------------- 1 | const COMMANDS: &[&str] = &["start_drag"]; 2 | 3 | fn main() { 4 | tauri_plugin::Builder::new(COMMANDS) 5 | .global_api_script_path("./src/api-iife.js") 6 | .build(); 7 | } 8 | -------------------------------------------------------------------------------- /crates/tauri-plugin-drag/permissions/autogenerated/commands/start_drag.toml: -------------------------------------------------------------------------------- 1 | # Automatically generated - DO NOT EDIT! 2 | 3 | "$schema" = "../../schemas/schema.json" 4 | 5 | [[permission]] 6 | identifier = "allow-start-drag" 7 | description = "Enables the start_drag command without any pre-configured scope." 8 | commands.allow = ["start_drag"] 9 | 10 | [[permission]] 11 | identifier = "deny-start-drag" 12 | description = "Denies the start_drag command without any pre-configured scope." 13 | commands.deny = ["start_drag"] 14 | -------------------------------------------------------------------------------- /crates/tauri-plugin-drag/permissions/autogenerated/reference.md: -------------------------------------------------------------------------------- 1 | ## Default Permission 2 | 3 | Default permissions for the plugin 4 | 5 | - `allow-start-drag` 6 | 7 | ## Permission Table 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 22 | 27 | 28 | 29 | 30 | 35 | 40 | 41 |
IdentifierDescription
18 | 19 | `drag:allow-start-drag` 20 | 21 | 23 | 24 | Enables the start_drag command without any pre-configured scope. 25 | 26 |
31 | 32 | `drag:deny-start-drag` 33 | 34 | 36 | 37 | Denies the start_drag command without any pre-configured scope. 38 | 39 |
42 | -------------------------------------------------------------------------------- /crates/tauri-plugin-drag/permissions/default.toml: -------------------------------------------------------------------------------- 1 | "$schema" = "schemas/schema.json" 2 | 3 | [default] 4 | description = "Default permissions for the plugin" 5 | permissions = ["allow-start-drag"] 6 | -------------------------------------------------------------------------------- /crates/tauri-plugin-drag/permissions/schemas/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "title": "PermissionFile", 4 | "description": "Permission file that can define a default permission, a set of permissions or a list of inlined permissions.", 5 | "type": "object", 6 | "properties": { 7 | "default": { 8 | "description": "The default permission set for the plugin", 9 | "anyOf": [ 10 | { 11 | "$ref": "#/definitions/DefaultPermission" 12 | }, 13 | { 14 | "type": "null" 15 | } 16 | ] 17 | }, 18 | "set": { 19 | "description": "A list of permissions sets defined", 20 | "type": "array", 21 | "items": { 22 | "$ref": "#/definitions/PermissionSet" 23 | } 24 | }, 25 | "permission": { 26 | "description": "A list of inlined permissions", 27 | "default": [], 28 | "type": "array", 29 | "items": { 30 | "$ref": "#/definitions/Permission" 31 | } 32 | } 33 | }, 34 | "definitions": { 35 | "DefaultPermission": { 36 | "description": "The default permission set of the plugin.\n\nWorks similarly to a permission with the \"default\" identifier.", 37 | "type": "object", 38 | "required": [ 39 | "permissions" 40 | ], 41 | "properties": { 42 | "version": { 43 | "description": "The version of the permission.", 44 | "type": [ 45 | "integer", 46 | "null" 47 | ], 48 | "format": "uint64", 49 | "minimum": 1.0 50 | }, 51 | "description": { 52 | "description": "Human-readable description of what the permission does. Tauri convention is to use

headings in markdown content for Tauri documentation generation purposes.", 53 | "type": [ 54 | "string", 55 | "null" 56 | ] 57 | }, 58 | "permissions": { 59 | "description": "All permissions this set contains.", 60 | "type": "array", 61 | "items": { 62 | "type": "string" 63 | } 64 | } 65 | } 66 | }, 67 | "PermissionSet": { 68 | "description": "A set of direct permissions grouped together under a new name.", 69 | "type": "object", 70 | "required": [ 71 | "description", 72 | "identifier", 73 | "permissions" 74 | ], 75 | "properties": { 76 | "identifier": { 77 | "description": "A unique identifier for the permission.", 78 | "type": "string" 79 | }, 80 | "description": { 81 | "description": "Human-readable description of what the permission does.", 82 | "type": "string" 83 | }, 84 | "permissions": { 85 | "description": "All permissions this set contains.", 86 | "type": "array", 87 | "items": { 88 | "$ref": "#/definitions/PermissionKind" 89 | } 90 | } 91 | } 92 | }, 93 | "Permission": { 94 | "description": "Descriptions of explicit privileges of commands.\n\nIt can enable commands to be accessible in the frontend of the application.\n\nIf the scope is defined it can be used to fine grain control the access of individual or multiple commands.", 95 | "type": "object", 96 | "required": [ 97 | "identifier" 98 | ], 99 | "properties": { 100 | "version": { 101 | "description": "The version of the permission.", 102 | "type": [ 103 | "integer", 104 | "null" 105 | ], 106 | "format": "uint64", 107 | "minimum": 1.0 108 | }, 109 | "identifier": { 110 | "description": "A unique identifier for the permission.", 111 | "type": "string" 112 | }, 113 | "description": { 114 | "description": "Human-readable description of what the permission does. Tauri internal convention is to use

headings in markdown content for Tauri documentation generation purposes.", 115 | "type": [ 116 | "string", 117 | "null" 118 | ] 119 | }, 120 | "commands": { 121 | "description": "Allowed or denied commands when using this permission.", 122 | "default": { 123 | "allow": [], 124 | "deny": [] 125 | }, 126 | "allOf": [ 127 | { 128 | "$ref": "#/definitions/Commands" 129 | } 130 | ] 131 | }, 132 | "scope": { 133 | "description": "Allowed or denied scoped when using this permission.", 134 | "allOf": [ 135 | { 136 | "$ref": "#/definitions/Scopes" 137 | } 138 | ] 139 | }, 140 | "platforms": { 141 | "description": "Target platforms this permission applies. By default all platforms are affected by this permission.", 142 | "type": [ 143 | "array", 144 | "null" 145 | ], 146 | "items": { 147 | "$ref": "#/definitions/Target" 148 | } 149 | } 150 | } 151 | }, 152 | "Commands": { 153 | "description": "Allowed and denied commands inside a permission.\n\nIf two commands clash inside of `allow` and `deny`, it should be denied by default.", 154 | "type": "object", 155 | "properties": { 156 | "allow": { 157 | "description": "Allowed command.", 158 | "default": [], 159 | "type": "array", 160 | "items": { 161 | "type": "string" 162 | } 163 | }, 164 | "deny": { 165 | "description": "Denied command, which takes priority.", 166 | "default": [], 167 | "type": "array", 168 | "items": { 169 | "type": "string" 170 | } 171 | } 172 | } 173 | }, 174 | "Scopes": { 175 | "description": "An argument for fine grained behavior control of Tauri commands.\n\nIt can be of any serde serializable type and is used to allow or prevent certain actions inside a Tauri command. The configured scope is passed to the command and will be enforced by the command implementation.\n\n## Example\n\n```json { \"allow\": [{ \"path\": \"$HOME/**\" }], \"deny\": [{ \"path\": \"$HOME/secret.txt\" }] } ```", 176 | "type": "object", 177 | "properties": { 178 | "allow": { 179 | "description": "Data that defines what is allowed by the scope.", 180 | "type": [ 181 | "array", 182 | "null" 183 | ], 184 | "items": { 185 | "$ref": "#/definitions/Value" 186 | } 187 | }, 188 | "deny": { 189 | "description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.", 190 | "type": [ 191 | "array", 192 | "null" 193 | ], 194 | "items": { 195 | "$ref": "#/definitions/Value" 196 | } 197 | } 198 | } 199 | }, 200 | "Value": { 201 | "description": "All supported ACL values.", 202 | "anyOf": [ 203 | { 204 | "description": "Represents a null JSON value.", 205 | "type": "null" 206 | }, 207 | { 208 | "description": "Represents a [`bool`].", 209 | "type": "boolean" 210 | }, 211 | { 212 | "description": "Represents a valid ACL [`Number`].", 213 | "allOf": [ 214 | { 215 | "$ref": "#/definitions/Number" 216 | } 217 | ] 218 | }, 219 | { 220 | "description": "Represents a [`String`].", 221 | "type": "string" 222 | }, 223 | { 224 | "description": "Represents a list of other [`Value`]s.", 225 | "type": "array", 226 | "items": { 227 | "$ref": "#/definitions/Value" 228 | } 229 | }, 230 | { 231 | "description": "Represents a map of [`String`] keys to [`Value`]s.", 232 | "type": "object", 233 | "additionalProperties": { 234 | "$ref": "#/definitions/Value" 235 | } 236 | } 237 | ] 238 | }, 239 | "Number": { 240 | "description": "A valid ACL number.", 241 | "anyOf": [ 242 | { 243 | "description": "Represents an [`i64`].", 244 | "type": "integer", 245 | "format": "int64" 246 | }, 247 | { 248 | "description": "Represents a [`f64`].", 249 | "type": "number", 250 | "format": "double" 251 | } 252 | ] 253 | }, 254 | "Target": { 255 | "description": "Platform target.", 256 | "oneOf": [ 257 | { 258 | "description": "MacOS.", 259 | "type": "string", 260 | "enum": [ 261 | "macOS" 262 | ] 263 | }, 264 | { 265 | "description": "Windows.", 266 | "type": "string", 267 | "enum": [ 268 | "windows" 269 | ] 270 | }, 271 | { 272 | "description": "Linux.", 273 | "type": "string", 274 | "enum": [ 275 | "linux" 276 | ] 277 | }, 278 | { 279 | "description": "Android.", 280 | "type": "string", 281 | "enum": [ 282 | "android" 283 | ] 284 | }, 285 | { 286 | "description": "iOS.", 287 | "type": "string", 288 | "enum": [ 289 | "iOS" 290 | ] 291 | } 292 | ] 293 | }, 294 | "PermissionKind": { 295 | "type": "string", 296 | "oneOf": [ 297 | { 298 | "description": "Enables the start_drag command without any pre-configured scope.", 299 | "type": "string", 300 | "const": "allow-start-drag" 301 | }, 302 | { 303 | "description": "Denies the start_drag command without any pre-configured scope.", 304 | "type": "string", 305 | "const": "deny-start-drag" 306 | }, 307 | { 308 | "description": "Default permissions for the plugin", 309 | "type": "string", 310 | "const": "default" 311 | } 312 | ] 313 | } 314 | } 315 | } -------------------------------------------------------------------------------- /crates/tauri-plugin-drag/src/api-iife.js: -------------------------------------------------------------------------------- 1 | if("__TAURI__"in window){var __TAURI_PLUGIN_DRAG__=function(t){"use strict";function e(t,e,r,i){if("a"===r&&!i)throw new TypeError("Private accessor was defined without a getter");if("function"==typeof e?t!==e||!i:!e.has(t))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===r?i:"a"===r?i.call(t):i?i.value:e.get(t)}function r(t,e,r,i,n){if("m"===i)throw new TypeError("Private method is not writable");if("a"===i&&!n)throw new TypeError("Private accessor was defined without a setter");if("function"==typeof e?t!==e||!n:!e.has(t))throw new TypeError("Cannot write private member to an object whose class did not declare it");return"a"===i?n.call(t,r):n?n.value=r:e.set(t,r),r}var i,n,s;"function"==typeof SuppressedError&&SuppressedError;class o{constructor(){this.__TAURI_CHANNEL_MARKER__=!0,i.set(this,(()=>{})),n.set(this,0),s.set(this,{}),this.id=function(t,e=!1){return window.__TAURI_INTERNALS__.transformCallback(t,e)}((({message:t,id:o})=>{if(o===e(this,n,"f")){r(this,n,o+1,"f"),e(this,i,"f").call(this,t);const a=Object.keys(e(this,s,"f"));if(a.length>0){let t=o+1;for(const r of a.sort()){if(parseInt(r)!==t)break;{const n=e(this,s,"f")[r];delete e(this,s,"f")[r],e(this,i,"f").call(this,n),t+=1}}r(this,n,t,"f")}}else e(this,s,"f")[o.toString()]=t}))}set onmessage(t){r(this,i,t,"f")}get onmessage(){return e(this,i,"f")}toJSON(){return`__CHANNEL__:${this.id}`}}return i=new WeakMap,n=new WeakMap,s=new WeakMap,t.startDrag=async function(t,e){const r=new o;e&&(r.onmessage=e),await async function(t,e={},r){return window.__TAURI_INTERNALS__.invoke(t,e,r)}("plugin:drag|start_drag",{item:t.item,image:t.icon,options:{mode:t.mode},onEvent:r})},t}({});Object.defineProperty(window.__TAURI__,"drag",{value:__TAURI_PLUGIN_DRAG__})} 2 | -------------------------------------------------------------------------------- /crates/tauri-plugin-drag/src/commands.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, path::PathBuf, sync::mpsc::channel}; 2 | 3 | use serde::{ser::Serializer, Deserialize, Deserializer, Serialize}; 4 | use tauri::{command, ipc::Channel, AppHandle, Runtime, WebviewWindow}; 5 | 6 | type Result = std::result::Result; 7 | 8 | #[derive(Debug, thiserror::Error)] 9 | pub enum Error { 10 | #[error(transparent)] 11 | Drag(#[from] drag::Error), 12 | #[error(transparent)] 13 | Tauri(#[from] tauri::Error), 14 | #[error(transparent)] 15 | Base64(#[from] base64::DecodeError), 16 | } 17 | 18 | impl Serialize for Error { 19 | fn serialize(&self, serializer: S) -> std::result::Result 20 | where 21 | S: Serializer, 22 | { 23 | serializer.serialize_str(self.to_string().as_ref()) 24 | } 25 | } 26 | 27 | pub struct Base64Image(String); 28 | 29 | impl<'de> Deserialize<'de> for Base64Image { 30 | fn deserialize(deserializer: D) -> std::result::Result 31 | where 32 | D: Deserializer<'de>, 33 | { 34 | let value = String::deserialize(deserializer)?; 35 | if let Some(data) = value.strip_prefix("data:image/png;base64,") { 36 | return Ok(Self(data.into())); 37 | } 38 | Err(serde::de::Error::custom( 39 | "expected an image/png base64 image string", 40 | )) 41 | } 42 | } 43 | 44 | #[derive(Deserialize)] 45 | #[serde(untagged)] 46 | pub enum Image { 47 | Base64(Base64Image), 48 | Raw(drag::Image), 49 | } 50 | 51 | #[derive(Deserialize)] 52 | #[serde(untagged)] 53 | pub enum DragItem { 54 | /// A list of files to be dragged. 55 | /// 56 | /// The paths must be absolute. 57 | Files(Vec), 58 | /// Data to share with another app. 59 | Data { 60 | data: SharedData, 61 | types: Vec, 62 | }, 63 | } 64 | 65 | #[derive(Deserialize)] 66 | #[serde(untagged)] 67 | pub enum SharedData { 68 | Fixed(String), 69 | Map(HashMap), 70 | } 71 | 72 | #[derive(Serialize, Clone)] 73 | pub struct CallbackResult { 74 | result: drag::DragResult, 75 | #[serde(rename = "cursorPos")] 76 | cursor_pos: drag::CursorPosition, 77 | } 78 | 79 | #[derive(Deserialize)] 80 | #[serde(rename_all = "camelCase")] 81 | pub enum DragMode { 82 | Copy, 83 | Move, 84 | } 85 | 86 | impl Default for DragMode { 87 | fn default() -> Self { 88 | Self::Copy 89 | } 90 | } 91 | 92 | impl From for drag::DragMode { 93 | fn from(value: DragMode) -> Self { 94 | match value { 95 | DragMode::Copy => Self::Copy, 96 | DragMode::Move => Self::Move, 97 | } 98 | } 99 | } 100 | 101 | #[derive(Default, Deserialize)] 102 | pub struct DragOptions { 103 | #[serde(default)] 104 | mode: DragMode, 105 | } 106 | 107 | impl From for drag::Options { 108 | fn from(options: DragOptions) -> Self { 109 | Self { 110 | skip_animatation_on_cancel_or_failure: false, 111 | mode: options.mode.into(), 112 | } 113 | } 114 | } 115 | 116 | #[command] 117 | pub async fn start_drag( 118 | app: AppHandle, 119 | window: WebviewWindow, 120 | item: DragItem, 121 | image: Image, 122 | options: Option, 123 | on_event: Channel, 124 | ) -> Result<()> { 125 | let (tx, rx) = channel(); 126 | 127 | let image = match image { 128 | Image::Raw(r) => r, 129 | Image::Base64(b) => { 130 | use base64::Engine; 131 | drag::Image::Raw(base64::engine::general_purpose::STANDARD.decode(b.0)?) 132 | } 133 | }; 134 | 135 | app.run_on_main_thread(move || { 136 | #[cfg(target_os = "linux")] 137 | let raw_window = window.gtk_window(); 138 | #[cfg(not(target_os = "linux"))] 139 | let raw_window = tauri::Result::Ok(window.clone()); 140 | 141 | let r = match raw_window { 142 | Ok(w) => drag::start_drag( 143 | &w, 144 | match item { 145 | DragItem::Files(f) => drag::DragItem::Files(f), 146 | DragItem::Data { data, types } => drag::DragItem::Data { 147 | provider: Box::new(move |data_type| match &data { 148 | SharedData::Fixed(d) => Some(d.as_bytes().to_vec()), 149 | SharedData::Map(m) => m.get(data_type).map(|d| d.as_bytes().to_vec()), 150 | }), 151 | types, 152 | }, 153 | }, 154 | image, 155 | move |result, cursor_pos| { 156 | let callback_result = CallbackResult { result, cursor_pos }; 157 | let _ = on_event.send(callback_result); 158 | }, 159 | options.unwrap_or_default().into(), 160 | ) 161 | .map_err(Into::into), 162 | Err(e) => Err(e.into()), 163 | }; 164 | tx.send(r).unwrap(); 165 | })?; 166 | 167 | rx.recv().unwrap() 168 | } 169 | -------------------------------------------------------------------------------- /crates/tauri-plugin-drag/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2023 CrabNebula Ltd. 2 | // SPDX-License-Identifier: Apache-2.0 3 | // SPDX-License-Identifier: MIT 4 | 5 | use tauri::{ 6 | plugin::{Builder, TauriPlugin}, 7 | Runtime, 8 | }; 9 | 10 | mod commands; 11 | 12 | /// Initializes the plugin. 13 | pub fn init() -> TauriPlugin { 14 | Builder::new("drag") 15 | .invoke_handler(tauri::generate_handler![commands::start_drag]) 16 | .build() 17 | } 18 | -------------------------------------------------------------------------------- /deny.toml: -------------------------------------------------------------------------------- 1 | # Target triples to include when checking. This is essentially our supported target list. 2 | targets = [ 3 | { triple = "x86_64-unknown-linux-gnu" }, 4 | { triple = "aarch64-unknown-linux-gnu" }, 5 | { triple = "x86_64-pc-windows-msvc" }, 6 | { triple = "i686-pc-windows-msvc" }, 7 | { triple = "x86_64-apple-darwin" }, 8 | { triple = "aarch64-apple-darwin" }, 9 | ] 10 | 11 | [licenses] 12 | # List of explicitly allowed licenses 13 | # See https://spdx.org/licenses/ for list of possible licenses 14 | # [possible values: any SPDX 3.11 short identifier (+ optional exception)]. 15 | allow = [ 16 | "MIT", 17 | "Apache-2.0", 18 | "Apache-2.0 WITH LLVM-exception", 19 | "ISC", 20 | # Apparently for us it's equivalent to BSD-3 which is considered compatible with MIT and Apache-2.0 21 | "Unicode-DFS-2016", 22 | # Used by webpki-roots and option-ext which we are using without modifications in a larger work, therefore okay. 23 | "MPL-2.0", 24 | "BSD-2-Clause", 25 | "BSD-3-Clause", 26 | "Zlib", 27 | ] 28 | 29 | [licenses.private] 30 | # If true, ignores workspace crates that aren't published, or are only 31 | # published to private registries. 32 | # To see how to mark a crate as unpublished (to the official registry), 33 | # visit https://doc.rust-lang.org/cargo/reference/manifest.html#the-publish-field. 34 | ignore = true 35 | -------------------------------------------------------------------------------- /examples/icon.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crabnebula-dev/drag-rs/8a0be885a087c076b8127743b217b795e8b4fe9d/examples/icon.bmp -------------------------------------------------------------------------------- /examples/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crabnebula-dev/drag-rs/8a0be885a087c076b8127743b217b795e8b4fe9d/examples/icon.ico -------------------------------------------------------------------------------- /examples/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crabnebula-dev/drag-rs/8a0be885a087c076b8127743b217b795e8b4fe9d/examples/icon.png -------------------------------------------------------------------------------- /examples/tao/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tao-app" 3 | version = "0.0.0" 4 | publish = false 5 | edition = "2021" 6 | 7 | [dependencies] 8 | drag.workspace = true 9 | tao.workspace = true 10 | -------------------------------------------------------------------------------- /examples/tao/src/main.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 Tauri Programme within The Commons Conservancy 2 | // SPDX-License-Identifier: Apache-2.0 3 | // SPDX-License-Identifier: MIT 4 | 5 | use drag::{start_drag, CursorPosition, DragItem, DragResult, Image}; 6 | use tao::{ 7 | dpi::LogicalSize, 8 | event::{ElementState, Event, MouseButton, StartCause, WindowEvent}, 9 | event_loop::{ControlFlow, EventLoop}, 10 | window::WindowBuilder, 11 | }; 12 | 13 | fn main() { 14 | let event_loop = EventLoop::new(); 15 | let window = WindowBuilder::new() 16 | .with_inner_size(LogicalSize::new(400., 100.)) 17 | .with_title("Drag Example") 18 | .build(&event_loop) 19 | .unwrap(); 20 | 21 | event_loop.run(move |event, _target, control_flow| { 22 | *control_flow = ControlFlow::Wait; 23 | 24 | match event { 25 | Event::NewEvents(StartCause::Init) => println!("Wry application started!"), 26 | Event::WindowEvent { 27 | event: WindowEvent::CloseRequested, 28 | .. 29 | } => { 30 | *control_flow = ControlFlow::Exit; 31 | } 32 | 33 | Event::WindowEvent { 34 | window_id: _, 35 | event: 36 | WindowEvent::MouseInput { 37 | device_id: _, 38 | state: ElementState::Pressed, 39 | button: MouseButton::Left, 40 | .. 41 | }, 42 | .. 43 | } => { 44 | start_drag( 45 | #[cfg(target_os = "linux")] 46 | { 47 | use tao::platform::unix::WindowExtUnix; 48 | window.gtk_window() 49 | }, 50 | #[cfg(not(target_os = "linux"))] 51 | &window, 52 | DragItem::Files(vec![std::fs::canonicalize("./examples/icon.png").unwrap()]), 53 | Image::Raw(include_bytes!("../../icon.png").to_vec()), 54 | // Image::File("./examples/icon.png".into()), 55 | |result: DragResult, cursor_pos: CursorPosition| { 56 | println!( 57 | "--> Drop Result: [{:?}], Cursor Pos:[{:?}]", 58 | result, cursor_pos 59 | ); 60 | }, 61 | Default::default(), 62 | ) 63 | .unwrap(); 64 | } 65 | 66 | _ => (), 67 | } 68 | }); 69 | } 70 | -------------------------------------------------------------------------------- /examples/tauri-dragout/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tauri-app-dragout" 3 | version = "0.0.0" 4 | publish = false 5 | edition = "2021" 6 | 7 | [lib] 8 | # The `_lib` suffix may seem redundant but it is necessary 9 | # to make the lib name unique and wouldn't conflict with the bin name. 10 | # This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519 11 | name = "tauri_plugin_drag_as_window_lib" 12 | crate-type = ["staticlib", "cdylib", "rlib"] 13 | 14 | [build-dependencies] 15 | tauri-build.workspace = true 16 | 17 | [dependencies] 18 | tauri = { workspace = true, features = [] } 19 | tauri-plugin-drag-as-window.workspace = true 20 | -------------------------------------------------------------------------------- /examples/tauri-dragout/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | tauri_build::build(); 3 | } 4 | -------------------------------------------------------------------------------- /examples/tauri-dragout/capabilities/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../gen/schemas/desktop-schema.json", 3 | "identifier": "default", 4 | "description": "Capability for the all window", 5 | "windows": ["*"], 6 | "permissions": [ 7 | "core:default", 8 | "core:webview:allow-create-webview-window", 9 | "core:window:allow-close", 10 | "drag-as-window:default" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /examples/tauri-dragout/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 20 | 21 | 22 | 23 |
24 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /examples/tauri-dragout/new_window.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 20 | 21 | 22 | 23 |
24 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /examples/tauri-dragout/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub fn run() { 2 | tauri::Builder::default() 3 | .plugin(tauri_plugin_drag_as_window::init()) 4 | .run(tauri::generate_context!("./tauri.conf.json")) 5 | .expect("failed to run app"); 6 | } 7 | -------------------------------------------------------------------------------- /examples/tauri-dragout/src/main.rs: -------------------------------------------------------------------------------- 1 | // Prevents additional console window on Windows in release, DO NOT REMOVE!! 2 | #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] 3 | 4 | fn main() { 5 | tauri_plugin_drag_as_window_lib::run() 6 | } 7 | -------------------------------------------------------------------------------- /examples/tauri-dragout/tauri.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "identifier": "dev.crabnebula.drag", 3 | "build": { 4 | "frontendDist": [ 5 | "./index.html", 6 | "./new_window.html" 7 | ] 8 | }, 9 | "bundle": { 10 | "icon": [ 11 | "../icon.png", 12 | "../icon.ico" 13 | ] 14 | }, 15 | "app": { 16 | "withGlobalTauri": true, 17 | "windows": [ 18 | { 19 | "title": "Drag", 20 | "width": 400, 21 | "height": 300, 22 | "dragDropEnabled": true 23 | } 24 | ] 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/tauri/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tauri-app" 3 | version = "0.0.0" 4 | publish = false 5 | edition = "2021" 6 | 7 | [lib] 8 | # The `_lib` suffix may seem redundant but it is necessary 9 | # to make the lib name unique and wouldn't conflict with the bin name. 10 | # This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519 11 | name = "tauri_plugin_drag_lib" 12 | crate-type = ["staticlib", "cdylib", "rlib"] 13 | 14 | [build-dependencies] 15 | tauri-build.workspace = true 16 | 17 | [dependencies] 18 | tauri = { workspace = true, features = [] } 19 | tauri-plugin-drag.workspace = true 20 | -------------------------------------------------------------------------------- /examples/tauri/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | tauri_build::build(); 3 | } 4 | -------------------------------------------------------------------------------- /examples/tauri/capabilities/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../gen/schemas/desktop-schema.json", 3 | "identifier": "default", 4 | "description": "Capability for the all window", 5 | "windows": ["*"], 6 | "permissions": ["core:default", "drag:default"] 7 | } 8 | -------------------------------------------------------------------------------- /examples/tauri/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 19 | 20 | 21 | 22 |
23 | 24 | 25 |
26 |
Drag me
27 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /examples/tauri/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub fn run() { 2 | tauri::Builder::default() 3 | .plugin(tauri_plugin_drag::init()) 4 | .run(tauri::generate_context!("./tauri.conf.json")) 5 | .expect("failed to run app"); 6 | } 7 | -------------------------------------------------------------------------------- /examples/tauri/src/main.rs: -------------------------------------------------------------------------------- 1 | // Prevents additional console window on Windows in release, DO NOT REMOVE!! 2 | #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] 3 | 4 | fn main() { 5 | tauri_plugin_drag_lib::run() 6 | } 7 | -------------------------------------------------------------------------------- /examples/tauri/tauri.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "identifier": "dev.crabnebula.drag", 3 | "build": { 4 | "frontendDist": "./index.html" 5 | }, 6 | "bundle": { 7 | "icon": [ 8 | "../icon.png", 9 | "../icon.ico" 10 | ], 11 | "resources": [ 12 | "../icon.png" 13 | ] 14 | }, 15 | "app": { 16 | "withGlobalTauri": true, 17 | "windows": [ 18 | { 19 | "title": "Drag", 20 | "width": 400, 21 | "height": 100 22 | } 23 | ] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /examples/winit/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "winit-app" 3 | version = "0.0.0" 4 | publish = false 5 | edition = "2021" 6 | 7 | [target."cfg(not(target_os = \"linux\"))".dependencies] 8 | drag.workspace = true 9 | winit.workspace = true -------------------------------------------------------------------------------- /examples/winit/src/main.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 Tauri Programme within The Commons Conservancy 2 | // SPDX-License-Identifier: Apache-2.0 3 | // SPDX-License-Identifier: MIT 4 | 5 | #[cfg(target_os = "linux")] 6 | fn main() { 7 | eprintln!("This example is not supported on Linux"); 8 | } 9 | 10 | #[cfg(not(target_os = "linux"))] 11 | fn main() { 12 | use drag::{start_drag, CursorPosition, DragItem, DragResult, Image}; 13 | use std::collections::HashMap; 14 | 15 | use winit::{ 16 | application::ApplicationHandler, 17 | dpi::LogicalSize, 18 | event::{DeviceEvent, ElementState, WindowEvent}, 19 | event_loop::EventLoop, 20 | window::{Window, WindowId}, 21 | }; 22 | 23 | let event_loop = EventLoop::new().unwrap(); 24 | 25 | struct Application { 26 | windows: HashMap, 27 | } 28 | 29 | impl ApplicationHandler for Application { 30 | fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) { 31 | let window = event_loop 32 | .create_window( 33 | Window::default_attributes() 34 | .with_inner_size(LogicalSize::new(400., 100.)) 35 | .with_title("Drag Example"), 36 | ) 37 | .unwrap(); 38 | 39 | self.windows.insert(window.id(), window); 40 | } 41 | 42 | fn window_event( 43 | &mut self, 44 | event_loop: &winit::event_loop::ActiveEventLoop, 45 | _window_id: winit::window::WindowId, 46 | event: winit::event::WindowEvent, 47 | ) { 48 | if let WindowEvent::CloseRequested = event { 49 | event_loop.exit() 50 | } 51 | } 52 | 53 | fn device_event( 54 | &mut self, 55 | _event_loop: &winit::event_loop::ActiveEventLoop, 56 | _device_id: winit::event::DeviceId, 57 | event: DeviceEvent, 58 | ) { 59 | if let DeviceEvent::Button { 60 | button: 0, 61 | state: ElementState::Pressed, 62 | } = event 63 | { 64 | start_drag( 65 | &self.windows.values().next().unwrap(), 66 | DragItem::Files(vec![std::fs::canonicalize("./examples/icon.png").unwrap()]), 67 | Image::Raw(include_bytes!("../../icon.png").to_vec()), 68 | // Image::File("./examples/icon.png".into()), 69 | |result: DragResult, cursor_pos: CursorPosition| { 70 | println!( 71 | "--> Drop Result: [{:?}], Cursor Pos:[{:?}]", 72 | result, cursor_pos 73 | ); 74 | }, 75 | Default::default(), 76 | ) 77 | .unwrap(); 78 | } 79 | } 80 | } 81 | 82 | let mut app = Application { 83 | windows: Default::default(), 84 | }; 85 | event_loop.run_app(&mut app).unwrap(); 86 | } 87 | -------------------------------------------------------------------------------- /examples/wry-dragout/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wry-app-dragout" 3 | version = "0.0.0" 4 | publish = false 5 | edition = "2021" 6 | 7 | [dependencies] 8 | base64.workspace = true 9 | drag.workspace = true 10 | serde.workspace = true 11 | serde_json.workspace = true 12 | wry.workspace = true 13 | dunce.workspace = true 14 | tao.workspace = true 15 | -------------------------------------------------------------------------------- /examples/wry-dragout/dummy/drag-1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crabnebula-dev/drag-rs/8a0be885a087c076b8127743b217b795e8b4fe9d/examples/wry-dragout/dummy/drag-1 -------------------------------------------------------------------------------- /examples/wry-dragout/dummy/drag-2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crabnebula-dev/drag-rs/8a0be885a087c076b8127743b217b795e8b4fe9d/examples/wry-dragout/dummy/drag-2 -------------------------------------------------------------------------------- /examples/wry-dragout/dummy/drag-3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crabnebula-dev/drag-rs/8a0be885a087c076b8127743b217b795e8b4fe9d/examples/wry-dragout/dummy/drag-3 -------------------------------------------------------------------------------- /examples/wry-dragout/src/main.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 Tauri Programme within The Commons Conservancy 2 | // SPDX-License-Identifier: Apache-2.0 3 | // SPDX-License-Identifier: MIT 4 | 5 | use base64::Engine; 6 | use drag::{start_drag, CursorPosition, DragItem, DragResult, Image}; 7 | use serde::{Deserialize, Deserializer}; 8 | use std::collections::HashMap; 9 | use std::path::PathBuf; 10 | use tao::dpi::LogicalPosition; 11 | use tao::event_loop::{EventLoopBuilder, EventLoopWindowTarget}; 12 | use tao::window::WindowId; 13 | use tao::{ 14 | dpi::LogicalSize, 15 | event::{Event, WindowEvent}, 16 | event_loop::{ControlFlow, EventLoopProxy}, 17 | window::{Window, WindowBuilder}, 18 | }; 19 | use wry::http::Request; 20 | use wry::WebView; 21 | use wry::{DragDropEvent, WebViewBuilder}; 22 | 23 | enum UserEvent { 24 | StartDragOut(WindowId, String, Option), 25 | StartDragBack(WindowId, String, Option), 26 | PopulateElement(WindowId, String), 27 | RemoveElement(WindowId, String), 28 | CloseWindow(WindowId), 29 | NewWindow(CursorPosition, String), 30 | } 31 | 32 | #[derive(Debug, Deserialize)] 33 | struct Payload { 34 | action: String, 35 | item: String, 36 | #[serde(rename = "iconDataURL")] 37 | icon_data_url: Base64Image, 38 | } 39 | 40 | #[derive(Debug)] 41 | struct Base64Image(String); 42 | 43 | impl<'de> Deserialize<'de> for Base64Image { 44 | fn deserialize(deserializer: D) -> std::result::Result 45 | where 46 | D: Deserializer<'de>, 47 | { 48 | let value = String::deserialize(deserializer)?; 49 | if let Some(data) = value.strip_prefix("data:image/png;base64,") { 50 | return Ok(Self(data.into())); 51 | } 52 | Err(serde::de::Error::custom( 53 | "expected an image/png base64 image string", 54 | )) 55 | } 56 | } 57 | 58 | fn main() -> wry::Result<()> { 59 | let event_loop = EventLoopBuilder::with_user_event().build(); 60 | let proxy = event_loop.create_proxy(); 61 | 62 | let mut webviews = HashMap::new(); 63 | 64 | let (window, webview) = create_main_window( 65 | String::from("Drag Example - First Window"), 66 | &event_loop, 67 | proxy.clone(), 68 | None, 69 | )?; 70 | webviews.insert(window.id(), (window, webview)); 71 | 72 | event_loop.run(move |event, event_loop, control_flow| { 73 | *control_flow = ControlFlow::Wait; 74 | 75 | match event { 76 | Event::WindowEvent { 77 | event: WindowEvent::CloseRequested, 78 | window_id, 79 | .. 80 | } => { 81 | webviews.remove(&window_id); 82 | if webviews.is_empty() { 83 | *control_flow = ControlFlow::Exit 84 | } 85 | } 86 | Event::UserEvent(UserEvent::NewWindow(cursor_pos, item)) => { 87 | let (window, webview) = create_new_window( 88 | format!("Window {}", webviews.len() + 1), 89 | event_loop, 90 | proxy.clone(), 91 | Some(cursor_pos), 92 | item, 93 | ) 94 | .unwrap(); 95 | webviews.insert(window.id(), (window, webview)); 96 | } 97 | Event::UserEvent(UserEvent::CloseWindow(id)) => { 98 | webviews.remove(&id); 99 | if webviews.is_empty() { 100 | *control_flow = ControlFlow::Exit 101 | } 102 | } 103 | Event::UserEvent(UserEvent::PopulateElement(id, item)) => { 104 | let (_window, webview) = &webviews.get(&id).unwrap(); 105 | let mut js = "window.appendElement('".to_owned(); 106 | js.push_str(&item); 107 | js.push_str("')"); 108 | let _ = webview.evaluate_script(&js); 109 | } 110 | Event::UserEvent(UserEvent::RemoveElement(id, item)) => { 111 | let (_window, webview) = &webviews.get(&id).unwrap(); 112 | let mut js = "window.removeElement('".to_owned(); 113 | js.push_str(&item); 114 | js.push_str("')"); 115 | let _ = webview.evaluate_script(&js); 116 | } 117 | 118 | Event::UserEvent(UserEvent::StartDragOut(id, item, icon)) => { 119 | let (window, _webview) = &webviews.get(&id).unwrap(); 120 | let proxy = proxy.clone(); 121 | 122 | let icon = match icon { 123 | Some(i) => i, 124 | None => { 125 | Image::Raw(include_bytes!("../../icon.png").to_vec()) 126 | // Image::File("./examples/icon.png".into()) 127 | } 128 | }; 129 | 130 | start_drag( 131 | #[cfg(target_os = "linux")] 132 | { 133 | use tao::platform::unix::WindowExtUnix; 134 | window.gtk_window() 135 | }, 136 | #[cfg(not(target_os = "linux"))] 137 | &window, 138 | DragItem::Data { 139 | provider: Box::new(|_| Some(Vec::new())), 140 | types: vec!["com.app.myapp.v2".into()], 141 | }, 142 | icon, 143 | move |result: DragResult, cursor_pos: CursorPosition| { 144 | println!( 145 | "--> Drop Result: [{:?}], Cursor Pos:[{:?}]", 146 | result, cursor_pos 147 | ); 148 | let _ = proxy.send_event(UserEvent::NewWindow(cursor_pos, item.clone())); 149 | let _ = proxy.send_event(UserEvent::RemoveElement(id, item.clone())); 150 | }, 151 | Default::default(), 152 | ) 153 | .unwrap(); 154 | } 155 | Event::UserEvent(UserEvent::StartDragBack(id, item, icon)) => { 156 | let (window, _webview) = &webviews.get(&id).unwrap(); 157 | let proxy = proxy.clone(); 158 | 159 | let icon = match icon { 160 | Some(i) => i, 161 | None => { 162 | Image::Raw(include_bytes!("../../icon.png").to_vec()) 163 | // Image::File("./examples/icon.png".into()) 164 | } 165 | }; 166 | let mut paths = Vec::new(); 167 | let dummy_path = "./examples/wry-dragout/dummy/".to_owned() + &item; 168 | paths.push(PathBuf::from(dummy_path).canonicalize().unwrap()); 169 | start_drag( 170 | #[cfg(target_os = "linux")] 171 | { 172 | use tao::platform::unix::WindowExtUnix; 173 | window.gtk_window() 174 | }, 175 | #[cfg(not(target_os = "linux"))] 176 | &window, 177 | DragItem::Files(paths), 178 | icon, 179 | move |result: DragResult, cursor_pos: CursorPosition| { 180 | println!( 181 | "--> Drop Result: [{:?}], Cursor Pos:[{:?}]", 182 | result, cursor_pos 183 | ); 184 | let _ = proxy.send_event(UserEvent::CloseWindow(id)); 185 | }, 186 | Default::default(), 187 | ) 188 | .unwrap(); 189 | } 190 | 191 | _ => (), 192 | } 193 | }) 194 | } 195 | 196 | fn create_main_window( 197 | title: String, 198 | event_loop: &EventLoopWindowTarget, 199 | proxy: EventLoopProxy, 200 | position: Option, 201 | ) -> wry::Result<(Window, WebView)> { 202 | const HTML: &str = r#" 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 222 | 223 | 224 | 225 |
226 | 271 | 272 | 273 | "#; 274 | 275 | let mut window_builder = WindowBuilder::new() 276 | .with_inner_size(LogicalSize::new(400., 300.)) 277 | .with_title(title); 278 | 279 | if let Some(position) = position { 280 | window_builder = window_builder.with_position(LogicalPosition::new(position.x, position.y)); 281 | } 282 | 283 | let window = window_builder.build(event_loop).unwrap(); 284 | let window_id = window.id(); 285 | 286 | let drag_drop_proxy = proxy.clone(); 287 | let handler = move |req: Request| { 288 | if let Ok(payload) = serde_json::from_str::(req.body()) { 289 | if payload.action == "start-drag" { 290 | let icon = drag::Image::Raw( 291 | base64::engine::general_purpose::STANDARD 292 | .decode(payload.icon_data_url.0) 293 | .unwrap(), 294 | ); 295 | let _ = 296 | proxy.send_event(UserEvent::StartDragOut(window_id, payload.item, Some(icon))); 297 | } 298 | } else if req.body() == "close" { 299 | let _ = proxy.send_event(UserEvent::CloseWindow(window_id)); 300 | } 301 | }; 302 | let drag_drop_handler = move |req: DragDropEvent| { 303 | if let DragDropEvent::Drop { paths, position: _ } = req { 304 | for f in paths { 305 | let _ = drag_drop_proxy.send_event(UserEvent::PopulateElement( 306 | window_id, 307 | dunce::canonicalize(f) 308 | .unwrap() 309 | .file_name() 310 | .unwrap() 311 | .to_os_string() 312 | .into_string() 313 | .unwrap(), 314 | )); 315 | } 316 | } 317 | // need to return true to prevent triggering OS drop behavior 318 | true 319 | }; 320 | let webview = WebViewBuilder::new() 321 | .with_html(HTML) 322 | .with_ipc_handler(handler) 323 | .with_accept_first_mouse(true) 324 | .with_drag_drop_handler(drag_drop_handler) 325 | .build(&window)?; 326 | Ok((window, webview)) 327 | } 328 | 329 | fn create_new_window( 330 | title: String, 331 | event_loop: &EventLoopWindowTarget, 332 | proxy: EventLoopProxy, 333 | position: Option, 334 | id: String, 335 | ) -> wry::Result<(Window, WebView)> { 336 | let html: String = r#" 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 356 | 357 | 358 | 359 |
360 | 395 | 396 | 397 | "#; 398 | 399 | let mut window_builder = WindowBuilder::new() 400 | .with_inner_size(LogicalSize::new(400., 100.)) 401 | .with_title(title); 402 | 403 | if let Some(position) = position { 404 | window_builder = window_builder.with_position(LogicalPosition::new(position.x, position.y)); 405 | } 406 | 407 | let window = window_builder.build(event_loop).unwrap(); 408 | let window_id = window.id(); 409 | 410 | let handler = move |request: Request| { 411 | if let Ok(payload) = serde_json::from_str::(request.body()) { 412 | if payload.action == "start-drag" { 413 | let icon = drag::Image::Raw( 414 | base64::engine::general_purpose::STANDARD 415 | .decode(payload.icon_data_url.0) 416 | .unwrap(), 417 | ); 418 | let _ = proxy.send_event(UserEvent::StartDragBack( 419 | window_id, 420 | payload.item, 421 | Some(icon), 422 | )); 423 | } 424 | } else if request.body() == "close" { 425 | let _ = proxy.send_event(UserEvent::CloseWindow(window_id)); 426 | } 427 | }; 428 | 429 | let webview = WebViewBuilder::new() 430 | .with_html(html) 431 | .with_ipc_handler(handler) 432 | .with_accept_first_mouse(true) 433 | .build(&window)?; 434 | 435 | Ok((window, webview)) 436 | } 437 | -------------------------------------------------------------------------------- /examples/wry/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wry-app" 3 | version = "0.0.0" 4 | publish = false 5 | edition = "2021" 6 | 7 | [dependencies] 8 | drag.workspace = true 9 | wry.workspace = true 10 | tao.workspace = true 11 | -------------------------------------------------------------------------------- /examples/wry/src/main.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 Tauri Programme within The Commons Conservancy 2 | // SPDX-License-Identifier: Apache-2.0 3 | // SPDX-License-Identifier: MIT 4 | 5 | use drag::{start_drag, CursorPosition, DragItem, DragResult, Image}; 6 | use tao::{ 7 | dpi::LogicalSize, 8 | event::{Event, StartCause, WindowEvent}, 9 | event_loop::{ControlFlow, EventLoopBuilder}, 10 | window::WindowBuilder, 11 | }; 12 | use wry::{http::Request, WebViewBuilder}; 13 | 14 | enum UserEvent { 15 | StartDrag, 16 | } 17 | 18 | fn main() -> wry::Result<()> { 19 | let event_loop = EventLoopBuilder::with_user_event().build(); 20 | let window = WindowBuilder::new() 21 | .with_inner_size(LogicalSize::new(400., 100.)) 22 | .with_title("Drag Example") 23 | .build(&event_loop) 24 | .unwrap(); 25 | 26 | const HTML: &str = r#" 27 | 28 | 29 | 30 | 31 | 32 | 33 | 45 | 46 | 47 | 48 |
49 | Drag me 50 |
51 | 57 | 58 | 59 | "#; 60 | 61 | let proxy = event_loop.create_proxy(); 62 | let handler = move |req: Request| { 63 | if req.body() == "startDrag" { 64 | let _ = proxy.send_event(UserEvent::StartDrag); 65 | } 66 | }; 67 | 68 | let _webview = WebViewBuilder::new() 69 | .with_html(HTML) 70 | .with_ipc_handler(handler) 71 | .with_accept_first_mouse(true) 72 | .build(&window)?; 73 | 74 | event_loop.run(move |event, _, control_flow| { 75 | *control_flow = ControlFlow::Wait; 76 | 77 | match event { 78 | Event::NewEvents(StartCause::Init) => println!("Wry application started!"), 79 | Event::WindowEvent { 80 | event: WindowEvent::CloseRequested, 81 | .. 82 | } => *control_flow = ControlFlow::Exit, 83 | 84 | Event::UserEvent(e) => match e { 85 | UserEvent::StartDrag => { 86 | start_drag( 87 | #[cfg(target_os = "linux")] 88 | { 89 | use tao::platform::unix::WindowExtUnix; 90 | window.gtk_window() 91 | }, 92 | #[cfg(not(target_os = "linux"))] 93 | &window, 94 | DragItem::Files(vec![ 95 | std::fs::canonicalize("./examples/icon.png").unwrap(), 96 | std::fs::canonicalize("./examples/icon.bmp").unwrap(), 97 | ]), 98 | Image::Raw(include_bytes!("../../icon.png").to_vec()), 99 | // Image::File("./examples/icon.png".into()), 100 | |result: DragResult, _cursor_pos: CursorPosition| { 101 | println!("--> Drop Result: [{:?}]", result); 102 | }, 103 | Default::default(), 104 | ) 105 | .unwrap(); 106 | } 107 | }, 108 | _ => (), 109 | } 110 | }) 111 | } 112 | -------------------------------------------------------------------------------- /packages/tauri-plugin-drag-api/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist-js/ -------------------------------------------------------------------------------- /packages/tauri-plugin-drag-api/.npmignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crabnebula-dev/drag-rs/8a0be885a087c076b8127743b217b795e8b4fe9d/packages/tauri-plugin-drag-api/.npmignore -------------------------------------------------------------------------------- /packages/tauri-plugin-drag-api/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## \[2.1.0] 4 | 5 | - [`229aa3e`](https://github.com/crabnebula-dev/drag-rs/commit/229aa3e26c85c31074abd3b4a4538b2ec65eb094) Added `mode` drag option to either copy or move a file. 6 | 7 | ## \[2.0.0] 8 | 9 | - [`244887f`](https://github.com/crabnebula-dev/drag-rs/commit/244887fa36b12ac615919b9d2d149edca3d1f1c7) Update to tauri v2. 10 | 11 | ## \[0.3.1] 12 | 13 | - [`6698e65`](https://github.com/crabnebula-dev/drag-rs/commit/6698e655215e649d8a40d4c8d6d328ca595ce2d8) Target ES2019 for macOS 10.14 compatibility. 14 | 15 | ## \[0.3.0] 16 | 17 | - [`dd3f087`](https://github.com/crabnebula-dev/drag-rs/commit/dd3f0873ae2406968d412d9dfdc1c79a5ed5533e)([#25](https://github.com/crabnebula-dev/drag-rs/pull/25)) Changed the onEvent callback payload from `DragResult` to `{ result: DragResult, cursorPos: CursorPosition }`. 18 | 19 | ## \[0.2.2] 20 | 21 | - [`dcafc76`](https://github.com/crabnebula-dev/drag-rs/commit/undefined) Fixes package ignoring dist files. 22 | 23 | ## \[0.2.1] 24 | 25 | - [`4d0ce4f`](https://github.com/crabnebula-dev/drag-rs/commit/4d0ce4f2a2d81596f67adba2bc6addd8af50a73c) Fixes package missing dist files. 26 | 27 | ## \[0.2.0] 28 | 29 | - [`1449076`](https://github.com/crabnebula-dev/drag-rs/commit/14490764de8ff50969a3f2299d204e44e091752e) The `startDrag` function now takes an argument for a callback function for the operation result (either `Dragged` or `Cancelled`). 30 | - [`f58ed78`](https://github.com/crabnebula-dev/drag-rs/commit/f58ed7838abe1fe5b23c4e3aa92df28e77564345) The `startDrag` function can now be used to drag arbitrary data strings on macOS (e.g. Final Cut Pro XMLs). 31 | 32 | ## \[0.1.0] 33 | 34 | - [`644cfa2`](https://github.com/crabnebula-dev/drag-rs/commit/644cfa28b09bee9c3de396bdcc1dc801a26d65bc) Initial release. 35 | -------------------------------------------------------------------------------- /packages/tauri-plugin-drag-api/guest-js/.gitignore: -------------------------------------------------------------------------------- 1 | # Build output 2 | /dist 3 | /api 4 | 5 | 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # node-waf configuration 29 | .lock-wscript 30 | 31 | # Compiled binary addons (http://nodejs.org/api/addons.html) 32 | build/Release 33 | 34 | # Dependency directories 35 | node_modules/ 36 | jspm_packages/ 37 | 38 | # Typescript v1 declaration files 39 | typings/ 40 | 41 | # Optional npm cache directory 42 | .npm 43 | 44 | # Optional eslint cache 45 | .eslintcache 46 | 47 | # Optional REPL history 48 | .node_repl_history 49 | 50 | # Output of 'npm pack' 51 | *.tgz 52 | 53 | # Yarn Integrity file 54 | .yarn-integrity 55 | 56 | # dotenv environment variables file 57 | .env 58 | 59 | /.vs 60 | .DS_Store 61 | .Thumbs.db 62 | *.sublime* 63 | .idea/ 64 | debug.log 65 | package-lock.json 66 | .vscode/settings.json 67 | -------------------------------------------------------------------------------- /packages/tauri-plugin-drag-api/guest-js/index.ts: -------------------------------------------------------------------------------- 1 | import { invoke, Channel } from "@tauri-apps/api/core"; 2 | 3 | export type DragItem = 4 | | string[] 5 | | { data: string | Record; types: string[] }; 6 | 7 | export type DragResult = "Dropped" | "Cancelled"; 8 | 9 | /** 10 | * Logical position of the cursor. 11 | */ 12 | export interface CursorPosition { 13 | x: Number; 14 | y: Number; 15 | } 16 | 17 | export interface Options { 18 | item: DragItem; 19 | icon: string; 20 | mode?: "copy" | "move"; 21 | } 22 | 23 | export interface CallbackPayload { 24 | result: DragResult; 25 | cursorPos: CursorPosition; 26 | } 27 | 28 | /** 29 | * Starts a drag operation. Can either send a list of files or data to another app. 30 | * 31 | * ```typescript 32 | * import { startDrag } from "@crabnebula/tauri-plugin-drag"; 33 | * 34 | * // drag a file: 35 | * startDrag({ 36 | * item: ["/path/to/file.png"], 37 | * icon: "/path/to/preview.png" 38 | * }); 39 | * 40 | * // drag Final Cut Pro data: 41 | * startDrag({ 42 | * item: { 43 | * // alternatively, you can pass an object mapping each type to its own XML format 44 | * data: '...', 45 | * types: [ 46 | * "com.apple.finalcutpro.xml.v1-10", 47 | * "com.apple.finalcutpro.xml.v1-9", 48 | * "com.apple.finalcutpro.xml" 49 | * ] 50 | * } 51 | * }); 52 | * ``` 53 | * 54 | * @param options the drag options containing data and preview image 55 | * @param onEvent on drag event handler 56 | */ 57 | export async function startDrag( 58 | options: Options, 59 | onEvent?: (result: CallbackPayload) => void 60 | ): Promise { 61 | const onEventChannel = new Channel(); 62 | if (onEvent) { 63 | onEventChannel.onmessage = onEvent; 64 | } 65 | await invoke("plugin:drag|start_drag", { 66 | item: options.item, 67 | image: options.icon, 68 | options: { 69 | mode: options.mode, 70 | }, 71 | onEvent: onEventChannel, 72 | }); 73 | } 74 | -------------------------------------------------------------------------------- /packages/tauri-plugin-drag-api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@crabnebula/tauri-plugin-drag", 3 | "version": "2.1.0", 4 | "author": "Lucas ", 5 | "description": "Start a drag operation out of a Tauri window", 6 | "type": "module", 7 | "types": "./dist-js/index.d.ts", 8 | "module": "./dist-js/index.js", 9 | "main": "./dist-js/index.cjs", 10 | "exports": { 11 | "types": "./dist-js/index.d.ts", 12 | "import": "./dist-js/index.js", 13 | "require": "./dist-js/index.cjs" 14 | }, 15 | "scripts": { 16 | "build": "rollup -c" 17 | }, 18 | "devDependencies": { 19 | "@rollup/plugin-node-resolve": "15.2.3", 20 | "@rollup/plugin-terser": "0.4.4", 21 | "@rollup/plugin-typescript": "11.1.5", 22 | "rollup": "4.22.4", 23 | "tslib": "^2.6.2", 24 | "typescript": "5.2.2" 25 | }, 26 | "dependencies": { 27 | "@tauri-apps/api": "^2.0.0" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/tauri-plugin-drag-api/rollup.config.js: -------------------------------------------------------------------------------- 1 | import { readFileSync } from "fs"; 2 | 3 | import { join } from "path"; 4 | import { cwd } from "process"; 5 | import { nodeResolve } from "@rollup/plugin-node-resolve"; 6 | import typescript from "@rollup/plugin-typescript"; 7 | import terser from "@rollup/plugin-terser"; 8 | 9 | const pkg = JSON.parse(readFileSync(join(cwd(), "package.json"), "utf8")); 10 | 11 | const pluginJsName = "drag"; 12 | const iifeVarName = `__TAURI_PLUGIN_${pluginJsName.toUpperCase()}__`; 13 | 14 | export default [ 15 | { 16 | input: "guest-js/index.ts", 17 | output: [ 18 | { 19 | file: pkg.exports.import, 20 | format: "esm", 21 | }, 22 | { 23 | file: pkg.exports.require, 24 | format: "cjs", 25 | }, 26 | ], 27 | plugins: [ 28 | typescript({ 29 | declaration: true, 30 | declarationDir: `./${pkg.exports.import.split("/")[0]}`, 31 | }), 32 | ], 33 | external: [ 34 | /^@tauri-apps\/api/, 35 | ...Object.keys(pkg.dependencies || {}), 36 | ...Object.keys(pkg.peerDependencies || {}), 37 | ], 38 | onwarn: (warning) => { 39 | throw Object.assign(new Error(), warning); 40 | }, 41 | }, 42 | 43 | { 44 | input: "guest-js/index.ts", 45 | output: { 46 | format: "iife", 47 | name: iifeVarName, 48 | // IIFE is in the format `var ${iifeVarName} = (() => {})()` 49 | // we check if __TAURI__ exists and inject the API object 50 | banner: "if ('__TAURI__' in window) {", 51 | // the last `}` closes the if in the banner 52 | footer: `Object.defineProperty(window.__TAURI__, '${pluginJsName}', { value: ${iifeVarName} }) }`, 53 | file: "../../crates/tauri-plugin-drag/src/api-iife.js", 54 | }, 55 | // and var is not guaranteed to assign to the global `window` object so we make sure to assign it 56 | plugins: [typescript(), terser(), nodeResolve()], 57 | onwarn: (warning) => { 58 | throw Object.assign(new Error(), warning); 59 | }, 60 | }, 61 | ]; 62 | -------------------------------------------------------------------------------- /packages/tauri-plugin-drag-api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2019", 4 | "module": "esnext", 5 | "moduleResolution": "bundler", 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noUnusedLocals": true, 9 | "noImplicitAny": true, 10 | "noEmit": true 11 | }, 12 | "exclude": ["dist-js", "node_modules", "test/types"], 13 | "include": ["guest-js/*.ts"] 14 | } 15 | -------------------------------------------------------------------------------- /packages/tauri-plugin-drag-as-window-api/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist-js/ -------------------------------------------------------------------------------- /packages/tauri-plugin-drag-as-window-api/.npmignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crabnebula-dev/drag-rs/8a0be885a087c076b8127743b217b795e8b4fe9d/packages/tauri-plugin-drag-as-window-api/.npmignore -------------------------------------------------------------------------------- /packages/tauri-plugin-drag-as-window-api/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## \[2.1.0] 4 | 5 | - [`229aa3e`](https://github.com/crabnebula-dev/drag-rs/commit/229aa3e26c85c31074abd3b4a4538b2ec65eb094) Added `mode` drag option to either copy or move a file. 6 | 7 | ## \[2.0.0] 8 | 9 | - [`244887f`](https://github.com/crabnebula-dev/drag-rs/commit/244887fa36b12ac615919b9d2d149edca3d1f1c7) Update to tauri v2. 10 | 11 | ## \[0.1.1] 12 | 13 | - [`6698e65`](https://github.com/crabnebula-dev/drag-rs/commit/6698e655215e649d8a40d4c8d6d328ca595ce2d8) Target ES2019 for macOS 10.14 compatibility. 14 | 15 | ## \[0.1.0] 16 | 17 | - [`f2d2631`](https://github.com/crabnebula-dev/drag-rs/commit/f2d26319d59dc9d92b4fc2b244d74f7b75f724b3) Initial release. 18 | -------------------------------------------------------------------------------- /packages/tauri-plugin-drag-as-window-api/guest-js/.gitignore: -------------------------------------------------------------------------------- 1 | # Build output 2 | /dist 3 | /api 4 | 5 | 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # node-waf configuration 29 | .lock-wscript 30 | 31 | # Compiled binary addons (http://nodejs.org/api/addons.html) 32 | build/Release 33 | 34 | # Dependency directories 35 | node_modules/ 36 | jspm_packages/ 37 | 38 | # Typescript v1 declaration files 39 | typings/ 40 | 41 | # Optional npm cache directory 42 | .npm 43 | 44 | # Optional eslint cache 45 | .eslintcache 46 | 47 | # Optional REPL history 48 | .node_repl_history 49 | 50 | # Output of 'npm pack' 51 | *.tgz 52 | 53 | # Yarn Integrity file 54 | .yarn-integrity 55 | 56 | # dotenv environment variables file 57 | .env 58 | 59 | /.vs 60 | .DS_Store 61 | .Thumbs.db 62 | *.sublime* 63 | .idea/ 64 | debug.log 65 | package-lock.json 66 | .vscode/settings.json 67 | -------------------------------------------------------------------------------- /packages/tauri-plugin-drag-as-window-api/guest-js/index.ts: -------------------------------------------------------------------------------- 1 | import { Channel, invoke } from "@tauri-apps/api/core"; 2 | import html2canvas from "html2canvas"; 3 | 4 | type DragResult = "Dropped" | "Cancelled"; 5 | 6 | /** 7 | * Logical position of the cursor. 8 | */ 9 | export interface CursorPosition { 10 | x: Number; 11 | y: Number; 12 | } 13 | 14 | interface RawCallbackPayload { 15 | result: DragResult; 16 | cursorPos: CursorPosition; 17 | } 18 | 19 | export interface CallbackPayload { 20 | cursorPos: CursorPosition; 21 | } 22 | 23 | export interface DragOptions { 24 | mode?: "copy" | "move"; 25 | } 26 | 27 | /** 28 | * Listen to a DOM element being dropped in this window. The data that was associated with the drop event is passed as argument to the handler closure. 29 | * 30 | * @param handler closure that is called when a DOM element gets dropped in this window 31 | */ 32 | export async function onElementDrop(handler: (data: any) => void) { 33 | const handlerChannel = new Channel(); 34 | handlerChannel.onmessage = handler; 35 | await invoke("plugin:drag-as-window|on_drop", { 36 | handler: handlerChannel, 37 | }); 38 | } 39 | 40 | /** 41 | * Starts a drag operation of the given DOM element. 42 | * 43 | * The intention of this drag operation is to drag the DOM element to any region in the desktop to create a new window. 44 | * The window creation is your responsibility, use the onDrop hook to achieve it. 45 | * 46 | * ```typescript 47 | * import { dragBack } from "@crabnebula/tauri-plugin-drag-as-window"; 48 | * import { WebviewWindow } from "@tauri-apps/api/webviewWindow"; 49 | * 50 | * const el = document.querySelector("#my-target") 51 | * await dragBack(el, (payload) => { 52 | * new WebviewWindow('label', { 53 | * url: 'new-window', 54 | * width: el.clientWidth, 55 | * height: el.clientHeight + 20, 56 | * x: payload.cursorPos.x, 57 | * y: payload.cursorPos.y, 58 | * }); 59 | * }); 60 | * ``` 61 | * 62 | * @param el the element or selector to drag 63 | * @param onDrop on drop handler, used to create the window 64 | */ 65 | export async function dragAsWindow( 66 | el: string | HTMLElement, 67 | onDrop?: (result: CallbackPayload) => void, 68 | options?: DragOptions 69 | ): Promise { 70 | const element = typeof el === "string" ? document.querySelector(el) : el; 71 | if (element === null) { 72 | throw new Error(`Element with selector "${el}" not found`); 73 | } 74 | const canvas = await html2canvas(element as HTMLElement, { logging: false }); 75 | 76 | const onDropChannel = new Channel(); 77 | if (onDrop) { 78 | onDropChannel.onmessage = (payload) => 79 | onDrop({ cursorPos: payload.cursorPos }); 80 | } 81 | 82 | await invoke("plugin:drag-as-window|drag_new_window", { 83 | imageBase64: canvas.toDataURL("image/png"), 84 | options, 85 | onEvent: onDropChannel, 86 | }); 87 | } 88 | 89 | /** 90 | * Starts a drag operation of the given DOM element. You can associate any JSON data with the operation to retrieve later. 91 | * 92 | * The intention of this drag operation is to get the DOM element back to an app window. 93 | * 94 | * ```typescript 95 | * import { dragBack } from "@crabnebula/tauri-plugin-drag-as-window"; 96 | * 97 | * await dragBack('#my-target', { id: 0 }); 98 | * ``` 99 | * 100 | * @param el the element or selector to drag 101 | * @param data the data to associate with the drag operation 102 | * @param onEvent on drop event handler 103 | */ 104 | export async function dragBack( 105 | el: string | HTMLElement, 106 | data: any, 107 | onEvent?: (result: CallbackPayload) => void, 108 | options?: DragOptions 109 | ): Promise { 110 | const element = typeof el === "string" ? document.querySelector(el) : el; 111 | if (element === null) { 112 | throw new Error(`Element with selector "${el}" not found`); 113 | } 114 | const canvas = await html2canvas(element as HTMLElement, { logging: false }); 115 | 116 | const onEventChannel = new Channel(); 117 | if (onEvent) { 118 | onEventChannel.onmessage = (payload) => 119 | onEvent({ cursorPos: payload.cursorPos }); 120 | } 121 | 122 | await invoke("plugin:drag-as-window|drag_back", { 123 | data, 124 | imageBase64: canvas.toDataURL("image/png"), 125 | options, 126 | onEvent: onEventChannel, 127 | }); 128 | } 129 | -------------------------------------------------------------------------------- /packages/tauri-plugin-drag-as-window-api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@crabnebula/tauri-plugin-drag-as-window", 3 | "version": "2.1.0", 4 | "author": "Lucas ", 5 | "description": "Start a drag operation from a DOM element to its own window", 6 | "type": "module", 7 | "types": "./dist-js/index.d.ts", 8 | "module": "./dist-js/index.js", 9 | "main": "./dist-js/index.cjs", 10 | "exports": { 11 | "types": "./dist-js/index.d.ts", 12 | "import": "./dist-js/index.js", 13 | "require": "./dist-js/index.cjs" 14 | }, 15 | "scripts": { 16 | "build": "rollup -c" 17 | }, 18 | "devDependencies": { 19 | "@rollup/plugin-node-resolve": "15.2.3", 20 | "@rollup/plugin-terser": "0.4.4", 21 | "@rollup/plugin-typescript": "11.1.5", 22 | "rollup": "4.22.4", 23 | "tslib": "^2.6.2", 24 | "typescript": "5.2.2" 25 | }, 26 | "dependencies": { 27 | "@tauri-apps/api": "^2.0.0", 28 | "html2canvas": "^1.4.1" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/tauri-plugin-drag-as-window-api/rollup.config.js: -------------------------------------------------------------------------------- 1 | import { readFileSync } from "fs"; 2 | 3 | import { join } from "path"; 4 | import { cwd } from "process"; 5 | import { nodeResolve } from "@rollup/plugin-node-resolve"; 6 | import typescript from "@rollup/plugin-typescript"; 7 | import terser from "@rollup/plugin-terser"; 8 | 9 | const pkg = JSON.parse(readFileSync(join(cwd(), "package.json"), "utf8")); 10 | 11 | const pluginJsName = "dragAsWindow"; 12 | const iifeVarName = `__TAURI_PLUGIN_${pluginJsName.toUpperCase()}__`; 13 | 14 | export default [ 15 | { 16 | input: "guest-js/index.ts", 17 | output: [ 18 | { 19 | file: pkg.exports.import, 20 | format: "esm", 21 | }, 22 | { 23 | file: pkg.exports.require, 24 | format: "cjs", 25 | }, 26 | ], 27 | plugins: [ 28 | typescript({ 29 | declaration: true, 30 | declarationDir: `./${pkg.exports.import.split("/")[0]}`, 31 | }), 32 | ], 33 | external: [ 34 | /^@tauri-apps\/api/, 35 | ...Object.keys(pkg.dependencies || {}), 36 | ...Object.keys(pkg.peerDependencies || {}), 37 | ], 38 | onwarn: (warning) => { 39 | throw Object.assign(new Error(), warning); 40 | }, 41 | }, 42 | 43 | { 44 | input: "guest-js/index.ts", 45 | output: { 46 | format: "iife", 47 | name: iifeVarName, 48 | // IIFE is in the format `var ${iifeVarName} = (() => {})()` 49 | // we check if __TAURI__ exists and inject the API object 50 | banner: "if ('__TAURI__' in window) {", 51 | // the last `}` closes the if in the banner 52 | footer: `Object.defineProperty(window.__TAURI__, '${pluginJsName}', { value: ${iifeVarName} }) }`, 53 | file: "../../crates/tauri-plugin-drag-as-window/src/api-iife.js", 54 | }, 55 | // and var is not guaranteed to assign to the global `window` object so we make sure to assign it 56 | plugins: [typescript(), terser(), nodeResolve()], 57 | onwarn: (warning) => { 58 | throw Object.assign(new Error(), warning); 59 | }, 60 | }, 61 | ]; 62 | -------------------------------------------------------------------------------- /packages/tauri-plugin-drag-as-window-api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2019", 4 | "module": "esnext", 5 | "moduleResolution": "bundler", 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noUnusedLocals": true, 9 | "noImplicitAny": true, 10 | "noEmit": true 11 | }, 12 | "exclude": ["dist-js", "node_modules", "test/types"], 13 | "include": ["guest-js/*.ts"] 14 | } 15 | --------------------------------------------------------------------------------