diff --git a/Cargo.lock b/Cargo.lock index 411bddc6..cd217ca6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2140,6 +2140,16 @@ dependencies = [ "cxx-build", ] +[[package]] +name = "ico" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031530fe562d8c8d71c0635013d6d155bbfe8ba0aa4b4d2d24ce8af6b71047bd" +dependencies = [ + "byteorder", + "png", +] + [[package]] name = "ico" version = "0.3.0" @@ -2208,6 +2218,15 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "infer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f178e61cdbfe084aa75a2f4f7a25a5bb09701a47ae1753608f194b15783c937a" +dependencies = [ + "cfb", +] + [[package]] name = "infer" version = "0.12.0" @@ -4711,13 +4730,16 @@ dependencies = [ "gtk", "heck 0.4.1", "http", + "ico 0.2.0", "ignore", + "infer 0.9.0", "jni", "libc", "log", "objc", "once_cell", "percent-encoding", + "png", "rand 0.8.5", "raw-window-handle", "reqwest", @@ -4772,7 +4794,7 @@ checksum = "818c570932ebc2ff6d498be89d93494b89ff142131937a7e56d7cfb9c8ef0ad0" dependencies = [ "base64 0.21.0", "brotli", - "ico", + "ico 0.3.0", "json-patch", "plist", "png", @@ -5097,6 +5119,15 @@ dependencies = [ "tokio-tungstenite", ] +[[package]] +name = "tauri-plugin-window" +version = "0.0.0" +dependencies = [ + "serde", + "tauri", + "thiserror", +] + [[package]] name = "tauri-plugin-window-state" version = "0.1.0" @@ -5164,7 +5195,7 @@ dependencies = [ "glob", "heck 0.4.1", "html5ever", - "infer", + "infer 0.12.0", "json-patch", "kuchiki", "memchr", diff --git a/examples/api/package.json b/examples/api/package.json index 6efab778..28521968 100644 --- a/examples/api/package.json +++ b/examples/api/package.json @@ -20,7 +20,8 @@ "tauri-plugin-http-api": "0.0.0", "tauri-plugin-notification-api": "0.0.0", "tauri-plugin-os-api": "0.0.0", - "tauri-plugin-shell-api": "0.0.0" + "tauri-plugin-shell-api": "0.0.0", + "tauri-plugin-window-api": "0.0.0" }, "devDependencies": { "@iconify-json/codicon": "^1.1.10", diff --git a/examples/api/src-tauri/Cargo.lock b/examples/api/src-tauri/Cargo.lock index 856eed84..4a5e7424 100644 --- a/examples/api/src-tauri/Cargo.lock +++ b/examples/api/src-tauri/Cargo.lock @@ -160,6 +160,7 @@ dependencies = [ "tauri-plugin-notification", "tauri-plugin-os", "tauri-plugin-shell", + "tauri-plugin-window", "tiny_http", "window-shadows", ] @@ -3726,6 +3727,15 @@ dependencies = [ "thiserror", ] +[[package]] +name = "tauri-plugin-window" +version = "0.0.0" +dependencies = [ + "serde", + "tauri", + "thiserror", +] + [[package]] name = "tauri-runtime" version = "0.13.0-alpha.4" diff --git a/examples/api/src-tauri/Cargo.toml b/examples/api/src-tauri/Cargo.toml index 185adce4..9f2bd245 100644 --- a/examples/api/src-tauri/Cargo.toml +++ b/examples/api/src-tauri/Cargo.toml @@ -26,6 +26,7 @@ tauri-plugin-http = { path = "../../../plugins/http", features = [ "http-multipa tauri-plugin-notification = { path = "../../../plugins/notification", features = [ "windows7-compat" ] } tauri-plugin-os = { path = "../../../plugins/os" } tauri-plugin-shell = { path = "../../../plugins/shell" } +tauri-plugin-window = { path = "../../../plugins/window" } [patch.crates-io] tauri = { git = "https://github.com/tauri-apps/tauri", branch = "next" } diff --git a/examples/api/src-tauri/src/lib.rs b/examples/api/src-tauri/src/lib.rs index dba74b62..fc834ed3 100644 --- a/examples/api/src-tauri/src/lib.rs +++ b/examples/api/src-tauri/src/lib.rs @@ -39,6 +39,7 @@ pub fn run() { .plugin(tauri_plugin_notification::init()) .plugin(tauri_plugin_os::init()) .plugin(tauri_plugin_shell::init()) + .plugin(tauri_plugin_window::init()) .setup(move |app| { #[cfg(desktop)] { diff --git a/examples/api/src/App.svelte b/examples/api/src/App.svelte index 95b2ff0f..e7662e0f 100644 --- a/examples/api/src/App.svelte +++ b/examples/api/src/App.svelte @@ -1,7 +1,7 @@
diff --git a/plugins/fs-watch/guest-js/index.ts b/plugins/fs-watch/guest-js/index.ts index 05ed07e5..634b964f 100644 --- a/plugins/fs-watch/guest-js/index.ts +++ b/plugins/fs-watch/guest-js/index.ts @@ -1,6 +1,6 @@ import { invoke } from "@tauri-apps/api/tauri"; import { UnlistenFn } from "@tauri-apps/api/event"; -import { appWindow, WebviewWindow } from "@tauri-apps/api/window"; +import { appWindow, WebviewWindow } from "tauri-plugin-window-api"; const w: WebviewWindow = appWindow; @@ -21,17 +21,17 @@ export type RawEvent = { type RawEventKind = | "any " | { - access?: unknown; - } + access?: unknown; + } | { - create?: unknown; - } + create?: unknown; + } | { - modify?: unknown; - } + modify?: unknown; + } | { - remove?: unknown; - } + remove?: unknown; + } | "other"; export type DebouncedEvent = diff --git a/plugins/fs-watch/package.json b/plugins/fs-watch/package.json index 4ac45241..61c5cf71 100644 --- a/plugins/fs-watch/package.json +++ b/plugins/fs-watch/package.json @@ -28,6 +28,7 @@ "tslib": "^2.5.0" }, "dependencies": { - "@tauri-apps/api": "^1.2.0" + "@tauri-apps/api": "^1.2.0", + "tauri-plugin-window-api": "0.0.0" } } diff --git a/plugins/upload/guest-js/index.ts b/plugins/upload/guest-js/index.ts index 26bc93b4..1a605633 100644 --- a/plugins/upload/guest-js/index.ts +++ b/plugins/upload/guest-js/index.ts @@ -1,5 +1,5 @@ import { invoke } from "@tauri-apps/api/tauri"; -import { appWindow } from "@tauri-apps/api/window"; +import { appWindow } from "tauri-plugin-window-api"; interface ProgressPayload { id: number; diff --git a/plugins/upload/package.json b/plugins/upload/package.json index acd01d42..e84f3b62 100644 --- a/plugins/upload/package.json +++ b/plugins/upload/package.json @@ -28,6 +28,7 @@ "tslib": "^2.5.0" }, "dependencies": { - "@tauri-apps/api": "^1.2.0" + "@tauri-apps/api": "^1.2.0", + "tauri-plugin-window-api": "0.0.0" } } diff --git a/plugins/window-state/guest-js/index.ts b/plugins/window-state/guest-js/index.ts index 11ead0f4..a40d8183 100644 --- a/plugins/window-state/guest-js/index.ts +++ b/plugins/window-state/guest-js/index.ts @@ -1,5 +1,5 @@ import { invoke } from "@tauri-apps/api/tauri"; -import { WindowLabel, getCurrent } from "@tauri-apps/api/window"; +import { WindowLabel, getCurrent } from "tauri-plugin-window-api"; export enum StateFlags { SIZE = 1 << 0, diff --git a/plugins/window-state/package.json b/plugins/window-state/package.json index f7938586..d6937f9f 100644 --- a/plugins/window-state/package.json +++ b/plugins/window-state/package.json @@ -28,6 +28,7 @@ "tslib": "^2.5.0" }, "dependencies": { - "@tauri-apps/api": "^1.2.0" + "@tauri-apps/api": "^1.2.0", + "tauri-plugin-window-api": "0.0.0" } } diff --git a/plugins/window/Cargo.toml b/plugins/window/Cargo.toml new file mode 100644 index 00000000..6fda6b38 --- /dev/null +++ b/plugins/window/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "tauri-plugin-window" +version = "0.0.0" +edition.workspace = true +authors.workspace = true +license.workspace = true + +[dependencies] +tauri.workspace = true +serde.workspace = true +thiserror.workspace = true + +[features] +icon-png = ["tauri/icon-png"] +icon-ico = ["tauri/icon-ico"] diff --git a/plugins/window/LICENSE.spdx b/plugins/window/LICENSE.spdx new file mode 100644 index 00000000..cdd0df5a --- /dev/null +++ b/plugins/window/LICENSE.spdx @@ -0,0 +1,20 @@ +SPDXVersion: SPDX-2.1 +DataLicense: CC0-1.0 +PackageName: tauri +DataFormat: SPDXRef-1 +PackageSupplier: Organization: The Tauri Programme in the Commons Conservancy +PackageHomePage: https://tauri.app +PackageLicenseDeclared: Apache-2.0 +PackageLicenseDeclared: MIT +PackageCopyrightText: 2019-2022, The Tauri Programme in the Commons Conservancy +PackageSummary: Tauri is a rust project that enables developers to make secure +and small desktop applications using a web frontend. + +PackageComment: The package includes the following libraries; see +Relationship information. + +Created: 2019-05-20T09:00:00Z +PackageDownloadLocation: git://github.com/tauri-apps/tauri +PackageDownloadLocation: git+https://github.com/tauri-apps/tauri.git +PackageDownloadLocation: git+ssh://github.com/tauri-apps/tauri.git +Creator: Person: Daniel Thompson-Yvetot \ No newline at end of file diff --git a/plugins/window/LICENSE_APACHE-2.0 b/plugins/window/LICENSE_APACHE-2.0 new file mode 100644 index 00000000..4947287f --- /dev/null +++ b/plugins/window/LICENSE_APACHE-2.0 @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS \ No newline at end of file diff --git a/plugins/window/LICENSE_MIT b/plugins/window/LICENSE_MIT new file mode 100644 index 00000000..4d754725 --- /dev/null +++ b/plugins/window/LICENSE_MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 - Present Tauri Apps Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/plugins/window/README.md b/plugins/window/README.md new file mode 100644 index 00000000..4d6a6c8a --- /dev/null +++ b/plugins/window/README.md @@ -0,0 +1,65 @@ +# Window plugin + +Interact with the Tauri window. + +## Install + +_This plugin requires a Rust version of at least **1.64**_ + +There are three general methods of installation that we can recommend. + +1. Use crates.io and npm (easiest, and requires you to trust that our publishing pipeline worked) +2. Pull sources directly from Github using git tags / revision hashes (most secure) +3. Git submodule install this repo in your tauri project and then use file protocol to ingest the source (most secure, but inconvenient to use) + +Install the Core plugin by adding the following to your `Cargo.toml` file: + +`src-tauri/Cargo.toml` + +```toml +[dependencies] +tauri-plugin-window = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" } +``` + +You can install the JavaScript Guest bindings using your preferred JavaScript package manager: + +> Note: Since most JavaScript package managers are unable to install packages from git monorepos we provide read-only mirrors of each plugin. This makes installation option 2 more ergonomic to use. + +```sh +pnpm add https://github.com/tauri-apps/tauri-plugin-window#v2 +# or +npm add https://github.com/tauri-apps/tauri-plugin-window#v2 +# or +yarn add https://github.com/tauri-apps/tauri-plugin-window#v2 +``` + +## Usage + +First you need to register the core plugin with Tauri: + +`src-tauri/src/main.rs` + +```rust +fn main() { + tauri::Builder::default() + .plugin(tauri_plugin_window::init()) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} +``` + +Afterwards all the plugin's APIs are available through the JavaScript guest bindings: + +```javascript +import * as tauriWindow from "tauri-plugin-window-api"; +``` + +## Contributing + +PRs accepted. Please make sure to read the Contributing Guide before making a pull request. + +## License + +Code: (c) 2015 - Present - The Tauri Programme within The Commons Conservancy. + +MIT or MIT/Apache 2.0 where applicable. diff --git a/plugins/window/guest-js/index.ts b/plugins/window/guest-js/index.ts new file mode 100644 index 00000000..597b508e --- /dev/null +++ b/plugins/window/guest-js/index.ts @@ -0,0 +1,1907 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +/** + * Provides APIs to create windows, communicate with other windows and manipulate the current window. + * + * The APIs must be added to [`tauri.allowlist.window`](https://tauri.app/v1/api/config/#allowlistconfig.window) in `tauri.conf.json`: + * ```json + * { + * "tauri": { + * "allowlist": { + * "window": { + * "all": true, // enable all window APIs + * "create": true, // enable window creation + * "center": true, + * "requestUserAttention": true, + * "setResizable": true, + * "setTitle": true, + * "maximize": true, + * "unmaximize": true, + * "minimize": true, + * "unminimize": true, + * "show": true, + * "hide": true, + * "close": true, + * "setDecorations": true, + * "setShadow": true, + * "setAlwaysOnTop": true, + * "setContentProtected": true, + * "setSize": true, + * "setMinSize": true, + * "setMaxSize": true, + * "setPosition": true, + * "setFullscreen": true, + * "setFocus": true, + * "setIcon": true, + * "setSkipTaskbar": true, + * "setCursorGrab": true, + * "setCursorVisible": true, + * "setCursorIcon": true, + * "setCursorPosition": true, + * "setIgnoreCursorEvents": true, + * "startDragging": true, + * "print": true + * } + * } + * } + * } + * ``` + * It is recommended to allowlist only the APIs you use for optimal bundle size and security. + * + * ## Window events + * + * Events can be listened to using `appWindow.listen`: + * ```typescript + * import { appWindow } from "tauri-plugin-window-api"; + * appWindow.listen("my-window-event", ({ event, payload }) => { }); + * ``` + * + * @module + */ + +import { invoke } from '@tauri-apps/api/tauri' +import type { Event, EventName, EventCallback, UnlistenFn } from '@tauri-apps/api/event' +import { emit, listen, once, TauriEvent } from '@tauri-apps/api/event' + +type Theme = 'light' | 'dark' +type TitleBarStyle = 'visible' | 'transparent' | 'overlay' + +/** + * Allows you to retrieve information about a given monitor. + * + * @since 1.0.0 + */ +interface Monitor { + /** Human-readable name of the monitor */ + name: string | null + /** The monitor's resolution. */ + size: PhysicalSize + /** the Top-left corner position of the monitor relative to the larger full screen area. */ + position: PhysicalPosition + /** The scale factor that can be used to map physical pixels to logical pixels. */ + scaleFactor: number +} + +/** + * The payload for the `scaleChange` event. + * + * @since 1.0.2 + */ +interface ScaleFactorChanged { + /** The new window scale factor. */ + scaleFactor: number + /** The new window size */ + size: PhysicalSize +} + +/** The file drop event types. */ +type FileDropEvent = + | { type: 'hover'; paths: string[] } + | { type: 'drop'; paths: string[] } + | { type: 'cancel' } + +/** + * A size represented in logical pixels. + * + * @since 1.0.0 + */ +class LogicalSize { + type = 'Logical' + width: number + height: number + + constructor(width: number, height: number) { + this.width = width + this.height = height + } +} + +/** + * A size represented in physical pixels. + * + * @since 1.0.0 + */ +class PhysicalSize { + type = 'Physical' + width: number + height: number + + constructor(width: number, height: number) { + this.width = width + this.height = height + } + + /** + * Converts the physical size to a logical one. + * @example + * ```typescript + * import { appWindow } from 'tauri-plugin-window-api'; + * const factor = await appWindow.scaleFactor(); + * const size = await appWindow.innerSize(); + * const logical = size.toLogical(factor); + * ``` + * */ + toLogical(scaleFactor: number): LogicalSize { + return new LogicalSize(this.width / scaleFactor, this.height / scaleFactor) + } +} + +/** + * A position represented in logical pixels. + * + * @since 1.0.0 + */ +class LogicalPosition { + type = 'Logical' + x: number + y: number + + constructor(x: number, y: number) { + this.x = x + this.y = y + } +} + +/** + * A position represented in physical pixels. + * + * @since 1.0.0 + */ +class PhysicalPosition { + type = 'Physical' + x: number + y: number + + constructor(x: number, y: number) { + this.x = x + this.y = y + } + + /** + * Converts the physical position to a logical one. + * @example + * ```typescript + * import { appWindow } from 'tauri-plugin-window-api'; + * const factor = await appWindow.scaleFactor(); + * const position = await appWindow.innerPosition(); + * const logical = position.toLogical(factor); + * ``` + * */ + toLogical(scaleFactor: number): LogicalPosition { + return new LogicalPosition(this.x / scaleFactor, this.y / scaleFactor) + } +} + +/** @ignore */ +interface WindowDef { + label: string +} + +/** @ignore */ +declare global { + interface Window { + __TAURI_METADATA__: { + __windows: WindowDef[] + __currentWindow: WindowDef + } + } +} + +/** + * Attention type to request on a window. + * + * @since 1.0.0 + */ +enum UserAttentionType { + /** + * #### Platform-specific + * - **macOS:** Bounces the dock icon until the application is in focus. + * - **Windows:** Flashes both the window and the taskbar button until the application is in focus. + */ + Critical = 1, + /** + * #### Platform-specific + * - **macOS:** Bounces the dock icon once. + * - **Windows:** Flashes the taskbar button until the application is in focus. + */ + Informational +} + +export type CursorIcon = + | 'default' + | 'crosshair' + | 'hand' + | 'arrow' + | 'move' + | 'text' + | 'wait' + | 'help' + | 'progress' + // something cannot be done + | 'notAllowed' + | 'contextMenu' + | 'cell' + | 'verticalText' + | 'alias' + | 'copy' + | 'noDrop' + // something can be grabbed + | 'grab' + /// something is grabbed + | 'grabbing' + | 'allScroll' + | 'zoomIn' + | 'zoomOut' + // edge is to be moved + | 'eResize' + | 'nResize' + | 'neResize' + | 'nwResize' + | 'sResize' + | 'seResize' + | 'swResize' + | 'wResize' + | 'ewResize' + | 'nsResize' + | 'neswResize' + | 'nwseResize' + | 'colResize' + | 'rowResize' + +/** + * Get an instance of `WebviewWindow` for the current webview window. + * + * @since 1.0.0 + */ +function getCurrent(): WebviewWindow { + return new WebviewWindow(window.__TAURI_METADATA__.__currentWindow.label, { + // @ts-expect-error `skip` is not defined in the public API but it is handled by the constructor + skip: true + }) +} + +/** + * Gets a list of instances of `WebviewWindow` for all available webview windows. + * + * @since 1.0.0 + */ +function getAll(): WebviewWindow[] { + return window.__TAURI_METADATA__.__windows.map( + (w) => + new WebviewWindow(w.label, { + // @ts-expect-error `skip` is not defined in the public API but it is handled by the constructor + skip: true + }) + ) +} + +/** @ignore */ +// events that are emitted right here instead of by the created webview +const localTauriEvents = ['tauri://created', 'tauri://error'] +/** @ignore */ +export type WindowLabel = string +/** + * A webview window handle allows emitting and listening to events from the backend that are tied to the window. + * + * @ignore + * @since 1.0.0 + */ +class WebviewWindowHandle { + /** The window label. It is a unique identifier for the window, can be used to reference it later. */ + label: WindowLabel + /** Local event listeners. */ + listeners: Record>> + + constructor(label: WindowLabel) { + this.label = label + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + this.listeners = Object.create(null) + } + + /** + * Listen to an event emitted by the backend that is tied to the webview window. + * + * @example + * ```typescript + * import { appWindow } from 'tauri-plugin-window-api'; + * const unlisten = await appWindow.listen('state-changed', (event) => { + * console.log(`Got error: ${payload}`); + * }); + * + * // you need to call unlisten if your handler goes out of scope e.g. the component is unmounted + * unlisten(); + * ``` + * + * @param event Event name. Must include only alphanumeric characters, `-`, `/`, `:` and `_`. + * @param handler Event handler. + * @returns A promise resolving to a function to unlisten to the event. + * Note that removing the listener is required if your listener goes out of scope e.g. the component is unmounted. + */ + async listen( + event: EventName, + handler: EventCallback + ): Promise { + if (this._handleTauriEvent(event, handler)) { + return Promise.resolve(() => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, security/detect-object-injection + const listeners = this.listeners[event] + listeners.splice(listeners.indexOf(handler), 1) + }) + } + // @ts-expect-error event will be added later // TODO + return listen(event, this.label, handler) + } + + /** + * Listen to an one-off event emitted by the backend that is tied to the webview window. + * + * @example + * ```typescript + * import { appWindow } from 'tauri-plugin-window-api'; + * const unlisten = await appWindow.once('initialized', (event) => { + * console.log(`Window initialized!`); + * }); + * + * // you need to call unlisten if your handler goes out of scope e.g. the component is unmounted + * unlisten(); + * ``` + * + * @param event Event name. Must include only alphanumeric characters, `-`, `/`, `:` and `_`. + * @param handler Event handler. + * @returns A promise resolving to a function to unlisten to the event. + * Note that removing the listener is required if your listener goes out of scope e.g. the component is unmounted. + */ + async once(event: string, handler: EventCallback): Promise { + if (this._handleTauriEvent(event, handler)) { + return Promise.resolve(() => { + // eslint-disable-next-line security/detect-object-injection + const listeners = this.listeners[event] + listeners.splice(listeners.indexOf(handler), 1) + }) + } + // @ts-expect-error event will be added later // TODO + return once(event, this.label, handler) + } + + /** + * Emits an event to the backend, tied to the webview window. + * @example + * ```typescript + * import { appWindow } from 'tauri-plugin-window-api'; + * await appWindow.emit('window-loaded', { loggedIn: true, token: 'authToken' }); + * ``` + * + * @param event Event name. Must include only alphanumeric characters, `-`, `/`, `:` and `_`. + * @param payload Event payload. + */ + async emit(event: string, payload?: unknown): Promise { + if (localTauriEvents.includes(event)) { + // eslint-disable-next-line + for (const handler of this.listeners[event] || []) { + handler({ event, id: -1, windowLabel: this.label, payload }) + } + return Promise.resolve() + } + // @ts-expect-error event will be added later // TODO + return emit(event, this.label, payload) + } + + /** @ignore */ + _handleTauriEvent(event: string, handler: EventCallback): boolean { + if (localTauriEvents.includes(event)) { + if (!(event in this.listeners)) { + // eslint-disable-next-line + this.listeners[event] = [handler] + } else { + // eslint-disable-next-line + this.listeners[event].push(handler) + } + return true + } + return false + } +} + +/** + * Manage the current window object. + * + * @ignore + * @since 1.0.0 + */ +class WindowManager extends WebviewWindowHandle { + // Getters + /** + * The scale factor that can be used to map physical pixels to logical pixels. + * @example + * ```typescript + * import { appWindow } from 'tauri-plugin-window-api'; + * const factor = await appWindow.scaleFactor(); + * ``` + * + * @returns The window's monitor scale factor. + * */ + async scaleFactor(): Promise { + return invoke('plugin:window|scale_factor', { + label: this.label + }) + } + + /** + * The position of the top-left hand corner of the window's client area relative to the top-left hand corner of the desktop. + * @example + * ```typescript + * import { appWindow } from 'tauri-plugin-window-api'; + * const position = await appWindow.innerPosition(); + * ``` + * + * @returns The window's inner position. + * */ + async innerPosition(): Promise { + return invoke<{ x: number; y: number }>('plugin:window|inner_position', { + label: this.label + }).then(({ x, y }) => new PhysicalPosition(x, y)) + } + + /** + * The position of the top-left hand corner of the window relative to the top-left hand corner of the desktop. + * @example + * ```typescript + * import { appWindow } from 'tauri-plugin-window-api'; + * const position = await appWindow.outerPosition(); + * ``` + * + * @returns The window's outer position. + * */ + async outerPosition(): Promise { + return invoke<{ x: number; y: number }>('plugin:window|outer_position', { + label: this.label, + }).then(({ x, y }) => new PhysicalPosition(x, y)) + } + + /** + * The physical size of the window's client area. + * The client area is the content of the window, excluding the title bar and borders. + * @example + * ```typescript + * import { appWindow } from 'tauri-plugin-window-api'; + * const size = await appWindow.innerSize(); + * ``` + * + * @returns The window's inner size. + */ + async innerSize(): Promise { + return invoke<{ width: number; height: number }>('plugin:window|inner_size', { + label: this.label, + }).then(({ width, height }) => new PhysicalSize(width, height)) + } + + /** + * The physical size of the entire window. + * These dimensions include the title bar and borders. If you don't want that (and you usually don't), use inner_size instead. + * @example + * ```typescript + * import { appWindow } from 'tauri-plugin-window-api'; + * const size = await appWindow.outerSize(); + * ``` + * + * @returns The window's outer size. + */ + async outerSize(): Promise { + return invoke<{ width: number; height: number }>('plugin:window|outer_size', { + label: this.label, + }).then(({ width, height }) => new PhysicalSize(width, height)) + } + + /** + * Gets the window's current fullscreen state. + * @example + * ```typescript + * import { appWindow } from 'tauri-plugin-window-api'; + * const fullscreen = await appWindow.isFullscreen(); + * ``` + * + * @returns Whether the window is in fullscreen mode or not. + * */ + async isFullscreen(): Promise { + return invoke('plugin:window|is_fullscreen', { + label: this.label + }) + } + + /** + * Gets the window's current minimized state. + * @example + * ```typescript + * import { appWindow } from 'tauri-plugin-window-api'; + * const minimized = await appWindow.isMinimized(); + * ``` + * + * @since 1.3.0 + * */ + async isMinimized(): Promise { + return invoke('plugin:window|is_minimized', { + label: this.label + }) + } + + /** + * Gets the window's current maximized state. + * @example + * ```typescript + * import { appWindow } from 'tauri-plugin-window-api'; + * const maximized = await appWindow.isMaximized(); + * ``` + * + * @returns Whether the window is maximized or not. + * */ + async isMaximized(): Promise { + return invoke('plugin:window|is_maximized', { + label: this.label + }) + } + + /** + * Gets the window's current decorated state. + * @example + * ```typescript + * import { appWindow } from 'tauri-plugin-window-api'; + * const decorated = await appWindow.isDecorated(); + * ``` + * + * @returns Whether the window is decorated or not. + * */ + async isDecorated(): Promise { + return invoke('plugin:window|is_decorated', { + label: this.label + }) + } + + /** + * Gets the window's current resizable state. + * @example + * ```typescript + * import { appWindow } from 'tauri-plugin-window-api'; + * const resizable = await appWindow.isResizable(); + * ``` + * + * @returns Whether the window is resizable or not. + * */ + async isResizable(): Promise { + return invoke('plugin:window|is_resizable', { + label: this.label + }) + } + + /** + * Gets the window's current visible state. + * @example + * ```typescript + * import { appWindow } from 'tauri-plugin-window-api'; + * const visible = await appWindow.isVisible(); + * ``` + * + * @returns Whether the window is visible or not. + * */ + async isVisible(): Promise { + return invoke('plugin:window|is_visible', { + label: this.label + }) + } + + /** + * Gets the window's current title. + * @example + * ```typescript + * import { appWindow } from 'tauri-plugin-window-api'; + * const title = await appWindow.title(); + * ``` + * + * @since 1.3.0 + * */ + async title(): Promise { + return invoke('plugin:window|title', { + label: this.label + }) + } + + /** + * Gets the window's current theme. + * + * #### Platform-specific + * + * - **macOS:** Theme was introduced on macOS 10.14. Returns `light` on macOS 10.13 and below. + * + * @example + * ```typescript + * import { appWindow } from 'tauri-plugin-window-api'; + * const theme = await appWindow.theme(); + * ``` + * + * @returns The window theme. + * */ + async theme(): Promise { + return invoke('plugin:window|theme', { + label: this.label + }) + } + + // Setters + + /** + * Centers the window. + * @example + * ```typescript + * import { appWindow } from 'tauri-plugin-window-api'; + * await appWindow.center(); + * ``` + * + * @param resizable + * @returns A promise indicating the success or failure of the operation. + */ + async center(): Promise { + return invoke('plugin:window|center', { + label: this.label + }) + } + + /** + * Requests user attention to the window, this has no effect if the application + * is already focused. How requesting for user attention manifests is platform dependent, + * see `UserAttentionType` for details. + * + * Providing `null` will unset the request for user attention. Unsetting the request for + * user attention might not be done automatically by the WM when the window receives input. + * + * #### Platform-specific + * + * - **macOS:** `null` has no effect. + * - **Linux:** Urgency levels have the same effect. + * @example + * ```typescript + * import { appWindow } from 'tauri-plugin-window-api'; + * await appWindow.requestUserAttention(); + * ``` + * + * @param resizable + * @returns A promise indicating the success or failure of the operation. + */ + async requestUserAttention( + requestType: UserAttentionType | null + ): Promise { + let requestType_ = null + if (requestType) { + if (requestType === UserAttentionType.Critical) { + requestType_ = { type: 'Critical' } + } else { + requestType_ = { type: 'Informational' } + } + } + + return invoke('plugin:window|request_user_attention', { + label: this.label, + value: requestType_ + }) + } + + /** + * Updates the window resizable flag. + * @example + * ```typescript + * import { appWindow } from 'tauri-plugin-window-api'; + * await appWindow.setResizable(false); + * ``` + * + * @param resizable + * @returns A promise indicating the success or failure of the operation. + */ + async setResizable(resizable: boolean): Promise { + return invoke('plugin:window|set_resizable', { + label: this.label, + value: resizable + }) + } + + /** + * Sets the window title. + * @example + * ```typescript + * import { appWindow } from 'tauri-plugin-window-api'; + * await appWindow.setTitle('Tauri'); + * ``` + * + * @param title The new title + * @returns A promise indicating the success or failure of the operation. + */ + async setTitle(title: string): Promise { + return invoke('plugin:window|set_title', { + label: this.label, + value: title + }) + } + + /** + * Maximizes the window. + * @example + * ```typescript + * import { appWindow } from 'tauri-plugin-window-api'; + * await appWindow.maximize(); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + async maximize(): Promise { + return invoke('plugin:window|maximize', { + label: this.label + }) + } + + /** + * Unmaximizes the window. + * @example + * ```typescript + * import { appWindow } from 'tauri-plugin-window-api'; + * await appWindow.unmaximize(); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + async unmaximize(): Promise { + return invoke('plugin:window|unmaximize', { + label: this.label, + }) + } + + /** + * Toggles the window maximized state. + * @example + * ```typescript + * import { appWindow } from 'tauri-plugin-window-api'; + * await appWindow.toggleMaximize(); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + async toggleMaximize(): Promise { + return invoke('plugin:window|toggle_maximize', { + label: this.label + }) + } + + /** + * Minimizes the window. + * @example + * ```typescript + * import { appWindow } from 'tauri-plugin-window-api'; + * await appWindow.minimize(); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + async minimize(): Promise { + return invoke('plugin:window|minimize', { + label: this.label + }) + } + + /** + * Unminimizes the window. + * @example + * ```typescript + * import { appWindow } from 'tauri-plugin-window-api'; + * await appWindow.unminimize(); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + async unminimize(): Promise { + return invoke('plugin:window|unminimize', { + label: this.label + }) + } + + /** + * Sets the window visibility to true. + * @example + * ```typescript + * import { appWindow } from 'tauri-plugin-window-api'; + * await appWindow.show(); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + async show(): Promise { + return invoke('plugin:window|show', { + label: this.label + }) + } + + /** + * Sets the window visibility to false. + * @example + * ```typescript + * import { appWindow } from 'tauri-plugin-window-api'; + * await appWindow.hide(); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + async hide(): Promise { + return invoke('plugin:window|hide', { + label: this.label + }) + } + + /** + * Closes the window. + * @example + * ```typescript + * import { appWindow } from 'tauri-plugin-window-api'; + * await appWindow.close(); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + async close(): Promise { + return invoke('plugin:window|close', { + label: this.label + }) + } + + /** + * Whether the window should have borders and bars. + * @example + * ```typescript + * import { appWindow } from 'tauri-plugin-window-api'; + * await appWindow.setDecorations(false); + * ``` + * + * @param decorations Whether the window should have borders and bars. + * @returns A promise indicating the success or failure of the operation. + */ + async setDecorations(decorations: boolean): Promise { + return invoke('plugin:window|set_decorations', { + label: this.label, + value: decorations + }) + } + + /** + * Whether or not the window should have shadow. + * + * #### Platform-specific + * + * - **Windows:** + * - `false` has no effect on decorated window, shadows are always ON. + * - `true` will make ndecorated window have a 1px white border, + * and on Windows 11, it will have a rounded corners. + * - **Linux:** Unsupported. + * + * @example + * ```typescript + * import { appWindow } from 'tauri-plugin-window-api'; + * await appWindow.setShadow(false); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + * + * @since 2.0 + */ + async setShadow(enable: boolean): Promise { + return invoke('plugin:window|set_shadow', { + label: this.label, + value: enable + }) + } + + /** + * Whether the window should always be on top of other windows. + * @example + * ```typescript + * import { appWindow } from 'tauri-plugin-window-api'; + * await appWindow.setAlwaysOnTop(true); + * ``` + * + * @param alwaysOnTop Whether the window should always be on top of other windows or not. + * @returns A promise indicating the success or failure of the operation. + */ + async setAlwaysOnTop(alwaysOnTop: boolean): Promise { + return invoke('plugin:window|set_always_on_top', { + label: this.label, + value: alwaysOnTop + }) + } + + /** + * Prevents the window contents from being captured by other apps. + * @example + * ```typescript + * import { appWindow } from 'tauri-plugin-window-api'; + * await appWindow.setContentProtected(true); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + * + * @since 1.2.0 + */ + async setContentProtected(protected_: boolean): Promise { + return invoke('plugin:window|set_content_protected', { + label: this.label, + value: protected_ + }) + } + + /** + * Resizes the window with a new inner size. + * @example + * ```typescript + * import { appWindow, LogicalSize } from 'tauri-plugin-window-api'; + * await appWindow.setSize(new LogicalSize(600, 500)); + * ``` + * + * @param size The logical or physical inner size. + * @returns A promise indicating the success or failure of the operation. + */ + async setSize(size: LogicalSize | PhysicalSize): Promise { + if (!size || (size.type !== 'Logical' && size.type !== 'Physical')) { + throw new Error( + 'the `size` argument must be either a LogicalSize or a PhysicalSize instance' + ) + } + + return invoke('plugin:window|set_size', { + label: this.label, + value: { + type: size.type, + data: { + width: size.width, + height: size.height + } + } + }) + } + + /** + * Sets the window minimum inner size. If the `size` argument is not provided, the constraint is unset. + * @example + * ```typescript + * import { appWindow, PhysicalSize } from 'tauri-plugin-window-api'; + * await appWindow.setMinSize(new PhysicalSize(600, 500)); + * ``` + * + * @param size The logical or physical inner size, or `null` to unset the constraint. + * @returns A promise indicating the success or failure of the operation. + */ + async setMinSize( + size: LogicalSize | PhysicalSize | null | undefined + ): Promise { + if (size && size.type !== 'Logical' && size.type !== 'Physical') { + throw new Error( + 'the `size` argument must be either a LogicalSize or a PhysicalSize instance' + ) + } + + return invoke('plugin:window|set_min_size', { + label: this.label, + value: size + ? { + type: size.type, + data: { + width: size.width, + height: size.height + } + } + : null + }) + } + + /** + * Sets the window maximum inner size. If the `size` argument is undefined, the constraint is unset. + * @example + * ```typescript + * import { appWindow, LogicalSize } from 'tauri-plugin-window-api'; + * await appWindow.setMaxSize(new LogicalSize(600, 500)); + * ``` + * + * @param size The logical or physical inner size, or `null` to unset the constraint. + * @returns A promise indicating the success or failure of the operation. + */ + async setMaxSize( + size: LogicalSize | PhysicalSize | null | undefined + ): Promise { + if (size && size.type !== 'Logical' && size.type !== 'Physical') { + throw new Error( + 'the `size` argument must be either a LogicalSize or a PhysicalSize instance' + ) + } + + return invoke('plugin:window|set_max_size', { + label: this.label, + value: size + ? { + type: size.type, + data: { + width: size.width, + height: size.height + } + } + : null + }) + } + + /** + * Sets the window outer position. + * @example + * ```typescript + * import { appWindow, LogicalPosition } from 'tauri-plugin-window-api'; + * await appWindow.setPosition(new LogicalPosition(600, 500)); + * ``` + * + * @param position The new position, in logical or physical pixels. + * @returns A promise indicating the success or failure of the operation. + */ + async setPosition( + position: LogicalPosition | PhysicalPosition + ): Promise { + if ( + !position || + (position.type !== 'Logical' && position.type !== 'Physical') + ) { + throw new Error( + 'the `position` argument must be either a LogicalPosition or a PhysicalPosition instance' + ) + } + + return invoke('plugin:window|set_position', { + label: this.label, + value: { + type: position.type, + data: { + x: position.x, + y: position.y + } + } + }) + } + + /** + * Sets the window fullscreen state. + * @example + * ```typescript + * import { appWindow } from 'tauri-plugin-window-api'; + * await appWindow.setFullscreen(true); + * ``` + * + * @param fullscreen Whether the window should go to fullscreen or not. + * @returns A promise indicating the success or failure of the operation. + */ + async setFullscreen(fullscreen: boolean): Promise { + return invoke('plugin:window|set_fullscreen', { + label: this.label, + value: fullscreen + }) + } + + /** + * Bring the window to front and focus. + * @example + * ```typescript + * import { appWindow } from 'tauri-plugin-window-api'; + * await appWindow.setFocus(); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + */ + async setFocus(): Promise { + return invoke('plugin:window|set_focus', { + label: this.label, + }) + } + + /** + * Sets the window icon. + * @example + * ```typescript + * import { appWindow } from 'tauri-plugin-window-api'; + * await appWindow.setIcon('/tauri/awesome.png'); + * ``` + * + * Note that you need the `icon-ico` or `icon-png` Cargo features to use this API. + * To enable it, change your Cargo.toml file: + * ```toml + * [dependencies] + * tauri = { version = "...", features = ["...", "icon-png"] } + * ``` + * + * @param icon Icon bytes or path to the icon file. + * @returns A promise indicating the success or failure of the operation. + */ + async setIcon(icon: string | Uint8Array): Promise { + return invoke('plugin:window|set_icon', { + label: this.label, + value: typeof icon === 'string' ? icon : Array.from(icon) + }) + } + + /** + * Whether the window icon should be hidden from the taskbar or not. + * + * #### Platform-specific + * + * - **macOS:** Unsupported. + * @example + * ```typescript + * import { appWindow } from 'tauri-plugin-window-api'; + * await appWindow.setSkipTaskbar(true); + * ``` + * + * @param skip true to hide window icon, false to show it. + * @returns A promise indicating the success or failure of the operation. + */ + async setSkipTaskbar(skip: boolean): Promise { + return invoke('plugin:window|set_skip_taskbar', { + label: this.label, + value: skip + }) + } + + /** + * Grabs the cursor, preventing it from leaving the window. + * + * There's no guarantee that the cursor will be hidden. You should + * hide it by yourself if you want so. + * + * #### Platform-specific + * + * - **Linux:** Unsupported. + * - **macOS:** This locks the cursor in a fixed location, which looks visually awkward. + * @example + * ```typescript + * import { appWindow } from 'tauri-plugin-window-api'; + * await appWindow.setCursorGrab(true); + * ``` + * + * @param grab `true` to grab the cursor icon, `false` to release it. + * @returns A promise indicating the success or failure of the operation. + */ + async setCursorGrab(grab: boolean): Promise { + return invoke('plugin:window|set_cursor_grab', { + label: this.label, + value: grab + }) + } + + /** + * Modifies the cursor's visibility. + * + * #### Platform-specific + * + * - **Windows:** The cursor is only hidden within the confines of the window. + * - **macOS:** The cursor is hidden as long as the window has input focus, even if the cursor is + * outside of the window. + * @example + * ```typescript + * import { appWindow } from 'tauri-plugin-window-api'; + * await appWindow.setCursorVisible(false); + * ``` + * + * @param visible If `false`, this will hide the cursor. If `true`, this will show the cursor. + * @returns A promise indicating the success or failure of the operation. + */ + async setCursorVisible(visible: boolean): Promise { + return invoke('plugin:window|set_cursor_visible', { + label: this.label, + value: visible + }) + } + + /** + * Modifies the cursor icon of the window. + * @example + * ```typescript + * import { appWindow } from 'tauri-plugin-window-api'; + * await appWindow.setCursorIcon('help'); + * ``` + * + * @param icon The new cursor icon. + * @returns A promise indicating the success or failure of the operation. + */ + async setCursorIcon(icon: CursorIcon): Promise { + return invoke('plugin:window|set_cursor_icon', { + label: this.label, + value: icon + }) + } + + /** + * Changes the position of the cursor in window coordinates. + * @example + * ```typescript + * import { appWindow, LogicalPosition } from 'tauri-plugin-window-api'; + * await appWindow.setCursorPosition(new LogicalPosition(600, 300)); + * ``` + * + * @param position The new cursor position. + * @returns A promise indicating the success or failure of the operation. + */ + async setCursorPosition( + position: LogicalPosition | PhysicalPosition + ): Promise { + if ( + !position || + (position.type !== 'Logical' && position.type !== 'Physical') + ) { + throw new Error( + 'the `position` argument must be either a LogicalPosition or a PhysicalPosition instance' + ) + } + + return invoke('plugin:window|set_cursor_position', { + label: this.label, + value: { + type: position.type, + data: { + x: position.x, + y: position.y + } + } + }) + } + + /** + * Changes the cursor events behavior. + * + * @example + * ```typescript + * import { appWindow } from 'tauri-plugin-window-api'; + * await appWindow.setIgnoreCursorEvents(true); + * ``` + * + * @param ignore `true` to ignore the cursor events; `false` to process them as usual. + * @returns A promise indicating the success or failure of the operation. + */ + async setIgnoreCursorEvents(ignore: boolean): Promise { + return invoke('plugin:window|set_ignore_cursor_events', { + label: this.label, + value: ignore + }) + } + + /** + * Starts dragging the window. + * @example + * ```typescript + * import { appWindow } from 'tauri-plugin-window-api'; + * await appWindow.startDragging(); + * ``` + * + * @return A promise indicating the success or failure of the operation. + */ + async startDragging(): Promise { + return invoke('plugin:window|start_dragging', { + label: this.label + }) + } + + // Listeners + + /** + * Listen to window resize. + * + * @example + * ```typescript + * import { appWindow } from "tauri-plugin-window-api"; + * const unlisten = await appWindow.onResized(({ payload: size }) => { + * console.log('Window resized', size); + * }); + * + * // you need to call unlisten if your handler goes out of scope e.g. the component is unmounted + * unlisten(); + * ``` + * + * @returns A promise resolving to a function to unlisten to the event. + * Note that removing the listener is required if your listener goes out of scope e.g. the component is unmounted. + * + * @since 1.0.2 + */ + async onResized(handler: EventCallback): Promise { + return this.listen(TauriEvent.WINDOW_RESIZED, (e) => { + e.payload = mapPhysicalSize(e.payload) + handler(e) + }) + } + + /** + * Listen to window move. + * + * @example + * ```typescript + * import { appWindow } from "tauri-plugin-window-api"; + * const unlisten = await appWindow.onMoved(({ payload: position }) => { + * console.log('Window moved', position); + * }); + * + * // you need to call unlisten if your handler goes out of scope e.g. the component is unmounted + * unlisten(); + * ``` + * + * @returns A promise resolving to a function to unlisten to the event. + * Note that removing the listener is required if your listener goes out of scope e.g. the component is unmounted. + * + * @since 1.0.2 + */ + async onMoved(handler: EventCallback): Promise { + return this.listen(TauriEvent.WINDOW_MOVED, (e) => { + e.payload = mapPhysicalPosition(e.payload) + handler(e) + }) + } + + /** + * Listen to window close requested. Emitted when the user requests to closes the window. + * + * @example + * ```typescript + * import { appWindow } from "tauri-plugin-window-api"; + * import { confirm } from '@tauri-apps/api/dialog'; + * const unlisten = await appWindow.onCloseRequested(async (event) => { + * const confirmed = await confirm('Are you sure?'); + * if (!confirmed) { + * // user did not confirm closing the window; let's prevent it + * event.preventDefault(); + * } + * }); + * + * // you need to call unlisten if your handler goes out of scope e.g. the component is unmounted + * unlisten(); + * ``` + * + * @returns A promise resolving to a function to unlisten to the event. + * Note that removing the listener is required if your listener goes out of scope e.g. the component is unmounted. + * + * @since 1.0.2 + */ + /* eslint-disable @typescript-eslint/promise-function-async */ + async onCloseRequested( + handler: (event: CloseRequestedEvent) => void | Promise + ): Promise { + return this.listen(TauriEvent.WINDOW_CLOSE_REQUESTED, (event) => { + const evt = new CloseRequestedEvent(event) + void Promise.resolve(handler(evt)).then(() => { + if (!evt.isPreventDefault()) { + return this.close() + } + }) + }) + } + /* eslint-enable */ + + /** + * Listen to window focus change. + * + * @example + * ```typescript + * import { appWindow } from "tauri-plugin-window-api"; + * const unlisten = await appWindow.onFocusChanged(({ payload: focused }) => { + * console.log('Focus changed, window is focused? ' + focused); + * }); + * + * // you need to call unlisten if your handler goes out of scope e.g. the component is unmounted + * unlisten(); + * ``` + * + * @returns A promise resolving to a function to unlisten to the event. + * Note that removing the listener is required if your listener goes out of scope e.g. the component is unmounted. + * + * @since 1.0.2 + */ + async onFocusChanged(handler: EventCallback): Promise { + const unlistenFocus = await this.listen( + TauriEvent.WINDOW_FOCUS, + (event) => { + handler({ ...event, payload: true }) + } + ) + const unlistenBlur = await this.listen( + TauriEvent.WINDOW_BLUR, + (event) => { + handler({ ...event, payload: false }) + } + ) + return () => { + unlistenFocus() + unlistenBlur() + } + } + + /** + * Listen to window scale change. Emitted when the window's scale factor has changed. + * The following user actions can cause DPI changes: + * - Changing the display's resolution. + * - Changing the display's scale factor (e.g. in Control Panel on Windows). + * - Moving the window to a display with a different scale factor. + * + * @example + * ```typescript + * import { appWindow } from "tauri-plugin-window-api"; + * const unlisten = await appWindow.onScaleChanged(({ payload }) => { + * console.log('Scale changed', payload.scaleFactor, payload.size); + * }); + * + * // you need to call unlisten if your handler goes out of scope e.g. the component is unmounted + * unlisten(); + * ``` + * + * @returns A promise resolving to a function to unlisten to the event. + * Note that removing the listener is required if your listener goes out of scope e.g. the component is unmounted. + * + * @since 1.0.2 + */ + async onScaleChanged( + handler: EventCallback + ): Promise { + return this.listen( + TauriEvent.WINDOW_SCALE_FACTOR_CHANGED, + handler + ) + } + + /** + * Listen to the window menu item click. The payload is the item id. + * + * @example + * ```typescript + * import { appWindow } from "tauri-plugin-window-api"; + * const unlisten = await appWindow.onMenuClicked(({ payload: menuId }) => { + * console.log('Menu clicked: ' + menuId); + * }); + * + * // you need to call unlisten if your handler goes out of scope e.g. the component is unmounted + * unlisten(); + * ``` + * + * @returns A promise resolving to a function to unlisten to the event. + * Note that removing the listener is required if your listener goes out of scope e.g. the component is unmounted. + * + * @since 1.0.2 + */ + async onMenuClicked(handler: EventCallback): Promise { + return this.listen(TauriEvent.MENU, handler) + } + + /** + * Listen to a file drop event. + * The listener is triggered when the user hovers the selected files on the window, + * drops the files or cancels the operation. + * + * @example + * ```typescript + * import { appWindow } from "tauri-plugin-window-api"; + * const unlisten = await appWindow.onFileDropEvent((event) => { + * if (event.payload.type === 'hover') { + * console.log('User hovering', event.payload.paths); + * } else if (event.payload.type === 'drop') { + * console.log('User dropped', event.payload.paths); + * } else { + * console.log('File drop cancelled'); + * } + * }); + * + * // you need to call unlisten if your handler goes out of scope e.g. the component is unmounted + * unlisten(); + * ``` + * + * @returns A promise resolving to a function to unlisten to the event. + * Note that removing the listener is required if your listener goes out of scope e.g. the component is unmounted. + * + * @since 1.0.2 + */ + async onFileDropEvent( + handler: EventCallback + ): Promise { + const unlistenFileDrop = await this.listen( + TauriEvent.WINDOW_FILE_DROP, + (event) => { + handler({ ...event, payload: { type: 'drop', paths: event.payload } }) + } + ) + + const unlistenFileHover = await this.listen( + TauriEvent.WINDOW_FILE_DROP_HOVER, + (event) => { + handler({ ...event, payload: { type: 'hover', paths: event.payload } }) + } + ) + + const unlistenCancel = await this.listen( + TauriEvent.WINDOW_FILE_DROP_CANCELLED, + (event) => { + handler({ ...event, payload: { type: 'cancel' } }) + } + ) + + return () => { + unlistenFileDrop() + unlistenFileHover() + unlistenCancel() + } + } + + /** + * Listen to the system theme change. + * + * @example + * ```typescript + * import { appWindow } from "tauri-plugin-window-api"; + * const unlisten = await appWindow.onThemeChanged(({ payload: theme }) => { + * console.log('New theme: ' + theme); + * }); + * + * // you need to call unlisten if your handler goes out of scope e.g. the component is unmounted + * unlisten(); + * ``` + * + * @returns A promise resolving to a function to unlisten to the event. + * Note that removing the listener is required if your listener goes out of scope e.g. the component is unmounted. + * + * @since 1.0.2 + */ + async onThemeChanged(handler: EventCallback): Promise { + return this.listen(TauriEvent.WINDOW_THEME_CHANGED, handler) + } +} + +/** + * @since 1.0.2 + */ +class CloseRequestedEvent { + /** Event name */ + event: EventName + /** The label of the window that emitted this event. */ + windowLabel: string + /** Event identifier used to unlisten */ + id: number + private _preventDefault = false + + constructor(event: Event) { + this.event = event.event + this.windowLabel = event.windowLabel + this.id = event.id + } + + preventDefault(): void { + this._preventDefault = true + } + + isPreventDefault(): boolean { + return this._preventDefault + } +} + +/** + * Create new webview windows and get a handle to existing ones. + * + * Windows are identified by a *label* a unique identifier that can be used to reference it later. + * It may only contain alphanumeric characters `a-zA-Z` plus the following special characters `-`, `/`, `:` and `_`. + * + * @example + * ```typescript + * // loading embedded asset: + * const webview = new WebviewWindow('theUniqueLabel', { + * url: 'path/to/page.html' + * }); + * // alternatively, load a remote URL: + * const webview = new WebviewWindow('theUniqueLabel', { + * url: 'https://github.com/tauri-apps/tauri' + * }); + * + * webview.once('tauri://created', function () { + * // webview window successfully created + * }); + * webview.once('tauri://error', function (e) { + * // an error happened creating the webview window + * }); + * + * // emit an event to the backend + * await webview.emit("some event", "data"); + * // listen to an event from the backend + * const unlisten = await webview.listen("event name", e => {}); + * unlisten(); + * ``` + * + * @since 1.0.2 + */ +class WebviewWindow extends WindowManager { + /** + * Creates a new WebviewWindow. + * @example + * ```typescript + * import { WebviewWindow } from 'tauri-plugin-window-api'; + * const webview = new WebviewWindow('my-label', { + * url: 'https://github.com/tauri-apps/tauri' + * }); + * webview.once('tauri://created', function () { + * // webview window successfully created + * }); + * webview.once('tauri://error', function (e) { + * // an error happened creating the webview window + * }); + * ``` + * + * * @param label The unique webview window label. Must be alphanumeric: `a-zA-Z-/:_`. + * @returns The WebviewWindow instance to communicate with the webview. + */ + constructor(label: WindowLabel, options: WindowOptions = {}) { + super(label) + // @ts-expect-error `skip` is not a public API so it is not defined in WindowOptions + if (!options?.skip) { + invoke('plugin:window|create', { + options: { + ...options, + label, + } + }) + .then(async () => this.emit('tauri://created')) + .catch(async (e: string) => this.emit('tauri://error', e)) + } + } + + /** + * Gets the WebviewWindow for the webview associated with the given label. + * @example + * ```typescript + * import { WebviewWindow } from 'tauri-plugin-window-api'; + * const mainWindow = WebviewWindow.getByLabel('main'); + * ``` + * + * @param label The webview window label. + * @returns The WebviewWindow instance to communicate with the webview or null if the webview doesn't exist. + */ + static getByLabel(label: string): WebviewWindow | null { + if (getAll().some((w) => w.label === label)) { + // @ts-expect-error `skip` is not defined in the public API but it is handled by the constructor + return new WebviewWindow(label, { skip: true }) + } + return null + } +} + +/** The WebviewWindow for the current window. */ +let appWindow: WebviewWindow +if ('__TAURI_METADATA__' in window) { + appWindow = new WebviewWindow( + window.__TAURI_METADATA__.__currentWindow.label, + { + // @ts-expect-error `skip` is not defined in the public API but it is handled by the constructor + skip: true + } + ) +} else { + console.warn( + `Could not find "window.__TAURI_METADATA__". The "appWindow" value will reference the "main" window label.\nNote that this is not an issue if running this frontend on a browser instead of a Tauri window.` + ) + appWindow = new WebviewWindow('main', { + // @ts-expect-error `skip` is not defined in the public API but it is handled by the constructor + skip: true + }) +} + +/** + * Configuration for the window to create. + * + * @since 1.0.0 + */ +interface WindowOptions { + /** + * Remote URL or local file path to open. + * + * - URL such as `https://github.com/tauri-apps` is opened directly on a Tauri window. + * - data: URL such as `data:text/html,...` is only supported with the `window-data-url` Cargo feature for the `tauri` dependency. + * - local file path or route such as `/path/to/page.html` or `/users` is appended to the application URL (the devServer URL on development, or `tauri://localhost/` and `https://tauri.localhost/` on production). + */ + url?: string + /** Show window in the center of the screen.. */ + center?: boolean + /** The initial vertical position. Only applies if `y` is also set. */ + x?: number + /** The initial horizontal position. Only applies if `x` is also set. */ + y?: number + /** The initial width. */ + width?: number + /** The initial height. */ + height?: number + /** The minimum width. Only applies if `minHeight` is also set. */ + minWidth?: number + /** The minimum height. Only applies if `minWidth` is also set. */ + minHeight?: number + /** The maximum width. Only applies if `maxHeight` is also set. */ + maxWidth?: number + /** The maximum height. Only applies if `maxWidth` is also set. */ + maxHeight?: number + /** Whether the window is resizable or not. */ + resizable?: boolean + /** Window title. */ + title?: string + /** Whether the window is in fullscreen mode or not. */ + fullscreen?: boolean + /** Whether the window will be initially focused or not. */ + focus?: boolean + /** + * Whether the window is transparent or not. + * Note that on `macOS` this requires the `macos-private-api` feature flag, enabled under `tauri.conf.json > tauri > macOSPrivateApi`. + * WARNING: Using private APIs on `macOS` prevents your application from being accepted to the `App Store`. + */ + transparent?: boolean + /** Whether the window should be maximized upon creation or not. */ + maximized?: boolean + /** Whether the window should be immediately visible upon creation or not. */ + visible?: boolean + /** Whether the window should have borders and bars or not. */ + decorations?: boolean + /** Whether the window should always be on top of other windows or not. */ + alwaysOnTop?: boolean + /** Prevents the window contents from being captured by other apps. */ + contentProtected?: boolean + /** Whether or not the window icon should be added to the taskbar. */ + skipTaskbar?: boolean + /** + * Whether or not the window has shadow. + * + * #### Platform-specific + * + * - **Windows:** + * - `false` has no effect on decorated window, shadows are always ON. + * - `true` will make ndecorated window have a 1px white border, + * and on Windows 11, it will have a rounded corners. + * - **Linux:** Unsupported. + * + * @since 2.0 + */ + shadow?: boolean + /** + * Whether the file drop is enabled or not on the webview. By default it is enabled. + * + * Disabling it is required to use drag and drop on the frontend on Windows. + */ + fileDropEnabled?: boolean + /** + * The initial window theme. Defaults to the system theme. + * + * Only implemented on Windows and macOS 10.14+. + */ + theme?: Theme + /** + * The style of the macOS title bar. + */ + titleBarStyle?: TitleBarStyle + /** + * If `true`, sets the window title to be hidden on macOS. + */ + hiddenTitle?: boolean + /** + * Whether clicking an inactive window also clicks through to the webview on macOS. + */ + acceptFirstMouse?: boolean + /** + * Defines the window [tabbing identifier](https://developer.apple.com/documentation/appkit/nswindow/1644704-tabbingidentifier) on macOS. + * + * Windows with the same tabbing identifier will be grouped together. + * If the tabbing identifier is not set, automatic tabbing will be disabled. + */ + tabbingIdentifier?: string + /** + * The user agent for the webview. + */ + userAgent?: string +} + +function mapMonitor(m: Monitor | null): Monitor | null { + return m === null + ? null + : { + name: m.name, + scaleFactor: m.scaleFactor, + position: mapPhysicalPosition(m.position), + size: mapPhysicalSize(m.size) + } +} + +function mapPhysicalPosition(m: PhysicalPosition): PhysicalPosition { + return new PhysicalPosition(m.x, m.y) +} + +function mapPhysicalSize(m: PhysicalSize): PhysicalSize { + return new PhysicalSize(m.width, m.height) +} + +/** + * Returns the monitor on which the window currently resides. + * Returns `null` if current monitor can't be detected. + * @example + * ```typescript + * import { currentMonitor } from 'tauri-plugin-window-api'; + * const monitor = currentMonitor(); + * ``` + * + * @since 1.0.0 + */ +async function currentMonitor(): Promise { + return invoke('plugin:window|current_monitor').then(mapMonitor) +} + +/** + * Returns the primary monitor of the system. + * Returns `null` if it can't identify any monitor as a primary one. + * @example + * ```typescript + * import { primaryMonitor } from 'tauri-plugin-window-api'; + * const monitor = primaryMonitor(); + * ``` + * + * @since 1.0.0 + */ +async function primaryMonitor(): Promise { + return invoke('plugin:window|primary_monitor').then(mapMonitor) +} + +/** + * Returns the list of all the monitors available on the system. + * @example + * ```typescript + * import { availableMonitors } from 'tauri-plugin-window-api'; + * const monitors = availableMonitors(); + * ``` + * + * @since 1.0.0 + */ +async function availableMonitors(): Promise { + return invoke('plugin:window|available_monitors').then((ms) => ms.map(mapMonitor) as Monitor[]) +} + +export { + WebviewWindow, + WebviewWindowHandle, + WindowManager, + CloseRequestedEvent, + getCurrent, + getAll, + appWindow, + LogicalSize, + PhysicalSize, + LogicalPosition, + PhysicalPosition, + UserAttentionType, + currentMonitor, + primaryMonitor, + availableMonitors +} + +export type { + Theme, + TitleBarStyle, + Monitor, + ScaleFactorChanged, + FileDropEvent, + WindowOptions +} diff --git a/plugins/window/package.json b/plugins/window/package.json new file mode 100644 index 00000000..f8502cfa --- /dev/null +++ b/plugins/window/package.json @@ -0,0 +1,32 @@ +{ + "name": "tauri-plugin-window-api", + "version": "0.0.0", + "license": "MIT or APACHE-2.0", + "authors": [ + "Tauri Programme within The Commons Conservancy" + ], + "type": "module", + "browser": "dist-js/index.min.js", + "module": "dist-js/index.mjs", + "types": "dist-js/index.d.ts", + "exports": { + "import": "./dist-js/index.mjs", + "types": "./dist-js/index.d.ts", + "browser": "./dist-js/index.min.js" + }, + "scripts": { + "build": "rollup -c" + }, + "files": [ + "dist-js", + "!dist-js/**/*.map", + "README.md", + "LICENSE" + ], + "devDependencies": { + "tslib": "^2.5.0" + }, + "dependencies": { + "@tauri-apps/api": "^1.2.0" + } +} \ No newline at end of file diff --git a/plugins/window/rollup.config.mjs b/plugins/window/rollup.config.mjs new file mode 100644 index 00000000..6555e98b --- /dev/null +++ b/plugins/window/rollup.config.mjs @@ -0,0 +1,11 @@ +import { readFileSync } from "fs"; + +import { createConfig } from "../../shared/rollup.config.mjs"; + +export default createConfig({ + input: "guest-js/index.ts", + pkg: JSON.parse( + readFileSync(new URL("./package.json", import.meta.url), "utf8") + ), + external: [/^@tauri-apps\/api/], +}); diff --git a/plugins/window/src/commands.rs b/plugins/window/src/commands.rs new file mode 100644 index 00000000..453c1f98 --- /dev/null +++ b/plugins/window/src/commands.rs @@ -0,0 +1,197 @@ +use serde::{Deserialize, Serialize, Serializer}; +use tauri::{ + utils::config::WindowConfig, AppHandle, CursorIcon, Icon, Manager, Monitor, PhysicalPosition, + PhysicalSize, Position, Runtime, Size, Theme, UserAttentionType, Window, +}; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("window not found")] + WindowNotFound, + #[error(transparent)] + Tauri(#[from] tauri::Error), +} + +impl Serialize for Error { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: Serializer, + { + serializer.serialize_str(self.to_string().as_ref()) + } +} + +type Result = std::result::Result; + +#[derive(Deserialize)] +#[serde(untagged)] +pub enum IconDto { + #[cfg(any(feature = "icon-png", feature = "icon-ico"))] + File(std::path::PathBuf), + #[cfg(any(feature = "icon-png", feature = "icon-ico"))] + Raw(Vec), + Rgba { + rgba: Vec, + width: u32, + height: u32, + }, +} + +impl From for Icon { + fn from(icon: IconDto) -> Self { + match icon { + #[cfg(any(feature = "icon-png", feature = "icon-ico"))] + IconDto::File(path) => Self::File(path), + #[cfg(any(feature = "icon-png", feature = "icon-ico"))] + IconDto::Raw(raw) => Self::Raw(raw), + IconDto::Rgba { + rgba, + width, + height, + } => Self::Rgba { + rgba, + width, + height, + }, + } + } +} + +#[tauri::command] +pub fn create(app: AppHandle, options: WindowConfig) -> Result<()> { + tauri::window::WindowBuilder::from_config(&app, options).build()?; + Ok(()) +} + +fn get_window(window: Window, label: Option) -> Result> { + match label { + Some(l) if !l.is_empty() => window.get_window(&l).ok_or(Error::WindowNotFound), + _ => Ok(window), + } +} + +macro_rules! getter { + ($cmd: ident, $ret: ty) => { + #[tauri::command] + pub fn $cmd(window: Window, label: Option) -> Result<$ret> { + get_window(window, label)?.$cmd().map_err(Into::into) + } + }; +} + +macro_rules! setter { + ($cmd: ident) => { + #[tauri::command] + pub fn $cmd(window: Window, label: Option) -> Result<()> { + get_window(window, label)?.$cmd().map_err(Into::into) + } + }; + + ($cmd: ident, $input: ty) => { + #[tauri::command] + pub fn $cmd( + window: Window, + label: Option, + value: $input, + ) -> Result<()> { + get_window(window, label)?.$cmd(value).map_err(Into::into) + } + }; +} + +getter!(scale_factor, f64); +getter!(inner_position, PhysicalPosition); +getter!(outer_position, PhysicalPosition); +getter!(inner_size, PhysicalSize); +getter!(outer_size, PhysicalSize); +getter!(is_fullscreen, bool); +getter!(is_minimized, bool); +getter!(is_maximized, bool); +getter!(is_decorated, bool); +getter!(is_resizable, bool); +getter!(is_visible, bool); +getter!(title, String); +getter!(current_monitor, Option); +getter!(primary_monitor, Option); +getter!(available_monitors, Vec); +getter!(theme, Theme); + +setter!(center); +setter!(request_user_attention, Option); +setter!(set_resizable, bool); +setter!(set_title, &str); +setter!(maximize); +setter!(unmaximize); +setter!(minimize); +setter!(unminimize); +setter!(show); +setter!(hide); +setter!(close); +setter!(set_decorations, bool); +setter!(set_shadow, bool); +setter!(set_always_on_top, bool); +setter!(set_content_protected, bool); +setter!(set_size, Size); +setter!(set_min_size, Option); +setter!(set_max_size, Option); +setter!(set_position, Position); +setter!(set_fullscreen, bool); +setter!(set_focus); +setter!(set_skip_taskbar, bool); +setter!(set_cursor_grab, bool); +setter!(set_cursor_visible, bool); +setter!(set_cursor_icon, CursorIcon); +setter!(set_cursor_position, Position); +setter!(set_ignore_cursor_events, bool); +setter!(start_dragging); +setter!(print); + +#[tauri::command] +pub fn set_icon( + window: Window, + label: Option, + value: IconDto, +) -> Result<()> { + get_window(window, label)? + .set_icon(value.into()) + .map_err(Into::into) +} + +#[tauri::command] +pub fn toggle_maximize(window: Window, label: Option) -> Result<()> { + let window = get_window(window, label)?; + match window.is_maximized()? { + true => window.unmaximize()?, + false => window.maximize()?, + }; + Ok(()) +} + +#[tauri::command] +pub fn internal_toggle_maximize( + window: Window, + label: Option, +) -> Result<()> { + let window = get_window(window, label)?; + if window.is_resizable()? { + match window.is_maximized()? { + true => window.unmaximize()?, + false => window.maximize()?, + }; + } + Ok(()) +} + +#[tauri::command] +pub fn internal_toggle_devtools( + window: Window, + label: Option, +) -> Result<()> { + let window = get_window(window, label)?; + if window.is_devtools_open() { + window.close_devtools(); + } else { + window.open_devtools(); + } + Ok(()) +} diff --git a/plugins/window/src/lib.rs b/plugins/window/src/lib.rs new file mode 100644 index 00000000..513a5d32 --- /dev/null +++ b/plugins/window/src/lib.rs @@ -0,0 +1,65 @@ +use tauri::{ + plugin::{Builder, TauriPlugin}, + Runtime, +}; + +mod commands; + +pub fn init() -> TauriPlugin { + Builder::new("window") + .invoke_handler(tauri::generate_handler![ + commands::create, + // getters + commands::scale_factor, + commands::inner_position, + commands::outer_position, + commands::inner_size, + commands::outer_size, + commands::is_fullscreen, + commands::is_minimized, + commands::is_maximized, + commands::is_decorated, + commands::is_resizable, + commands::is_visible, + commands::title, + commands::current_monitor, + commands::primary_monitor, + commands::available_monitors, + commands::theme, + // setters + commands::center, + commands::request_user_attention, + commands::set_resizable, + commands::set_title, + commands::maximize, + commands::unmaximize, + commands::minimize, + commands::unminimize, + commands::show, + commands::hide, + commands::close, + commands::set_decorations, + commands::set_shadow, + commands::set_always_on_top, + commands::set_content_protected, + commands::set_size, + commands::set_min_size, + commands::set_max_size, + commands::set_position, + commands::set_fullscreen, + commands::set_focus, + commands::set_skip_taskbar, + commands::set_cursor_grab, + commands::set_cursor_visible, + commands::set_cursor_icon, + commands::set_cursor_position, + commands::set_ignore_cursor_events, + commands::start_dragging, + commands::print, + commands::set_icon, + commands::toggle_maximize, + commands::internal_toggle_maximize, + commands::internal_toggle_devtools, + ]) + .build() +} diff --git a/plugins/window/tsconfig.json b/plugins/window/tsconfig.json new file mode 100644 index 00000000..5098169a --- /dev/null +++ b/plugins/window/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["guest-js/*.ts"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f8fa2c08..7e9023ae 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -91,6 +91,9 @@ importers: tauri-plugin-shell-api: specifier: 0.0.0 version: link:../../plugins/shell + tauri-plugin-window-api: + specifier: 0.0.0 + version: link:../../plugins/window devDependencies: '@iconify-json/codicon': specifier: ^1.1.10 @@ -189,6 +192,9 @@ importers: '@tauri-apps/api': specifier: ^1.2.0 version: 1.2.0 + tauri-plugin-window-api: + specifier: 0.0.0 + version: link:../window devDependencies: tslib: specifier: ^2.5.0 @@ -305,6 +311,9 @@ importers: '@tauri-apps/api': specifier: ^1.2.0 version: 1.2.0 + tauri-plugin-window-api: + specifier: 0.0.0 + version: link:../window devDependencies: tslib: specifier: ^2.5.0 @@ -351,11 +360,24 @@ importers: specifier: ^4.2.1 version: 4.3.3 + plugins/window: + dependencies: + '@tauri-apps/api': + specifier: ^1.2.0 + version: 1.2.0 + devDependencies: + tslib: + specifier: ^2.5.0 + version: 2.5.0 + plugins/window-state: dependencies: '@tauri-apps/api': specifier: ^1.2.0 version: 1.2.0 + tauri-plugin-window-api: + specifier: 0.0.0 + version: link:../window devDependencies: tslib: specifier: ^2.5.0