From bb26f7d7107f0b8507e07851f3d1b41745ce2c16 Mon Sep 17 00:00:00 2001 From: Lucas Fernandes Nogueira Date: Fri, 12 May 2023 03:58:20 -0700 Subject: [PATCH] feat(window): add plugin (#352) --- Cargo.lock | 35 +- examples/api/package.json | 3 +- examples/api/src-tauri/Cargo.lock | 24 +- examples/api/src-tauri/Cargo.toml | 1 + examples/api/src-tauri/src/lib.rs | 1 + examples/api/src/App.svelte | 5 +- examples/api/src/views/Communication.svelte | 38 +- examples/api/src/views/Window.svelte | 262 +-- plugins/fs-watch/guest-js/index.ts | 2 +- plugins/fs-watch/package.json | 3 +- plugins/upload/guest-js/index.ts | 2 +- plugins/upload/package.json | 3 +- plugins/window-state/guest-js/index.ts | 2 +- plugins/window-state/package.json | 3 +- plugins/window/Cargo.toml | 15 + plugins/window/LICENSE.spdx | 20 + plugins/window/LICENSE_APACHE-2.0 | 177 ++ plugins/window/LICENSE_MIT | 21 + plugins/window/README.md | 65 + plugins/window/guest-js/event.ts | 98 + plugins/window/guest-js/index.ts | 1923 +++++++++++++++++++ plugins/window/package.json | 32 + plugins/window/rollup.config.mjs | 11 + plugins/window/src/commands.rs | 197 ++ plugins/window/src/lib.rs | 65 + plugins/window/tsconfig.json | 4 + pnpm-lock.yaml | 22 + 27 files changed, 2865 insertions(+), 169 deletions(-) create mode 100644 plugins/window/Cargo.toml create mode 100644 plugins/window/LICENSE.spdx create mode 100644 plugins/window/LICENSE_APACHE-2.0 create mode 100644 plugins/window/LICENSE_MIT create mode 100644 plugins/window/README.md create mode 100644 plugins/window/guest-js/event.ts create mode 100644 plugins/window/guest-js/index.ts create mode 100644 plugins/window/package.json create mode 100644 plugins/window/rollup.config.mjs create mode 100644 plugins/window/src/commands.rs create mode 100644 plugins/window/src/lib.rs create mode 100644 plugins/window/tsconfig.json diff --git a/Cargo.lock b/Cargo.lock index 5096fc70..b1f52a19 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", @@ -5104,6 +5126,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" @@ -5171,7 +5202,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 ff0a11bf..c6801ef7 100644 --- a/examples/api/package.json +++ b/examples/api/package.json @@ -22,7 +22,8 @@ "tauri-plugin-os-api": "0.0.0", "tauri-plugin-process-api": "0.0.0", "tauri-plugin-shell-api": "0.0.0", - "tauri-plugin-updater-api": "0.0.0" + "tauri-plugin-updater-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 a83fefe3..d7722fc9 100644 --- a/examples/api/src-tauri/Cargo.lock +++ b/examples/api/src-tauri/Cargo.lock @@ -180,6 +180,7 @@ dependencies = [ "tauri-plugin-process", "tauri-plugin-shell", "tauri-plugin-updater", + "tauri-plugin-window", "tiny_http", "window-shadows", ] @@ -3603,7 +3604,7 @@ checksum = "fd1ba337640d60c3e96bc6f0638a939b9c9a7f2c316a1598c279828b3d1dc8c5" [[package]] name = "tauri" version = "2.0.0-alpha.8" -source = "git+https://github.com/tauri-apps/tauri?branch=next#9122e27ed863a5cb2bf13dc0dd0433c7387e141b" +source = "git+https://github.com/tauri-apps/tauri?branch=next#0ab5f40d3a4207f20e4440587b41c4e78f91d233" dependencies = [ "anyhow", "bytes", @@ -3659,7 +3660,7 @@ dependencies = [ [[package]] name = "tauri-build" version = "2.0.0-alpha.4" -source = "git+https://github.com/tauri-apps/tauri?branch=next#9122e27ed863a5cb2bf13dc0dd0433c7387e141b" +source = "git+https://github.com/tauri-apps/tauri?branch=next#0ab5f40d3a4207f20e4440587b41c4e78f91d233" dependencies = [ "anyhow", "cargo_toml", @@ -3680,7 +3681,7 @@ dependencies = [ [[package]] name = "tauri-codegen" version = "2.0.0-alpha.4" -source = "git+https://github.com/tauri-apps/tauri?branch=next#9122e27ed863a5cb2bf13dc0dd0433c7387e141b" +source = "git+https://github.com/tauri-apps/tauri?branch=next#0ab5f40d3a4207f20e4440587b41c4e78f91d233" dependencies = [ "base64 0.21.0", "brotli", @@ -3705,7 +3706,7 @@ dependencies = [ [[package]] name = "tauri-macros" version = "2.0.0-alpha.4" -source = "git+https://github.com/tauri-apps/tauri?branch=next#9122e27ed863a5cb2bf13dc0dd0433c7387e141b" +source = "git+https://github.com/tauri-apps/tauri?branch=next#0ab5f40d3a4207f20e4440587b41c4e78f91d233" dependencies = [ "heck 0.4.1", "proc-macro2", @@ -3894,10 +3895,19 @@ dependencies = [ "url", ] +[[package]] +name = "tauri-plugin-window" +version = "0.0.0" +dependencies = [ + "serde", + "tauri", + "thiserror", +] + [[package]] name = "tauri-runtime" version = "0.13.0-alpha.4" -source = "git+https://github.com/tauri-apps/tauri?branch=next#9122e27ed863a5cb2bf13dc0dd0433c7387e141b" +source = "git+https://github.com/tauri-apps/tauri?branch=next#0ab5f40d3a4207f20e4440587b41c4e78f91d233" dependencies = [ "gtk", "http", @@ -3918,7 +3928,7 @@ dependencies = [ [[package]] name = "tauri-runtime-wry" version = "0.13.0-alpha.4" -source = "git+https://github.com/tauri-apps/tauri?branch=next#9122e27ed863a5cb2bf13dc0dd0433c7387e141b" +source = "git+https://github.com/tauri-apps/tauri?branch=next#0ab5f40d3a4207f20e4440587b41c4e78f91d233" dependencies = [ "cocoa", "gtk", @@ -3938,7 +3948,7 @@ dependencies = [ [[package]] name = "tauri-utils" version = "2.0.0-alpha.4" -source = "git+https://github.com/tauri-apps/tauri?branch=next#9122e27ed863a5cb2bf13dc0dd0433c7387e141b" +source = "git+https://github.com/tauri-apps/tauri?branch=next#0ab5f40d3a4207f20e4440587b41c4e78f91d233" dependencies = [ "aes-gcm", "brotli", diff --git a/examples/api/src-tauri/Cargo.toml b/examples/api/src-tauri/Cargo.toml index 4d30e57d..fe8c70fe 100644 --- a/examples/api/src-tauri/Cargo.toml +++ b/examples/api/src-tauri/Cargo.toml @@ -28,6 +28,7 @@ tauri-plugin-os = { path = "../../../plugins/os" } tauri-plugin-process = { path = "../../../plugins/process" } tauri-plugin-shell = { path = "../../../plugins/shell" } tauri-plugin-updater = { path = "../../../plugins/updater" } +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 9e146179..d5d77a67 100644 --- a/examples/api/src-tauri/src/lib.rs +++ b/examples/api/src-tauri/src/lib.rs @@ -41,6 +41,7 @@ pub fn run() { .plugin(tauri_plugin_process::init()) .plugin(tauri_plugin_shell::init()) .plugin(tauri_plugin_updater::Builder::new().build()) + .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..3bd71a0d 100644 --- a/examples/api/src/App.svelte +++ b/examples/api/src/App.svelte @@ -1,7 +1,7 @@ diff --git a/examples/api/src/views/Window.svelte b/examples/api/src/views/Window.svelte index df77d9b0..5a54f942 100644 --- a/examples/api/src/views/Window.svelte +++ b/examples/api/src/views/Window.svelte @@ -5,217 +5,217 @@ LogicalSize, UserAttentionType, PhysicalSize, - PhysicalPosition - } from '@tauri-apps/api/window' - import { open as openDialog } from 'tauri-plugin-dialog-api' - import { open } from 'tauri-plugin-shell-api' + PhysicalPosition, + } from "tauri-plugin-window-api"; + import { open as openDialog } from "tauri-plugin-dialog-api"; + import { open } from "tauri-plugin-shell-api"; - let selectedWindow = appWindow.label + let selectedWindow = appWindow.label; const windowMap = { - [appWindow.label]: appWindow - } + [appWindow.label]: appWindow, + }; const cursorIconOptions = [ - 'default', - 'crosshair', - 'hand', - 'arrow', - 'move', - 'text', - 'wait', - 'help', - 'progress', + "default", + "crosshair", + "hand", + "arrow", + "move", + "text", + "wait", + "help", + "progress", // something cannot be done - 'notAllowed', - 'contextMenu', - 'cell', - 'verticalText', - 'alias', - 'copy', - 'noDrop', + "notAllowed", + "contextMenu", + "cell", + "verticalText", + "alias", + "copy", + "noDrop", // something can be grabbed - 'grab', + "grab", /// something is grabbed - 'grabbing', - 'allScroll', - 'zoomIn', - 'zoomOut', + "grabbing", + "allScroll", + "zoomIn", + "zoomOut", // edge is to be moved - 'eResize', - 'nResize', - 'neResize', - 'nwResize', - 'sResize', - 'seResize', - 'swResize', - 'wResize', - 'ewResize', - 'nsResize', - 'neswResize', - 'nwseResize', - 'colResize', - 'rowResize' - ] + "eResize", + "nResize", + "neResize", + "nwResize", + "sResize", + "seResize", + "swResize", + "wResize", + "ewResize", + "nsResize", + "neswResize", + "nwseResize", + "colResize", + "rowResize", + ]; - export let onMessage + export let onMessage; - let newWindowLabel + let newWindowLabel; - let urlValue = 'https://tauri.app' - let resizable = true - let maximized = false - let decorations = true - let alwaysOnTop = false - let contentProtected = true - let fullscreen = false - let width = null - let height = null - let minWidth = null - let minHeight = null - let maxWidth = null - let maxHeight = null - let x = null - let y = null - let scaleFactor = 1 - let innerPosition = new PhysicalPosition(x, y) - let outerPosition = new PhysicalPosition(x, y) - let innerSize = new PhysicalSize(width, height) - let outerSize = new PhysicalSize(width, height) - let resizeEventUnlisten - let moveEventUnlisten - let cursorGrab = false - let cursorVisible = true - let cursorX = null - let cursorY = null - let cursorIcon = 'default' - let cursorIgnoreEvents = false - let windowTitle = 'Awesome Tauri Example!' + let urlValue = "https://tauri.app"; + let resizable = true; + let maximized = false; + let decorations = true; + let alwaysOnTop = false; + let contentProtected = true; + let fullscreen = false; + let width = null; + let height = null; + let minWidth = null; + let minHeight = null; + let maxWidth = null; + let maxHeight = null; + let x = null; + let y = null; + let scaleFactor = 1; + let innerPosition = new PhysicalPosition(x, y); + let outerPosition = new PhysicalPosition(x, y); + let innerSize = new PhysicalSize(width, height); + let outerSize = new PhysicalSize(width, height); + let resizeEventUnlisten; + let moveEventUnlisten; + let cursorGrab = false; + let cursorVisible = true; + let cursorX = null; + let cursorY = null; + let cursorIcon = "default"; + let cursorIgnoreEvents = false; + let windowTitle = "Awesome Tauri Example!"; function openUrl() { - open(urlValue) + open(urlValue); } function setTitle_() { - windowMap[selectedWindow].setTitle(windowTitle) + windowMap[selectedWindow].setTitle(windowTitle); } function hide_() { - windowMap[selectedWindow].hide() - setTimeout(windowMap[selectedWindow].show, 2000) + windowMap[selectedWindow].hide(); + setTimeout(windowMap[selectedWindow].show, 2000); } function minimize_() { - windowMap[selectedWindow].minimize() - setTimeout(windowMap[selectedWindow].unminimize, 2000) + windowMap[selectedWindow].minimize(); + setTimeout(windowMap[selectedWindow].unminimize, 2000); } function getIcon() { openDialog({ - multiple: false + multiple: false, }).then((path) => { - if (typeof path === 'string') { - windowMap[selectedWindow].setIcon(path) + if (typeof path === "string") { + windowMap[selectedWindow].setIcon(path); } - }) + }); } function createWindow() { - if (!newWindowLabel) return + if (!newWindowLabel) return; - const webview = new WebviewWindow(newWindowLabel) - windowMap[newWindowLabel] = webview - webview.once('tauri://error', function () { - onMessage('Error creating new webview') - }) + const webview = new WebviewWindow(newWindowLabel); + windowMap[newWindowLabel] = webview; + webview.once("tauri://error", function () { + onMessage("Error creating new webview"); + }); } function loadWindowSize() { windowMap[selectedWindow].innerSize().then((response) => { - innerSize = response - width = innerSize.width - height = innerSize.height - }) + innerSize = response; + width = innerSize.width; + height = innerSize.height; + }); windowMap[selectedWindow].outerSize().then((response) => { - outerSize = response - }) + outerSize = response; + }); } function loadWindowPosition() { windowMap[selectedWindow].innerPosition().then((response) => { - innerPosition = response - }) + innerPosition = response; + }); windowMap[selectedWindow].outerPosition().then((response) => { - outerPosition = response - x = outerPosition.x - y = outerPosition.y - }) + outerPosition = response; + x = outerPosition.x; + y = outerPosition.y; + }); } async function addWindowEventListeners(window) { - if (!window) return + if (!window) return; if (resizeEventUnlisten) { - resizeEventUnlisten() + resizeEventUnlisten(); } if (moveEventUnlisten) { - moveEventUnlisten() + moveEventUnlisten(); } - moveEventUnlisten = await window.listen('tauri://move', loadWindowPosition) - resizeEventUnlisten = await window.listen('tauri://resize', loadWindowSize) + moveEventUnlisten = await window.listen("tauri://move", loadWindowPosition); + resizeEventUnlisten = await window.listen("tauri://resize", loadWindowSize); } async function requestUserAttention_() { - await windowMap[selectedWindow].minimize() + await windowMap[selectedWindow].minimize(); await windowMap[selectedWindow].requestUserAttention( UserAttentionType.Critical - ) - await new Promise((resolve) => setTimeout(resolve, 3000)) - await windowMap[selectedWindow].requestUserAttention(null) + ); + await new Promise((resolve) => setTimeout(resolve, 3000)); + await windowMap[selectedWindow].requestUserAttention(null); } $: { - windowMap[selectedWindow] - loadWindowPosition() - loadWindowSize() + windowMap[selectedWindow]; + loadWindowPosition(); + loadWindowSize(); } - $: windowMap[selectedWindow]?.setResizable(resizable) + $: windowMap[selectedWindow]?.setResizable(resizable); $: maximized ? windowMap[selectedWindow]?.maximize() - : windowMap[selectedWindow]?.unmaximize() - $: windowMap[selectedWindow]?.setDecorations(decorations) - $: windowMap[selectedWindow]?.setAlwaysOnTop(alwaysOnTop) - $: windowMap[selectedWindow]?.setContentProtected(contentProtected) - $: windowMap[selectedWindow]?.setFullscreen(fullscreen) + : windowMap[selectedWindow]?.unmaximize(); + $: windowMap[selectedWindow]?.setDecorations(decorations); + $: windowMap[selectedWindow]?.setAlwaysOnTop(alwaysOnTop); + $: windowMap[selectedWindow]?.setContentProtected(contentProtected); + $: windowMap[selectedWindow]?.setFullscreen(fullscreen); $: width && height && - windowMap[selectedWindow]?.setSize(new PhysicalSize(width, height)) + windowMap[selectedWindow]?.setSize(new PhysicalSize(width, height)); $: minWidth && minHeight ? windowMap[selectedWindow]?.setMinSize( new LogicalSize(minWidth, minHeight) ) - : windowMap[selectedWindow]?.setMinSize(null) + : windowMap[selectedWindow]?.setMinSize(null); $: maxWidth > 800 && maxHeight > 400 ? windowMap[selectedWindow]?.setMaxSize( new LogicalSize(maxWidth, maxHeight) ) - : windowMap[selectedWindow]?.setMaxSize(null) + : windowMap[selectedWindow]?.setMaxSize(null); $: x !== null && y !== null && - windowMap[selectedWindow]?.setPosition(new PhysicalPosition(x, y)) + windowMap[selectedWindow]?.setPosition(new PhysicalPosition(x, y)); $: windowMap[selectedWindow] ?.scaleFactor() - .then((factor) => (scaleFactor = factor)) - $: addWindowEventListeners(windowMap[selectedWindow]) + .then((factor) => (scaleFactor = factor)); + $: addWindowEventListeners(windowMap[selectedWindow]); - $: windowMap[selectedWindow]?.setCursorGrab(cursorGrab) - $: windowMap[selectedWindow]?.setCursorVisible(cursorVisible) - $: windowMap[selectedWindow]?.setCursorIcon(cursorIcon) + $: windowMap[selectedWindow]?.setCursorGrab(cursorGrab); + $: windowMap[selectedWindow]?.setCursorVisible(cursorVisible); + $: windowMap[selectedWindow]?.setCursorIcon(cursorIcon); $: cursorX !== null && cursorY !== null && windowMap[selectedWindow]?.setCursorPosition( new PhysicalPosition(cursorX, cursorY) - ) - $: windowMap[selectedWindow]?.setIgnoreCursorEvents(cursorIgnoreEvents) + ); + $: windowMap[selectedWindow]?.setIgnoreCursorEvents(cursorIgnoreEvents);
diff --git a/plugins/fs-watch/guest-js/index.ts b/plugins/fs-watch/guest-js/index.ts index 05ed07e5..9d25096f 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; 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/event.ts b/plugins/window/guest-js/event.ts new file mode 100644 index 00000000..798b4fb4 --- /dev/null +++ b/plugins/window/guest-js/event.ts @@ -0,0 +1,98 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { invoke, transformCallback } from "@tauri-apps/api/tauri"; + +export interface Event { + /** Event name */ + event: string; + /** The label of the window that emitted this event. */ + windowLabel: string; + /** Event identifier used to unlisten */ + id: number; + /** Event payload */ + payload: T; +} + +export type EventCallback = (event: Event) => void; + +export type UnlistenFn = () => void; + +/** + * Unregister the event listener associated with the given name and id. + * + * @ignore + * @param event The event name + * @param eventId Event identifier + * @returns + */ +async function _unlisten(event: string, eventId: number): Promise { + await invoke("plugin:event|unlisten", { + event, + eventId, + }); +} + +/** + * Emits an event to the backend. + * + * @param event Event name. Must include only alphanumeric characters, `-`, `/`, `:` and `_`. + * @param [windowLabel] The label of the window to which the event is sent, if null/undefined the event will be sent to all windows + * @param [payload] Event payload + * @returns + */ +async function emit( + event: string, + windowLabel?: string, + payload?: unknown +): Promise { + await invoke("plugin:event|emit", { + event, + windowLabel, + payload, + }); +} + +/** + * Listen to an event from the backend. + * + * @param event Event name. Must include only alphanumeric characters, `-`, `/`, `:` and `_`. + * @param handler Event handler callback. + * @return A promise resolving to a function to unlisten to the event. + */ +async function listen( + event: string, + windowLabel: string | null, + handler: EventCallback +): Promise { + return invoke("plugin:event|listen", { + event, + windowLabel, + handler: transformCallback(handler), + }).then((eventId) => { + return async () => _unlisten(event, eventId); + }); +} + +/** + * Listen to an one-off event from the backend. + * + * @param event Event name. Must include only alphanumeric characters, `-`, `/`, `:` and `_`. + * @param handler Event handler callback. + * @returns A promise resolving to a function to unlisten to the event. + */ +async function once( + event: string, + windowLabel: string | null, + handler: EventCallback +): Promise { + return listen(event, windowLabel, (eventData) => { + handler(eventData); + _unlisten(event, eventData.id).catch(() => { + // do nothing + }); + }); +} + +export { emit, listen, once }; diff --git a/plugins/window/guest-js/index.ts b/plugins/window/guest-js/index.ts new file mode 100644 index 00000000..59e7f77a --- /dev/null +++ b/plugins/window/guest-js/index.ts @@ -0,0 +1,1923 @@ +// 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 { TauriEvent } from "@tauri-apps/api/event"; +// TODO: use from @tauri-apps/api v2 +import { emit, listen, once } from "./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); + }); + } + 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); + }); + } + 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(); + } + 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..6e564693 --- /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" + } +} 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 7ac17f00..c43adf01 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -97,6 +97,9 @@ importers: tauri-plugin-updater-api: specifier: 0.0.0 version: link:../../plugins/updater + tauri-plugin-window-api: + specifier: 0.0.0 + version: link:../../plugins/window devDependencies: '@iconify-json/codicon': specifier: ^1.1.10 @@ -195,6 +198,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 @@ -331,6 +337,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 @@ -377,11 +386,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