├── examples
├── windows-common-controls-v6
│ ├── .gitignore
│ ├── manifest.rc
│ ├── build.rs
│ ├── app.exe.manifest
│ ├── Cargo.toml
│ └── src
│ │ └── main.rs
├── icon.png
├── tao.rs
├── winit.rs
└── wry.rs
├── renovate.json
├── .gitignore
├── .changes
├── fix-top-level-submenu-padding-issue.md
├── config.json
└── readme.md
├── src
├── builders
│ ├── mod.rs
│ ├── normal.rs
│ ├── check.rs
│ ├── submenu.rs
│ └── icon.rs
├── platform_impl
│ ├── macos
│ │ ├── util.rs
│ │ ├── icon.rs
│ │ └── accelerator.rs
│ ├── gtk
│ │ ├── icon.rs
│ │ └── accelerator.rs
│ ├── mod.rs
│ └── windows
│ │ ├── util.rs
│ │ ├── icon.rs
│ │ ├── accelerator.rs
│ │ └── dark_menu_bar.rs
├── util.rs
├── error.rs
├── items
│ ├── mod.rs
│ ├── normal.rs
│ ├── check.rs
│ ├── icon.rs
│ ├── submenu.rs
│ └── predefined.rs
├── menu_id.rs
├── about_metadata.rs
├── icon.rs
└── menu.rs
├── .github
└── workflows
│ ├── covector-status.yml
│ ├── audit.yml
│ ├── test.yml
│ ├── clippy-fmt.yml
│ └── covector-version-or-publish.yml
├── LICENSE.spdx
├── LICENSE-MIT
├── Cargo.toml
├── README.md
└── LICENSE-APACHE
/examples/windows-common-controls-v6/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | Cargo.lock
--------------------------------------------------------------------------------
/examples/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tauri-apps/muda/HEAD/examples/icon.png
--------------------------------------------------------------------------------
/examples/windows-common-controls-v6/manifest.rc:
--------------------------------------------------------------------------------
1 | #define RT_MANIFEST 24
2 | 1 RT_MANIFEST "app.exe.manifest"
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "extends": ["config:base", ":disableDependencyDashboard"]
4 | }
5 |
--------------------------------------------------------------------------------
/examples/windows-common-controls-v6/build.rs:
--------------------------------------------------------------------------------
1 | fn main() {
2 | #[cfg(target_os = "windows")]
3 | embed_resource::compile("manifest.rc", embed_resource::NONE);
4 | }
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Copyright 2022-2022 Tauri Programme within The Commons Conservancy
2 | # SPDX-License-Identifier: Apache-2.0
3 | # SPDX-License-Identifier: MIT
4 |
5 | /target
6 | /.vscode
--------------------------------------------------------------------------------
/.changes/fix-top-level-submenu-padding-issue.md:
--------------------------------------------------------------------------------
1 | ---
2 | "muda": patch
3 | ---
4 |
5 | Fix padding unconditionally added to top-level submenus for icons even when there's no icon on Linux
6 |
--------------------------------------------------------------------------------
/src/builders/mod.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2022-2022 Tauri Programme within The Commons Conservancy
2 | // SPDX-License-Identifier: Apache-2.0
3 | // SPDX-License-Identifier: MIT
4 |
5 | //! A module containting builder types
6 |
7 | mod check;
8 | mod icon;
9 | mod normal;
10 | mod submenu;
11 |
12 | pub use crate::about_metadata::AboutMetadataBuilder;
13 | pub use check::*;
14 | pub use icon::*;
15 | pub use normal::*;
16 | pub use submenu::*;
17 |
--------------------------------------------------------------------------------
/examples/windows-common-controls-v6/app.exe.manifest:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/platform_impl/macos/util.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2022-2022 Tauri Programme within The Commons Conservancy
2 | // SPDX-License-Identifier: Apache-2.0
3 | // SPDX-License-Identifier: MIT
4 |
5 | use std::str;
6 |
7 | /// Strips single `&` characters from the string.
8 | ///
9 | /// `&` can be escaped as `&&` to prevent stripping, in which case a single `&` will be output.
10 | pub fn strip_mnemonic>(string: S) -> String {
11 | string
12 | .as_ref()
13 | .replace("&&", "[~~]")
14 | .replace('&', "")
15 | .replace("[~~]", "&")
16 | }
17 |
--------------------------------------------------------------------------------
/examples/windows-common-controls-v6/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "windows-common-controls-v6"
3 | version = "0.1.0"
4 | edition = "2021"
5 |
6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7 |
8 | [dependencies]
9 | muda = { path = "../../", features = ["common-controls-v6"] }
10 | tao = "0.28"
11 | image = "0.25"
12 |
13 | [target."cfg(target_os = \"windows\")".dependencies.windows-sys]
14 | version = "0.59"
15 | features = ["Win32_UI_WindowsAndMessaging", "Win32_Foundation"]
16 |
17 | [build-dependencies]
18 | embed-resource = "2"
19 |
--------------------------------------------------------------------------------
/.github/workflows/covector-status.yml:
--------------------------------------------------------------------------------
1 | # Copyright 2022-2022 Tauri Programme within The Commons Conservancy
2 | # SPDX-License-Identifier: Apache-2.0
3 | # SPDX-License-Identifier: MIT
4 |
5 | name: covector status
6 | on: [pull_request]
7 |
8 | jobs:
9 | covector:
10 | runs-on: ubuntu-latest
11 |
12 | steps:
13 | - uses: actions/checkout@v4
14 | - name: covector status
15 | uses: jbolda/covector/packages/action@covector-v0
16 | id: covector
17 | with:
18 | command: "status"
19 | token: ${{ secrets.GITHUB_TOKEN }}
20 | comment: true
--------------------------------------------------------------------------------
/src/util.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2022-2022 Tauri Programme within The Commons Conservancy
2 | // SPDX-License-Identifier: Apache-2.0
3 | // SPDX-License-Identifier: MIT
4 |
5 | use std::sync::atomic::{AtomicU32, Ordering};
6 |
7 | #[derive(Clone, Copy, Debug)]
8 | pub enum AddOp {
9 | Append,
10 | Insert(usize),
11 | }
12 |
13 | pub struct Counter(AtomicU32);
14 |
15 | impl Counter {
16 | #[allow(unused)]
17 | pub const fn new() -> Self {
18 | Self(AtomicU32::new(1))
19 | }
20 |
21 | #[allow(unused)]
22 | pub const fn new_with_start(start: u32) -> Self {
23 | Self(AtomicU32::new(start))
24 | }
25 |
26 | pub fn next(&self) -> u32 {
27 | self.0.fetch_add(1, Ordering::Relaxed)
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/.github/workflows/audit.yml:
--------------------------------------------------------------------------------
1 | # Copyright 2022-2022 Tauri Programme within The Commons Conservancy
2 | # SPDX-License-Identifier: Apache-2.0
3 | # SPDX-License-Identifier: MIT
4 |
5 | name: audit
6 |
7 | on:
8 | workflow_dispatch:
9 | schedule:
10 | - cron: '0 0 * * *'
11 | push:
12 | branches:
13 | - dev
14 | paths:
15 | - 'Cargo.lock'
16 | - 'Cargo.toml'
17 | pull_request:
18 | paths:
19 | - 'Cargo.lock'
20 | - 'Cargo.toml'
21 |
22 | concurrency:
23 | group: ${{ github.workflow }}-${{ github.ref }}
24 | cancel-in-progress: true
25 |
26 | jobs:
27 | audit:
28 | runs-on: ubuntu-latest
29 | steps:
30 | - uses: actions/checkout@v4
31 | - uses: rustsec/audit-check@v1
32 | with:
33 | token: ${{ secrets.GITHUB_TOKEN }}
--------------------------------------------------------------------------------
/LICENSE.spdx:
--------------------------------------------------------------------------------
1 | SPDXVersion: SPDX-2.1
2 | DataLicense: CC0-1.0
3 | PackageName: muda
4 | DataFormat: SPDXRef-1
5 | PackageSupplier: Organization: The Tauri Programme in the Commons Conservancy
6 | PackageHomePage: https://tauri.app
7 | PackageLicenseDeclared: Apache-2.0
8 | PackageLicenseDeclared: MIT
9 | PackageCopyrightText: 2020-2022, The Tauri Programme in the Commons Conservancy
10 | PackageSummary: Menu Utilities for Desktop Applications.
11 |
12 | PackageComment: The package includes the following libraries; see
13 | Relationship information.
14 |
15 | Created: 2022-12-05T09:00:00Z
16 | PackageDownloadLocation: git://github.com/tauri-apps/muda
17 | PackageDownloadLocation: git+https://github.com/tauri-apps/muda.git
18 | PackageDownloadLocation: git+ssh://github.com/tauri-apps/muda.git
19 | Creator: Person: Daniel Thompson-Yvetot
--------------------------------------------------------------------------------
/LICENSE-MIT:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022-2022 Tauri Programme within The Commons Conservancy
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | # Copyright 2022-2022 Tauri Programme within The Commons Conservancy
2 | # SPDX-License-Identifier: Apache-2.0
3 | # SPDX-License-Identifier: MIT
4 |
5 | name: test
6 |
7 | on:
8 | push:
9 | branches:
10 | - dev
11 | pull_request:
12 |
13 | env:
14 | RUST_BACKTRACE: 1
15 |
16 | concurrency:
17 | group: ${{ github.workflow }}-${{ github.ref }}
18 | cancel-in-progress: true
19 |
20 | jobs:
21 | test:
22 | strategy:
23 | fail-fast: false
24 | matrix:
25 | platform: ["windows-latest", "macos-latest", "ubuntu-latest"]
26 |
27 | runs-on: ${{ matrix.platform }}
28 |
29 | steps:
30 | - uses: actions/checkout@v4
31 |
32 | - name: install system deps
33 | if: matrix.platform == 'ubuntu-latest'
34 | run: |
35 | sudo apt-get update
36 | sudo apt-get install -y libgtk-3-dev libxdo-dev libwebkit2gtk-4.1-dev
37 |
38 | - uses: dtolnay/rust-toolchain@1.71
39 | - run: cargo build
40 |
41 | - uses: dtolnay/rust-toolchain@stable
42 | - run: cargo test
43 |
44 | - uses: dtolnay/rust-toolchain@nightly
45 | with:
46 | components: miri
47 | - run: cargo +nightly miri test
48 |
--------------------------------------------------------------------------------
/src/error.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2022-2022 Tauri Programme within The Commons Conservancy
2 | // SPDX-License-Identifier: Apache-2.0
3 | // SPDX-License-Identifier: MIT
4 |
5 | use thiserror::Error;
6 |
7 | pub use crate::accelerator::AcceleratorParseError;
8 |
9 | /// Errors returned by muda.
10 | #[non_exhaustive]
11 | #[derive(Error, Debug)]
12 | pub enum Error {
13 | #[error("This menu item is not a child of this `Menu` or `Submenu`")]
14 | NotAChildOfThisMenu,
15 | #[cfg(windows)]
16 | #[error("This menu has not been initialized for this hwnd`")]
17 | NotInitialized,
18 | #[cfg(all(target_os = "linux", feature = "gtk"))]
19 | #[error("This menu has not been initialized for this gtk window`")]
20 | NotInitialized,
21 | #[cfg(windows)]
22 | #[error("This menu has already been initialized for this hwnd`")]
23 | AlreadyInitialized,
24 | #[cfg(all(target_os = "linux", feature = "gtk"))]
25 | #[error("This menu has already been initialized for this gtk window`")]
26 | AlreadyInitialized,
27 | #[error(transparent)]
28 | AcceleratorParseError(#[from] AcceleratorParseError),
29 | }
30 |
31 | /// Convenient type alias of Result type for muda.
32 | pub type Result = std::result::Result;
33 |
--------------------------------------------------------------------------------
/.github/workflows/clippy-fmt.yml:
--------------------------------------------------------------------------------
1 | # Copyright 2022-2022 Tauri Programme within The Commons Conservancy
2 | # SPDX-License-Identifier: Apache-2.0
3 | # SPDX-License-Identifier: MIT
4 |
5 | name: clippy & fmt
6 |
7 | on:
8 | push:
9 | branches:
10 | - dev
11 | pull_request:
12 |
13 | concurrency:
14 | group: ${{ github.workflow }}-${{ github.ref }}
15 | cancel-in-progress: true
16 |
17 | jobs:
18 | clippy:
19 | strategy:
20 | fail-fast: false
21 | matrix:
22 | platform: [ubuntu-latest, macos-latest, windows-latest]
23 |
24 | runs-on: ${{ matrix.platform }}
25 |
26 | steps:
27 | - uses: actions/checkout@v4
28 | - name: install system deps
29 | if: matrix.platform == 'ubuntu-latest'
30 | run: |
31 | sudo apt-get update
32 | sudo apt-get install -y libgtk-3-dev libxdo-dev libwebkit2gtk-4.1-dev
33 |
34 | - uses: dtolnay/rust-toolchain@stable
35 | with:
36 | components: clippy
37 |
38 | - run: cargo clippy --all-targets --all-features -- -D warnings
39 |
40 | fmt:
41 | runs-on: ubuntu-latest
42 | steps:
43 | - uses: actions/checkout@v4
44 | - uses: dtolnay/rust-toolchain@stable
45 | with:
46 | components: rustfmt
47 |
48 | - run: cargo fmt --all -- --check
49 |
--------------------------------------------------------------------------------
/.changes/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "gitSiteUrl": "https://www.github.com/tauri-apps/muda/",
3 | "timeout": 3600000,
4 | "pkgManagers": {
5 | "rust": {
6 | "version": true,
7 | "getPublishedVersion": "cargo search ${ pkg.pkg } --limit 1 | sed -nE 's/^[^\"]*\"//; s/\".*//1p' -",
8 | "prepublish": [
9 | "sudo apt-get update",
10 | "sudo apt-get install -y libgtk-3-dev libxdo-dev"
11 | ],
12 | "publish": [
13 | {
14 | "command": "cargo package --no-verify",
15 | "dryRunCommand": true
16 | },
17 | {
18 | "command": "echo '\nCargo Publish
\n\n```'",
19 | "dryRunCommand": true,
20 | "pipe": true
21 | },
22 | {
23 | "command": "cargo publish",
24 | "dryRunCommand": "cargo publish --dry-run",
25 | "pipe": true
26 | },
27 | {
28 | "command": "echo '```\n\n \n'",
29 | "dryRunCommand": true,
30 | "pipe": true
31 | }
32 | ],
33 | "postpublish": [
34 | "git tag ${ pkg.pkg }-v${ pkgFile.versionMajor } -f",
35 | "git tag ${ pkg.pkg }-v${ pkgFile.versionMajor }.${ pkgFile.versionMinor } -f",
36 | "git push --tags -f"
37 | ]
38 | }
39 | },
40 | "packages": {
41 | "muda": {
42 | "path": ".",
43 | "manager": "rust",
44 | "assets": [
45 | {
46 | "path": "${ pkg.path }/target/package/muda-${ pkgFile.version }.crate",
47 | "name": "${ pkg.pkg }-${ pkgFile.version }.crate"
48 | }
49 | ]
50 | }
51 | }
52 | }
--------------------------------------------------------------------------------
/.changes/readme.md:
--------------------------------------------------------------------------------
1 | # Changes
2 |
3 | ##### via https://github.com/jbolda/covector
4 |
5 | As you create PRs and make changes that require a version bump, please add a new markdown file in this folder. You do not note the version _number_, but rather the type of bump that you expect: major, minor, or patch. The filename is not important, as long as it is a `.md`, but we recommend it represents the overall change for our sanity.
6 |
7 | When you select the version bump required, you do _not_ need to consider dependencies. Only note the package with the actual change, and any packages that depend on that package will be bumped automatically in the process.
8 |
9 | Use the following format:
10 |
11 | ```md
12 | ---
13 | "muda": patch
14 | ---
15 |
16 | Change summary goes here
17 | ```
18 |
19 | Summaries do not have a specific character limit, but are text only. These summaries are used within the (future implementation of) changelogs. They will give context to the change and also point back to the original PR if more details and context are needed.
20 |
21 | Changes will be designated as a `major`, `minor` or `patch` as further described in [semver](https://semver.org/).
22 |
23 | Given a version number MAJOR.MINOR.PATCH, increment the:
24 |
25 | - MAJOR version when you make incompatible API changes,
26 | - MINOR version when you add functionality in a backwards compatible manner, and
27 | - PATCH version when you make backwards compatible bug fixes.
28 |
29 | Additional labels for pre-release and build metadata are available as extensions to the MAJOR.MINOR.PATCH format, but will be discussed prior to usage (as extra steps will be necessary in consideration of merging and publishing).
30 |
--------------------------------------------------------------------------------
/src/builders/normal.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2022-2022 Tauri Programme within The Commons Conservancy
2 | // SPDX-License-Identifier: Apache-2.0
3 | // SPDX-License-Identifier: MIT
4 |
5 | use crate::{accelerator::Accelerator, MenuId, MenuItem};
6 |
7 | /// A builder type for [`MenuItem`]
8 | #[derive(Clone, Debug, Default)]
9 | pub struct MenuItemBuilder {
10 | text: String,
11 | enabled: bool,
12 | id: Option,
13 | accelerator: Option,
14 | }
15 |
16 | impl MenuItemBuilder {
17 | pub fn new() -> Self {
18 | Default::default()
19 | }
20 |
21 | /// Set the id this menu item.
22 | pub fn id(mut self, id: MenuId) -> Self {
23 | self.id.replace(id);
24 | self
25 | }
26 |
27 | /// Set the text for this menu item.
28 | ///
29 | /// See [`MenuItem::set_text`] for more info.
30 | pub fn text>(mut self, text: S) -> Self {
31 | self.text = text.into();
32 | self
33 | }
34 |
35 | /// Enable or disable this menu item.
36 | pub fn enabled(mut self, enabled: bool) -> Self {
37 | self.enabled = enabled;
38 | self
39 | }
40 |
41 | /// Set this menu item accelerator.
42 | pub fn accelerator>(
43 | mut self,
44 | accelerator: Option,
45 | ) -> crate::Result
46 | where
47 | crate::Error: From<>::Error>,
48 | {
49 | self.accelerator = accelerator.map(|a| a.try_into()).transpose()?;
50 | Ok(self)
51 | }
52 |
53 | /// Build this menu item.
54 | pub fn build(self) -> MenuItem {
55 | if let Some(id) = self.id {
56 | MenuItem::with_id(id, self.text, self.enabled, self.accelerator)
57 | } else {
58 | MenuItem::new(self.text, self.enabled, self.accelerator)
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/platform_impl/gtk/icon.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2014-2021 The winit contributors
2 | // Copyright 2021-2022 Tauri Programme within The Commons Conservancy
3 | // SPDX-License-Identifier: Apache-2.0
4 |
5 | use gtk::gdk_pixbuf::{Colorspace, Pixbuf};
6 |
7 | use crate::icon::BadIcon;
8 |
9 | /// An icon used for the window titlebar, taskbar, etc.
10 | #[derive(Debug, Clone)]
11 | pub struct PlatformIcon {
12 | raw: Vec,
13 | width: i32,
14 | height: i32,
15 | row_stride: i32,
16 | }
17 |
18 | impl From for Pixbuf {
19 | fn from(icon: PlatformIcon) -> Self {
20 | Pixbuf::from_mut_slice(
21 | icon.raw,
22 | gtk::gdk_pixbuf::Colorspace::Rgb,
23 | true,
24 | 8,
25 | icon.width,
26 | icon.height,
27 | icon.row_stride,
28 | )
29 | }
30 | }
31 |
32 | impl PlatformIcon {
33 | /// Creates an `Icon` from 32bpp RGBA data.
34 | ///
35 | /// The length of `rgba` must be divisible by 4, and `width * height` must equal
36 | /// `rgba.len() / 4`. Otherwise, this will return a `BadIcon` error.
37 | pub fn from_rgba(rgba: Vec, width: u32, height: u32) -> Result {
38 | let row_stride =
39 | Pixbuf::calculate_rowstride(Colorspace::Rgb, true, 8, width as i32, height as i32);
40 | Ok(Self {
41 | raw: rgba,
42 | width: width as i32,
43 | height: height as i32,
44 | row_stride,
45 | })
46 | }
47 |
48 | pub fn to_pixbuf(&self) -> Pixbuf {
49 | Pixbuf::from_mut_slice(
50 | self.raw.clone(),
51 | gtk::gdk_pixbuf::Colorspace::Rgb,
52 | true,
53 | 8,
54 | self.width,
55 | self.height,
56 | self.row_stride,
57 | )
58 | }
59 |
60 | pub fn to_pixbuf_scale(&self, w: i32, h: i32) -> Pixbuf {
61 | self.to_pixbuf()
62 | .scale_simple(w, h, gtk::gdk_pixbuf::InterpType::Bilinear)
63 | .unwrap()
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/.github/workflows/covector-version-or-publish.yml:
--------------------------------------------------------------------------------
1 | # Copyright 2022-2022 Tauri Programme within The Commons Conservancy
2 | # SPDX-License-Identifier: Apache-2.0
3 | # SPDX-License-Identifier: MIT
4 |
5 | name: covector version or publish
6 |
7 | on:
8 | push:
9 | branches:
10 | - dev
11 |
12 | jobs:
13 | version-or-publish:
14 | runs-on: ubuntu-latest
15 | timeout-minutes: 65
16 | outputs:
17 | change: ${{ steps.covector.outputs.change }}
18 | commandRan: ${{ steps.covector.outputs.commandRan }}
19 | successfulPublish: ${{ steps.covector.outputs.successfulPublish }}
20 |
21 | steps:
22 | - uses: actions/checkout@v4
23 | with:
24 | fetch-depth: 0
25 |
26 | - name: cargo login
27 | run: cargo login ${{ secrets.ORG_CRATES_IO_TOKEN }}
28 |
29 | - name: git config
30 | run: |
31 | git config --global user.name "${{ github.event.pusher.name }}"
32 | git config --global user.email "${{ github.event.pusher.email }}"
33 |
34 | - name: covector version or publish (publish when no change files present)
35 | uses: jbolda/covector/packages/action@covector-v0
36 | id: covector
37 | env:
38 | NODE_AUTH_TOKEN: ${{ secrets.ORG_NPM_TOKEN }}
39 | with:
40 | token: ${{ secrets.GITHUB_TOKEN }}
41 | command: 'version-or-publish'
42 | createRelease: true
43 | recognizeContributors: true
44 |
45 | - name: Sync Cargo.lock
46 | if: steps.covector.outputs.commandRan == 'version'
47 | run: cargo tree --depth 0
48 |
49 | - name: Create Pull Request With Versions Bumped
50 | if: steps.covector.outputs.commandRan == 'version'
51 | uses: tauri-apps/create-pull-request@v3
52 | with:
53 | token: ${{ secrets.GITHUB_TOKEN }}
54 | title: Apply Version Updates From Current Changes
55 | commit-message: 'apply version updates'
56 | labels: 'version updates'
57 | branch: 'release'
58 | body: ${{ steps.covector.outputs.change }}
--------------------------------------------------------------------------------
/src/items/mod.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2022-2022 Tauri Programme within The Commons Conservancy
2 | // SPDX-License-Identifier: Apache-2.0
3 | // SPDX-License-Identifier: MIT
4 |
5 | mod check;
6 | mod icon;
7 | mod normal;
8 | mod predefined;
9 | mod submenu;
10 |
11 | pub use check::*;
12 | pub use icon::*;
13 | pub use normal::*;
14 | pub use predefined::*;
15 | pub use submenu::*;
16 |
17 | #[cfg(test)]
18 | mod test {
19 | use crate::{CheckMenuItem, IconMenuItem, MenuId, MenuItem, PredefinedMenuItem, Submenu};
20 |
21 | #[test]
22 | #[cfg_attr(all(miri, not(target_os = "linux")), ignore)]
23 | fn it_returns_same_id() {
24 | let id = MenuId::new("1");
25 | assert_eq!(id, MenuItem::with_id(id.clone(), "", true, None).id());
26 | assert_eq!(id, Submenu::with_id(id.clone(), "", true).id());
27 | assert_eq!(
28 | id,
29 | CheckMenuItem::with_id(id.clone(), "", true, true, None).id()
30 | );
31 | assert_eq!(
32 | id,
33 | IconMenuItem::with_id(id.clone(), "", true, None, None).id()
34 | );
35 | }
36 |
37 | #[test]
38 | #[cfg_attr(all(miri, not(target_os = "linux")), ignore)]
39 | fn test_convert_from_id_and_into_id() {
40 | let id = "TEST ID";
41 | let expected = MenuId(id.to_string());
42 |
43 | let item = CheckMenuItem::with_id(id, "test", true, true, None);
44 | assert_eq!(item.id(), &expected);
45 | assert_eq!(item.into_id(), expected);
46 |
47 | let item = IconMenuItem::with_id(id, "test", true, None, None);
48 | assert_eq!(item.id(), &expected);
49 | assert_eq!(item.into_id(), expected);
50 |
51 | let item = MenuItem::with_id(id, "test", true, None);
52 | assert_eq!(item.id(), &expected);
53 | assert_eq!(item.into_id(), expected);
54 |
55 | let item = Submenu::with_id(id, "test", true);
56 | assert_eq!(item.id(), &expected);
57 | assert_eq!(item.into_id(), expected);
58 |
59 | let item = PredefinedMenuItem::separator();
60 | assert_eq!(item.id().clone(), item.into_id());
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/builders/check.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2022-2022 Tauri Programme within The Commons Conservancy
2 | // SPDX-License-Identifier: Apache-2.0
3 | // SPDX-License-Identifier: MIT
4 |
5 | use crate::{accelerator::Accelerator, CheckMenuItem, MenuId};
6 |
7 | /// A builder type for [`CheckMenuItem`]
8 | #[derive(Clone, Debug, Default)]
9 | pub struct CheckMenuItemBuilder {
10 | text: String,
11 | enabled: bool,
12 | checked: bool,
13 | accelerator: Option,
14 | id: Option,
15 | }
16 |
17 | impl CheckMenuItemBuilder {
18 | pub fn new() -> Self {
19 | Default::default()
20 | }
21 |
22 | /// Set the id this check menu item.
23 | pub fn id(mut self, id: MenuId) -> Self {
24 | self.id.replace(id);
25 | self
26 | }
27 |
28 | /// Set the text for this check menu item.
29 | ///
30 | /// See [`CheckMenuItem::set_text`] for more info.
31 | pub fn text>(mut self, text: S) -> Self {
32 | self.text = text.into();
33 | self
34 | }
35 |
36 | /// Enable or disable this menu item.
37 | pub fn enabled(mut self, enabled: bool) -> Self {
38 | self.enabled = enabled;
39 | self
40 | }
41 |
42 | /// Check or uncheck this menu item.
43 | pub fn checked(mut self, checked: bool) -> Self {
44 | self.checked = checked;
45 | self
46 | }
47 |
48 | /// Set this check menu item accelerator.
49 | pub fn accelerator>(
50 | mut self,
51 | accelerator: Option,
52 | ) -> crate::Result
53 | where
54 | crate::Error: From<>::Error>,
55 | {
56 | self.accelerator = accelerator.map(|a| a.try_into()).transpose()?;
57 | Ok(self)
58 | }
59 |
60 | /// Build this check menu item.
61 | pub fn build(self) -> CheckMenuItem {
62 | if let Some(id) = self.id {
63 | CheckMenuItem::with_id(id, self.text, self.enabled, self.checked, self.accelerator)
64 | } else {
65 | CheckMenuItem::new(self.text, self.enabled, self.checked, self.accelerator)
66 | }
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/platform_impl/macos/icon.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2022-2022 Tauri Programme within The Commons Conservancy
2 | // SPDX-License-Identifier: Apache-2.0
3 | // SPDX-License-Identifier: MIT
4 |
5 | use objc2::{rc::Retained, AllocAnyThread};
6 | use objc2_app_kit::NSImage;
7 | use objc2_core_foundation::CGFloat;
8 | use objc2_foundation::{NSData, NSSize};
9 |
10 | use crate::icon::{BadIcon, RgbaIcon};
11 | use std::io::Cursor;
12 |
13 | #[derive(Debug, Clone)]
14 | pub struct PlatformIcon(RgbaIcon);
15 |
16 | impl PlatformIcon {
17 | pub fn from_rgba(rgba: Vec, width: u32, height: u32) -> Result {
18 | Ok(PlatformIcon(RgbaIcon::from_rgba(rgba, width, height)?))
19 | }
20 |
21 | pub fn get_size(&self) -> (u32, u32) {
22 | (self.0.width, self.0.height)
23 | }
24 |
25 | pub fn to_png(&self) -> Vec {
26 | let mut png = Vec::new();
27 |
28 | {
29 | let mut encoder =
30 | png::Encoder::new(Cursor::new(&mut png), self.0.width as _, self.0.height as _);
31 | encoder.set_color(png::ColorType::Rgba);
32 | encoder.set_depth(png::BitDepth::Eight);
33 |
34 | let mut writer = encoder.write_header().unwrap();
35 | writer.write_image_data(&self.0.rgba).unwrap();
36 | }
37 |
38 | png
39 | }
40 |
41 | pub fn to_nsimage(&self, fixed_height: Option) -> Retained {
42 | let (width, height) = self.get_size();
43 | let icon = self.to_png();
44 |
45 | let (icon_width, icon_height) = match fixed_height {
46 | Some(fixed_height) => {
47 | let icon_height: CGFloat = fixed_height as CGFloat;
48 | let icon_width: CGFloat = (width as CGFloat) / (height as CGFloat / icon_height);
49 |
50 | (icon_width, icon_height)
51 | }
52 |
53 | None => (width as CGFloat, height as CGFloat),
54 | };
55 |
56 | let nsdata = NSData::with_bytes(&icon);
57 |
58 | let nsimage = NSImage::initWithData(NSImage::alloc(), &nsdata).unwrap();
59 | let new_size = NSSize::new(icon_width, icon_height);
60 | unsafe { nsimage.setSize(new_size) };
61 |
62 | nsimage
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/menu_id.rs:
--------------------------------------------------------------------------------
1 | use std::{convert::Infallible, str::FromStr};
2 |
3 | /// An unique id that is associated with a menu or a menu item.
4 | #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Default, Hash)]
5 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
6 | pub struct MenuId(pub String);
7 |
8 | impl MenuId {
9 | /// Create a new menu id.
10 | pub fn new>(id: S) -> Self {
11 | Self(id.as_ref().to_string())
12 | }
13 | }
14 |
15 | impl AsRef for MenuId {
16 | fn as_ref(&self) -> &str {
17 | self.0.as_ref()
18 | }
19 | }
20 |
21 | impl From for MenuId {
22 | fn from(value: T) -> Self {
23 | Self::new(value.to_string())
24 | }
25 | }
26 |
27 | impl FromStr for MenuId {
28 | type Err = Infallible;
29 |
30 | fn from_str(s: &str) -> std::result::Result {
31 | Ok(Self::new(s))
32 | }
33 | }
34 |
35 | impl PartialEq<&str> for MenuId {
36 | fn eq(&self, other: &&str) -> bool {
37 | self.0 == *other
38 | }
39 | }
40 |
41 | impl PartialEq<&str> for &MenuId {
42 | fn eq(&self, other: &&str) -> bool {
43 | self.0 == *other
44 | }
45 | }
46 |
47 | impl PartialEq for MenuId {
48 | fn eq(&self, other: &String) -> bool {
49 | self.0 == *other
50 | }
51 | }
52 |
53 | impl PartialEq for &MenuId {
54 | fn eq(&self, other: &String) -> bool {
55 | self.0 == *other
56 | }
57 | }
58 |
59 | impl PartialEq<&String> for MenuId {
60 | fn eq(&self, other: &&String) -> bool {
61 | self.0 == **other
62 | }
63 | }
64 |
65 | impl PartialEq<&MenuId> for MenuId {
66 | fn eq(&self, other: &&MenuId) -> bool {
67 | other.0 == self.0
68 | }
69 | }
70 |
71 | #[cfg(test)]
72 | mod test {
73 | use crate::MenuId;
74 |
75 | #[test]
76 | fn is_eq() {
77 | assert_eq!(MenuId::new("t"), "t",);
78 | assert_eq!(MenuId::new("t"), String::from("t"));
79 | assert_eq!(MenuId::new("t"), &String::from("t"));
80 | assert_eq!(MenuId::new("t"), MenuId::new("t"));
81 | assert_eq!(MenuId::new("t"), &MenuId::new("t"));
82 | assert_eq!(&MenuId::new("t"), &MenuId::new("t"));
83 | assert_eq!(MenuId::new("t").as_ref(), "t");
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "muda"
3 | version = "0.17.1"
4 | description = "Menu Utilities for Desktop Applications"
5 | edition = "2021"
6 | keywords = ["windowing", "menu"]
7 | license = "Apache-2.0 OR MIT"
8 | readme = "README.md"
9 | repository = "https://github.com/tauri-apps/muda"
10 | documentation = "https://docs.rs/muda"
11 | categories = ["gui"]
12 | rust-version = "1.71"
13 | include = ["README.md", "src/**/*.rs", "Cargo.toml", "LICENSE-APACHE", "LICENSE-MIT", "LICENSE.spdx"]
14 |
15 | [features]
16 | default = ["libxdo", "gtk"]
17 | libxdo = ["dep:libxdo"]
18 | gtk = ["dep:gtk"]
19 | common-controls-v6 = []
20 | serde = ["dep:serde", "dpi/serde"]
21 |
22 | [dependencies]
23 | crossbeam-channel = "0.5"
24 | keyboard-types = "0.7"
25 | once_cell = "1"
26 | thiserror = "2"
27 | serde = { version = "1", optional = true }
28 | dpi = "0.1"
29 |
30 | [target.'cfg(target_os = "windows")'.dependencies.windows-sys]
31 | version = "0.60"
32 | features = [
33 | "Win32_UI_WindowsAndMessaging",
34 | "Win32_Foundation",
35 | "Win32_Graphics_Gdi",
36 | "Win32_UI_Shell",
37 | "Win32_Globalization",
38 | "Win32_UI_Input_KeyboardAndMouse",
39 | "Win32_System_SystemServices",
40 | "Win32_UI_Accessibility",
41 | "Win32_UI_HiDpi",
42 | "Win32_System_LibraryLoader",
43 | "Win32_UI_Controls",
44 | ]
45 |
46 | [target.'cfg(target_os = "linux")'.dependencies]
47 | gtk = { version = "0.18", optional = true }
48 | libxdo = { version = "0.6.0", optional = true }
49 |
50 | [target.'cfg(target_os = "macos")'.dependencies]
51 | objc2 = "0.6.0"
52 | objc2-core-foundation = { version = "0.3.0", default-features = false, features = [
53 | "std",
54 | "CFCGTypes",
55 | ] }
56 | objc2-foundation = { version = "0.3.0", default-features = false, features = [
57 | "std",
58 | "NSAttributedString",
59 | "NSData",
60 | "NSDictionary",
61 | "NSGeometry",
62 | "NSString",
63 | "NSThread",
64 | ] }
65 | objc2-app-kit = { version = "0.3.0", default-features = false, features = [
66 | "std",
67 | "objc2-core-foundation",
68 | "NSApplication",
69 | "NSCell",
70 | "NSEvent",
71 | "NSImage",
72 | "NSMenu",
73 | "NSMenuItem",
74 | "NSResponder",
75 | "NSRunningApplication",
76 | "NSView",
77 | "NSWindow",
78 | ] }
79 | png = "0.17"
80 |
81 | [dev-dependencies]
82 | winit = "0.30"
83 | tao = "0.33"
84 | wry = "0.45"
85 | image = "0.25"
86 |
--------------------------------------------------------------------------------
/src/builders/submenu.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2022-2022 Tauri Programme within The Commons Conservancy
2 | // SPDX-License-Identifier: Apache-2.0
3 | // SPDX-License-Identifier: MIT
4 |
5 | use crate::{Icon, IsMenuItem, MenuId, NativeIcon, Submenu};
6 |
7 | /// A builder type for [`Submenu`]
8 | #[derive(Clone, Default)]
9 | pub struct SubmenuBuilder<'a> {
10 | text: String,
11 | enabled: bool,
12 | id: Option,
13 | items: Vec<&'a dyn IsMenuItem>,
14 | icon: Option,
15 | native_icon: Option,
16 | }
17 |
18 | impl std::fmt::Debug for SubmenuBuilder<'_> {
19 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
20 | f.debug_struct("SubmenuBuilder")
21 | .field("text", &self.text)
22 | .field("enabled", &self.enabled)
23 | .finish()
24 | }
25 | }
26 |
27 | impl<'a> SubmenuBuilder<'a> {
28 | pub fn new() -> Self {
29 | Default::default()
30 | }
31 |
32 | /// Set the id this submenu.
33 | pub fn id(mut self, id: MenuId) -> Self {
34 | self.id.replace(id);
35 | self
36 | }
37 |
38 | /// Set the text for this submenu.
39 | ///
40 | /// See [`Submenu::set_text`] for more info.
41 | pub fn text>(mut self, text: S) -> Self {
42 | self.text = text.into();
43 | self
44 | }
45 |
46 | /// Enable or disable this submenu.
47 | pub fn enabled(mut self, enabled: bool) -> Self {
48 | self.enabled = enabled;
49 | self
50 | }
51 |
52 | /// Add an item to this submenu.
53 | pub fn item(mut self, item: &'a dyn IsMenuItem) -> Self {
54 | self.items.push(item);
55 | self
56 | }
57 |
58 | /// Add these items to this submenu.
59 | pub fn items(mut self, items: &[&'a dyn IsMenuItem]) -> Self {
60 | self.items.extend_from_slice(items);
61 | self
62 | }
63 |
64 | /// Set an icon for this submenu.
65 | pub fn icon(mut self, icon: Icon) -> Self {
66 | self.icon = Some(icon);
67 | self
68 | }
69 |
70 | /// Set a native icon for this submenu.
71 | pub fn native_icon(mut self, icon: NativeIcon) -> Self {
72 | self.native_icon = Some(icon);
73 | self
74 | }
75 |
76 | /// Build this menu item.
77 | pub fn build(self) -> crate::Result {
78 | let submenu = if let Some(id) = self.id {
79 | Submenu::with_id_and_items(id, self.text, self.enabled, &self.items)?
80 | } else {
81 | Submenu::with_items(self.text, self.enabled, &self.items)?
82 | };
83 |
84 | if let Some(icon) = self.icon {
85 | submenu.set_icon(Some(icon));
86 | }
87 |
88 | if let Some(native_icon) = self.native_icon {
89 | submenu.set_native_icon(Some(native_icon));
90 | }
91 |
92 | Ok(submenu)
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/src/builders/icon.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2022-2022 Tauri Programme within The Commons Conservancy
2 | // SPDX-License-Identifier: Apache-2.0
3 | // SPDX-License-Identifier: MIT
4 |
5 | use crate::{
6 | accelerator::Accelerator,
7 | icon::{Icon, NativeIcon},
8 | IconMenuItem, MenuId,
9 | };
10 |
11 | /// A builder type for [`IconMenuItem`]
12 | #[derive(Clone, Debug, Default)]
13 | pub struct IconMenuItemBuilder {
14 | text: String,
15 | enabled: bool,
16 | id: Option,
17 | accelerator: Option,
18 | icon: Option,
19 | native_icon: Option,
20 | }
21 |
22 | impl IconMenuItemBuilder {
23 | pub fn new() -> Self {
24 | Default::default()
25 | }
26 |
27 | /// Set the id this icon menu item.
28 | pub fn id(mut self, id: MenuId) -> Self {
29 | self.id.replace(id);
30 | self
31 | }
32 |
33 | /// Set the text for this icon menu item.
34 | ///
35 | /// See [`IconMenuItem::set_text`] for more info.
36 | pub fn text>(mut self, text: S) -> Self {
37 | self.text = text.into();
38 | self
39 | }
40 |
41 | /// Enable or disable this menu item.
42 | pub fn enabled(mut self, enabled: bool) -> Self {
43 | self.enabled = enabled;
44 | self
45 | }
46 |
47 | /// Set this icon menu item icon.
48 | pub fn icon(mut self, icon: Option) -> Self {
49 | self.icon = icon;
50 | self.native_icon = None;
51 | self
52 | }
53 |
54 | /// Set this icon menu item native icon.
55 | pub fn native_icon(mut self, icon: Option) -> Self {
56 | self.native_icon = icon;
57 | self.icon = None;
58 | self
59 | }
60 |
61 | /// Set this icon menu item accelerator.
62 | pub fn accelerator>(
63 | mut self,
64 | accelerator: Option,
65 | ) -> crate::Result
66 | where
67 | crate::Error: From<>::Error>,
68 | {
69 | self.accelerator = accelerator.map(|a| a.try_into()).transpose()?;
70 | Ok(self)
71 | }
72 |
73 | /// Build this icon menu item.
74 | pub fn build(self) -> IconMenuItem {
75 | if let Some(id) = self.id {
76 | if self.icon.is_some() {
77 | IconMenuItem::with_id(id, self.text, self.enabled, self.icon, self.accelerator)
78 | } else {
79 | IconMenuItem::with_id_and_native_icon(
80 | id,
81 | self.text,
82 | self.enabled,
83 | self.native_icon,
84 | self.accelerator,
85 | )
86 | }
87 | } else if self.icon.is_some() {
88 | IconMenuItem::new(self.text, self.enabled, self.icon, self.accelerator)
89 | } else {
90 | IconMenuItem::with_native_icon(
91 | self.text,
92 | self.enabled,
93 | self.native_icon,
94 | self.accelerator,
95 | )
96 | }
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/src/items/normal.rs:
--------------------------------------------------------------------------------
1 | use std::{cell::RefCell, mem, rc::Rc};
2 |
3 | use crate::{accelerator::Accelerator, sealed::IsMenuItemBase, IsMenuItem, MenuId, MenuItemKind};
4 |
5 | /// A menu item inside a [`Menu`] or [`Submenu`] and contains only text.
6 | ///
7 | /// [`Menu`]: crate::Menu
8 | /// [`Submenu`]: crate::Submenu
9 | #[derive(Clone)]
10 | pub struct MenuItem {
11 | pub(crate) id: Rc,
12 | pub(crate) inner: Rc>,
13 | }
14 |
15 | impl IsMenuItemBase for MenuItem {}
16 | impl IsMenuItem for MenuItem {
17 | fn kind(&self) -> MenuItemKind {
18 | MenuItemKind::MenuItem(self.clone())
19 | }
20 |
21 | fn id(&self) -> &MenuId {
22 | self.id()
23 | }
24 |
25 | fn into_id(self) -> MenuId {
26 | self.into_id()
27 | }
28 | }
29 |
30 | impl MenuItem {
31 | /// Create a new menu item.
32 | ///
33 | /// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic
34 | /// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`.
35 | pub fn new>(text: S, enabled: bool, accelerator: Option) -> Self {
36 | let item = crate::platform_impl::MenuChild::new(text.as_ref(), enabled, accelerator, None);
37 | Self {
38 | id: Rc::new(item.id().clone()),
39 | inner: Rc::new(RefCell::new(item)),
40 | }
41 | }
42 |
43 | /// Create a new menu item with the specified id.
44 | ///
45 | /// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic
46 | /// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`.
47 | pub fn with_id, S: AsRef>(
48 | id: I,
49 | text: S,
50 | enabled: bool,
51 | accelerator: Option,
52 | ) -> Self {
53 | let id = id.into();
54 | Self {
55 | id: Rc::new(id.clone()),
56 | inner: Rc::new(RefCell::new(crate::platform_impl::MenuChild::new(
57 | text.as_ref(),
58 | enabled,
59 | accelerator,
60 | Some(id),
61 | ))),
62 | }
63 | }
64 |
65 | /// Returns a unique identifier associated with this menu item.
66 | pub fn id(&self) -> &MenuId {
67 | &self.id
68 | }
69 |
70 | /// Set the text for this menu item.
71 | pub fn text(&self) -> String {
72 | self.inner.borrow().text()
73 | }
74 |
75 | /// Set the text for this menu item. `text` could optionally contain
76 | /// an `&` before a character to assign this character as the mnemonic
77 | /// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`.
78 | pub fn set_text>(&self, text: S) {
79 | self.inner.borrow_mut().set_text(text.as_ref())
80 | }
81 |
82 | /// Get whether this menu item is enabled or not.
83 | pub fn is_enabled(&self) -> bool {
84 | self.inner.borrow().is_enabled()
85 | }
86 |
87 | /// Enable or disable this menu item.
88 | pub fn set_enabled(&self, enabled: bool) {
89 | self.inner.borrow_mut().set_enabled(enabled)
90 | }
91 |
92 | /// Set this menu item accelerator.
93 | pub fn set_accelerator(&self, accelerator: Option) -> crate::Result<()> {
94 | self.inner.borrow_mut().set_accelerator(accelerator)
95 | }
96 |
97 | /// Convert this menu item into its menu ID.
98 | pub fn into_id(mut self) -> MenuId {
99 | // Note: `Rc::into_inner` is available from Rust 1.70
100 | if let Some(id) = Rc::get_mut(&mut self.id) {
101 | mem::take(id)
102 | } else {
103 | self.id().clone()
104 | }
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/src/platform_impl/mod.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2022-2022 Tauri Programme within The Commons Conservancy
2 | // SPDX-License-Identifier: Apache-2.0
3 | // SPDX-License-Identifier: MIT
4 |
5 | #[cfg(target_os = "windows")]
6 | #[path = "windows/mod.rs"]
7 | mod platform;
8 | #[cfg(all(target_os = "linux", feature = "gtk"))]
9 | #[path = "gtk/mod.rs"]
10 | mod platform;
11 | #[cfg(target_os = "macos")]
12 | #[path = "macos/mod.rs"]
13 | mod platform;
14 |
15 | use std::{
16 | cell::{Ref, RefCell, RefMut},
17 | rc::Rc,
18 | };
19 |
20 | use crate::{items::*, IsMenuItem, MenuItemKind, MenuItemType};
21 |
22 | pub(crate) use self::platform::*;
23 |
24 | impl dyn IsMenuItem + '_ {
25 | fn child(&self) -> Rc> {
26 | match self.kind() {
27 | MenuItemKind::MenuItem(i) => i.inner,
28 | MenuItemKind::Submenu(i) => i.inner,
29 | MenuItemKind::Predefined(i) => i.inner,
30 | MenuItemKind::Check(i) => i.inner,
31 | MenuItemKind::Icon(i) => i.inner,
32 | }
33 | }
34 | }
35 |
36 | /// Internal utilities
37 | impl MenuChild {
38 | fn kind(&self, c: Rc>) -> MenuItemKind {
39 | match self.item_type() {
40 | MenuItemType::Submenu => {
41 | let id = c.borrow().id().clone();
42 | MenuItemKind::Submenu(Submenu {
43 | id: Rc::new(id),
44 | inner: c,
45 | })
46 | }
47 | MenuItemType::MenuItem => {
48 | let id = c.borrow().id().clone();
49 | MenuItemKind::MenuItem(MenuItem {
50 | id: Rc::new(id),
51 | inner: c,
52 | })
53 | }
54 | MenuItemType::Predefined => {
55 | let id = c.borrow().id().clone();
56 | MenuItemKind::Predefined(PredefinedMenuItem {
57 | id: Rc::new(id),
58 | inner: c,
59 | })
60 | }
61 | MenuItemType::Check => {
62 | let id = c.borrow().id().clone();
63 | MenuItemKind::Check(CheckMenuItem {
64 | id: Rc::new(id),
65 | inner: c,
66 | })
67 | }
68 | MenuItemType::Icon => {
69 | let id = c.borrow().id().clone();
70 | MenuItemKind::Icon(IconMenuItem {
71 | id: Rc::new(id),
72 | inner: c,
73 | })
74 | }
75 | }
76 | }
77 | }
78 |
79 | #[allow(unused)]
80 | impl MenuItemKind {
81 | pub(crate) fn as_ref(&self) -> &dyn IsMenuItem {
82 | match self {
83 | MenuItemKind::MenuItem(i) => i,
84 | MenuItemKind::Submenu(i) => i,
85 | MenuItemKind::Predefined(i) => i,
86 | MenuItemKind::Check(i) => i,
87 | MenuItemKind::Icon(i) => i,
88 | }
89 | }
90 |
91 | pub(crate) fn child(&self) -> Ref<'_, MenuChild> {
92 | match self {
93 | MenuItemKind::MenuItem(i) => i.inner.borrow(),
94 | MenuItemKind::Submenu(i) => i.inner.borrow(),
95 | MenuItemKind::Predefined(i) => i.inner.borrow(),
96 | MenuItemKind::Check(i) => i.inner.borrow(),
97 | MenuItemKind::Icon(i) => i.inner.borrow(),
98 | }
99 | }
100 |
101 | pub(crate) fn child_mut(&self) -> RefMut<'_, MenuChild> {
102 | match self {
103 | MenuItemKind::MenuItem(i) => i.inner.borrow_mut(),
104 | MenuItemKind::Submenu(i) => i.inner.borrow_mut(),
105 | MenuItemKind::Predefined(i) => i.inner.borrow_mut(),
106 | MenuItemKind::Check(i) => i.inner.borrow_mut(),
107 | MenuItemKind::Icon(i) => i.inner.borrow_mut(),
108 | }
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ### muda
2 |
3 | Menu Utilities library for Desktop Applications.
4 |
5 | [](https://docs.rs/muda/latest/muda/)
6 |
7 | ## Platforms supported:
8 |
9 | - Windows
10 | - macOS
11 | - Linux (gtk Only)
12 |
13 | ## Platform-specific notes:
14 |
15 | - On Windows, accelerators don't work unless the win32 message loop calls
16 | [`TranslateAcceleratorW`](https://docs.rs/windows-sys/latest/windows_sys/Win32/UI/WindowsAndMessaging/fn.TranslateAcceleratorW.html).
17 | See [`Menu::init_for_hwnd`](https://docs.rs/muda/latest/x86_64-pc-windows-msvc/muda/struct.Menu.html#method.init_for_hwnd) for more details
18 |
19 | ### Cargo Features
20 |
21 | - `common-controls-v6`: Use `TaskDialogIndirect` API from `ComCtl32.dll` v6 on Windows for showing the predefined `About` menu item dialog.
22 | - `libxdo`: Enables linking to `libxdo` on Linux which is used for the predefined `Copy`, `Cut`, `Paste` and `SelectAll` menu item.
23 | - `serde`: Enables de/serializing the dpi types.
24 | - `gtk`: Enables the `gtk` crate dependency on Linux. This is required for `muda` to function properly on Linux.
25 |
26 | ## Dependencies (Linux Only)
27 |
28 | `gtk` is used for menus and `libxdo` is used to make the predfined `Copy`, `Cut`, `Paste` and `SelectAll` menu items work. Be sure to install following packages before building:
29 |
30 | #### Arch Linux / Manjaro:
31 |
32 | ```sh
33 | pacman -S gtk3 xdotool
34 | ```
35 |
36 | #### Debian / Ubuntu:
37 |
38 | ```sh
39 | sudo apt install libgtk-3-dev libxdo-dev
40 | ```
41 |
42 | ## Example
43 |
44 | Create the menu and add your items
45 |
46 | ```rs
47 | let menu = Menu::new();
48 | let menu_item2 = MenuItem::new("Menu item #2", false, None);
49 | let submenu = Submenu::with_items("Submenu Outer", true,&[
50 | &MenuItem::new("Menu item #1", true, Some(Accelerator::new(Some(Modifiers::ALT), Code::KeyD))),
51 | &PredefinedMenuItem::separator(),
52 | &menu_item2,
53 | &MenuItem::new("Menu item #3", true, None),
54 | &PredefinedMenuItem::separator(),
55 | &Submenu::with_items("Submenu Inner", true,&[
56 | &MenuItem::new("Submenu item #1", true, None),
57 | &PredefinedMenuItem::separator(),
58 | &menu_item2,
59 | ])
60 | ]);
61 |
62 | ```
63 |
64 | Then add your root menu to a Window on Windows and Linux
65 | or use it as your global app menu on macOS
66 |
67 | ```rs
68 | // --snip--
69 | #[cfg(target_os = "windows")]
70 | unsafe { menu.init_for_hwnd(window.hwnd() as isize) };
71 | #[cfg(target_os = "linux")]
72 | menu.init_for_gtk_window(>k_window, Some(&vertical_gtk_box));
73 | #[cfg(target_os = "macos")]
74 | menu.init_for_nsapp();
75 | ```
76 |
77 | ## Context menus (Popup menus)
78 |
79 | You can also use a [`Menu`] or a [`Submenu`] show a context menu.
80 |
81 | ```rs
82 | // --snip--
83 | let position = muda::PhysicalPosition { x: 100., y: 120. };
84 | #[cfg(target_os = "windows")]
85 | unsafe { menu.show_context_menu_for_hwnd(window.hwnd() as isize, Some(position.into())) };
86 | #[cfg(target_os = "linux")]
87 | menu.show_context_menu_for_gtk_window(>k_window, Some(position.into()));
88 | #[cfg(target_os = "macos")]
89 | unsafe { menu.show_context_menu_for_nsview(nsview, Some(position.into())) };
90 | ```
91 |
92 | ## Processing menu events
93 |
94 | You can use `MenuEvent::receiver` to get a reference to the `MenuEventReceiver`
95 | which you can use to listen to events when a menu item is activated
96 |
97 | ```rs
98 | if let Ok(event) = MenuEvent::receiver().try_recv() {
99 | match event.id {
100 | _ if event.id == save_item.id() => {
101 | println!("Save menu item activated");
102 | },
103 | _ => {}
104 | }
105 | }
106 | ```
107 |
108 | ### Note for [winit] or [tao] users:
109 |
110 | You should use [`MenuEvent::set_event_handler`] and forward
111 | the menu events to the event loop by using [`EventLoopProxy`]
112 | so that the event loop is awakened on each menu event.
113 |
114 | ```rust
115 | enum UserEvent {
116 | MenuEvent(muda::MenuEvent)
117 | }
118 |
119 | let event_loop = EventLoop::::with_user_event().build().unwrap();
120 |
121 | let proxy = event_loop.create_proxy();
122 | muda::MenuEvent::set_event_handler(Some(move |event| {
123 | proxy.send_event(UserEvent::MenuEvent(event));
124 | }));
125 | ```
126 |
127 | [`EventLoopProxy`]: https://docs.rs/winit/latest/winit/event_loop/struct.EventLoopProxy.html
128 | [winit]: https://docs.rs/winit
129 | [tao]: https://docs.rs/tao
130 |
131 | ## License
132 |
133 | Apache-2.0/MIT
134 |
--------------------------------------------------------------------------------
/src/items/check.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2022-2022 Tauri Programme within The Commons Conservancy
2 | // SPDX-License-Identifier: Apache-2.inner
3 | // SPDX-License-Identifier: MIT
4 |
5 | use std::{cell::RefCell, mem, rc::Rc};
6 |
7 | use crate::{accelerator::Accelerator, sealed::IsMenuItemBase, IsMenuItem, MenuId, MenuItemKind};
8 |
9 | /// A check menu item inside a [`Menu`] or [`Submenu`]
10 | /// and usually contains a text and a check mark or a similar toggle
11 | /// that corresponds to a checked and unchecked states.
12 | ///
13 | /// [`Menu`]: crate::Menu
14 | /// [`Submenu`]: crate::Submenu
15 | #[derive(Clone)]
16 | pub struct CheckMenuItem {
17 | pub(crate) id: Rc,
18 | pub(crate) inner: Rc>,
19 | }
20 |
21 | impl IsMenuItemBase for CheckMenuItem {}
22 | impl IsMenuItem for CheckMenuItem {
23 | fn kind(&self) -> MenuItemKind {
24 | MenuItemKind::Check(self.clone())
25 | }
26 |
27 | fn id(&self) -> &MenuId {
28 | self.id()
29 | }
30 |
31 | fn into_id(self) -> MenuId {
32 | self.into_id()
33 | }
34 | }
35 |
36 | impl CheckMenuItem {
37 | /// Create a new check menu item.
38 | ///
39 | /// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic
40 | /// for this check menu item. To display a `&` without assigning a mnemenonic, use `&&`.
41 | pub fn new>(
42 | text: S,
43 | enabled: bool,
44 | checked: bool,
45 | accelerator: Option,
46 | ) -> Self {
47 | let item = crate::platform_impl::MenuChild::new_check(
48 | text.as_ref(),
49 | enabled,
50 | checked,
51 | accelerator,
52 | None,
53 | );
54 | Self {
55 | id: Rc::new(item.id().clone()),
56 | inner: Rc::new(RefCell::new(item)),
57 | }
58 | }
59 |
60 | /// Create a new check menu item with the specified id.
61 | ///
62 | /// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic
63 | /// for this check menu item. To display a `&` without assigning a mnemenonic, use `&&`.
64 | pub fn with_id, S: AsRef>(
65 | id: I,
66 | text: S,
67 | enabled: bool,
68 | checked: bool,
69 | accelerator: Option,
70 | ) -> Self {
71 | let id = id.into();
72 | Self {
73 | id: Rc::new(id.clone()),
74 | inner: Rc::new(RefCell::new(crate::platform_impl::MenuChild::new_check(
75 | text.as_ref(),
76 | enabled,
77 | checked,
78 | accelerator,
79 | Some(id),
80 | ))),
81 | }
82 | }
83 |
84 | /// Returns a unique identifier associated with this submenu.
85 | pub fn id(&self) -> &MenuId {
86 | &self.id
87 | }
88 |
89 | /// Get the text for this check menu item.
90 | pub fn text(&self) -> String {
91 | self.inner.borrow().text()
92 | }
93 |
94 | /// Set the text for this check menu item. `text` could optionally contain
95 | /// an `&` before a character to assign this character as the mnemonic
96 | /// for this check menu item. To display a `&` without assigning a mnemenonic, use `&&`.
97 | pub fn set_text>(&self, text: S) {
98 | self.inner.borrow_mut().set_text(text.as_ref())
99 | }
100 |
101 | /// Get whether this check menu item is enabled or not.
102 | pub fn is_enabled(&self) -> bool {
103 | self.inner.borrow().is_enabled()
104 | }
105 |
106 | /// Enable or disable this check menu item.
107 | pub fn set_enabled(&self, enabled: bool) {
108 | self.inner.borrow_mut().set_enabled(enabled)
109 | }
110 |
111 | /// Set this check menu item accelerator.
112 | pub fn set_accelerator(&self, accelerator: Option) -> crate::Result<()> {
113 | self.inner.borrow_mut().set_accelerator(accelerator)
114 | }
115 |
116 | /// Get whether this check menu item is checked or not.
117 | pub fn is_checked(&self) -> bool {
118 | self.inner.borrow().is_checked()
119 | }
120 |
121 | /// Check or Uncheck this check menu item.
122 | pub fn set_checked(&self, checked: bool) {
123 | self.inner.borrow_mut().set_checked(checked)
124 | }
125 |
126 | /// Convert this menu item into its menu ID.
127 | pub fn into_id(mut self) -> MenuId {
128 | // Note: `Rc::into_inner` is available from Rust 1.70
129 | if let Some(id) = Rc::get_mut(&mut self.id) {
130 | mem::take(id)
131 | } else {
132 | self.id().clone()
133 | }
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/src/platform_impl/macos/accelerator.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2022-2022 Tauri Programme within The Commons Conservancy
2 | // SPDX-License-Identifier: Apache-2.0
3 | // SPDX-License-Identifier: MIT
4 |
5 | use keyboard_types::{Code, Modifiers};
6 | use objc2_app_kit::NSEventModifierFlags;
7 |
8 | use crate::accelerator::{Accelerator, AcceleratorParseError};
9 |
10 | impl Accelerator {
11 | /// Return the string value of this hotkey, without modifiers.
12 | ///
13 | /// Returns the empty string if no key equivalent is known.
14 | pub fn key_equivalent(self) -> Result {
15 | Ok(match self.key {
16 | Code::KeyA => "a".into(),
17 | Code::KeyB => "b".into(),
18 | Code::KeyC => "c".into(),
19 | Code::KeyD => "d".into(),
20 | Code::KeyE => "e".into(),
21 | Code::KeyF => "f".into(),
22 | Code::KeyG => "g".into(),
23 | Code::KeyH => "h".into(),
24 | Code::KeyI => "i".into(),
25 | Code::KeyJ => "j".into(),
26 | Code::KeyK => "k".into(),
27 | Code::KeyL => "l".into(),
28 | Code::KeyM => "m".into(),
29 | Code::KeyN => "n".into(),
30 | Code::KeyO => "o".into(),
31 | Code::KeyP => "p".into(),
32 | Code::KeyQ => "q".into(),
33 | Code::KeyR => "r".into(),
34 | Code::KeyS => "s".into(),
35 | Code::KeyT => "t".into(),
36 | Code::KeyU => "u".into(),
37 | Code::KeyV => "v".into(),
38 | Code::KeyW => "w".into(),
39 | Code::KeyX => "x".into(),
40 | Code::KeyY => "y".into(),
41 | Code::KeyZ => "z".into(),
42 | Code::Digit0 => "0".into(),
43 | Code::Digit1 => "1".into(),
44 | Code::Digit2 => "2".into(),
45 | Code::Digit3 => "3".into(),
46 | Code::Digit4 => "4".into(),
47 | Code::Digit5 => "5".into(),
48 | Code::Digit6 => "6".into(),
49 | Code::Digit7 => "7".into(),
50 | Code::Digit8 => "8".into(),
51 | Code::Digit9 => "9".into(),
52 | Code::Comma => ",".into(),
53 | Code::Minus => "-".into(),
54 | Code::Period => ".".into(),
55 | Code::Space => "\u{0020}".into(),
56 | Code::Equal => "=".into(),
57 | Code::Semicolon => ";".into(),
58 | Code::Slash => "/".into(),
59 | Code::Backslash => "\\".into(),
60 | Code::Quote => "\'".into(),
61 | Code::Backquote => "`".into(),
62 | Code::BracketLeft => "[".into(),
63 | Code::BracketRight => "]".into(),
64 | Code::Tab => "⇥".into(),
65 | Code::Escape => "\u{001b}".into(),
66 | // from NSText.h
67 | Code::Enter => "\u{0003}".into(),
68 | Code::Backspace => "\u{0008}".into(),
69 | Code::Delete => "\u{007f}".into(),
70 | // from NSEvent.h
71 | Code::Insert => "\u{F727}".into(),
72 | Code::Home => "\u{F729}".into(),
73 | Code::End => "\u{F72B}".into(),
74 | Code::PageUp => "\u{F72C}".into(),
75 | Code::PageDown => "\u{F72D}".into(),
76 | Code::PrintScreen => "\u{F72E}".into(),
77 | Code::ScrollLock => "\u{F72F}".into(),
78 | Code::ArrowUp => "\u{F700}".into(),
79 | Code::ArrowDown => "\u{F701}".into(),
80 | Code::ArrowLeft => "\u{F702}".into(),
81 | Code::ArrowRight => "\u{F703}".into(),
82 | Code::F1 => "\u{F704}".into(),
83 | Code::F2 => "\u{F705}".into(),
84 | Code::F3 => "\u{F706}".into(),
85 | Code::F4 => "\u{F707}".into(),
86 | Code::F5 => "\u{F708}".into(),
87 | Code::F6 => "\u{F709}".into(),
88 | Code::F7 => "\u{F70A}".into(),
89 | Code::F8 => "\u{F70B}".into(),
90 | Code::F9 => "\u{F70C}".into(),
91 | Code::F10 => "\u{F70D}".into(),
92 | Code::F11 => "\u{F70E}".into(),
93 | Code::F12 => "\u{F70F}".into(),
94 | Code::F13 => "\u{F710}".into(),
95 | Code::F14 => "\u{F711}".into(),
96 | Code::F15 => "\u{F712}".into(),
97 | Code::F16 => "\u{F713}".into(),
98 | Code::F17 => "\u{F714}".into(),
99 | Code::F18 => "\u{F715}".into(),
100 | Code::F19 => "\u{F716}".into(),
101 | Code::F20 => "\u{F717}".into(),
102 | Code::F21 => "\u{F718}".into(),
103 | Code::F22 => "\u{F719}".into(),
104 | Code::F23 => "\u{F71A}".into(),
105 | Code::F24 => "\u{F71B}".into(),
106 | key => return Err(AcceleratorParseError::UnsupportedKey(key.to_string())),
107 | })
108 | }
109 |
110 | /// Return the modifiers of this hotkey, as an NSEventModifierFlags bitflag.
111 | pub fn key_modifier_mask(self) -> NSEventModifierFlags {
112 | let mods: Modifiers = self.mods;
113 | let mut flags = NSEventModifierFlags::empty();
114 | if mods.contains(Modifiers::SHIFT) {
115 | flags.insert(NSEventModifierFlags::Shift);
116 | }
117 | if mods.contains(Modifiers::SUPER) {
118 | flags.insert(NSEventModifierFlags::Command);
119 | }
120 | if mods.contains(Modifiers::ALT) {
121 | flags.insert(NSEventModifierFlags::Option);
122 | }
123 | if mods.contains(Modifiers::CONTROL) {
124 | flags.insert(NSEventModifierFlags::Control);
125 | }
126 | flags
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/src/platform_impl/gtk/accelerator.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2022-2022 Tauri Programme within The Commons Conservancy
2 | // SPDX-License-Identifier: Apache-2.0
3 | // SPDX-License-Identifier: MIT
4 |
5 | use gtk::gdk;
6 | use keyboard_types::{Code, Modifiers};
7 |
8 | use crate::accelerator::{Accelerator, AcceleratorParseError};
9 |
10 | pub fn to_gtk_mnemonic>(string: S) -> String {
11 | string
12 | .as_ref()
13 | .replace("_", "__")
14 | .replace("&&", "[~~]")
15 | .replace('&', "_")
16 | .replace("[~~]", "&")
17 | }
18 |
19 | pub fn from_gtk_mnemonic>(string: S) -> String {
20 | string
21 | .as_ref()
22 | .replace("&", "&&")
23 | .replace("__", "[~~]")
24 | .replace('_', "&")
25 | .replace("[~~]", "_")
26 | }
27 |
28 | pub fn parse_accelerator(
29 | accelerator: &Accelerator,
30 | ) -> Result<(gdk::ModifierType, u32), AcceleratorParseError> {
31 | let key = match &accelerator.key {
32 | Code::KeyA => 'A' as u32,
33 | Code::KeyB => 'B' as u32,
34 | Code::KeyC => 'C' as u32,
35 | Code::KeyD => 'D' as u32,
36 | Code::KeyE => 'E' as u32,
37 | Code::KeyF => 'F' as u32,
38 | Code::KeyG => 'G' as u32,
39 | Code::KeyH => 'H' as u32,
40 | Code::KeyI => 'I' as u32,
41 | Code::KeyJ => 'J' as u32,
42 | Code::KeyK => 'K' as u32,
43 | Code::KeyL => 'L' as u32,
44 | Code::KeyM => 'M' as u32,
45 | Code::KeyN => 'N' as u32,
46 | Code::KeyO => 'O' as u32,
47 | Code::KeyP => 'P' as u32,
48 | Code::KeyQ => 'Q' as u32,
49 | Code::KeyR => 'R' as u32,
50 | Code::KeyS => 'S' as u32,
51 | Code::KeyT => 'T' as u32,
52 | Code::KeyU => 'U' as u32,
53 | Code::KeyV => 'V' as u32,
54 | Code::KeyW => 'W' as u32,
55 | Code::KeyX => 'X' as u32,
56 | Code::KeyY => 'Y' as u32,
57 | Code::KeyZ => 'Z' as u32,
58 | Code::Digit0 => '0' as u32,
59 | Code::Digit1 => '1' as u32,
60 | Code::Digit2 => '2' as u32,
61 | Code::Digit3 => '3' as u32,
62 | Code::Digit4 => '4' as u32,
63 | Code::Digit5 => '5' as u32,
64 | Code::Digit6 => '6' as u32,
65 | Code::Digit7 => '7' as u32,
66 | Code::Digit8 => '8' as u32,
67 | Code::Digit9 => '9' as u32,
68 | Code::Comma => ',' as u32,
69 | Code::Minus => '-' as u32,
70 | Code::Period => '.' as u32,
71 | Code::Space => ' ' as u32,
72 | Code::Equal => '=' as u32,
73 | Code::Semicolon => ';' as u32,
74 | Code::Slash => '/' as u32,
75 | Code::Backslash => '\\' as u32,
76 | Code::Quote => '\'' as u32,
77 | Code::Backquote => '`' as u32,
78 | Code::BracketLeft => '[' as u32,
79 | Code::BracketRight => ']' as u32,
80 | key => {
81 | if let Some(gdk_key) = key_to_raw_key(key) {
82 | *gdk_key
83 | } else {
84 | return Err(AcceleratorParseError::UnsupportedKey(key.to_string()));
85 | }
86 | }
87 | };
88 |
89 | Ok((modifiers_to_gdk_modifier_type(accelerator.mods), key))
90 | }
91 |
92 | fn modifiers_to_gdk_modifier_type(modifiers: Modifiers) -> gdk::ModifierType {
93 | let mut result = gdk::ModifierType::empty();
94 |
95 | result.set(
96 | gdk::ModifierType::MOD1_MASK,
97 | modifiers.contains(Modifiers::ALT),
98 | );
99 | result.set(
100 | gdk::ModifierType::CONTROL_MASK,
101 | modifiers.contains(Modifiers::CONTROL),
102 | );
103 | result.set(
104 | gdk::ModifierType::SHIFT_MASK,
105 | modifiers.contains(Modifiers::SHIFT),
106 | );
107 | result.set(
108 | gdk::ModifierType::META_MASK,
109 | modifiers.contains(Modifiers::SUPER),
110 | );
111 |
112 | result
113 | }
114 |
115 | fn key_to_raw_key(src: &Code) -> Option {
116 | use gdk::keys::constants::*;
117 | Some(match src {
118 | Code::Escape => Escape,
119 | Code::Backspace => BackSpace,
120 |
121 | Code::Tab => Tab,
122 | Code::Enter => Return,
123 |
124 | Code::ControlLeft => Control_L,
125 | Code::AltLeft => Alt_L,
126 | Code::ShiftLeft => Shift_L,
127 | Code::MetaLeft => Super_L,
128 |
129 | Code::ControlRight => Control_R,
130 | Code::AltRight => Alt_R,
131 | Code::ShiftRight => Shift_R,
132 | Code::MetaRight => Super_R,
133 |
134 | Code::CapsLock => Caps_Lock,
135 | Code::F1 => F1,
136 | Code::F2 => F2,
137 | Code::F3 => F3,
138 | Code::F4 => F4,
139 | Code::F5 => F5,
140 | Code::F6 => F6,
141 | Code::F7 => F7,
142 | Code::F8 => F8,
143 | Code::F9 => F9,
144 | Code::F10 => F10,
145 | Code::F11 => F11,
146 | Code::F12 => F12,
147 | Code::F13 => F13,
148 | Code::F14 => F14,
149 | Code::F15 => F15,
150 | Code::F16 => F16,
151 | Code::F17 => F17,
152 | Code::F18 => F18,
153 | Code::F19 => F19,
154 | Code::F20 => F20,
155 | Code::F21 => F21,
156 | Code::F22 => F22,
157 | Code::F23 => F23,
158 | Code::F24 => F24,
159 |
160 | Code::PrintScreen => Print,
161 | Code::ScrollLock => Scroll_Lock,
162 | // Pause/Break not audio.
163 | Code::Pause => Pause,
164 |
165 | Code::Insert => Insert,
166 | Code::Delete => Delete,
167 | Code::Home => Home,
168 | Code::End => End,
169 | Code::PageUp => Page_Up,
170 | Code::PageDown => Page_Down,
171 |
172 | Code::NumLock => Num_Lock,
173 |
174 | Code::ArrowUp => Up,
175 | Code::ArrowDown => Down,
176 | Code::ArrowLeft => Left,
177 | Code::ArrowRight => Right,
178 |
179 | Code::ContextMenu => Menu,
180 | Code::WakeUp => WakeUp,
181 | _ => return None,
182 | })
183 | }
184 |
--------------------------------------------------------------------------------
/src/platform_impl/windows/util.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2022-2022 Tauri Programme within The Commons Conservancy
2 | // SPDX-License-Identifier: Apache-2.0
3 | // SPDX-License-Identifier: MIT
4 |
5 | use std::ops::{Deref, DerefMut};
6 |
7 | use once_cell::sync::Lazy;
8 | use windows_sys::{
9 | core::HRESULT,
10 | Win32::{
11 | Foundation::{FARPROC, HWND, S_OK},
12 | Graphics::Gdi::{
13 | GetDC, GetDeviceCaps, MonitorFromWindow, HMONITOR, LOGPIXELSX, MONITOR_DEFAULTTONEAREST,
14 | },
15 | System::LibraryLoader::{GetProcAddress, LoadLibraryW},
16 | UI::{
17 | HiDpi::{MDT_EFFECTIVE_DPI, MONITOR_DPI_TYPE},
18 | WindowsAndMessaging::{IsProcessDPIAware, ACCEL},
19 | },
20 | },
21 | };
22 |
23 | pub fn encode_wide>(string: S) -> Vec {
24 | std::os::windows::prelude::OsStrExt::encode_wide(string.as_ref())
25 | .chain(std::iter::once(0))
26 | .collect()
27 | }
28 |
29 | #[allow(non_snake_case)]
30 | pub fn LOWORD(dword: u32) -> u16 {
31 | (dword & 0xFFFF) as u16
32 | }
33 |
34 | pub fn decode_wide(w_str: *mut u16) -> String {
35 | let len = unsafe { windows_sys::Win32::Globalization::lstrlenW(w_str) } as usize;
36 | let w_str_slice = unsafe { std::slice::from_raw_parts(w_str, len) };
37 | String::from_utf16_lossy(w_str_slice)
38 | }
39 |
40 | /// ACCEL wrapper to implement Debug
41 | #[derive(Clone)]
42 | #[repr(transparent)]
43 | pub struct Accel(pub ACCEL);
44 |
45 | impl std::fmt::Debug for Accel {
46 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47 | f.debug_struct("ACCEL")
48 | .field("key", &self.0.key)
49 | .field("cmd", &self.0.cmd)
50 | .field("fVirt", &self.0.fVirt)
51 | .finish()
52 | }
53 | }
54 |
55 | impl Deref for Accel {
56 | type Target = ACCEL;
57 |
58 | fn deref(&self) -> &Self::Target {
59 | &self.0
60 | }
61 | }
62 |
63 | impl DerefMut for Accel {
64 | fn deref_mut(&mut self) -> &mut Self::Target {
65 | &mut self.0
66 | }
67 | }
68 |
69 | // taken from winit's code base
70 | // https://github.com/rust-windowing/winit/blob/ee88e38f13fbc86a7aafae1d17ad3cd4a1e761df/src/platform_impl/windows/util.rs#L138
71 | pub fn get_instance_handle() -> windows_sys::Win32::Foundation::HMODULE {
72 | // Gets the instance handle by taking the address of the
73 | // pseudo-variable created by the microsoft linker:
74 | // https://devblogs.microsoft.com/oldnewthing/20041025-00/?p=37483
75 |
76 | // This is preferred over GetModuleHandle(NULL) because it also works in DLLs:
77 | // https://stackoverflow.com/questions/21718027/getmodulehandlenull-vs-hinstance
78 |
79 | extern "C" {
80 | static __ImageBase: windows_sys::Win32::System::SystemServices::IMAGE_DOS_HEADER;
81 | }
82 |
83 | unsafe { &__ImageBase as *const _ as _ }
84 | }
85 |
86 | fn get_function_impl(library: &str, function: &str) -> FARPROC {
87 | let library = encode_wide(library);
88 | assert_eq!(function.chars().last(), Some('\0'));
89 |
90 | // Library names we will use are ASCII so we can use the A version to avoid string conversion.
91 | let module = unsafe { LoadLibraryW(library.as_ptr()) };
92 | if module.is_null() {
93 | return None;
94 | }
95 |
96 | unsafe { GetProcAddress(module, function.as_ptr()) }
97 | }
98 |
99 | macro_rules! get_function {
100 | ($lib:expr, $func:ident) => {
101 | crate::platform_impl::platform::util::get_function_impl(
102 | $lib,
103 | concat!(stringify!($func), '\0'),
104 | )
105 | .map(|f| unsafe { std::mem::transmute::<_, $func>(f) })
106 | };
107 | }
108 |
109 | pub type GetDpiForWindow = unsafe extern "system" fn(hwnd: HWND) -> u32;
110 | pub type GetDpiForMonitor = unsafe extern "system" fn(
111 | hmonitor: HMONITOR,
112 | dpi_type: MONITOR_DPI_TYPE,
113 | dpi_x: *mut u32,
114 | dpi_y: *mut u32,
115 | ) -> HRESULT;
116 |
117 | static GET_DPI_FOR_WINDOW: Lazy