├── .changes
├── 0.2.0.md
├── 0.3.0.md
├── 0.4.0.md
├── 0.5.0.md
├── 0.6.0.md
├── 0.6.1.md
├── 0.6.2.md
├── 0.7.0.md
├── 0.7.1.md
├── 0.8.0.md
├── 0.8.1.md
├── 0.8.2.md
├── config.json
├── initial-release.md
└── readme.md
├── .github
└── workflows
│ ├── audit.yml
│ ├── covector-status.yml
│ ├── format.yml
│ └── test.yml
├── .gitignore
├── Cargo.toml
├── LICENSE.md
├── README.md
├── assets
└── screenshot.png
├── examples
├── multi
│ ├── .gitignore
│ ├── package.json
│ ├── public
│ │ ├── index.html
│ │ └── index.js
│ └── src-tauri
│ │ ├── .gitignore
│ │ ├── Cargo.toml
│ │ ├── assets
│ │ └── 16x16.png
│ │ ├── build.rs
│ │ ├── icons
│ │ ├── 128x128.png
│ │ ├── 128x128@2x.png
│ │ ├── 32x32.png
│ │ ├── icon.icns
│ │ ├── icon.ico
│ │ └── icon.png
│ │ ├── rustfmt.toml
│ │ ├── src
│ │ └── main.rs
│ │ └── tauri.conf.json
├── ts-utility
│ ├── .gitignore
│ ├── esbuild.js
│ ├── package.json
│ ├── public
│ │ └── index.html
│ ├── src-tauri
│ │ ├── .gitignore
│ │ ├── Cargo.toml
│ │ ├── assets
│ │ │ └── 16x16.png
│ │ ├── build.rs
│ │ ├── icons
│ │ │ ├── 128x128.png
│ │ │ ├── 128x128@2x.png
│ │ │ ├── 32x32.png
│ │ │ ├── icon.icns
│ │ │ ├── icon.ico
│ │ │ └── icon.png
│ │ ├── rustfmt.toml
│ │ ├── src
│ │ │ └── main.rs
│ │ └── tauri.conf.json
│ ├── src
│ │ ├── index.d.ts
│ │ └── index.ts
│ └── tsconfig.json
└── vanilla
│ ├── .gitignore
│ ├── package.json
│ ├── public
│ ├── index.html
│ └── index.js
│ └── src-tauri
│ ├── .gitignore
│ ├── Cargo.toml
│ ├── assets
│ └── 16x16.png
│ ├── build.rs
│ ├── icons
│ ├── 128x128.png
│ ├── 128x128@2x.png
│ ├── 32x32.png
│ ├── icon.icns
│ ├── icon.ico
│ └── icon.png
│ ├── rustfmt.toml
│ ├── src
│ └── main.rs
│ └── tauri.conf.json
├── lerna.json
├── package.json
├── plugin
├── dist
│ ├── index.d.ts
│ ├── index.js
│ ├── index.js.map
│ ├── index.spec.d.ts
│ └── types.d.ts
├── esbuild.js
├── index.spec.ts
├── index.ts
├── jest.config.cjs
├── package.json
├── tsconfig.json
└── types.ts
└── src
├── keymap.rs
├── lib.rs
├── linux.rs
├── macos.rs
├── macos_window_holder.rs
├── menu_item.rs
├── theme.rs
├── win.rs
└── win_image_handler.rs
/.changes/0.2.0.md:
--------------------------------------------------------------------------------
1 | ---
2 | "tauri-plugin-context-menu": "major"
3 | ---
4 |
5 | - Add separator support on MacOS
6 | - Add menuDidClose event on MacOS
7 | - Add icon support on MacOS
8 | - Fix disabled subitems on MacOS
9 | - Update Tauri to 1.4
10 |
--------------------------------------------------------------------------------
/.changes/0.3.0.md:
--------------------------------------------------------------------------------
1 | ---
2 | "tauri-plugin-context-menu": "major"
3 | ---
4 |
5 | - Add support for Windows
6 |
--------------------------------------------------------------------------------
/.changes/0.4.0.md:
--------------------------------------------------------------------------------
1 | ---
2 | "tauri-plugin-context-menu": "minor"
3 | ---
4 |
5 | - Add icon support on Windows
6 | - Add shortcut support on Windows
7 | - Add icon-width and icon-height support
8 | - Add is_absolute option to Position
9 |
10 | ### Upgrade from 0.x.x
11 | - Change `icon_path` option to `icon.path`
--------------------------------------------------------------------------------
/.changes/0.5.0.md:
--------------------------------------------------------------------------------
1 | ---
2 | "tauri-plugin-context-menu": "minor"
3 | ---
4 |
5 | - Add payload support #3
6 | - Support more keycodes #6
7 | - Update Tauri to 1.5.0
8 |
9 | ### Upgrade from 0.x.x
10 | - Change `icon_path` option to `icon.path`
--------------------------------------------------------------------------------
/.changes/0.6.0.md:
--------------------------------------------------------------------------------
1 | ---
2 | "tauri-plugin-context-menu": "minor"
3 | ---
4 |
5 | - Add linux support
--------------------------------------------------------------------------------
/.changes/0.6.1.md:
--------------------------------------------------------------------------------
1 | ---
2 | "tauri-plugin-context-menu": "patch"
3 | ---
4 |
5 | - Fix #16
--------------------------------------------------------------------------------
/.changes/0.6.2.md:
--------------------------------------------------------------------------------
1 | ---
2 | "tauri-plugin-context-menu": "patch"
3 | ---
4 |
5 | - Fix event warning on linux
--------------------------------------------------------------------------------
/.changes/0.7.0.md:
--------------------------------------------------------------------------------
1 | ---
2 | "tauri-plugin-context-menu": "minor"
3 | ---
4 |
5 | - Add checked items support #10
6 | - Refactoring
--------------------------------------------------------------------------------
/.changes/0.7.1.md:
--------------------------------------------------------------------------------
1 | ---
2 | "tauri-plugin-context-menu": "patch"
3 | ---
4 |
5 | - Fix #21
--------------------------------------------------------------------------------
/.changes/0.8.0.md:
--------------------------------------------------------------------------------
1 | ---
2 | "tauri-plugin-context-menu": "minor"
3 | ---
4 |
5 | - Add support for `theme` option
6 | - Update Tauri to 1.7
--------------------------------------------------------------------------------
/.changes/0.8.1.md:
--------------------------------------------------------------------------------
1 | ---
2 | "tauri-plugin-context-menu": "patch"
3 | ---
4 | - Fix support for external display on MacOS #25
--------------------------------------------------------------------------------
/.changes/0.8.2.md:
--------------------------------------------------------------------------------
1 | ---
2 | "tauri-plugin-context-menu": "patch"
3 | ---
4 | - Fix window crashing on Windows when unfocused #29
--------------------------------------------------------------------------------
/.changes/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "gitSiteUrl": "https://www.github.com/your-org/tauri-plugin-context-menu/",
3 | "pkgManagers": {
4 | "rust": {
5 | "version": true,
6 | "getPublishedVersion": "cargo search ${ pkg.pkg } --limit 1 | sed -nE 's/^[^\"]*\"//; s/\".*//1p' -",
7 | "prepublish": [
8 | "sudo apt-get update",
9 | "sudo apt-get install -y webkit2gtk-4.0",
10 | "cargo install cargo-audit",
11 | {
12 | "command": "cargo generate-lockfile",
13 | "dryRunCommand": true,
14 | "pipe": true
15 | },
16 | {
17 | "command": "echo '\nCargo Audit
\n\n```'",
18 | "dryRunCommand": true,
19 | "pipe": true
20 | },
21 | {
22 | "command": "cargo audit ${ process.env.CARGO_AUDIT_OPTIONS || '' }",
23 | "dryRunCommand": true,
24 | "pipe": true
25 | },
26 | {
27 | "command": "echo '```\n\n \n'",
28 | "dryRunCommand": true,
29 | "pipe": true
30 | }
31 | ],
32 | "publish": [
33 | {
34 | "command": "cargo package --no-verify",
35 | "dryRunCommand": true
36 | },
37 | {
38 | "command": "echo '\nCargo Publish
\n\n```'",
39 | "dryRunCommand": true,
40 | "pipe": true
41 | },
42 | {
43 | "command": "cargo publish",
44 | "dryRunCommand": "cargo publish --dry-run",
45 | "pipe": true
46 | },
47 | {
48 | "command": "echo '```\n\n \n'",
49 | "dryRunCommand": true,
50 | "pipe": true
51 | }
52 | ]
53 | }
54 | },
55 | "packages": {
56 | "tauri-plugin-context-menu": {
57 | "path": ".",
58 | "manager": "rust",
59 | "releaseTag": "v${ pkgFile.version }"
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/.changes/initial-release.md:
--------------------------------------------------------------------------------
1 | ---
2 | "tauri-plugin-context-menu": "minor"
3 | ---
4 |
5 | Initial release.
6 |
--------------------------------------------------------------------------------
/.changes/readme.md:
--------------------------------------------------------------------------------
1 | # Changes
2 | ##### via https://github.com/jbolda/covector
3 |
4 | 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 it represents the overall change for our sanity.
5 |
6 | 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.
7 |
8 | Use the following format:
9 | ```md
10 | ---
11 | "tauri-plugin-context-menu": patch
12 | ---
13 |
14 | Change summary goes here
15 |
16 | ```
--------------------------------------------------------------------------------
/.github/workflows/audit.yml:
--------------------------------------------------------------------------------
1 | name: Audit
2 |
3 | on:
4 | schedule:
5 | - cron: '0 0 * * *'
6 | push:
7 | branches:
8 | - main
9 | paths:
10 | - "**/Cargo.lock"
11 | - "**/Cargo.toml"
12 | pull_request:
13 | branches:
14 | - main
15 | paths:
16 | - "**/Cargo.lock"
17 | - "**/Cargo.toml"
18 |
19 | jobs:
20 | audit:
21 | runs-on: ubuntu-latest
22 | steps:
23 | - uses: actions/checkout@v2
24 | - uses: actions-rs/audit-check@v1
25 | with:
26 | token: ${{ secrets.GITHUB_TOKEN }}
27 |
--------------------------------------------------------------------------------
/.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@v2
10 | with:
11 | fetch-depth: 0
12 | - name: covector status
13 | uses: jbolda/covector/packages/action@covector-v0
14 | with:
15 | command: 'status'
16 |
--------------------------------------------------------------------------------
/.github/workflows/format.yml:
--------------------------------------------------------------------------------
1 | name: Format
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | branches:
9 | - main
10 | - dev
11 |
12 | jobs:
13 | format:
14 | runs-on: ubuntu-latest
15 | strategy:
16 | fail-fast: false
17 |
18 | steps:
19 | - uses: actions/checkout@v2
20 | - name: Install rustfmt with stable toolchain
21 | uses: actions-rs/toolchain@v1
22 | with:
23 | profile: minimal
24 | toolchain: stable
25 | override: true
26 | components: rustfmt
27 | - uses: actions-rs/cargo@v1
28 | with:
29 | command: fmt
30 | args: --manifest-path=Cargo.toml --all -- --check
31 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Test
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | branches:
9 | - main
10 | - dev
11 | paths-ignore:
12 | - 'webview-src/**'
13 | - 'webview-dist/**'
14 | - 'examples/**'
15 |
16 | jobs:
17 | build-and-test:
18 | runs-on: ${{ matrix.os }}
19 | strategy:
20 | fail-fast: false
21 | matrix:
22 | os: [ubuntu-latest, macos-latest, windows-latest]
23 |
24 | steps:
25 | - uses: actions/checkout@v2
26 |
27 | - name: Install stable toolchain
28 | uses: actions-rs/toolchain@v1
29 | with:
30 | toolchain: stable
31 | override: true
32 |
33 | - name: Install Linux dependencies
34 | if: matrix.os == 'ubuntu-latest'
35 | run: |
36 | sudo apt-get update
37 | sudo apt-get install -y webkit2gtk-4.0
38 |
39 | - uses: Swatinem/rust-cache@v2
40 |
41 | - name: Run tests
42 | uses: actions-rs/cargo@v1
43 | with:
44 | command: test
45 | args: --manifest-path=Cargo.toml --release
46 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | .DS_Store
3 | .Thumbs.db
4 | package-lock.json
5 | .vscode/
6 | .yarn
7 | yarn.lock
8 | node_modules/
9 | Cargo.lock
10 | lerna-debug.log
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "tauri-plugin-context-menu"
3 | version = "0.8.2"
4 | authors = [ "c2r0b" ]
5 | description = "Handle native Context Menu in Tauri"
6 | license = "MIT OR Apache-2.0"
7 | edition = "2021"
8 | rust-version = "1.59"
9 | exclude = ["/examples", "/assets", ".DS_Store", "/.github", "/.changes", "/webview-dist", "/webview-src", "package.json", "package-lock.json"]
10 |
11 | [dependencies]
12 | tauri = { version = "1.7" }
13 | serde = { version = "1.0", features = ["derive"] }
14 | lazy_static = "1.4"
15 | time = "0.3.28"
16 |
17 | [target.'cfg(target_os = "windows")'.dependencies]
18 | winapi = { version = "0.3", features = ["winuser"] }
19 | image = "0.24.7"
20 |
21 | [target.'cfg(target_os = "macos")'.dependencies]
22 | cocoa = "0.24.1"
23 | objc = "0.2.7"
24 | libc = "0.2.147"
25 | dispatch = "0.2.0"
26 |
27 | [target.'cfg(target_os = "linux")'.dependencies]
28 | gtk = "0.15.1"
29 | gdk = "0.15.1"
30 | glib = "0.15.1"
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 c2r0b
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.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Tauri v1 Plugin Context Menu
2 |
3 | A Tauri plugin to display native context menu in Tauri v1.x.
4 | The Tauri API v1 does not support native context menu out of the box, so this plugin is created to fill the gap.
5 |
6 |
7 |
8 | ⚠️ **Maintenance Mode with Community Contributions** ⚠️
9 |
10 | Tauri v2 [has been released](https://tauri.app/blog/tauri-20/) and it supports creating native context menu without plugins ([here the docs](https://v2.tauri.app/reference/javascript/api/namespacemenu/)).
11 |
12 | Bug fixes will still be implemented for this plugin.
13 | New features will not be developed by the main maintainer, but PRs from the community are still welcome.
14 |
15 | ## Support
16 | | Windows | MacOS | Linux |
17 | | ------- | ----- | ----- |
18 | | ✅ | ✅ | ✅ |
19 |
20 | ## Installation
21 | Crate: https://crates.io/crates/tauri-plugin-context-menu
22 |
23 | `cargo add tauri-plugin-context-menu` to add the package.
24 |
25 | Or add the following to your `Cargo.toml` for the latest unpublished version (not recommanded).
26 |
27 | ```toml
28 | tauri-plugin-context-menu = { git = "https://github.com/c2r0b/tauri-plugin-context-menu", branch = "main" }
29 | ```
30 |
31 | See ["Using a Plugin" Tauri official guide](https://tauri.app/v1/guides/features/plugin#using-a-plugin) to initialize the plugin.
32 |
33 | This project provides a typescript utility to simplify the usage of the plugin. Run the following to install the JavaScript/TypeScript package:
34 |
35 | ```bash
36 | npm i tauri-plugin-context-menu
37 | ```
38 |
39 | ## Examples
40 | Check out the `examples` directory for sample usages.
41 |
42 | A vanilla JS example is provided in `examples/vanilla`.
43 | After `npm install`, to run the example use the following command:
44 |
45 | ```bash
46 | npm run examples/vanilla
47 | ```
48 |
49 | A typescript example using the utility package is provided in `examples/ts-utility`.
50 | You can run it with the same command as above (replace `examples/vanilla` with `examples/ts-utility`).
51 |
52 | ## Sample Usage
53 | ### Without the JS/TS Package
54 | ```ts
55 | import { invoke } from "@tauri-apps/api";
56 | import { listen } from "@tauri-apps/api/event";
57 | import { resolveResource } from "@tauri-apps/api/path";
58 |
59 | // Listen to the event emitted when the first menu item is clicked
60 | listen("item1clicked", (event) => {
61 | alert(event.payload);
62 | });
63 |
64 | window.addEventListener("contextmenu", async (e) => {
65 | e.preventDefault();
66 | const iconUrl = await resolveResource('assets/16x16.png');
67 |
68 | // Show the context menu
69 | invoke("plugin:context_menu|show_context_menu", {
70 | items: [
71 | {
72 | label: "Item 1",
73 | disabled: false,
74 | event: "item1clicked",
75 | payload: "Hello World!",
76 | shortcut: "ctrl+M",
77 | icon: {
78 | path: iconUrl
79 | },
80 | subitems: [
81 | {
82 | label: "Subitem 1",
83 | disabled: true,
84 | event: "subitem1clicked",
85 | },
86 | {
87 | is_separator: true,
88 | },
89 | {
90 | label: "Subitem 2",
91 | disabled: false,
92 | checked: true,
93 | event: "subitem2clicked",
94 | }
95 | ]
96 | }
97 | ],
98 | });
99 | });
100 | ```
101 |
102 | ### With the JS/TS Package
103 | ```ts
104 | import { showMenu } from "tauri-plugin-context-menu";
105 |
106 | showMenu({
107 | pos: {...}, // Position of the menu (see below for options)
108 | theme: "light", // Theme of the menu
109 | items: [
110 | ...,
111 | {
112 | ..., // Menu item (see below for options)
113 | event: () => {
114 | // Do something
115 | }
116 | }
117 | ]
118 | });
119 | ```
120 | You can also use it to respond to window events with the `onEventShowMenu` function:
121 | ```ts
122 | import { onEventShowMenu } from "tauri-plugin-context-menu";
123 | onEventShowMenu("contextmenu", (e) => ({ /* menuOptions */ }));
124 | ```
125 |
126 | ## Options
127 | List of options that can be passed to the plugin.
128 | | Option | Type | Optional | Description | OS compatibility |
129 | | ------ | ----------------- | ---------- | ------------------------------------------------------ | ------------------------------------------------------------------------------ |
130 | | items | `MenuItem[]` | | List of menu items to be displayed. | All |
131 | | pos | `Position` | `optional` | Position of the menu. Defaults to the cursor position. | All |
132 | | theme | `light` \| `dark` | `optional` | Theme of the menu. Defaults to system theme. | MacOS only [#25](https://github.com/c2r0b/tauri-plugin-context-menu/issues/25) |
133 |
134 | ### MenuItem
135 | | Option | Type | Optional | Default | Description | JS/TS pkg |
136 | | ------------ | -------------- | ---------- | ------- | ------------------------------------------------------- | ---------------------------------------------------------------- |
137 | | label | `string` | | | Displayed test of the menu item. | |
138 | | disabled | `boolean` | `optional` | `false` | Whether the menu item is disabled. |
139 | | event | `string` | `optional` | | Event name to be emitted when the menu item is clicked. | You can pass a function to be executed instead of an event name. |
140 | | payload | `string` | `optional` | | Payload to be passed to the event. | You can pass any type of data. |
141 | | checked | `boolean` | `optional` | | Whether the menu item is checked. |
142 | | subitems | `MenuItem[]` | `optional` | `[]` | List of sub menu items to be displayed. |
143 | | shortcut | `string` | `optional` | | Keyboard shortcut displayed on the right. |
144 | | icon | `MenuItemIcon` | `optional` | | Icon to be displayed on the left. |
145 | | is_separator | `boolean` | `optional` | `false` | Whether the menu item is a separator. |
146 |
147 |
148 | ### MenuItemIcon
149 | | Option | Type | Optional | Default | Description | JS/TS pkg |
150 | | ------ | -------- | ---------- | ------- | ------------------------------- | ------------------------------------------------------------------------- |
151 | | path | `string` | | | Absolute path to the icon file. | You can use `assetToPath` to convert a relative path to an absolute path. |
152 | | width | `number` | `optional` | `16` | Width of the icon. |
153 | | height | `number` | `optional` | `16` | Height of the icon. |
154 |
155 | ### Position
156 | Position coordinates must be relative to the currently active window when `is_absolute` is set to `false`.
157 | | Option | Type | Optional | Default | Description |
158 | | ----------- | --------- | ---------- | ------- | --------------------------------------- |
159 | | x | `number` | | | X position of the menu. |
160 | | y | `number` | | | Y position of the menu. |
161 | | is_absolute | `boolean` | `optional` | `false` | Is the position absolute to the screen. |
162 |
163 | ### Modifier Keys
164 | Modifier keys can be used in the `shortcut` option of a menu item to display the corresponding symbol (`⌘`, `⌃`, `⌥`, `⇧`).
165 |
166 | On MacOS this also makes the shortcut work when the modifier key is pressed (since it is handled by default by the OS).
167 |
168 |
169 | Key codes list
170 |
171 | #### Modifiers
172 | - `cmd`
173 | - `cmd_or_ctrl` (Alias for `cmd` and `ctrl`)
174 | - `shift`
175 | - `alt`
176 | - `ctrl`
177 | - `opt` (Alias for `alt`)
178 | - `altgr`
179 | - `super`
180 | - `win`
181 | - `meta`
182 |
183 | #### Keys
184 | - `plus`
185 | - `space`
186 | - `tab`
187 | - `capslock`
188 | - `numlock`
189 | - `scrolllock`
190 | - `backspace`
191 | - `delete`
192 | - `insert`
193 | - `return`
194 | - `enter`
195 | - `up`
196 | - `down`
197 | - `left`
198 | - `right`
199 | - `home`
200 | - `end`
201 | - `pageup`
202 | - `pagedown`
203 | - `escape`
204 | - `esc`
205 | - `num0...9`
206 | - `numdec`
207 | - `numadd`
208 | - `numsub`
209 | - `nummult`
210 | - `numdiv`
211 | - `f1...24`
212 |
213 |
214 | ## Events
215 | ### Item Clicked
216 | Emitted when a menu item is clicked. The event name is the same as the `event` option of the menu item:
217 |
218 | ```ts
219 | import { listen } from "@tauri-apps/api/event";
220 | import { invoke } from "@tauri-apps/api";
221 |
222 | listen("[EVENTNAME]", () => {
223 | alert("menu item clicked");
224 | });
225 |
226 | invoke(...{
227 | items: [{
228 | ...
229 | event: "[EVENTNAME]",
230 | ...
231 | }]
232 | });
233 | ```
234 |
235 | ### Menu Did Close
236 | Emitted when the menu is closed. This event is emitted regardless of whether the menu is closed by clicking on a menu item or by clicking outside the menu.
237 | You can catch this event using the following code:
238 |
239 | ```ts
240 | import { listen } from "@tauri-apps/api/event";
241 |
242 | listen("menu-did-close", () => {
243 | alert("menu closed");
244 | });
245 | ```
246 |
--------------------------------------------------------------------------------
/assets/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/c2r0b/tauri-plugin-context-menu/41926bd19c9defde81ce393ab3bcf5806ced1af7/assets/screenshot.png
--------------------------------------------------------------------------------
/examples/multi/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | package-lock.json
--------------------------------------------------------------------------------
/examples/multi/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "multi-example",
3 | "version": "0.8.2",
4 | "main": "index.js",
5 | "scripts": {
6 | "test": "echo \"Error: no test specified\" && exit 1",
7 | "tauri": "tauri"
8 | },
9 | "author": "",
10 | "license": "MIT",
11 | "dependencies": {
12 | "@tauri-apps/cli": "^1.5.0"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/examples/multi/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
30 |
31 |
32 |
33 |
34 | Right click here
35 |
36 |
37 | Or here
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/examples/multi/public/index.js:
--------------------------------------------------------------------------------
1 | import * as tauriApi from 'https://esm.run/@tauri-apps/api';
2 |
3 | document.getElementById('firstBox').addEventListener('contextmenu', (e) => {
4 | e.preventDefault();
5 |
6 | tauriApi.invoke('plugin:context_menu|show_context_menu', {
7 | theme: 'light',
8 | items: [
9 | {
10 | label: "This is a menu item",
11 | disabled: false,
12 | }
13 | ]
14 | });
15 | });
16 |
17 | document.getElementById('secondBox').addEventListener('contextmenu', (e) => {
18 | e.preventDefault();
19 |
20 | tauriApi.invoke('plugin:context_menu|show_context_menu', {
21 | theme: 'dark',
22 | items: [
23 | {
24 | label: "This is ANOTHER menu item",
25 | disabled: true
26 | }
27 | ]
28 | });
29 | });
--------------------------------------------------------------------------------
/examples/multi/src-tauri/.gitignore:
--------------------------------------------------------------------------------
1 | # Generated by Cargo
2 | # will have compiled files and executables
3 | /target/
4 | Cargo.lock
5 |
--------------------------------------------------------------------------------
/examples/multi/src-tauri/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "app"
3 | version = "0.1.0"
4 | description = "A Tauri App"
5 | authors = [ "You" ]
6 | repository = ""
7 | edition = "2021"
8 | rust-version = "1.59"
9 |
10 | [dependencies]
11 | serde_json = "1.0"
12 | serde = { version = "1.0", features = [ "derive" ] }
13 | tauri = { version = "1.5", features = [ "path-all", "dialog-all" ] }
14 | tauri-plugin-context-menu = { path = "../../../" }
15 |
16 | [build-dependencies]
17 | tauri-build = { version = "1.5.0", features = [] }
18 |
19 | [features]
20 | default = [ "custom-protocol" ]
21 | custom-protocol = [ "tauri/custom-protocol" ]
22 |
--------------------------------------------------------------------------------
/examples/multi/src-tauri/assets/16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/c2r0b/tauri-plugin-context-menu/41926bd19c9defde81ce393ab3bcf5806ced1af7/examples/multi/src-tauri/assets/16x16.png
--------------------------------------------------------------------------------
/examples/multi/src-tauri/build.rs:
--------------------------------------------------------------------------------
1 | fn main() {
2 | tauri_build::build()
3 | }
4 |
--------------------------------------------------------------------------------
/examples/multi/src-tauri/icons/128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/c2r0b/tauri-plugin-context-menu/41926bd19c9defde81ce393ab3bcf5806ced1af7/examples/multi/src-tauri/icons/128x128.png
--------------------------------------------------------------------------------
/examples/multi/src-tauri/icons/128x128@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/c2r0b/tauri-plugin-context-menu/41926bd19c9defde81ce393ab3bcf5806ced1af7/examples/multi/src-tauri/icons/128x128@2x.png
--------------------------------------------------------------------------------
/examples/multi/src-tauri/icons/32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/c2r0b/tauri-plugin-context-menu/41926bd19c9defde81ce393ab3bcf5806ced1af7/examples/multi/src-tauri/icons/32x32.png
--------------------------------------------------------------------------------
/examples/multi/src-tauri/icons/icon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/c2r0b/tauri-plugin-context-menu/41926bd19c9defde81ce393ab3bcf5806ced1af7/examples/multi/src-tauri/icons/icon.icns
--------------------------------------------------------------------------------
/examples/multi/src-tauri/icons/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/c2r0b/tauri-plugin-context-menu/41926bd19c9defde81ce393ab3bcf5806ced1af7/examples/multi/src-tauri/icons/icon.ico
--------------------------------------------------------------------------------
/examples/multi/src-tauri/icons/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/c2r0b/tauri-plugin-context-menu/41926bd19c9defde81ce393ab3bcf5806ced1af7/examples/multi/src-tauri/icons/icon.png
--------------------------------------------------------------------------------
/examples/multi/src-tauri/rustfmt.toml:
--------------------------------------------------------------------------------
1 | max_width = 100
2 | hard_tabs = false
3 | tab_spaces = 2
4 | newline_style = "Auto"
5 | use_small_heuristics = "Default"
6 | reorder_imports = true
7 | reorder_modules = true
8 | remove_nested_parens = true
9 | edition = "2021"
10 | merge_derives = true
11 | use_try_shorthand = false
12 | use_field_init_shorthand = false
13 | force_explicit_abi = true
14 | imports_granularity = "Crate"
15 |
--------------------------------------------------------------------------------
/examples/multi/src-tauri/src/main.rs:
--------------------------------------------------------------------------------
1 | #![cfg_attr(
2 | all(not(debug_assertions), target_os = "windows"),
3 | windows_subsystem = "windows"
4 | )]
5 |
6 | fn main() {
7 | tauri::Builder::default()
8 | .plugin(tauri_plugin_context_menu::init())
9 | .run(tauri::generate_context!())
10 | .expect("error while running tauri application");
11 | }
12 |
--------------------------------------------------------------------------------
/examples/multi/src-tauri/tauri.conf.json:
--------------------------------------------------------------------------------
1 | {
2 | "package": {
3 | "productName": "app",
4 | "version": "0.1.0"
5 | },
6 | "build": {
7 | "distDir": "../public",
8 | "devPath": "../public"
9 | },
10 | "tauri": {
11 | "bundle": {
12 | "active": true,
13 | "targets": "all",
14 | "identifier": "com.tauri.context-menu",
15 | "icon": [
16 | "icons/32x32.png",
17 | "icons/128x128.png",
18 | "icons/128x128@2x.png",
19 | "icons/icon.icns",
20 | "icons/icon.ico"
21 | ],
22 | "resources": [
23 | "assets/**/*"
24 | ],
25 | "externalBin": [],
26 | "copyright": "",
27 | "category": "DeveloperTool",
28 | "shortDescription": "",
29 | "longDescription": "",
30 | "deb": {
31 | "depends": []
32 | },
33 | "macOS": {
34 | "frameworks": [],
35 | "exceptionDomain": "",
36 | "signingIdentity": null,
37 | "entitlements": null
38 | },
39 | "windows": {
40 | "certificateThumbprint": null,
41 | "digestAlgorithm": "sha256",
42 | "timestampUrl": ""
43 | }
44 | },
45 | "updater": {
46 | "active": false
47 | },
48 | "allowlist": {
49 | "all": false,
50 | "dialog": {
51 | "all": true
52 | },
53 | "path": {
54 | "all": true
55 | }
56 | },
57 | "windows": [
58 | {
59 | "title": "app",
60 | "width": 800,
61 | "height": 600,
62 | "resizable": true,
63 | "fullscreen": false
64 | }
65 | ],
66 | "security": {
67 | "csp": "default-src blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self' img-src: 'self'"
68 | }
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/examples/ts-utility/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | package-lock.json
3 | public/*.js
4 | public/*.js.map
--------------------------------------------------------------------------------
/examples/ts-utility/esbuild.js:
--------------------------------------------------------------------------------
1 | import esbuild from 'esbuild';
2 |
3 | esbuild.build({
4 | entryPoints: ['./src/index.ts'],
5 | outdir: './public',
6 | bundle: true,
7 | format: 'esm',
8 | splitting: true,
9 | chunkNames: '[name]',
10 | minify: true,
11 | loader: {
12 | '.ts': 'ts'
13 | },
14 | tsconfig: './tsconfig.json',
15 | platform: 'node',
16 | sourcemap: true
17 | }).catch(() => process.exit(1));
18 |
--------------------------------------------------------------------------------
/examples/ts-utility/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ts-utility-example",
3 | "version": "0.8.2",
4 | "main": "index.js",
5 | "type": "module",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1",
8 | "build": "node ./esbuild.js",
9 | "tauri": "npm run build && tauri"
10 | },
11 | "author": "",
12 | "license": "MIT",
13 | "dependencies": {
14 | "@tauri-apps/cli": "^1.5.0",
15 | "tauri-plugin-context-menu": "file:../../plugin"
16 | },
17 | "devDependencies": {
18 | "esbuild": "^0.19.3",
19 | "tslib": "^2.6.2",
20 | "typescript": "^5.2.2"
21 | },
22 | "description": ""
23 | }
24 |
--------------------------------------------------------------------------------
/examples/ts-utility/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Right click anywhere
7 |
8 |
9 |
--------------------------------------------------------------------------------
/examples/ts-utility/src-tauri/.gitignore:
--------------------------------------------------------------------------------
1 | # Generated by Cargo
2 | # will have compiled files and executables
3 | /target/
4 | Cargo.lock
5 |
--------------------------------------------------------------------------------
/examples/ts-utility/src-tauri/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "app"
3 | version = "0.1.0"
4 | description = "A Tauri App"
5 | authors = [ "You" ]
6 | repository = ""
7 | edition = "2021"
8 | rust-version = "1.59"
9 |
10 | [dependencies]
11 | serde_json = "1.0"
12 | serde = { version = "1.0", features = [ "derive" ] }
13 | tauri = { version = "1.7", features = [ "path-all", "dialog-all" ] }
14 | tauri-plugin-context-menu = { path = "../../../" }
15 |
16 | [build-dependencies]
17 | tauri-build = { version = "1.5", features = [] }
18 |
19 | [features]
20 | default = [ "custom-protocol" ]
21 | custom-protocol = [ "tauri/custom-protocol" ]
22 |
--------------------------------------------------------------------------------
/examples/ts-utility/src-tauri/assets/16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/c2r0b/tauri-plugin-context-menu/41926bd19c9defde81ce393ab3bcf5806ced1af7/examples/ts-utility/src-tauri/assets/16x16.png
--------------------------------------------------------------------------------
/examples/ts-utility/src-tauri/build.rs:
--------------------------------------------------------------------------------
1 | fn main() {
2 | tauri_build::build()
3 | }
4 |
--------------------------------------------------------------------------------
/examples/ts-utility/src-tauri/icons/128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/c2r0b/tauri-plugin-context-menu/41926bd19c9defde81ce393ab3bcf5806ced1af7/examples/ts-utility/src-tauri/icons/128x128.png
--------------------------------------------------------------------------------
/examples/ts-utility/src-tauri/icons/128x128@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/c2r0b/tauri-plugin-context-menu/41926bd19c9defde81ce393ab3bcf5806ced1af7/examples/ts-utility/src-tauri/icons/128x128@2x.png
--------------------------------------------------------------------------------
/examples/ts-utility/src-tauri/icons/32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/c2r0b/tauri-plugin-context-menu/41926bd19c9defde81ce393ab3bcf5806ced1af7/examples/ts-utility/src-tauri/icons/32x32.png
--------------------------------------------------------------------------------
/examples/ts-utility/src-tauri/icons/icon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/c2r0b/tauri-plugin-context-menu/41926bd19c9defde81ce393ab3bcf5806ced1af7/examples/ts-utility/src-tauri/icons/icon.icns
--------------------------------------------------------------------------------
/examples/ts-utility/src-tauri/icons/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/c2r0b/tauri-plugin-context-menu/41926bd19c9defde81ce393ab3bcf5806ced1af7/examples/ts-utility/src-tauri/icons/icon.ico
--------------------------------------------------------------------------------
/examples/ts-utility/src-tauri/icons/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/c2r0b/tauri-plugin-context-menu/41926bd19c9defde81ce393ab3bcf5806ced1af7/examples/ts-utility/src-tauri/icons/icon.png
--------------------------------------------------------------------------------
/examples/ts-utility/src-tauri/rustfmt.toml:
--------------------------------------------------------------------------------
1 | max_width = 100
2 | hard_tabs = false
3 | tab_spaces = 2
4 | newline_style = "Auto"
5 | use_small_heuristics = "Default"
6 | reorder_imports = true
7 | reorder_modules = true
8 | remove_nested_parens = true
9 | edition = "2021"
10 | merge_derives = true
11 | use_try_shorthand = false
12 | use_field_init_shorthand = false
13 | force_explicit_abi = true
14 | imports_granularity = "Crate"
15 |
--------------------------------------------------------------------------------
/examples/ts-utility/src-tauri/src/main.rs:
--------------------------------------------------------------------------------
1 | #![cfg_attr(
2 | all(not(debug_assertions), target_os = "windows"),
3 | windows_subsystem = "windows"
4 | )]
5 |
6 | fn main() {
7 | tauri::Builder::default()
8 | .plugin(tauri_plugin_context_menu::init())
9 | .run(tauri::generate_context!())
10 | .expect("error while running tauri application");
11 | }
12 |
--------------------------------------------------------------------------------
/examples/ts-utility/src-tauri/tauri.conf.json:
--------------------------------------------------------------------------------
1 | {
2 | "package": {
3 | "productName": "app",
4 | "version": "0.1.0"
5 | },
6 | "build": {
7 | "distDir": "../public",
8 | "devPath": "../public"
9 | },
10 | "tauri": {
11 | "bundle": {
12 | "active": true,
13 | "targets": "all",
14 | "identifier": "com.tauri.context-menu",
15 | "icon": [
16 | "icons/32x32.png",
17 | "icons/128x128.png",
18 | "icons/128x128@2x.png",
19 | "icons/icon.icns",
20 | "icons/icon.ico"
21 | ],
22 | "resources": [
23 | "assets/**/*"
24 | ],
25 | "externalBin": [],
26 | "copyright": "",
27 | "category": "DeveloperTool",
28 | "shortDescription": "",
29 | "longDescription": "",
30 | "deb": {
31 | "depends": []
32 | },
33 | "macOS": {
34 | "frameworks": [],
35 | "exceptionDomain": "",
36 | "signingIdentity": null,
37 | "entitlements": null
38 | },
39 | "windows": {
40 | "certificateThumbprint": null,
41 | "digestAlgorithm": "sha256",
42 | "timestampUrl": ""
43 | }
44 | },
45 | "updater": {
46 | "active": false
47 | },
48 | "allowlist": {
49 | "all": false,
50 | "dialog": {
51 | "all": true
52 | },
53 | "path": {
54 | "all": true
55 | }
56 | },
57 | "windows": [
58 | {
59 | "title": "app",
60 | "width": 800,
61 | "height": 600,
62 | "resizable": true,
63 | "fullscreen": false
64 | }
65 | ],
66 | "security": {
67 | "csp": "default-src blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self' img-src: 'self'"
68 | }
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/examples/ts-utility/src/index.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'tauri-plugin-context-menu';
--------------------------------------------------------------------------------
/examples/ts-utility/src/index.ts:
--------------------------------------------------------------------------------
1 | import { assetToPath, onEventShowMenu } from 'tauri-plugin-context-menu';
2 |
3 | onEventShowMenu('contextmenu', async (_e:MouseEvent) => {
4 | const options = {
5 | theme: 'dark',
6 | items: [
7 | {
8 | label: "My first item",
9 | disabled: false,
10 | event: (e:any) => {
11 | alert(e.payload?.message);
12 | },
13 | payload: { message: "Hello from the payload!" },
14 | shortcut: "alt+m",
15 | icon: {
16 | path: await assetToPath('assets/16x16.png'),
17 | width: 32,
18 | height: 32
19 | }
20 | },
21 | {
22 | is_separator: true
23 | },
24 | {
25 | label: "My second item",
26 | disabled: false,
27 | event: "my_second_item",
28 | shortcut: "cmd+C"
29 | },
30 | {
31 | label: "My third item",
32 | disabled: false,
33 | subitems: [
34 | {
35 | label: "My first subitem",
36 | checked: true,
37 | event: () => {
38 | alert('My first subitem clicked');
39 | },
40 | shortcut: "ctrl+m"
41 | },
42 | {
43 | label: "My second subitem",
44 | checked: false,
45 | disabled: true
46 | }
47 | ]
48 | }
49 | ]
50 | };
51 | return options;
52 | });
--------------------------------------------------------------------------------
/examples/ts-utility/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES5",
4 | "strict": true,
5 | "allowJs": true,
6 | "esModuleInterop": true,
7 | "baseUrl": ".",
8 | "paths": {
9 | "types": ["@types"]
10 | },
11 | "declaration": true,
12 | "declarationDir": "./public"
13 | },
14 | "include": [
15 | "./src/index.ts",
16 | "./src/index.d.ts"
17 | ]
18 | }
--------------------------------------------------------------------------------
/examples/vanilla/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | package-lock.json
--------------------------------------------------------------------------------
/examples/vanilla/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vanilla-example",
3 | "version": "0.8.2",
4 | "main": "index.js",
5 | "scripts": {
6 | "test": "echo \"Error: no test specified\" && exit 1",
7 | "tauri": "tauri"
8 | },
9 | "author": "",
10 | "license": "MIT",
11 | "dependencies": {
12 | "@tauri-apps/cli": "^1.5.0"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/examples/vanilla/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Right click anywhere
7 |
8 |
9 |
--------------------------------------------------------------------------------
/examples/vanilla/public/index.js:
--------------------------------------------------------------------------------
1 | import * as tauriApi from 'https://esm.run/@tauri-apps/api';
2 | import * as tauriEvent from 'https://esm.run/@tauri-apps/api/event';
3 | import * as tauriApiPath from 'https://esm.run/@tauri-apps/api/path';
4 |
5 | async function registerListeners() {
6 | // on context menu item click
7 | await tauriEvent.listen('my_first_item', (event) => {
8 | alert(event.payload);
9 | });
10 |
11 | // on context menu item click
12 | await tauriEvent.listen('my_second_item', (event) => {
13 | alert(event.event);
14 | });
15 |
16 | // on context menu item click
17 | await tauriEvent.listen('my_first_subitem', (event) => {
18 | alert(event.event);
19 | });
20 |
21 | // on context menu item closed
22 | await tauriEvent.listen('menu-did-close', (event) => {
23 | alert(event.event);
24 | });
25 | }
26 | registerListeners(); // Register event listeners once
27 |
28 | window.addEventListener('contextmenu', (e) => {
29 | e.preventDefault();
30 |
31 | // get icon path
32 | (tauriApiPath.resolveResource('assets/16x16.png')).then((assetUrl) => {
33 | // show context menu
34 | tauriApi.invoke('plugin:context_menu|show_context_menu', {
35 | pos: {
36 | x: e.clientX,
37 | y: e.clientY
38 | },
39 | theme: 'light',
40 | items: [
41 | {
42 | label: "My first item",
43 | disabled: false,
44 | event: "my_first_item",
45 | payload: "Hello from Tauri!",
46 | shortcut: "alt+m",
47 | icon: {
48 | path: assetUrl,
49 | width: 32,
50 | height: 32
51 | }
52 | },
53 | {
54 | is_separator: true
55 | },
56 | {
57 | label: "My second item",
58 | disabled: false,
59 | event: "my_second_item",
60 | shortcut: "cmd_or_ctrl+backspace"
61 | },
62 | {
63 | label: "My third item",
64 | disabled: false,
65 | subitems: [
66 | {
67 | label: "My first subitem",
68 | event: "my_first_subitem",
69 | checked: true,
70 | shortcut: "ctrl+m"
71 | },
72 | {
73 | label: "My second subitem",
74 | checked: false,
75 | disabled: true
76 | }
77 | ]
78 | }
79 | ]
80 | });
81 | });
82 | });
--------------------------------------------------------------------------------
/examples/vanilla/src-tauri/.gitignore:
--------------------------------------------------------------------------------
1 | # Generated by Cargo
2 | # will have compiled files and executables
3 | /target/
4 | Cargo.lock
5 |
--------------------------------------------------------------------------------
/examples/vanilla/src-tauri/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "app"
3 | version = "0.1.0"
4 | description = "A Tauri App"
5 | authors = [ "You" ]
6 | repository = ""
7 | edition = "2021"
8 | rust-version = "1.59"
9 |
10 | [dependencies]
11 | serde_json = "1.0"
12 | serde = { version = "1.0", features = [ "derive" ] }
13 | tauri = { version = "1.5", features = [ "path-all", "dialog-all" ] }
14 | tauri-plugin-context-menu = { path = "../../../" }
15 |
16 | [build-dependencies]
17 | tauri-build = { version = "1.5.0", features = [] }
18 |
19 | [features]
20 | default = [ "custom-protocol" ]
21 | custom-protocol = [ "tauri/custom-protocol" ]
22 |
--------------------------------------------------------------------------------
/examples/vanilla/src-tauri/assets/16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/c2r0b/tauri-plugin-context-menu/41926bd19c9defde81ce393ab3bcf5806ced1af7/examples/vanilla/src-tauri/assets/16x16.png
--------------------------------------------------------------------------------
/examples/vanilla/src-tauri/build.rs:
--------------------------------------------------------------------------------
1 | fn main() {
2 | tauri_build::build()
3 | }
4 |
--------------------------------------------------------------------------------
/examples/vanilla/src-tauri/icons/128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/c2r0b/tauri-plugin-context-menu/41926bd19c9defde81ce393ab3bcf5806ced1af7/examples/vanilla/src-tauri/icons/128x128.png
--------------------------------------------------------------------------------
/examples/vanilla/src-tauri/icons/128x128@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/c2r0b/tauri-plugin-context-menu/41926bd19c9defde81ce393ab3bcf5806ced1af7/examples/vanilla/src-tauri/icons/128x128@2x.png
--------------------------------------------------------------------------------
/examples/vanilla/src-tauri/icons/32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/c2r0b/tauri-plugin-context-menu/41926bd19c9defde81ce393ab3bcf5806ced1af7/examples/vanilla/src-tauri/icons/32x32.png
--------------------------------------------------------------------------------
/examples/vanilla/src-tauri/icons/icon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/c2r0b/tauri-plugin-context-menu/41926bd19c9defde81ce393ab3bcf5806ced1af7/examples/vanilla/src-tauri/icons/icon.icns
--------------------------------------------------------------------------------
/examples/vanilla/src-tauri/icons/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/c2r0b/tauri-plugin-context-menu/41926bd19c9defde81ce393ab3bcf5806ced1af7/examples/vanilla/src-tauri/icons/icon.ico
--------------------------------------------------------------------------------
/examples/vanilla/src-tauri/icons/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/c2r0b/tauri-plugin-context-menu/41926bd19c9defde81ce393ab3bcf5806ced1af7/examples/vanilla/src-tauri/icons/icon.png
--------------------------------------------------------------------------------
/examples/vanilla/src-tauri/rustfmt.toml:
--------------------------------------------------------------------------------
1 | max_width = 100
2 | hard_tabs = false
3 | tab_spaces = 2
4 | newline_style = "Auto"
5 | use_small_heuristics = "Default"
6 | reorder_imports = true
7 | reorder_modules = true
8 | remove_nested_parens = true
9 | edition = "2021"
10 | merge_derives = true
11 | use_try_shorthand = false
12 | use_field_init_shorthand = false
13 | force_explicit_abi = true
14 | imports_granularity = "Crate"
15 |
--------------------------------------------------------------------------------
/examples/vanilla/src-tauri/src/main.rs:
--------------------------------------------------------------------------------
1 | #![cfg_attr(
2 | all(not(debug_assertions), target_os = "windows"),
3 | windows_subsystem = "windows"
4 | )]
5 |
6 | fn main() {
7 | tauri::Builder::default()
8 | .plugin(tauri_plugin_context_menu::init())
9 | .run(tauri::generate_context!())
10 | .expect("error while running tauri application");
11 | }
12 |
--------------------------------------------------------------------------------
/examples/vanilla/src-tauri/tauri.conf.json:
--------------------------------------------------------------------------------
1 | {
2 | "package": {
3 | "productName": "app",
4 | "version": "0.1.0"
5 | },
6 | "build": {
7 | "distDir": "../public",
8 | "devPath": "../public"
9 | },
10 | "tauri": {
11 | "bundle": {
12 | "active": true,
13 | "targets": "all",
14 | "identifier": "com.tauri.context-menu",
15 | "icon": [
16 | "icons/32x32.png",
17 | "icons/128x128.png",
18 | "icons/128x128@2x.png",
19 | "icons/icon.icns",
20 | "icons/icon.ico"
21 | ],
22 | "resources": [
23 | "assets/**/*"
24 | ],
25 | "externalBin": [],
26 | "copyright": "",
27 | "category": "DeveloperTool",
28 | "shortDescription": "",
29 | "longDescription": "",
30 | "deb": {
31 | "depends": []
32 | },
33 | "macOS": {
34 | "frameworks": [],
35 | "exceptionDomain": "",
36 | "signingIdentity": null,
37 | "entitlements": null
38 | },
39 | "windows": {
40 | "certificateThumbprint": null,
41 | "digestAlgorithm": "sha256",
42 | "timestampUrl": ""
43 | }
44 | },
45 | "updater": {
46 | "active": false
47 | },
48 | "allowlist": {
49 | "all": false,
50 | "dialog": {
51 | "all": true
52 | },
53 | "path": {
54 | "all": true
55 | }
56 | },
57 | "windows": [
58 | {
59 | "title": "app",
60 | "width": 800,
61 | "height": 600,
62 | "resizable": true,
63 | "fullscreen": false
64 | }
65 | ],
66 | "security": {
67 | "csp": "default-src blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self' img-src: 'self'"
68 | }
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/lerna.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "node_modules/lerna/schemas/lerna-schema.json",
3 | "version": "0.8.2"
4 | }
5 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tauri-plugin-context-menu",
3 | "version": "0.8.2",
4 | "author": "c2r0b",
5 | "description": "",
6 | "homepage": "https://github.com/c2r0b/tauri-plugin-context-menu",
7 | "scripts": {
8 | "examples/vanilla": "npm run tauri dev --workspace examples/vanilla",
9 | "examples/multi": "npm run tauri dev --workspace examples/multi",
10 | "examples/ts-utility": "npm run tauri dev --workspace examples/ts-utility"
11 | },
12 | "repository": {
13 | "type": "git",
14 | "url": "git+https://github.com/c2r0b/tauri-plugin-context-menu.git"
15 | },
16 | "bugs": {
17 | "url": "https://github.com/c2r0b/tauri-plugin-context-menu/issues"
18 | },
19 | "dependencies": {},
20 | "devDependencies": {
21 | "lerna": "^7.3.0"
22 | },
23 | "workspaces": [
24 | "plugin",
25 | "examples/vanilla",
26 | "examples/multi",
27 | "examples/ts-utility"
28 | ]
29 | }
30 |
--------------------------------------------------------------------------------
/plugin/dist/index.d.ts:
--------------------------------------------------------------------------------
1 | import * as ContextMenu from './types';
2 | export type { ContextMenu };
3 | export declare function assetToPath(asset: string): Promise;
4 | export declare function showMenu(options: ContextMenu.Options): Promise;
5 | export declare function onEventShowMenu(eventName: string, options: ContextMenu.EventOptions): void;
6 |
--------------------------------------------------------------------------------
/plugin/dist/index.js:
--------------------------------------------------------------------------------
1 | function R(){return window.crypto.getRandomValues(new Uint32Array(1))[0]}function u(t,e=!1){let a=R(),i=`_${a}`;return Object.defineProperty(window,i,{value:r=>(e&&Reflect.deleteProperty(window,i),t?.(r)),writable:!1,configurable:!0}),a}async function _(t,e={}){return new Promise((a,i)=>{let r=u(l=>{a(l),Reflect.deleteProperty(window,`_${o}`)},!0),o=u(l=>{i(l),Reflect.deleteProperty(window,`_${r}`)},!0);window.__TAURI_IPC__({cmd:t,callback:r,error:o,...e})})}async function n(t){return _("tauri",t)}async function W(t,e){return n({__tauriModule:"Event",message:{cmd:"unlisten",event:t,eventId:e}})}async function g(t,e,a){await n({__tauriModule:"Event",message:{cmd:"emit",event:t,windowLabel:e,payload:a}})}async function c(t,e,a){return n({__tauriModule:"Event",message:{cmd:"listen",event:t,windowLabel:e,handler:u(a)}}).then(i=>async()=>W(t,i))}async function w(t,e,a){return c(t,e,i=>{a(i),W(t,i.id).catch(()=>{})})}var s;(function(t){t.WINDOW_RESIZED="tauri://resize",t.WINDOW_MOVED="tauri://move",t.WINDOW_CLOSE_REQUESTED="tauri://close-requested",t.WINDOW_CREATED="tauri://window-created",t.WINDOW_DESTROYED="tauri://destroyed",t.WINDOW_FOCUS="tauri://focus",t.WINDOW_BLUR="tauri://blur",t.WINDOW_SCALE_FACTOR_CHANGED="tauri://scale-change",t.WINDOW_THEME_CHANGED="tauri://theme-changed",t.WINDOW_FILE_DROP="tauri://file-drop",t.WINDOW_FILE_DROP_HOVER="tauri://file-drop-hover",t.WINDOW_FILE_DROP_CANCELLED="tauri://file-drop-cancelled",t.MENU="tauri://menu",t.CHECK_UPDATE="tauri://update",t.UPDATE_AVAILABLE="tauri://update-available",t.INSTALL_UPDATE="tauri://update-install",t.STATUS_UPDATE="tauri://update-status",t.DOWNLOAD_PROGRESS="tauri://update-download-progress"})(s||(s={}));async function f(t,e){return c(t,null,e)}var y;(function(t){t[t.Audio=1]="Audio",t[t.Cache=2]="Cache",t[t.Config=3]="Config",t[t.Data=4]="Data",t[t.LocalData=5]="LocalData",t[t.Desktop=6]="Desktop",t[t.Document=7]="Document",t[t.Download=8]="Download",t[t.Executable=9]="Executable",t[t.Font=10]="Font",t[t.Home=11]="Home",t[t.Picture=12]="Picture",t[t.Public=13]="Public",t[t.Runtime=14]="Runtime",t[t.Template=15]="Template",t[t.Video=16]="Video",t[t.Resource=17]="Resource",t[t.App=18]="App",t[t.Log=19]="Log",t[t.Temp=20]="Temp",t[t.AppConfig=21]="AppConfig",t[t.AppData=22]="AppData",t[t.AppLocalData=23]="AppLocalData",t[t.AppCache=24]="AppCache",t[t.AppLog=25]="AppLog"})(y||(y={}));var D;(function(t){t[t.JSON=1]="JSON",t[t.Text=2]="Text",t[t.Binary=3]="Binary"})(D||(D={}));function d(){return navigator.appVersion.includes("Win")}async function L(t){return n({__tauriModule:"Path",message:{cmd:"resolvePath",path:t,directory:y.Resource}})}var Pe=d()?"\\":"/",Ae=d()?";":":";var b=class{constructor(e,a){this.type="Logical",this.width=e,this.height=a}},m=class{constructor(e,a){this.type="Physical",this.width=e,this.height=a}toLogical(e){return new b(this.width/e,this.height/e)}},M=class{constructor(e,a){this.type="Logical",this.x=e,this.y=a}},p=class{constructor(e,a){this.type="Physical",this.x=e,this.y=a}toLogical(e){return new M(this.x/e,this.y/e)}},v;(function(t){t[t.Critical=1]="Critical",t[t.Informational=2]="Informational"})(v||(v={}));function T(){return window.__TAURI_METADATA__.__windows.map(t=>new h(t.label,{skip:!0}))}var O=["tauri://created","tauri://error"],P=class{constructor(e){this.label=e,this.listeners=Object.create(null)}async listen(e,a){return this._handleTauriEvent(e,a)?Promise.resolve(()=>{let i=this.listeners[e];i.splice(i.indexOf(a),1)}):c(e,this.label,a)}async once(e,a){return this._handleTauriEvent(e,a)?Promise.resolve(()=>{let i=this.listeners[e];i.splice(i.indexOf(a),1)}):w(e,this.label,a)}async emit(e,a){if(O.includes(e)){for(let i of this.listeners[e]||[])i({event:e,id:-1,windowLabel:this.label,payload:a});return Promise.resolve()}return g(e,this.label,a)}_handleTauriEvent(e,a){return O.includes(e)?(e in this.listeners?this.listeners[e].push(a):this.listeners[e]=[a],!0):!1}},A=class extends P{async scaleFactor(){return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"scaleFactor"}}}})}async innerPosition(){return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"innerPosition"}}}}).then(({x:e,y:a})=>new p(e,a))}async outerPosition(){return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"outerPosition"}}}}).then(({x:e,y:a})=>new p(e,a))}async innerSize(){return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"innerSize"}}}}).then(({width:e,height:a})=>new m(e,a))}async outerSize(){return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"outerSize"}}}}).then(({width:e,height:a})=>new m(e,a))}async isFullscreen(){return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"isFullscreen"}}}})}async isMinimized(){return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"isMinimized"}}}})}async isMaximized(){return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"isMaximized"}}}})}async isFocused(){return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"isFocused"}}}})}async isDecorated(){return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"isDecorated"}}}})}async isResizable(){return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"isResizable"}}}})}async isMaximizable(){return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"isMaximizable"}}}})}async isMinimizable(){return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"isMinimizable"}}}})}async isClosable(){return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"isClosable"}}}})}async isVisible(){return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"isVisible"}}}})}async title(){return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"title"}}}})}async theme(){return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"theme"}}}})}async center(){return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"center"}}}})}async requestUserAttention(e){let a=null;return e&&(e===v.Critical?a={type:"Critical"}:a={type:"Informational"}),n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"requestUserAttention",payload:a}}}})}async setResizable(e){return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"setResizable",payload:e}}}})}async setMaximizable(e){return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"setMaximizable",payload:e}}}})}async setMinimizable(e){return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"setMinimizable",payload:e}}}})}async setClosable(e){return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"setClosable",payload:e}}}})}async setTitle(e){return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"setTitle",payload:e}}}})}async maximize(){return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"maximize"}}}})}async unmaximize(){return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"unmaximize"}}}})}async toggleMaximize(){return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"toggleMaximize"}}}})}async minimize(){return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"minimize"}}}})}async unminimize(){return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"unminimize"}}}})}async show(){return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"show"}}}})}async hide(){return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"hide"}}}})}async close(){return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"close"}}}})}async setDecorations(e){return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"setDecorations",payload:e}}}})}async setAlwaysOnTop(e){return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"setAlwaysOnTop",payload:e}}}})}async setContentProtected(e){return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"setContentProtected",payload:e}}}})}async setSize(e){if(!e||e.type!=="Logical"&&e.type!=="Physical")throw new Error("the `size` argument must be either a LogicalSize or a PhysicalSize instance");return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"setSize",payload:{type:e.type,data:{width:e.width,height:e.height}}}}}})}async setMinSize(e){if(e&&e.type!=="Logical"&&e.type!=="Physical")throw new Error("the `size` argument must be either a LogicalSize or a PhysicalSize instance");return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"setMinSize",payload:e?{type:e.type,data:{width:e.width,height:e.height}}:null}}}})}async setMaxSize(e){if(e&&e.type!=="Logical"&&e.type!=="Physical")throw new Error("the `size` argument must be either a LogicalSize or a PhysicalSize instance");return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"setMaxSize",payload:e?{type:e.type,data:{width:e.width,height:e.height}}:null}}}})}async setPosition(e){if(!e||e.type!=="Logical"&&e.type!=="Physical")throw new Error("the `position` argument must be either a LogicalPosition or a PhysicalPosition instance");return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"setPosition",payload:{type:e.type,data:{x:e.x,y:e.y}}}}}})}async setFullscreen(e){return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"setFullscreen",payload:e}}}})}async setFocus(){return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"setFocus"}}}})}async setIcon(e){return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"setIcon",payload:{icon:typeof e=="string"?e:Array.from(e)}}}}})}async setSkipTaskbar(e){return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"setSkipTaskbar",payload:e}}}})}async setCursorGrab(e){return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"setCursorGrab",payload:e}}}})}async setCursorVisible(e){return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"setCursorVisible",payload:e}}}})}async setCursorIcon(e){return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"setCursorIcon",payload:e}}}})}async setCursorPosition(e){if(!e||e.type!=="Logical"&&e.type!=="Physical")throw new Error("the `position` argument must be either a LogicalPosition or a PhysicalPosition instance");return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"setCursorPosition",payload:{type:e.type,data:{x:e.x,y:e.y}}}}}})}async setIgnoreCursorEvents(e){return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"setIgnoreCursorEvents",payload:e}}}})}async startDragging(){return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"startDragging"}}}})}async onResized(e){return this.listen(s.WINDOW_RESIZED,a=>{a.payload=X(a.payload),e(a)})}async onMoved(e){return this.listen(s.WINDOW_MOVED,a=>{a.payload=Z(a.payload),e(a)})}async onCloseRequested(e){return this.listen(s.WINDOW_CLOSE_REQUESTED,a=>{let i=new x(a);Promise.resolve(e(i)).then(()=>{if(!i.isPreventDefault())return this.close()})})}async onFocusChanged(e){let a=await this.listen(s.WINDOW_FOCUS,r=>{e({...r,payload:!0})}),i=await this.listen(s.WINDOW_BLUR,r=>{e({...r,payload:!1})});return()=>{a(),i()}}async onScaleChanged(e){return this.listen(s.WINDOW_SCALE_FACTOR_CHANGED,e)}async onMenuClicked(e){return this.listen(s.MENU,e)}async onFileDropEvent(e){let a=await this.listen(s.WINDOW_FILE_DROP,o=>{e({...o,payload:{type:"drop",paths:o.payload}})}),i=await this.listen(s.WINDOW_FILE_DROP_HOVER,o=>{e({...o,payload:{type:"hover",paths:o.payload}})}),r=await this.listen(s.WINDOW_FILE_DROP_CANCELLED,o=>{e({...o,payload:{type:"cancel"}})});return()=>{a(),i(),r()}}async onThemeChanged(e){return this.listen(s.WINDOW_THEME_CHANGED,e)}},x=class{constructor(e){this._preventDefault=!1,this.event=e.event,this.windowLabel=e.windowLabel,this.id=e.id}preventDefault(){this._preventDefault=!0}isPreventDefault(){return this._preventDefault}},h=class t extends A{constructor(e,a={}){super(e),a?.skip||n({__tauriModule:"Window",message:{cmd:"createWebview",data:{options:{label:e,...a}}}}).then(async()=>this.emit("tauri://created")).catch(async i=>this.emit("tauri://error",i))}static getByLabel(e){return T().some(a=>a.label===e)?new t(e,{skip:!0}):null}static async getFocusedWindow(){for(let e of T())if(await e.isFocused())return e;return null}},S;"__TAURI_METADATA__"in window?S=new h(window.__TAURI_METADATA__.__currentWindow.label,{skip:!0}):(console.warn(`Could not find "window.__TAURI_METADATA__". The "appWindow" value will reference the "main" window label.
2 | Note that this is not an issue if running this frontend on a browser instead of a Tauri window.`),S=new h("main",{skip:!0}));function Z(t){return new p(t.x,t.y)}function X(t){return new m(t.width,t.height)}var Ie=d()?`\r
3 | `:`
4 | `;var F=_;var ae="plugin:context_menu|show_context_menu";async function Re(t){return await L(t)}async function k(t,e){let a=[],i=[...t.map(r=>({...r}))];for(let r=0;r{let N={...I,payload:t[r].payload};o(N)})),i[r].event=l,i[r].payload=void 0}if(t[r].subitems){let l=await k(t[r].subitems,`${e}_${r}`);a.push(...l.unlisteners),i[r].subitems=l.processed}}return{unlisteners:a,processed:i}}async function ne(t){let{unlisteners:e,processed:a}=await k(t.items,"root"),i=await f("menu-did-close",()=>{e.forEach(r=>r()),e.length=0,i()});F(ae,{...t,items:a})}function ze(t,e){window.addEventListener(t,async a=>{a.preventDefault(),typeof e=="function"&&(e=await e(a)),await ne(e)})}export{Re as assetToPath,ze as onEventShowMenu,ne as showMenu};
5 | //# sourceMappingURL=index.js.map
6 |
--------------------------------------------------------------------------------
/plugin/dist/index.spec.d.ts:
--------------------------------------------------------------------------------
1 | export {};
2 |
--------------------------------------------------------------------------------
/plugin/dist/types.d.ts:
--------------------------------------------------------------------------------
1 | import type { Event, UnlistenFn } from "@tauri-apps/api/event";
2 | export interface Position {
3 | x: number;
4 | y: number;
5 | is_absolute?: boolean;
6 | }
7 | export interface Icon {
8 | path: string;
9 | width?: number;
10 | height?: number;
11 | }
12 | export interface CallbackEvent extends Event {
13 | payload: any;
14 | }
15 | export interface Item {
16 | label?: string;
17 | disabled?: boolean;
18 | is_separator?: boolean;
19 | event?: string | ((e?: CallbackEvent) => any);
20 | payload?: any;
21 | checked?: boolean;
22 | shortcut?: string;
23 | icon?: Icon;
24 | subitems?: Item[];
25 | }
26 | export type Theme = 'light' | 'dark';
27 | export interface Options {
28 | pos?: Position;
29 | theme?: Theme;
30 | items: Item[];
31 | }
32 | export interface ProcessResult {
33 | unlisteners: UnlistenFn[];
34 | processed: Item[];
35 | }
36 | export type EventOptionsFunction = (e?: MouseEvent) => Options | Promise;
37 | export type EventOptions = Options | EventOptionsFunction;
38 |
--------------------------------------------------------------------------------
/plugin/esbuild.js:
--------------------------------------------------------------------------------
1 | import esbuild from 'esbuild';
2 |
3 | esbuild.build({
4 | entryPoints: ['./index.ts'],
5 | outdir: './dist',
6 | bundle: true,
7 | format: 'esm',
8 | splitting: true,
9 | chunkNames: '[name]',
10 | minify: true,
11 | loader: {
12 | '.ts': 'ts'
13 | },
14 | tsconfig: './tsconfig.json',
15 | platform: 'node',
16 | sourcemap: true
17 | }).catch(() => process.exit(1));
18 |
--------------------------------------------------------------------------------
/plugin/index.spec.ts:
--------------------------------------------------------------------------------
1 | import * as tauriApi from '@tauri-apps/api';
2 | import * as tauriEvent from '@tauri-apps/api/event';
3 | import * as tauriApiPath from '@tauri-apps/api/path';
4 | import { assetToPath, showMenu, onEventShowMenu, ContextMenu } from './index';
5 |
6 | jest.mock('@tauri-apps/api', () => ({
7 | invoke: jest.fn()
8 | }));
9 |
10 | jest.mock('@tauri-apps/api/event', () => ({
11 | listen: jest.fn()
12 | }));
13 |
14 | jest.mock('@tauri-apps/api/path', () => ({
15 | resolveResource: jest.fn()
16 | }));
17 |
18 | describe('assetToPath', () => {
19 | it('calls tauriApiPath.resolveResource', async () => {
20 | const asset = 'testAsset';
21 | await assetToPath(asset);
22 | expect(tauriApiPath.resolveResource).toHaveBeenCalledWith(asset);
23 | });
24 | });
25 |
26 | describe('showMenu', () => {
27 | it('sets up event listeners for item events', async () => {
28 | const items = [
29 | { event: jest.fn() },
30 | {
31 | event: jest.fn(),
32 | subitems: [
33 | { event: jest.fn() }
34 | ]
35 | }
36 | ];
37 | await showMenu({ items });
38 | expect(tauriEvent.listen).toHaveBeenCalledTimes(4); // events + menu-did-close
39 | });
40 |
41 | it('invokes tauriApi with the SHOW_COMMAND', () => {
42 | showMenu({ items: [] });
43 | expect(tauriApi.invoke).toHaveBeenCalledWith(expect.stringMatching('plugin:context_menu|show_context_menu'), expect.any(Object));
44 | });
45 | });
46 |
47 | describe('onEventShowMenu', () => {
48 | it('sets up a window event listener', () => {
49 | const addEventListenerSpy = jest.spyOn(window, 'addEventListener');
50 | onEventShowMenu('testEvent', {} as ContextMenu.Options);
51 | expect(addEventListenerSpy).toHaveBeenCalledWith('testEvent', expect.any(Function));
52 | addEventListenerSpy.mockRestore();
53 | });
54 | });
--------------------------------------------------------------------------------
/plugin/index.ts:
--------------------------------------------------------------------------------
1 | import * as tauriApi from '@tauri-apps/api';
2 | import * as tauriEvent from '@tauri-apps/api/event';
3 | import * as tauriApiPath from '@tauri-apps/api/path';
4 |
5 | const SHOW_COMMAND = 'plugin:context_menu|show_context_menu';
6 |
7 | import * as ContextMenu from './types';
8 | export type { ContextMenu };
9 |
10 | export async function assetToPath(asset: string): Promise {
11 | return await tauriApiPath.resolveResource(asset);
12 | }
13 |
14 | // for each item, if it is a function, replace it with an event listener
15 | async function processItems(items: ContextMenu.Item[], prefix: string): Promise {
16 | const unlisteners: tauriEvent.UnlistenFn[] = [];
17 |
18 | // Copy the items array so we don't mutate the original
19 | // (needed if called multiple times)
20 | const processed:ContextMenu.Item[] = [ ...items.map((item) => ({ ...item })) ];
21 |
22 | for (let i = 0; i < processed.length; i++) {
23 | const itemEvent = processed[i].event;
24 |
25 | if (typeof itemEvent === 'function') {
26 | const eventName = `${prefix}_context_menu_item_${i}`;
27 |
28 | // Listen to the event and call the function directly
29 | unlisteners.push(await tauriEvent.listen(eventName, (e) => {
30 | const data:ContextMenu.CallbackEvent = { ...e, payload: items[i].payload };
31 | itemEvent(data);
32 | }));
33 |
34 | // Set the event name on the item instead of the function
35 | processed[i].event = eventName;
36 |
37 | // Remove the payload from the item so it doesn't get sent to the plugin
38 | // (it's already been sent to the event listener)
39 | processed[i].payload = undefined;
40 | }
41 |
42 | // Recurse into subitems if they exist
43 | if (items[i].subitems) {
44 | const result = await processItems(items[i].subitems as ContextMenu.Item[], `${prefix}_${i}`);
45 | unlisteners.push(...result.unlisteners);
46 | processed[i].subitems = result.processed;
47 | }
48 | }
49 |
50 | return { unlisteners, processed };
51 | }
52 |
53 | export async function showMenu(options: ContextMenu.Options) {
54 | const { unlisteners, processed } = await processItems(options.items, 'root');
55 |
56 | // unlisten all events when the menu closes
57 | const unlistenMenuClose = await tauriEvent.listen("menu-did-close", () => {
58 | unlisteners.forEach((unlistener) => unlistener());
59 | unlisteners.length = 0;
60 | unlistenMenuClose();
61 | });
62 |
63 | // send the options to the plugin
64 | tauriApi.invoke(SHOW_COMMAND, { ...options, items: processed } as any);
65 | }
66 |
67 | export function onEventShowMenu(eventName: string, options: ContextMenu.EventOptions): void {
68 | window.addEventListener(eventName, async (e) => {
69 | e.preventDefault();
70 |
71 | // if options is a function, call it to get the options
72 | if (typeof options === 'function') {
73 | options = await options(e as MouseEvent);
74 | }
75 |
76 | await showMenu(options);
77 | });
78 | }
--------------------------------------------------------------------------------
/plugin/jest.config.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | preset: 'ts-jest',
3 | testEnvironment: 'jsdom',
4 | transform: {
5 | '^.+\\.tsx?$': 'ts-jest',
6 | },
7 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
8 | };
9 |
--------------------------------------------------------------------------------
/plugin/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tauri-plugin-context-menu",
3 | "version": "0.8.2",
4 | "author": "c2r0b",
5 | "type": "module",
6 | "description": "",
7 | "browser": "dist/index.js",
8 | "main": "dist/index.js",
9 | "types": "dist/index.d.ts",
10 | "homepage": "https://github.com/c2r0b/tauri-plugin-context-menu",
11 | "scripts": {
12 | "prebuild": "jest",
13 | "build": "node esbuild.js",
14 | "postbuild": "tsc --emitDeclarationOnly",
15 | "pretest": "npm run build"
16 | },
17 | "repository": {
18 | "type": "git",
19 | "url": "git+https://github.com/c2r0b/tauri-plugin-context-menu.git"
20 | },
21 | "bugs": {
22 | "url": "https://github.com/c2r0b/tauri-plugin-context-menu/issues"
23 | },
24 | "files": [
25 | "dist"
26 | ],
27 | "dependencies": {
28 | "@tauri-apps/api": "^1.5.0"
29 | },
30 | "devDependencies": {
31 | "@types/jest": "^29.5.5",
32 | "esbuild": "^0.19.4",
33 | "jest": "^29.7.0",
34 | "jest-environment-jsdom": "^29.7.0",
35 | "lerna": "^7.3.0",
36 | "ts-jest": "^29.1.1",
37 | "tslib": "^2.6.2",
38 | "typescript": "^5.2.2"
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/plugin/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES5",
4 | "strict": true,
5 | "allowJs": true,
6 | "esModuleInterop": true,
7 | "baseUrl": ".",
8 | "paths": {
9 | "types": ["@types"]
10 | },
11 | "declaration": true,
12 | "declarationDir": "dist",
13 | "lib": ["ES5", "DOM", "ES2015"]
14 | },
15 | "include": [
16 | "./**/*.ts",
17 | "./**/*.d.ts"
18 | ]
19 | }
--------------------------------------------------------------------------------
/plugin/types.ts:
--------------------------------------------------------------------------------
1 | import type { Event, UnlistenFn } from "@tauri-apps/api/event"
2 |
3 | export interface Position {
4 | x: number
5 | y: number
6 | is_absolute?: boolean
7 | }
8 |
9 | export interface Icon {
10 | path: string
11 | width?: number
12 | height?: number
13 | }
14 |
15 | export interface CallbackEvent extends Event {
16 | payload: any
17 | }
18 |
19 | export interface Item {
20 | label?: string
21 | disabled?: boolean
22 | is_separator?: boolean
23 | event?: string|((e?:CallbackEvent) => any)
24 | payload?: any
25 | checked?: boolean
26 | shortcut?: string
27 | icon?: Icon
28 | subitems?: Item[]
29 | }
30 |
31 | export type Theme = 'light' | 'dark'
32 |
33 | export interface Options {
34 | pos?: Position
35 | theme?: Theme
36 | items: Item[]
37 | }
38 |
39 | export interface ProcessResult {
40 | unlisteners: UnlistenFn[]
41 | processed: Item[]
42 | }
43 |
44 | export type EventOptionsFunction = (e?: MouseEvent) => Options | Promise;
45 |
46 | export type EventOptions = Options | EventOptionsFunction;
--------------------------------------------------------------------------------
/src/keymap.rs:
--------------------------------------------------------------------------------
1 | use std::collections::HashMap;
2 |
3 | #[cfg(target_os = "linux")]
4 | use gdk::keys::constants;
5 |
6 | #[cfg(target_os = "windows")]
7 | pub fn get_key_map() -> HashMap<&'static str, &'static str> {
8 | let mut key_map = HashMap::new();
9 | key_map.insert("cmd", "Ctrl"); // Alias for "ctrl"
10 | key_map.insert("cmd_or_ctrl", "Ctrl"); // Alias for "ctrl"
11 | key_map.insert("shift", "Shift");
12 | key_map.insert("alt", "Alt");
13 | key_map.insert("ctrl", "Ctrl");
14 | key_map.insert("opt", "Alt"); // Alias for "alt"
15 | key_map.insert("altgr", "AltGr");
16 | key_map.insert("super", "Super");
17 | key_map.insert("win", "Win");
18 | key_map.insert("meta", "Meta");
19 | key_map.insert("plus", "Plus");
20 | key_map.insert("space", "Space");
21 | key_map.insert("tab", "Tab");
22 | key_map.insert("capslock", "CapsLock");
23 | key_map.insert("numlock", "NumLock");
24 | key_map.insert("scrolllock", "ScrollLock");
25 | key_map.insert("backspace", "Backspace");
26 | key_map.insert("delete", "Delete");
27 | key_map.insert("insert", "Insert");
28 | key_map.insert("return", "Return");
29 | key_map.insert("enter", "Return");
30 | key_map.insert("up", "UpArrow");
31 | key_map.insert("down", "DownArrow");
32 | key_map.insert("left", "LeftArrow");
33 | key_map.insert("right", "RightArrow");
34 | key_map.insert("home", "Home");
35 | key_map.insert("end", "End");
36 | key_map.insert("pageup", "PageUp");
37 | key_map.insert("pagedown", "PageDown");
38 | key_map.insert("escape", "Escape");
39 | key_map.insert("esc", "Escape");
40 | key_map.insert("num0", "Numpad0");
41 | key_map.insert("num1", "Numpad1");
42 | key_map.insert("num2", "Numpad2");
43 | key_map.insert("num3", "Numpad3");
44 | key_map.insert("num4", "Numpad4");
45 | key_map.insert("num5", "Numpad5");
46 | key_map.insert("num6", "Numpad6");
47 | key_map.insert("num7", "Numpad7");
48 | key_map.insert("num8", "Numpad8");
49 | key_map.insert("num9", "Numpad9");
50 | key_map.insert("numdec", "NumpadDecimal");
51 | key_map.insert("numadd", "NumpadAdd");
52 | key_map.insert("numsub", "NumpadSubtract");
53 | key_map.insert("nummult", "NumpadMultiply");
54 | key_map.insert("numdiv", "NumpadDivide");
55 | key_map.insert("f1", "F1");
56 | key_map.insert("f2", "F2");
57 | key_map.insert("f3", "F3");
58 | key_map.insert("f4", "F4");
59 | key_map.insert("f5", "F5");
60 | key_map.insert("f6", "F6");
61 | key_map.insert("f7", "F7");
62 | key_map.insert("f8", "F8");
63 | key_map.insert("f9", "F9");
64 | key_map.insert("f10", "F10");
65 | key_map.insert("f11", "F11");
66 | key_map.insert("f12", "F12");
67 | key_map.insert("f13", "F13");
68 | key_map.insert("f14", "F14");
69 | key_map.insert("f15", "F15");
70 | key_map.insert("f16", "F16");
71 | key_map.insert("f17", "F17");
72 | key_map.insert("f18", "F18");
73 | key_map.insert("f19", "F19");
74 | key_map.insert("f20", "F20");
75 | key_map.insert("f21", "F21");
76 | key_map.insert("f22", "F22");
77 | key_map.insert("f23", "F23");
78 | key_map.insert("f24", "F24");
79 |
80 | key_map
81 | }
82 |
83 | #[cfg(target_os = "macos")]
84 | pub fn get_key_map() -> HashMap<&'static str, &'static str> {
85 | let mut key_map = HashMap::new();
86 | key_map.insert("plus", "+");
87 | key_map.insert("space", " ");
88 | key_map.insert("tab", "\u{21e5}");
89 | key_map.insert("capslock", "\u{1000}");
90 | key_map.insert("numlock", "\u{1001}");
91 | key_map.insert("scrolllock", "\u{1002}");
92 | key_map.insert("backspace", "\u{232b}");
93 | key_map.insert("delete", "\u{2326}");
94 | key_map.insert("insert", "\u{2380}");
95 | key_map.insert("return", "\u{23ce}");
96 | key_map.insert("enter", "\u{23ce}");
97 | key_map.insert("up", "\u{2191}");
98 | key_map.insert("down", "\u{2193}");
99 | key_map.insert("left", "\u{2190}");
100 | key_map.insert("right", "\u{2192}");
101 | key_map.insert("home", "\u{2196}");
102 | key_map.insert("end", "\u{2198}");
103 | key_map.insert("pageup", "\u{21DE}");
104 | key_map.insert("pagedown", "\u{21DF}");
105 | key_map.insert("escape", "\u{238b}");
106 | key_map.insert("esc", "\u{238b}");
107 | key_map.insert("num0", "\u{30}");
108 | key_map.insert("num1", "\u{31}");
109 | key_map.insert("num2", "\u{32}");
110 | key_map.insert("num3", "\u{33}");
111 | key_map.insert("num4", "\u{34}");
112 | key_map.insert("num5", "\u{35}");
113 | key_map.insert("num6", "\u{36}");
114 | key_map.insert("num7", "\u{37}");
115 | key_map.insert("num8", "\u{38}");
116 | key_map.insert("num9", "\u{39}");
117 | key_map.insert("numdec", "\u{2e}");
118 | key_map.insert("numadd", "\u{2b}");
119 | key_map.insert("numsub", "\u{2d}");
120 | key_map.insert("nummult", "\u{2a}");
121 | key_map.insert("numdiv", "\u{2f}");
122 | key_map.insert("f1", "\u{F704}");
123 | key_map.insert("f2", "\u{F705}");
124 | key_map.insert("f3", "\u{F706}");
125 | key_map.insert("f4", "\u{F707}");
126 | key_map.insert("f5", "\u{F708}");
127 | key_map.insert("f6", "\u{F709}");
128 | key_map.insert("f7", "\u{F70A}");
129 | key_map.insert("f8", "\u{F70B}");
130 | key_map.insert("f9", "\u{F70C}");
131 | key_map.insert("f10", "\u{F70D}");
132 | key_map.insert("f11", "\u{F70E}");
133 | key_map.insert("f12", "\u{F70F}");
134 | key_map.insert("f13", "\u{F710}");
135 | key_map.insert("f14", "\u{F711}");
136 | key_map.insert("f15", "\u{F712}");
137 | key_map.insert("f16", "\u{F713}");
138 | key_map.insert("f17", "\u{F714}");
139 | key_map.insert("f18", "\u{F715}");
140 | key_map.insert("f19", "\u{F716}");
141 | key_map.insert("f20", "\u{F717}");
142 | key_map.insert("f21", "\u{F718}");
143 | key_map.insert("f22", "\u{F719}");
144 | key_map.insert("f23", "\u{F71A}");
145 | key_map.insert("f24", "\u{F71B}");
146 |
147 | key_map
148 | }
149 |
150 | #[cfg(target_os = "macos")]
151 | pub fn get_modifier_map() -> HashMap<&'static str, cocoa::appkit::NSEventModifierFlags> {
152 | let mut mod_map = HashMap::new();
153 | mod_map.insert("cmd", cocoa::appkit::NSEventModifierFlags::NSCommandKeyMask);
154 | mod_map.insert(
155 | "cmd_or_ctrl",
156 | cocoa::appkit::NSEventModifierFlags::NSCommandKeyMask,
157 | ); // Alias for "cmd"
158 | mod_map.insert("shift", cocoa::appkit::NSEventModifierFlags::NSShiftKeyMask);
159 | mod_map.insert(
160 | "alt",
161 | cocoa::appkit::NSEventModifierFlags::NSAlternateKeyMask,
162 | );
163 | mod_map.insert(
164 | "ctrl",
165 | cocoa::appkit::NSEventModifierFlags::NSControlKeyMask,
166 | );
167 | mod_map.insert(
168 | "opt",
169 | cocoa::appkit::NSEventModifierFlags::NSAlternateKeyMask,
170 | ); // Alias for "alt"
171 | mod_map.insert(
172 | "altgr",
173 | cocoa::appkit::NSEventModifierFlags::NSAlternateKeyMask,
174 | ); // Alias for "alt"
175 | mod_map.insert(
176 | "super",
177 | cocoa::appkit::NSEventModifierFlags::NSCommandKeyMask,
178 | ); // Alias for "cmd"
179 | mod_map.insert("win", cocoa::appkit::NSEventModifierFlags::NSCommandKeyMask); // Alias for "cmd"
180 | mod_map.insert(
181 | "meta",
182 | cocoa::appkit::NSEventModifierFlags::NSCommandKeyMask,
183 | );
184 | mod_map
185 | }
186 |
187 | #[cfg(target_os = "linux")]
188 | pub fn get_key_map() -> HashMap<&'static str, gdk::keys::Key> {
189 | let mut key_map = HashMap::new();
190 | key_map.insert("cmd", constants::Control_L); // Alias for "ctrl"
191 | key_map.insert("cmd_or_ctrl", constants::Control_L); // Alias for "ctrl"
192 | key_map.insert("shift", constants::Shift_L);
193 | key_map.insert("alt", constants::Alt_L);
194 | key_map.insert("ctrl", constants::Control_L);
195 | key_map.insert("opt", constants::Alt_L); // Alias for "alt"
196 | key_map.insert("altgr", constants::Alt_R);
197 | key_map.insert("super", constants::Super_L);
198 | key_map.insert("win", constants::Super_L);
199 | key_map.insert("meta", constants::Super_L);
200 | key_map.insert("plus", constants::plus);
201 | key_map.insert("space", constants::space);
202 | key_map.insert("tab", constants::Tab);
203 | key_map.insert("capslock", constants::Caps_Lock);
204 | key_map.insert("numlock", constants::Num_Lock);
205 | key_map.insert("scrolllock", constants::Scroll_Lock);
206 | key_map.insert("backspace", constants::BackSpace);
207 | key_map.insert("delete", constants::Delete);
208 | key_map.insert("insert", constants::Insert);
209 | key_map.insert("return", constants::Return);
210 | key_map.insert("enter", constants::Return);
211 | key_map.insert("up", constants::Up);
212 | key_map.insert("down", constants::Down);
213 | key_map.insert("left", constants::Left);
214 | key_map.insert("right", constants::Right);
215 | key_map.insert("home", constants::Home);
216 | key_map.insert("end", constants::End);
217 | key_map.insert("pageup", constants::Page_Up);
218 | key_map.insert("pagedown", constants::Page_Down);
219 | key_map.insert("escape", constants::Escape);
220 | key_map.insert("esc", constants::Escape);
221 | key_map.insert("num0", constants::KP_0);
222 | key_map.insert("num1", constants::KP_1);
223 | key_map.insert("num2", constants::KP_2);
224 | key_map.insert("num3", constants::KP_3);
225 | key_map.insert("num4", constants::KP_4);
226 | key_map.insert("num5", constants::KP_5);
227 | key_map.insert("num6", constants::KP_6);
228 | key_map.insert("num7", constants::KP_7);
229 | key_map.insert("num8", constants::KP_8);
230 | key_map.insert("num9", constants::KP_9);
231 | key_map.insert("numdec", constants::KP_Decimal);
232 | key_map.insert("numadd", constants::KP_Add);
233 | key_map.insert("numsub", constants::KP_Subtract);
234 | key_map.insert("nummult", constants::KP_Multiply);
235 | key_map.insert("numdiv", constants::KP_Divide);
236 | key_map.insert("f1", constants::F1);
237 | key_map.insert("f2", constants::F2);
238 | key_map.insert("f3", constants::F3);
239 | key_map.insert("f4", constants::F4);
240 | key_map.insert("f5", constants::F5);
241 | key_map.insert("f6", constants::F6);
242 | key_map.insert("f7", constants::F7);
243 | key_map.insert("f8", constants::F8);
244 | key_map.insert("f9", constants::F9);
245 | key_map.insert("f10", constants::F10);
246 | key_map.insert("f11", constants::F11);
247 | key_map.insert("f12", constants::F12);
248 | key_map.insert("f13", constants::F13);
249 | key_map.insert("f14", constants::F14);
250 | key_map.insert("f15", constants::F15);
251 | key_map.insert("f16", constants::F16);
252 | key_map.insert("f17", constants::F17);
253 | key_map.insert("f18", constants::F18);
254 | key_map.insert("f19", constants::F19);
255 | key_map.insert("f20", constants::F20);
256 | key_map.insert("f21", constants::F21);
257 | key_map.insert("f22", constants::F22);
258 | key_map.insert("f23", constants::F23);
259 | key_map.insert("f24", constants::F24);
260 |
261 | key_map
262 | }
263 |
264 | #[cfg(target_os = "linux")]
265 | pub fn get_mod_map() -> HashMap<&'static str, gdk::ModifierType> {
266 | use gdk::ModifierType;
267 |
268 | let mut mod_map = HashMap::new();
269 | mod_map.insert("cmd", ModifierType::CONTROL_MASK); // Alias for "ctrl"
270 | mod_map.insert("cmd_or_ctrl", ModifierType::CONTROL_MASK); // Alias for "ctrl"
271 | mod_map.insert("shift", ModifierType::SHIFT_MASK);
272 | mod_map.insert("alt", ModifierType::MOD1_MASK);
273 | mod_map.insert("ctrl", ModifierType::CONTROL_MASK);
274 | mod_map.insert("opt", ModifierType::MOD1_MASK); // Alias for "alt"
275 | mod_map.insert("altgr", ModifierType::MOD5_MASK);
276 | mod_map.insert("super", ModifierType::SUPER_MASK);
277 | mod_map.insert("win", ModifierType::SUPER_MASK);
278 | mod_map.insert("meta", ModifierType::META_MASK);
279 |
280 | mod_map
281 | }
282 |
--------------------------------------------------------------------------------
/src/lib.rs:
--------------------------------------------------------------------------------
1 | use serde::Deserialize;
2 | use tauri::{plugin::Builder, plugin::TauriPlugin, Runtime, Window};
3 |
4 | mod keymap;
5 | mod menu_item;
6 | mod theme;
7 |
8 | use menu_item::MenuItem;
9 | use theme::Theme;
10 |
11 | #[cfg(target_os = "windows")]
12 | mod win_image_handler;
13 |
14 | #[cfg(target_os = "windows")]
15 | #[path = "win.rs"]
16 | mod os;
17 |
18 | #[cfg(target_os = "macos")]
19 | mod macos_window_holder;
20 |
21 | #[cfg(target_os = "macos")]
22 | #[path = "macos.rs"]
23 | mod os;
24 |
25 | #[cfg(target_os = "linux")]
26 | #[path = "linux.rs"]
27 | mod os;
28 |
29 | #[derive(Clone, Deserialize)]
30 | pub struct Position {
31 | x: f64,
32 | y: f64,
33 | is_absolute: Option,
34 | }
35 |
36 | #[tauri::command]
37 | fn show_context_menu(
38 | window: Window,
39 | pos: Option,
40 | items: Option>,
41 | theme: Option,
42 | ) {
43 | let theme = theme.and_then(|s| Theme::from_str(&s));
44 | os::show_context_menu(window, pos, items, theme);
45 | }
46 | pub fn init() -> TauriPlugin {
47 | Builder::new("context_menu")
48 | .invoke_handler(tauri::generate_handler![show_context_menu])
49 | .build()
50 | }
51 |
--------------------------------------------------------------------------------
/src/linux.rs:
--------------------------------------------------------------------------------
1 | use gdk::{keys::Key, Display, ModifierType};
2 | use gtk::{prelude::*, traits::WidgetExt, AccelFlags, AccelGroup, Menu};
3 | use std::{env, mem, thread::sleep, time};
4 | use tauri::{Runtime, Window};
5 |
6 | use crate::keymap::{get_key_map, get_mod_map};
7 | use crate::theme::Theme;
8 | use crate::{MenuItem, Position};
9 |
10 | pub fn on_context_menu(
11 | pos: Option,
12 | items: Option>,
13 | window: Window,
14 | ) {
15 | // Create and show the context menu
16 | let gtk_window = window.gtk_window().unwrap();
17 |
18 | // Check if the window is realized
19 | if !gtk_window.is_realized() {
20 | gtk_window.realize();
21 | }
22 |
23 | // Create a new menu.
24 | let menu = Menu::new();
25 | if let Some(menu_items) = items {
26 | for item in menu_items.iter() {
27 | append_menu_item(&window, >k_window, &menu, item);
28 | }
29 | }
30 |
31 | let (mut x, mut y) = match pos {
32 | Some(ref position) => (position.x as i32, position.y as i32),
33 | None => {
34 | if let Some(display) = Display::default() {
35 | if let Some(seat) = display.default_seat() {
36 | let pointer = seat.pointer();
37 | let (_screen, x, y) = match pointer {
38 | Some(p) => p.position(),
39 | None => {
40 | eprintln!("Failed to get pointer position");
41 | (display.default_screen(), 0, 0)
42 | }
43 | };
44 | (x, y)
45 | } else {
46 | eprintln!("Failed to get default seat");
47 | (0, 0)
48 | }
49 | } else {
50 | eprintln!("Failed to get default display");
51 | (0, 0)
52 | }
53 | }
54 | };
55 |
56 | // Adjust for X11 backend
57 | let is_x11 = env::var("GDK_BACKEND").map_or(false, |v| v == "x11");
58 |
59 | if is_x11 {
60 | // Get the display and the default seat
61 | let display = gdk::Display::default().expect("Failed to get default display");
62 | let gdk_window = gtk_window.window().unwrap();
63 |
64 | // Identify the monitor where the window is displayed
65 | let monitor = display
66 | .monitor_at_window(&gdk_window)
67 | .expect("Failed to get monitor at window");
68 |
69 | // Get the scale factor for the monitor
70 | let scale_factor = monitor.scale_factor();
71 |
72 | // Get the geometry of the monitor
73 | let monitor_geometry = monitor.geometry();
74 |
75 | // Adjust `x` and `y` based on the monitor's geometry and scale factor
76 | x = (x + monitor_geometry.x()) * scale_factor;
77 | y = (y + monitor_geometry.y()) * scale_factor;
78 | }
79 |
80 | let is_absolute = if let Some(position) = pos.clone() {
81 | position.is_absolute
82 | } else {
83 | Some(false)
84 | };
85 | if is_absolute.unwrap_or(false) || pos.is_none() {
86 | // Adjust x and y if the coordinates are not relative to the window
87 | let window_position = window.outer_position().unwrap();
88 | x -= window_position.x;
89 | y -= window_position.y;
90 | }
91 |
92 | // Required otherwise the menu doesn't show properly
93 | sleep(time::Duration::from_millis(100));
94 |
95 | // Delay the display of the context menu to ensure the window is ready
96 | glib::idle_add_local(move || {
97 | // Show the context menu at the specified position.
98 | let gdk_window = gtk_window.window().unwrap();
99 | let rect = &gdk::Rectangle::new(x, y, 0, 0);
100 | let mut event = gdk::Event::new(gdk::EventType::ButtonPress);
101 | event.set_device(
102 | gdk_window
103 | .display()
104 | .default_seat()
105 | .and_then(|d| d.pointer())
106 | .as_ref(),
107 | );
108 | menu.show_all();
109 | menu.popup_at_rect(
110 | &gdk_window,
111 | rect,
112 | gdk::Gravity::NorthWest,
113 | gdk::Gravity::NorthWest,
114 | Some(&event),
115 | );
116 | Continue(false)
117 | });
118 | }
119 |
120 | pub fn show_context_menu(
121 | window: Window,
122 | pos: Option,
123 | items: Option>,
124 | _theme: Option,
125 | ) {
126 | on_context_menu(pos, items, window);
127 | }
128 |
129 | fn append_menu_item(
130 | window: &Window,
131 | gtk_window: >k::ApplicationWindow,
132 | menu: &Menu,
133 | item: &MenuItem,
134 | ) {
135 | if item.is_separator.unwrap_or(false) {
136 | menu.append(>k::SeparatorMenuItem::builder().visible(true).build());
137 | } else {
138 | let menu_item = match item.checked {
139 | Some(state) => {
140 | // Create a CheckMenuItem for checkable items
141 | let check_menu_item = gtk::CheckMenuItem::new();
142 | check_menu_item.set_active(state);
143 | check_menu_item.upcast()
144 | }
145 | None => {
146 | // Create a regular MenuItem for non-checkable items
147 | gtk::MenuItem::new()
148 | }
149 | };
150 |
151 | // Create a Box to hold the image and label
152 | let hbox = gtk::Box::new(gtk::Orientation::Horizontal, 0);
153 | hbox.set_homogeneous(false);
154 |
155 | // Handle icon
156 | if let Some(icon) = &item.icon {
157 | let image = gtk::Image::from_file(&icon.path);
158 | if let Some(width) = icon.width {
159 | if let Some(height) = icon.height {
160 | image.set_pixel_size(width as i32);
161 | image.set_pixel_size(height as i32);
162 | }
163 | }
164 | hbox.pack_start(&image, false, false, 0);
165 | }
166 |
167 | // Add label to the Box
168 | let label = item.label.as_deref().unwrap_or("");
169 | let accel_label = gtk::AccelLabel::new(label);
170 | accel_label.set_xalign(0.0); // Align the label to the left
171 | hbox.pack_start(&accel_label, true, true, 0);
172 |
173 | // Add the Box to the MenuItem
174 | menu_item.add(&hbox);
175 |
176 | // Handle enabled/disabled state
177 | if item.disabled.unwrap_or(false) {
178 | menu_item.set_sensitive(false);
179 | }
180 |
181 | // If an event is provided, you can connect to the "activate" signal (from item.event and item.payload)
182 | if let Some(event) = &item.event {
183 | let window_clone = window.clone();
184 |
185 | // payload may exist
186 | let payload_clone = item.payload.clone();
187 |
188 | // get event from String to str
189 | let event_clone = event.clone();
190 | menu_item.connect_activate(move |_| {
191 | window_clone
192 | .emit(event_clone.as_str(), &payload_clone)
193 | .unwrap(); // Emit the event to JavaScript
194 | });
195 | }
196 |
197 | // Handle shortcut
198 | if let Some(shortcut) = &item.shortcut {
199 | let accel_group = AccelGroup::new();
200 | gtk_window.add_accel_group(&accel_group);
201 |
202 | // Parse and assign the shortcut
203 | let (key, mods) = parse_shortcut(shortcut);
204 | accel_label.set_accel_widget(Some(&menu_item));
205 | menu_item.add_accelerator("activate", &accel_group, key, mods, AccelFlags::VISIBLE);
206 | }
207 |
208 | if let Some(subitems) = &item.subitems {
209 | let submenu = Menu::new();
210 | for subitem in subitems.iter() {
211 | append_menu_item(window, gtk_window, &submenu, subitem);
212 | }
213 | menu_item.set_submenu(Some(&submenu));
214 | }
215 |
216 | menu.append(&menu_item);
217 | }
218 | }
219 |
220 | fn key_to_u32(key: gdk::keys::Key) -> u32 {
221 | unsafe { mem::transmute(key) }
222 | }
223 |
224 | fn parse_shortcut(shortcut: &str) -> (u32, ModifierType) {
225 | let key_map = get_key_map();
226 | let mod_map = get_mod_map(); // This should map strings like "ctrl" to ModifierType
227 | let parts: Vec<&str> = shortcut.split('+').collect();
228 |
229 | // Assuming last part is always the key
230 | let key_str = parts.last().unwrap_or(&"");
231 |
232 | // Get the key from the key map
233 | let key = if let Some(key) = key_map.get(key_str) {
234 | // Clone the key value to get ownership of it
235 | key.clone()
236 | } else {
237 | // If the key is not in the map, assume it's a character
238 | Key::from_name(key_str)
239 | };
240 |
241 | let key_u32 = key_to_u32(key);
242 |
243 | let mut mods = ModifierType::empty();
244 |
245 | // Process all parts except the last one as modifiers
246 | for &mod_str in &parts[..parts.len() - 1] {
247 | if let Some(&mod_type) = mod_map.get(mod_str) {
248 | mods.insert(mod_type);
249 | }
250 | }
251 |
252 | (key_u32, mods)
253 | }
254 |
--------------------------------------------------------------------------------
/src/macos.rs:
--------------------------------------------------------------------------------
1 | use cocoa::appkit::{NSControl, NSMenuItem};
2 | use cocoa::base::{id, nil, selector};
3 | use cocoa::foundation::{NSPoint, NSRect, NSSize, NSString};
4 | use objc::declare::ClassDecl;
5 | use objc::runtime::{Object, Sel, NO, YES};
6 | use objc::{class, msg_send, sel, sel_impl};
7 | use std::sync::Arc;
8 | use tauri::{Runtime, Window};
9 |
10 | use crate::keymap::{get_key_map, get_modifier_map};
11 | use crate::macos_window_holder::CURRENT_WINDOW;
12 | use crate::theme::Theme;
13 | use crate::{MenuItem, Position};
14 |
15 | extern "C" {
16 | fn NSPointInRect(aPoint: NSPoint, aRect: NSRect) -> bool;
17 | }
18 |
19 | extern "C" fn menu_item_action(_self: &Object, _cmd: Sel, _item: id) {
20 | // Get the window from the CURRENT_WINDOW static
21 | let window_arc: Arc> = match CURRENT_WINDOW.get_window() {
22 | Some(window_arc) => window_arc,
23 | None => return println!("No window found"),
24 | };
25 |
26 | // Get the event name and payload from the representedObject of the NSMenuItem
27 | let nsstring_obj: id = unsafe { msg_send![_item, representedObject] };
28 | let combined_str: String = unsafe {
29 | let cstr: *const std::os::raw::c_char = msg_send![nsstring_obj, UTF8String];
30 | std::ffi::CStr::from_ptr(cstr)
31 | .to_string_lossy()
32 | .into_owned()
33 | };
34 | let parts: Vec<&str> = combined_str.split(":::").collect();
35 | let event_name = parts.get(0).unwrap_or(&"").to_string();
36 | let payload = parts.get(1).cloned();
37 |
38 | // Dereferencing the Arc to get a reference to the Window
39 | let window = &*window_arc;
40 |
41 | // Emit the event on the window
42 | window.emit(&event_name, payload).unwrap();
43 | }
44 |
45 | extern "C" fn menu_did_close(_self: &Object, _cmd: Sel, _menu: id) {
46 | if let Some(window) = CURRENT_WINDOW.get_window::() {
47 | window.emit("menu-did-close", ()).unwrap();
48 | } else {
49 | println!("Menu did close, but no window was found.");
50 | }
51 | }
52 |
53 | fn register_menu_item_action() -> Sel {
54 | let selector_name = "menuAction:";
55 |
56 | let exists: bool;
57 | let class = objc::runtime::Class::get("MenuItemDelegate");
58 | exists = class.is_some();
59 |
60 | if !exists {
61 | let superclass = objc::runtime::Class::get("NSObject").unwrap();
62 | let mut decl = ClassDecl::new("MenuItemDelegate", superclass).unwrap();
63 |
64 | unsafe {
65 | decl.add_method(
66 | selector(selector_name),
67 | menu_item_action:: as extern "C" fn(&Object, Sel, id),
68 | );
69 | decl.add_method(
70 | selector("menuDidClose:"),
71 | menu_did_close:: as extern "C" fn(&Object, Sel, id),
72 | );
73 | decl.register();
74 | }
75 | }
76 |
77 | selector(selector_name)
78 | }
79 |
80 | fn create_custom_menu_item(option: &MenuItem) -> id {
81 | // If the item is a separator, return a separator item
82 | if option.is_separator.unwrap_or(false) {
83 | let separator: id = unsafe { msg_send![class!(NSMenuItem), separatorItem] };
84 | return separator;
85 | }
86 |
87 | let sel = register_menu_item_action::();
88 | let menu_item: id = unsafe {
89 | let title = match &option.label {
90 | Some(label) => NSString::alloc(nil).init_str(label),
91 | None => NSString::alloc(nil).init_str(""),
92 | };
93 |
94 | // Parse the shortcut
95 | let (key, mask) = match &option.shortcut {
96 | Some(shortcut) => {
97 | let parts: Vec<&str> = shortcut.split('+').collect();
98 |
99 | let key_map = get_key_map();
100 | let modifier_map = get_modifier_map();
101 |
102 | // Default values
103 | let mut key_str = "";
104 | let mut mask = cocoa::appkit::NSEventModifierFlags::empty();
105 |
106 | for part in parts.iter() {
107 | if let Some(k) = key_map.get(*part) {
108 | key_str = k;
109 | } else if let Some(m) = modifier_map.get(*part) {
110 | mask.insert(*m);
111 | } else {
112 | key_str = *part; // Assuming the last item or the only item without a '+' is the main key.
113 | }
114 | }
115 |
116 | (NSString::alloc(nil).init_str(key_str), mask)
117 | }
118 | None => (
119 | NSString::alloc(nil).init_str(""),
120 | cocoa::appkit::NSEventModifierFlags::empty(),
121 | ),
122 | };
123 |
124 | let item = cocoa::appkit::NSMenuItem::alloc(nil)
125 | .initWithTitle_action_keyEquivalent_(title, sel, key);
126 | item.setKeyEquivalentModifierMask_(mask);
127 |
128 | // Set the enabled state (disabled flag is optional)
129 | item.setEnabled_(match option.disabled {
130 | Some(true) => NO,
131 | _ => YES,
132 | });
133 |
134 | // Set the represented object as the event name and payload
135 | let string_payload = match &option.payload {
136 | Some(payload) => format!(
137 | "{}:::{}",
138 | &option.event.as_ref().unwrap_or(&"".to_string()),
139 | payload
140 | ),
141 | None => option.event.as_ref().unwrap_or(&"".to_string()).clone(),
142 | };
143 | let ns_string_payload = NSString::alloc(nil).init_str(&string_payload);
144 | let _: () = msg_send![item, setRepresentedObject:ns_string_payload];
145 |
146 | // Set the icon if it exists
147 | if let Some(icon) = &option.icon {
148 | let ns_string_path: id = NSString::alloc(nil).init_str(&icon.path);
149 | let image: *mut Object = msg_send![class!(NSImage), alloc];
150 | let image: *mut Object = msg_send![image, initWithContentsOfFile:ns_string_path];
151 | if image.is_null() {
152 | println!("Failed to load image from path: {}", icon.path);
153 | } else {
154 | let width = icon.width.unwrap_or(16);
155 | let height = icon.height.unwrap_or(16);
156 | let size = NSSize::new(width as f64, height as f64);
157 | let _: () = msg_send![image, setSize:size];
158 |
159 | let _: () = msg_send![item, setImage:image];
160 | }
161 | }
162 |
163 | // Set the delegate
164 | let delegate_class_name = "MenuItemDelegate";
165 | let delegate_class: &'static objc::runtime::Class =
166 | objc::runtime::Class::get(delegate_class_name).expect("Class should exist");
167 | let delegate_instance: id = msg_send![delegate_class, new];
168 | item.setTarget_(delegate_instance);
169 |
170 | // Set the submenu if it exists
171 | if let Some(subitems) = &option.subitems {
172 | let submenu: id = msg_send![class!(NSMenu), new];
173 | let _: () = msg_send![submenu, setAutoenablesItems:NO];
174 | for subitem in subitems.iter() {
175 | let sub_menu_item: id = create_custom_menu_item::(subitem);
176 | let _: () = msg_send![submenu, addItem:sub_menu_item];
177 | }
178 | let _: () = msg_send![item, setSubmenu:submenu];
179 | }
180 |
181 | // Handle checkable menu items
182 | let state = match option.checked {
183 | Some(true) => 1,
184 | _ => 0,
185 | };
186 | let _: () = msg_send![item, setState:state];
187 |
188 | item
189 | };
190 |
191 | menu_item
192 | }
193 |
194 | fn create_context_menu(
195 | options: &[MenuItem],
196 | window: &Window,
197 | theme: Option,
198 | ) -> id {
199 | let _: () = CURRENT_WINDOW.set_window(window.clone());
200 | unsafe {
201 | let title = NSString::alloc(nil).init_str("Menu");
202 | let menu: id = msg_send![class!(NSMenu), alloc];
203 | let menu: id = msg_send![menu, initWithTitle: title];
204 |
205 | // Set the theme menu
206 | if let Some(theme) = theme {
207 | let appearance_name: id = match theme {
208 | Theme::Dark => NSString::alloc(nil).init_str("NSAppearanceNameDarkAqua"),
209 | Theme::Light => NSString::alloc(nil).init_str("NSAppearanceNameAqua"),
210 | };
211 |
212 | let appearance: id = msg_send![class!(NSAppearance), appearanceNamed:appearance_name];
213 | let _: () = msg_send![menu, setAppearance: appearance];
214 | }
215 |
216 | let _: () = msg_send![menu, setAutoenablesItems:NO];
217 |
218 | for option in options.iter().cloned() {
219 | let item: id = create_custom_menu_item::(&option);
220 | let _: () = msg_send![menu, addItem:item];
221 | }
222 |
223 | let delegate_class_name = "MenuItemDelegate";
224 | let delegate_class: &'static objc::runtime::Class =
225 | objc::runtime::Class::get(delegate_class_name).expect("Class should exist");
226 | let delegate_instance: id = msg_send![delegate_class, new];
227 | let _: () = msg_send![menu, setDelegate:delegate_instance];
228 |
229 | menu
230 | }
231 | }
232 |
233 | pub fn show_context_menu(
234 | window: Window,
235 | pos: Option,
236 | items: Option>,
237 | theme: Option,
238 | ) {
239 | let main_queue = dispatch::Queue::main();
240 | main_queue.exec_async(move || {
241 | let items_slice = items.as_ref().map(|v| v.as_slice()).unwrap_or(&[]);
242 | let menu = create_context_menu(items_slice, &window, theme);
243 | let location = match pos {
244 | // Convert web page coordinates to screen coordinates
245 | Some(pos) if pos.x != 0.0 || pos.y != 0.0 => unsafe {
246 | let window_position = window.outer_position().unwrap();
247 |
248 | // Get all screens and the mouse location
249 | let screens: id = msg_send![class!(NSScreen), screens];
250 | let screen_count: usize = msg_send![screens, count];
251 | let mouse_location: NSPoint = msg_send![class!(NSEvent), mouseLocation];
252 |
253 | // Find the screen under the mouse cursor
254 | let mut target_screen: id = nil;
255 | let mut target_screen_frame: NSRect =
256 | NSRect::new(NSPoint::new(0.0, 0.0), NSSize::new(0.0, 0.0));
257 |
258 | for i in 0..screen_count {
259 | let screen: id = msg_send![screens, objectAtIndex:i];
260 | let frame: NSRect = msg_send![screen, frame];
261 | if NSPointInRect(mouse_location, frame) {
262 | target_screen = screen;
263 | target_screen_frame = frame;
264 | break;
265 | }
266 | }
267 |
268 | // Fallback to the main screen if no specific screen found
269 | if target_screen == nil {
270 | target_screen = msg_send![class!(NSScreen), mainScreen];
271 | target_screen_frame = msg_send![target_screen, frame];
272 | }
273 |
274 | let screen_height = target_screen_frame.size.height;
275 | let screen_origin_y = target_screen_frame.origin.y;
276 | let scale_factor = match window.scale_factor() {
277 | Ok(factor) => factor,
278 | Err(_) => 1.0, // Default to 1.0 if scale factor can't be retrieved
279 | };
280 |
281 | if pos.is_absolute.unwrap_or(false) {
282 | let x = pos.x;
283 | let y = screen_origin_y + screen_height - pos.y;
284 | NSPoint::new(x, y)
285 | } else {
286 | let x = pos.x + (window_position.x as f64 / scale_factor);
287 | let y = screen_origin_y + screen_height
288 | - (window_position.y as f64 / scale_factor)
289 | - pos.y;
290 | NSPoint::new(x, y)
291 | }
292 | },
293 | // Get the current mouse location if the web page didn't specify a position
294 | _ => unsafe {
295 | let event: NSPoint = msg_send![class!(NSEvent), mouseLocation];
296 | NSPoint::new(event.x, event.y)
297 | },
298 | };
299 | unsafe {
300 | let _: () =
301 | msg_send![menu, popUpMenuPositioningItem:nil atLocation:location inView:nil];
302 | }
303 | });
304 | }
305 |
--------------------------------------------------------------------------------
/src/macos_window_holder.rs:
--------------------------------------------------------------------------------
1 | use std::any::Any;
2 | use std::sync::{Arc, Mutex};
3 | use tauri::{Runtime, Window};
4 |
5 | pub struct WindowHolder {
6 | window: Arc>>>,
7 | }
8 |
9 | impl WindowHolder {
10 | pub fn new() -> Self {
11 | Self {
12 | window: Arc::new(Mutex::new(None)),
13 | }
14 | }
15 |
16 | pub fn set_window(&self, window: Window) {
17 | let mut lock = self.window.lock().unwrap();
18 | *lock = Some(Arc::new(window));
19 | }
20 |
21 | pub fn get_window(&self) -> Option>> {
22 | let lock = self.window.lock().unwrap();
23 | match &*lock {
24 | Some(window) => Some(window.clone().downcast::>().unwrap()),
25 | None => None,
26 | }
27 | }
28 | }
29 |
30 | lazy_static::lazy_static! {
31 | pub static ref CURRENT_WINDOW: WindowHolder = WindowHolder::new();
32 | }
33 |
--------------------------------------------------------------------------------
/src/menu_item.rs:
--------------------------------------------------------------------------------
1 | use serde::Deserialize;
2 |
3 | #[derive(Clone, Deserialize)]
4 | pub struct MenuItem {
5 | pub label: Option,
6 | pub disabled: Option,
7 | pub shortcut: Option,
8 | pub event: Option,
9 | pub payload: Option,
10 | pub subitems: Option>,
11 | pub icon: Option,
12 | pub checked: Option,
13 | pub is_separator: Option,
14 | }
15 |
16 | #[derive(Clone, Deserialize)]
17 | pub struct MenuItemIcon {
18 | pub path: String,
19 | pub width: Option,
20 | pub height: Option,
21 | }
22 |
23 | impl Default for MenuItem {
24 | fn default() -> Self {
25 | Self {
26 | label: None,
27 | disabled: Some(false),
28 | shortcut: None,
29 | event: None,
30 | payload: None,
31 | subitems: None,
32 | icon: None,
33 | checked: Some(false),
34 | is_separator: Some(false),
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/theme.rs:
--------------------------------------------------------------------------------
1 | use serde::Deserialize;
2 |
3 | #[derive(Debug, Clone, Copy, Deserialize)]
4 | pub enum Theme {
5 | Light,
6 | Dark,
7 | }
8 |
9 | impl Theme {
10 | pub fn from_str(s: &str) -> Option {
11 | match s {
12 | "light" => Some(Theme::Light),
13 | "dark" => Some(Theme::Dark),
14 | _ => None,
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/win.rs:
--------------------------------------------------------------------------------
1 | use std::collections::HashMap;
2 | use std::convert::TryInto;
3 | use std::ptr::null_mut;
4 | use std::sync::{Arc, Mutex};
5 | use tauri::{Runtime, Window};
6 | use winapi::{
7 | shared::minwindef::LOWORD,
8 | shared::windef::{HMENU, HWND, HWND__, POINT},
9 | um::winuser::{
10 | AppendMenuW, ClientToScreen, CreatePopupMenu, DestroyMenu, DispatchMessageW, GetCursorPos,
11 | GetMessageW, PostQuitMessage, SetMenuItemBitmaps, TrackPopupMenu, TranslateMessage,
12 | MF_BYCOMMAND, MF_CHECKED, MF_DISABLED, MF_ENABLED, MF_POPUP, MF_SEPARATOR, MF_STRING, MSG,
13 | TPM_LEFTALIGN, TPM_RIGHTBUTTON, TPM_TOPALIGN, WM_ACTIVATE, WM_COMMAND,
14 | },
15 | };
16 |
17 | use crate::keymap::get_key_map;
18 | use crate::theme::Theme;
19 | use crate::win_image_handler::{convert_to_hbitmap, load_bitmap_from_file};
20 | use crate::{MenuItem, Position};
21 |
22 | const ID_MENU_ITEM_BASE: u32 = 1000;
23 | const WA_INACTIVE: u16 = 0;
24 |
25 | // We use a lazy_static Mutex to ensure thread safety.
26 | // This will store a map from menu item IDs to events.
27 | lazy_static::lazy_static! {
28 | static ref CALLBACK_MAP: Mutex)>> = Mutex::new(HashMap::new());
29 | }
30 |
31 | pub fn get_label_with_shortcut(label: &str, shortcut: Option<&str>) -> String {
32 | let key_map = get_key_map();
33 |
34 | label.to_string()
35 | + &shortcut.map_or_else(String::new, |s| {
36 | format!(
37 | "\t{}",
38 | s.split('+')
39 | .map(|part| {
40 | let mut c = part.chars();
41 | // If the part exists in the key_map, use the key_map value.
42 | // Otherwise, use the original logic.
43 | key_map.get(part).map_or_else(
44 | || c.next().unwrap_or_default().to_uppercase().to_string() + c.as_str(),
45 | |value| value.to_string(),
46 | )
47 | })
48 | .collect::>()
49 | .join("+")
50 | )
51 | })
52 | }
53 |
54 | fn append_menu_item(menu: HMENU, item: &MenuItem, counter: &mut u32) -> Result {
55 | let id = *counter;
56 | *counter += 1;
57 |
58 | if item.is_separator.unwrap_or(false) {
59 | unsafe {
60 | AppendMenuW(menu, MF_SEPARATOR, 0, null_mut());
61 | }
62 | } else {
63 | let label = item.label.as_deref().unwrap_or("");
64 | let shortcut = item.shortcut.as_deref();
65 | let menu_label = get_label_with_shortcut(label, shortcut);
66 | let label_wide: Vec = menu_label
67 | .encode_utf16()
68 | .chain(std::iter::once(0))
69 | .collect(); // Add a null terminator
70 | let mut flags: u32 = MF_STRING;
71 |
72 | // Check if the item should be disabled
73 | if item.disabled.unwrap_or(false) {
74 | flags |= MF_DISABLED;
75 | } else {
76 | flags |= MF_ENABLED;
77 | }
78 |
79 | // Check if the item is checkable and set the initial state
80 | if item.checked.unwrap_or(false) {
81 | flags |= MF_CHECKED;
82 | }
83 |
84 | if let Some(subitems) = &item.subitems {
85 | let submenu = unsafe { CreatePopupMenu() };
86 | for subitem in subitems.iter() {
87 | let _ = append_menu_item(submenu, subitem, counter);
88 | }
89 | unsafe {
90 | AppendMenuW(
91 | menu,
92 | MF_POPUP | flags,
93 | (submenu as u32).try_into().unwrap(),
94 | label_wide.as_ptr(),
95 | );
96 | }
97 | } else {
98 | unsafe {
99 | AppendMenuW(menu, flags, id.try_into().unwrap(), label_wide.as_ptr());
100 | };
101 | }
102 |
103 | // If an event is provided, store it in the callback map
104 | if let Some(event) = &item.event {
105 | CALLBACK_MAP
106 | .lock()
107 | .unwrap()
108 | .insert(id, (event.clone(), item.payload.clone()));
109 | }
110 |
111 | // If the icon path is provided, load the bitmap and set it for the menu item.
112 | if let Some(icon) = &item.icon {
113 | match load_bitmap_from_file(&icon.path, icon.width, icon.height) {
114 | Ok(bitmap) => match convert_to_hbitmap(bitmap) {
115 | Ok(hbitmap) => {
116 | if !hbitmap.is_null() {
117 | unsafe {
118 | SetMenuItemBitmaps(menu, id as u32, MF_BYCOMMAND, hbitmap, hbitmap);
119 | }
120 | } else {
121 | return Err(format!("Failed to load bitmap from path: {}", icon.path));
122 | }
123 | }
124 | Err(err_msg) => return Err(err_msg),
125 | },
126 | Err(err) => {
127 | return Err(format!(
128 | "Failed to load image from path: {}. Error: {:?}",
129 | icon.path, err
130 | ))
131 | }
132 | }
133 | }
134 | }
135 |
136 | Ok(id)
137 | }
138 |
139 | // This function would be called when a WM_COMMAND message is received, with the ID of the menu item that was clicked
140 | pub fn handle_menu_item_click(id: u32, window: Window) {
141 | if let Some((event, payload)) = CALLBACK_MAP.lock().unwrap().get(&id) {
142 | window.emit(event, &payload).unwrap(); // Emit the event to JavaScript
143 | }
144 | }
145 |
146 | pub fn show_context_menu(
147 | window: Window,
148 | pos: Option,
149 | items: Option>,
150 | _theme: Option,
151 | ) {
152 | // Clear the callback map at the start of each context menu display
153 | CALLBACK_MAP.lock().unwrap().clear();
154 |
155 | let menu = unsafe { CreatePopupMenu() };
156 | let hwnd = window.hwnd().unwrap().0 as *mut HWND__;
157 |
158 | let mut counter = ID_MENU_ITEM_BASE;
159 | if let Some(menu_items) = items {
160 | for item in menu_items.iter() {
161 | let _ = append_menu_item(menu, item, &mut counter);
162 | }
163 | }
164 |
165 | let position = match pos {
166 | Some(p) => {
167 | let scale_factor = window.scale_factor().unwrap_or(1.0); // Use 1.0 as a default if getting the scale factor fails
168 | let mut point = POINT {
169 | x: (p.x * scale_factor) as i32,
170 | y: (p.y * scale_factor) as i32,
171 | };
172 |
173 | if p.is_absolute.unwrap_or(false) {
174 | point
175 | } else {
176 | unsafe {
177 | ClientToScreen(hwnd as HWND, &mut point);
178 | }
179 | point
180 | }
181 | }
182 | None => {
183 | // Get the current cursor position using GetCursorPos
184 | let mut current_pos = POINT { x: 0, y: 0 };
185 | unsafe {
186 | GetCursorPos(&mut current_pos);
187 | }
188 | current_pos
189 | }
190 | };
191 |
192 | unsafe {
193 | TrackPopupMenu(
194 | menu,
195 | TPM_LEFTALIGN | TPM_TOPALIGN | TPM_RIGHTBUTTON,
196 | position.x,
197 | position.y,
198 | 0, // reserved param
199 | hwnd as HWND,
200 | std::ptr::null_mut(),
201 | );
202 |
203 | DestroyMenu(menu);
204 |
205 | // Post a quit message to exit the message loop
206 | PostQuitMessage(0);
207 | }
208 |
209 | // Emit the menu-did-close event to JavaScript
210 | window.emit("menu-did-close", ()).unwrap();
211 |
212 | let mut msg: MSG = unsafe { std::mem::zeroed() };
213 | while unsafe { GetMessageW(&mut msg, null_mut(), 0, 0) } > 0 {
214 | match msg.message {
215 | WM_COMMAND => {
216 | // Extract the menu item ID from wParam
217 | let menu_item_id = LOWORD(msg.wParam as u32);
218 | handle_menu_item_click(menu_item_id.into(), window.clone());
219 | }
220 | WM_ACTIVATE => {
221 | if LOWORD(msg.wParam as u32) == WA_INACTIVE {
222 | unsafe { DestroyMenu(menu) };
223 | }
224 | }
225 | _ => unsafe {
226 | TranslateMessage(&msg);
227 | DispatchMessageW(&msg);
228 | },
229 | }
230 | }
231 | }
232 |
--------------------------------------------------------------------------------
/src/win_image_handler.rs:
--------------------------------------------------------------------------------
1 | use image::io::Reader as ImageReader;
2 | use std::ptr::null_mut;
3 | use winapi::{
4 | shared::minwindef::BYTE,
5 | shared::ntdef::VOID,
6 | shared::windef::HBITMAP,
7 | um::wingdi::{CreateCompatibleDC, CreateDIBSection, BITMAPINFO},
8 | um::winuser::GetDC,
9 | };
10 |
11 | pub fn load_bitmap_from_file(
12 | path: &str,
13 | width: Option,
14 | height: Option,
15 | ) -> Result {
16 | let mut image_reader = ImageReader::open(path)?.decode()?;
17 | let (resized_width, resized_height) = (width.unwrap_or(16), height.unwrap_or(16));
18 | image_reader = image_reader.resize(
19 | resized_width,
20 | resized_height,
21 | image::imageops::FilterType::Nearest,
22 | );
23 | Ok(image_reader)
24 | }
25 |
26 | pub fn convert_to_hbitmap(img: image::DynamicImage) -> Result {
27 | // Convert the image to a monochrome 1-bit per pixel format
28 | let img = img.to_rgba8();
29 | let (width, height) = img.dimensions();
30 |
31 | // Get the device context for the screen
32 | let hdc_screen = unsafe { GetDC(null_mut()) };
33 | if hdc_screen.is_null() {
34 | return Err("Failed to get device context.".to_string());
35 | }
36 |
37 | // Create a compatible memory device context
38 | let hdc_memory = unsafe { CreateCompatibleDC(hdc_screen) };
39 | if hdc_memory.is_null() {
40 | return Err("Failed to create memory device context.".to_string());
41 | }
42 |
43 | // Create a compatible bitmap
44 | let mut bmi: BITMAPINFO = unsafe { std::mem::zeroed() };
45 | bmi.bmiHeader.biSize = std::mem::size_of::() as u32;
46 | bmi.bmiHeader.biWidth = width as i32;
47 | bmi.bmiHeader.biHeight = -(height as i32); // Top-down
48 | bmi.bmiHeader.biPlanes = 1;
49 | bmi.bmiHeader.biBitCount = 32; // 32 bits per pixel for RGBA
50 | bmi.bmiHeader.biCompression = winapi::um::wingdi::BI_RGB;
51 |
52 | let mut bits: *mut VOID = std::ptr::null_mut();
53 | let hbitmap = unsafe {
54 | CreateDIBSection(
55 | hdc_screen,
56 | &bmi,
57 | winapi::um::wingdi::DIB_RGB_COLORS,
58 | &mut bits,
59 | std::ptr::null_mut(),
60 | 0,
61 | )
62 | };
63 | if hbitmap.is_null() {
64 | return Err("Failed to create DIB section.".to_string());
65 | }
66 |
67 | // Copy image pixels to the bitmap
68 | for y in 0..height {
69 | for x in 0..width {
70 | let pixel = img.get_pixel(x, y);
71 | let offset = (y * width + x) * 4;
72 | let dst = unsafe { bits.offset(offset as isize) as *mut BYTE };
73 | let alpha = pixel[3] as f32 / 255.0;
74 | unsafe {
75 | *dst.offset(0) = (pixel[2] as f32 * alpha) as BYTE; // Blue channel
76 | *dst.offset(1) = (pixel[1] as f32 * alpha) as BYTE; // Green channel
77 | *dst.offset(2) = (pixel[0] as f32 * alpha) as BYTE; // Red channel
78 | *dst.offset(3) = pixel[3]; // Alpha channel
79 | }
80 | }
81 | }
82 |
83 | Ok(hbitmap)
84 | }
85 |
--------------------------------------------------------------------------------