From 6bd4dd0489fb72419f5354e2c0553e99d08b4996 Mon Sep 17 00:00:00 2001 From: Jonas Kruckenberg Date: Thu, 3 Nov 2022 11:09:17 +0100 Subject: [PATCH] feat: add window module --- Cargo.toml | 8 +- build.rs | 7 +- dist/window.js | 1004 ++++++++++++++++++++++++++++++ examples/api/src/main.rs | 3 + examples/api/src/views/mod.rs | 4 + examples/api/src/views/window.rs | 63 ++ src/lib.rs | 2 + src/window.rs | 744 ++++++++++++++++++++++ 8 files changed, 1830 insertions(+), 5 deletions(-) create mode 100644 dist/window.js create mode 100644 examples/api/src/views/window.rs create mode 100644 src/window.rs diff --git a/Cargo.toml b/Cargo.toml index 8c2523a..6c3aecd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,10 +19,14 @@ semver = { version = "1.0.14", optional = true } wasm-bindgen-test = "0.3.33" tauri-sys = { path = ".", features = ["all"] } +[package.metadata.docs.rs] +all-features = true + [features] -all = ["app", "clipboard", "event", "mocks", "tauri"] +all = ["app", "clipboard", "event", "mocks", "tauri", "window"] app = ["dep:semver"] clipboard = [] event = [] mocks = [] -tauri = ["dep:url"] \ No newline at end of file +tauri = ["dep:url"] +window = [] \ No newline at end of file diff --git a/build.rs b/build.rs index f0eae84..edbc302 100644 --- a/build.rs +++ b/build.rs @@ -2,7 +2,7 @@ use std::process::Command; fn main() { /* Shared arguments */ - let sargs: [&str; 8] = [ + let sargs: &[&str] = &[ "--outdir=dist", "--format=esm", "--bundle", @@ -11,18 +11,19 @@ fn main() { "tauri/tooling/api/src/tauri.ts", "tauri/tooling/api/src/event.ts", "tauri/tooling/api/src/mocks.ts", + "tauri/tooling/api/src/window.ts" ]; if cfg!(windows) { /* Use cmd if the target is windows */ Command::new("cmd") .args(&["/C", "esbuild"]) - .args(&sargs) + .args(sargs) .output() .unwrap(); } else if cfg!(unix) { Command::new("esbuild") - .args(&sargs) + .args(sargs) .output() .unwrap(); } else { diff --git a/dist/window.js b/dist/window.js new file mode 100644 index 0000000..e0b637b --- /dev/null +++ b/dist/window.js @@ -0,0 +1,1004 @@ +// tauri/tooling/api/src/tauri.ts +function uid() { + return window.crypto.getRandomValues(new Uint32Array(1))[0]; +} +function transformCallback(callback, once2 = false) { + const identifier = uid(); + const prop = `_${identifier}`; + Object.defineProperty(window, prop, { + value: (result) => { + if (once2) { + Reflect.deleteProperty(window, prop); + } + return callback?.(result); + }, + writable: false, + configurable: true + }); + return identifier; +} +async function invoke(cmd, args = {}) { + return new Promise((resolve, reject) => { + const callback = transformCallback((e) => { + resolve(e); + Reflect.deleteProperty(window, `_${error}`); + }, true); + const error = transformCallback((e) => { + reject(e); + Reflect.deleteProperty(window, `_${callback}`); + }, true); + window.__TAURI_IPC__({ + cmd, + callback, + error, + ...args + }); + }); +} + +// tauri/tooling/api/src/helpers/tauri.ts +async function invokeTauriCommand(command) { + return invoke("tauri", command); +} + +// tauri/tooling/api/src/helpers/event.ts +async function _unlisten(event, eventId) { + return invokeTauriCommand({ + __tauriModule: "Event", + message: { + cmd: "unlisten", + event, + eventId + } + }); +} +async function emit(event, windowLabel, payload) { + await invokeTauriCommand({ + __tauriModule: "Event", + message: { + cmd: "emit", + event, + windowLabel, + payload + } + }); +} +async function listen(event, windowLabel, handler) { + return invokeTauriCommand({ + __tauriModule: "Event", + message: { + cmd: "listen", + event, + windowLabel, + handler: transformCallback(handler) + } + }).then((eventId) => { + return async () => _unlisten(event, eventId); + }); +} +async function once(event, windowLabel, handler) { + return listen(event, windowLabel, (eventData) => { + handler(eventData); + _unlisten(event, eventData.id).catch(() => { + }); + }); +} + +// tauri/tooling/api/src/window.ts +var LogicalSize = class { + constructor(width, height) { + this.type = "Logical"; + this.width = width; + this.height = height; + } +}; +var PhysicalSize = class { + constructor(width, height) { + this.type = "Physical"; + this.width = width; + this.height = height; + } + toLogical(scaleFactor) { + return new LogicalSize(this.width / scaleFactor, this.height / scaleFactor); + } +}; +var LogicalPosition = class { + constructor(x, y) { + this.type = "Logical"; + this.x = x; + this.y = y; + } +}; +var PhysicalPosition = class { + constructor(x, y) { + this.type = "Physical"; + this.x = x; + this.y = y; + } + toLogical(scaleFactor) { + return new LogicalPosition(this.x / scaleFactor, this.y / scaleFactor); + } +}; +var UserAttentionType = /* @__PURE__ */ ((UserAttentionType2) => { + UserAttentionType2[UserAttentionType2["Critical"] = 1] = "Critical"; + UserAttentionType2[UserAttentionType2["Informational"] = 2] = "Informational"; + return UserAttentionType2; +})(UserAttentionType || {}); +function getCurrent() { + return new WebviewWindow(window.__TAURI_METADATA__.__currentWindow.label, { + skip: true + }); +} +function getAll() { + return window.__TAURI_METADATA__.__windows.map( + (w) => new WebviewWindow(w.label, { + skip: true + }) + ); +} +var localTauriEvents = ["tauri://created", "tauri://error"]; +var WebviewWindowHandle = class { + constructor(label) { + this.label = label; + this.listeners = /* @__PURE__ */ Object.create(null); + } + async listen(event, handler) { + if (this._handleTauriEvent(event, handler)) { + return Promise.resolve(() => { + const listeners = this.listeners[event]; + listeners.splice(listeners.indexOf(handler), 1); + }); + } + return listen(event, this.label, handler); + } + async once(event, handler) { + if (this._handleTauriEvent(event, handler)) { + return Promise.resolve(() => { + const listeners = this.listeners[event]; + listeners.splice(listeners.indexOf(handler), 1); + }); + } + return once(event, this.label, handler); + } + async emit(event, payload) { + if (localTauriEvents.includes(event)) { + for (const handler of this.listeners[event] || []) { + handler({ event, id: -1, windowLabel: this.label, payload }); + } + return Promise.resolve(); + } + return emit(event, this.label, payload); + } + _handleTauriEvent(event, handler) { + if (localTauriEvents.includes(event)) { + if (!(event in this.listeners)) { + this.listeners[event] = [handler]; + } else { + this.listeners[event].push(handler); + } + return true; + } + return false; + } +}; +var WindowManager = class extends WebviewWindowHandle { + async scaleFactor() { + return invokeTauriCommand({ + __tauriModule: "Window", + message: { + cmd: "manage", + data: { + label: this.label, + cmd: { + type: "scaleFactor" + } + } + } + }); + } + async innerPosition() { + return invokeTauriCommand({ + __tauriModule: "Window", + message: { + cmd: "manage", + data: { + label: this.label, + cmd: { + type: "innerPosition" + } + } + } + }).then(({ x, y }) => new PhysicalPosition(x, y)); + } + async outerPosition() { + return invokeTauriCommand({ + __tauriModule: "Window", + message: { + cmd: "manage", + data: { + label: this.label, + cmd: { + type: "outerPosition" + } + } + } + }).then(({ x, y }) => new PhysicalPosition(x, y)); + } + async innerSize() { + return invokeTauriCommand({ + __tauriModule: "Window", + message: { + cmd: "manage", + data: { + label: this.label, + cmd: { + type: "innerSize" + } + } + } + }).then(({ width, height }) => new PhysicalSize(width, height)); + } + async outerSize() { + return invokeTauriCommand({ + __tauriModule: "Window", + message: { + cmd: "manage", + data: { + label: this.label, + cmd: { + type: "outerSize" + } + } + } + }).then(({ width, height }) => new PhysicalSize(width, height)); + } + async isFullscreen() { + return invokeTauriCommand({ + __tauriModule: "Window", + message: { + cmd: "manage", + data: { + label: this.label, + cmd: { + type: "isFullscreen" + } + } + } + }); + } + async isMaximized() { + return invokeTauriCommand({ + __tauriModule: "Window", + message: { + cmd: "manage", + data: { + label: this.label, + cmd: { + type: "isMaximized" + } + } + } + }); + } + async isDecorated() { + return invokeTauriCommand({ + __tauriModule: "Window", + message: { + cmd: "manage", + data: { + label: this.label, + cmd: { + type: "isDecorated" + } + } + } + }); + } + async isResizable() { + return invokeTauriCommand({ + __tauriModule: "Window", + message: { + cmd: "manage", + data: { + label: this.label, + cmd: { + type: "isResizable" + } + } + } + }); + } + async isVisible() { + return invokeTauriCommand({ + __tauriModule: "Window", + message: { + cmd: "manage", + data: { + label: this.label, + cmd: { + type: "isVisible" + } + } + } + }); + } + async theme() { + return invokeTauriCommand({ + __tauriModule: "Window", + message: { + cmd: "manage", + data: { + label: this.label, + cmd: { + type: "theme" + } + } + } + }); + } + async center() { + return invokeTauriCommand({ + __tauriModule: "Window", + message: { + cmd: "manage", + data: { + label: this.label, + cmd: { + type: "center" + } + } + } + }); + } + async requestUserAttention(requestType) { + let requestType_ = null; + if (requestType) { + if (requestType === 1 /* Critical */) { + requestType_ = { type: "Critical" }; + } else { + requestType_ = { type: "Informational" }; + } + } + return invokeTauriCommand({ + __tauriModule: "Window", + message: { + cmd: "manage", + data: { + label: this.label, + cmd: { + type: "requestUserAttention", + payload: requestType_ + } + } + } + }); + } + async setResizable(resizable) { + return invokeTauriCommand({ + __tauriModule: "Window", + message: { + cmd: "manage", + data: { + label: this.label, + cmd: { + type: "setResizable", + payload: resizable + } + } + } + }); + } + async setTitle(title) { + return invokeTauriCommand({ + __tauriModule: "Window", + message: { + cmd: "manage", + data: { + label: this.label, + cmd: { + type: "setTitle", + payload: title + } + } + } + }); + } + async maximize() { + return invokeTauriCommand({ + __tauriModule: "Window", + message: { + cmd: "manage", + data: { + label: this.label, + cmd: { + type: "maximize" + } + } + } + }); + } + async unmaximize() { + return invokeTauriCommand({ + __tauriModule: "Window", + message: { + cmd: "manage", + data: { + label: this.label, + cmd: { + type: "unmaximize" + } + } + } + }); + } + async toggleMaximize() { + return invokeTauriCommand({ + __tauriModule: "Window", + message: { + cmd: "manage", + data: { + label: this.label, + cmd: { + type: "toggleMaximize" + } + } + } + }); + } + async minimize() { + return invokeTauriCommand({ + __tauriModule: "Window", + message: { + cmd: "manage", + data: { + label: this.label, + cmd: { + type: "minimize" + } + } + } + }); + } + async unminimize() { + return invokeTauriCommand({ + __tauriModule: "Window", + message: { + cmd: "manage", + data: { + label: this.label, + cmd: { + type: "unminimize" + } + } + } + }); + } + async show() { + return invokeTauriCommand({ + __tauriModule: "Window", + message: { + cmd: "manage", + data: { + label: this.label, + cmd: { + type: "show" + } + } + } + }); + } + async hide() { + return invokeTauriCommand({ + __tauriModule: "Window", + message: { + cmd: "manage", + data: { + label: this.label, + cmd: { + type: "hide" + } + } + } + }); + } + async close() { + return invokeTauriCommand({ + __tauriModule: "Window", + message: { + cmd: "manage", + data: { + label: this.label, + cmd: { + type: "close" + } + } + } + }); + } + async setDecorations(decorations) { + return invokeTauriCommand({ + __tauriModule: "Window", + message: { + cmd: "manage", + data: { + label: this.label, + cmd: { + type: "setDecorations", + payload: decorations + } + } + } + }); + } + async setAlwaysOnTop(alwaysOnTop) { + return invokeTauriCommand({ + __tauriModule: "Window", + message: { + cmd: "manage", + data: { + label: this.label, + cmd: { + type: "setAlwaysOnTop", + payload: alwaysOnTop + } + } + } + }); + } + async setSize(size) { + if (!size || size.type !== "Logical" && size.type !== "Physical") { + throw new Error( + "the `size` argument must be either a LogicalSize or a PhysicalSize instance" + ); + } + return invokeTauriCommand({ + __tauriModule: "Window", + message: { + cmd: "manage", + data: { + label: this.label, + cmd: { + type: "setSize", + payload: { + type: size.type, + data: { + width: size.width, + height: size.height + } + } + } + } + } + }); + } + async setMinSize(size) { + if (size && size.type !== "Logical" && size.type !== "Physical") { + throw new Error( + "the `size` argument must be either a LogicalSize or a PhysicalSize instance" + ); + } + return invokeTauriCommand({ + __tauriModule: "Window", + message: { + cmd: "manage", + data: { + label: this.label, + cmd: { + type: "setMinSize", + payload: size ? { + type: size.type, + data: { + width: size.width, + height: size.height + } + } : null + } + } + } + }); + } + async setMaxSize(size) { + if (size && size.type !== "Logical" && size.type !== "Physical") { + throw new Error( + "the `size` argument must be either a LogicalSize or a PhysicalSize instance" + ); + } + return invokeTauriCommand({ + __tauriModule: "Window", + message: { + cmd: "manage", + data: { + label: this.label, + cmd: { + type: "setMaxSize", + payload: size ? { + type: size.type, + data: { + width: size.width, + height: size.height + } + } : null + } + } + } + }); + } + async setPosition(position) { + if (!position || position.type !== "Logical" && position.type !== "Physical") { + throw new Error( + "the `position` argument must be either a LogicalPosition or a PhysicalPosition instance" + ); + } + return invokeTauriCommand({ + __tauriModule: "Window", + message: { + cmd: "manage", + data: { + label: this.label, + cmd: { + type: "setPosition", + payload: { + type: position.type, + data: { + x: position.x, + y: position.y + } + } + } + } + } + }); + } + async setFullscreen(fullscreen) { + return invokeTauriCommand({ + __tauriModule: "Window", + message: { + cmd: "manage", + data: { + label: this.label, + cmd: { + type: "setFullscreen", + payload: fullscreen + } + } + } + }); + } + async setFocus() { + return invokeTauriCommand({ + __tauriModule: "Window", + message: { + cmd: "manage", + data: { + label: this.label, + cmd: { + type: "setFocus" + } + } + } + }); + } + async setIcon(icon) { + return invokeTauriCommand({ + __tauriModule: "Window", + message: { + cmd: "manage", + data: { + label: this.label, + cmd: { + type: "setIcon", + payload: { + icon: typeof icon === "string" ? icon : Array.from(icon) + } + } + } + } + }); + } + async setSkipTaskbar(skip) { + return invokeTauriCommand({ + __tauriModule: "Window", + message: { + cmd: "manage", + data: { + label: this.label, + cmd: { + type: "setSkipTaskbar", + payload: skip + } + } + } + }); + } + async setCursorGrab(grab) { + return invokeTauriCommand({ + __tauriModule: "Window", + message: { + cmd: "manage", + data: { + label: this.label, + cmd: { + type: "setCursorGrab", + payload: grab + } + } + } + }); + } + async setCursorVisible(visible) { + return invokeTauriCommand({ + __tauriModule: "Window", + message: { + cmd: "manage", + data: { + label: this.label, + cmd: { + type: "setCursorVisible", + payload: visible + } + } + } + }); + } + async setCursorIcon(icon) { + return invokeTauriCommand({ + __tauriModule: "Window", + message: { + cmd: "manage", + data: { + label: this.label, + cmd: { + type: "setCursorIcon", + payload: icon + } + } + } + }); + } + async setCursorPosition(position) { + if (!position || position.type !== "Logical" && position.type !== "Physical") { + throw new Error( + "the `position` argument must be either a LogicalPosition or a PhysicalPosition instance" + ); + } + return invokeTauriCommand({ + __tauriModule: "Window", + message: { + cmd: "manage", + data: { + label: this.label, + cmd: { + type: "setCursorPosition", + payload: { + type: position.type, + data: { + x: position.x, + y: position.y + } + } + } + } + } + }); + } + async setIgnoreCursorEvents(ignore) { + return invokeTauriCommand({ + __tauriModule: "Window", + message: { + cmd: "manage", + data: { + label: this.label, + cmd: { + type: "setIgnoreCursorEvents", + payload: ignore + } + } + } + }); + } + async startDragging() { + return invokeTauriCommand({ + __tauriModule: "Window", + message: { + cmd: "manage", + data: { + label: this.label, + cmd: { + type: "startDragging" + } + } + } + }); + } + async onResized(handler) { + return this.listen("tauri://resize" /* WINDOW_RESIZED */, handler); + } + async onMoved(handler) { + return this.listen("tauri://move" /* WINDOW_MOVED */, handler); + } + async onCloseRequested(handler) { + return this.listen("tauri://close-requested" /* WINDOW_CLOSE_REQUESTED */, (event) => { + const evt = new CloseRequestedEvent(event); + void Promise.resolve(handler(evt)).then(() => { + if (!evt.isPreventDefault()) { + return this.close(); + } + }); + }); + } + async onFocusChanged(handler) { + const unlistenFocus = await this.listen( + "tauri://focus" /* WINDOW_FOCUS */, + (event) => { + handler({ ...event, payload: true }); + } + ); + const unlistenBlur = await this.listen( + "tauri://blur" /* WINDOW_BLUR */, + (event) => { + handler({ ...event, payload: false }); + } + ); + return () => { + unlistenFocus(); + unlistenBlur(); + }; + } + async onScaleChanged(handler) { + return this.listen( + "tauri://scale-change" /* WINDOW_SCALE_FACTOR_CHANGED */, + handler + ); + } + async onMenuClicked(handler) { + return this.listen("tauri://menu" /* MENU */, handler); + } + async onFileDropEvent(handler) { + const unlistenFileDrop = await this.listen( + "tauri://file-drop" /* WINDOW_FILE_DROP */, + (event) => { + handler({ ...event, payload: { type: "drop", paths: event.payload } }); + } + ); + const unlistenFileHover = await this.listen( + "tauri://file-drop-hover" /* WINDOW_FILE_DROP_HOVER */, + (event) => { + handler({ ...event, payload: { type: "hover", paths: event.payload } }); + } + ); + const unlistenCancel = await this.listen( + "tauri://file-drop-cancelled" /* WINDOW_FILE_DROP_CANCELLED */, + (event) => { + handler({ ...event, payload: { type: "cancel" } }); + } + ); + return () => { + unlistenFileDrop(); + unlistenFileHover(); + unlistenCancel(); + }; + } + async onThemeChanged(handler) { + return this.listen("tauri://theme-changed" /* WINDOW_THEME_CHANGED */, handler); + } +}; +var CloseRequestedEvent = class { + constructor(event) { + this._preventDefault = false; + this.event = event.event; + this.windowLabel = event.windowLabel; + this.id = event.id; + } + preventDefault() { + this._preventDefault = true; + } + isPreventDefault() { + return this._preventDefault; + } +}; +var WebviewWindow = class extends WindowManager { + constructor(label, options = {}) { + super(label); + if (!options?.skip) { + invokeTauriCommand({ + __tauriModule: "Window", + message: { + cmd: "createWebview", + data: { + options: { + label, + ...options + } + } + } + }).then(async () => this.emit("tauri://created")).catch(async (e) => this.emit("tauri://error", e)); + } + } + static getByLabel(label) { + if (getAll().some((w) => w.label === label)) { + return new WebviewWindow(label, { skip: true }); + } + return null; + } +}; +var appWindow; +if ("__TAURI_METADATA__" in window) { + appWindow = new WebviewWindow( + window.__TAURI_METADATA__.__currentWindow.label, + { + skip: true + } + ); +} else { + console.warn( + `Could not find "window.__TAURI_METADATA__". The "appWindow" value will reference the "main" window label. +Note that this is not an issue if running this frontend on a browser instead of a Tauri window.` + ); + appWindow = new WebviewWindow("main", { + skip: true + }); +} +function mapMonitor(m) { + return m === null ? null : { + name: m.name, + scaleFactor: m.scaleFactor, + position: new PhysicalPosition(m.position.x, m.position.y), + size: new PhysicalSize(m.size.width, m.size.height) + }; +} +async function currentMonitor() { + return invokeTauriCommand({ + __tauriModule: "Window", + message: { + cmd: "manage", + data: { + cmd: { + type: "currentMonitor" + } + } + } + }).then(mapMonitor); +} +async function primaryMonitor() { + return invokeTauriCommand({ + __tauriModule: "Window", + message: { + cmd: "manage", + data: { + cmd: { + type: "primaryMonitor" + } + } + } + }).then(mapMonitor); +} +async function availableMonitors() { + return invokeTauriCommand({ + __tauriModule: "Window", + message: { + cmd: "manage", + data: { + cmd: { + type: "availableMonitors" + } + } + } + }).then((ms) => ms.map(mapMonitor)); +} +export { + CloseRequestedEvent, + LogicalPosition, + LogicalSize, + PhysicalPosition, + PhysicalSize, + UserAttentionType, + WebviewWindow, + WebviewWindowHandle, + WindowManager, + appWindow, + availableMonitors, + currentMonitor, + getAll, + getCurrent, + primaryMonitor +}; diff --git a/examples/api/src/main.rs b/examples/api/src/main.rs index c317bcc..c3f88ef 100644 --- a/examples/api/src/main.rs +++ b/examples/api/src/main.rs @@ -20,6 +20,9 @@ fn Header(cx: Scope) -> View { a(href="/communication") { "Communication" } + a(href="/window") { + "Window" + } } } } diff --git a/examples/api/src/views/mod.rs b/examples/api/src/views/mod.rs index 88f6dca..f735ed2 100644 --- a/examples/api/src/views/mod.rs +++ b/examples/api/src/views/mod.rs @@ -2,6 +2,7 @@ mod app; mod clipboard; mod communication; mod welcome; +mod window; use sycamore::view::View; use sycamore_router::Route; @@ -15,6 +16,8 @@ pub enum Page { Clipboard, #[to("/communication")] Communication, + #[to("/window")] + Window, #[not_found] NotFound } @@ -24,6 +27,7 @@ pub fn switch(cx: Scope, route: &ReadSignal) -> View { Page::App => app::App(cx), Page::Clipboard => clipboard::Clipboard(cx), Page::Communication => communication::Communication(cx), + Page::Window => window::Window(cx), Page::NotFound => welcome::Welcome(cx) } } \ No newline at end of file diff --git a/examples/api/src/views/window.rs b/examples/api/src/views/window.rs new file mode 100644 index 0000000..a8e67e8 --- /dev/null +++ b/examples/api/src/views/window.rs @@ -0,0 +1,63 @@ +use sycamore::prelude::*; +use tauri_sys::window; + +#[component] +pub fn Window(cx: Scope) -> View { + let get_current = |_| { + let win = window::current_window(); + + log::debug!("{:?}", win); + }; + + let get_all = |_| { + let windows = window::all_windows(); + + log::debug!("{:?}", windows); + }; + + let get_current_monitor = |_| { + sycamore::futures::spawn_local(async move { + let monitor = window::current_monitor().await; + + log::debug!("{:?}", monitor); + }); + }; + + let get_primary_monitor = |_| { + sycamore::futures::spawn_local(async move { + let monitor = window::primary_monitor().await; + + log::debug!("{:?}", monitor); + }); + }; + + let get_all_monitors = |_| { + sycamore::futures::spawn_local(async move { + let monitors = window::available_monitors().await.collect::>(); + + log::debug!("{:?}", monitors); + }); + }; + + view! { cx, + div { + button(class="btn",id="get_name",on:click=get_current) { + "Get Current Window" + } + button(class="btn",id="get_version",on:click=get_all) { + "Get All Windows" + } + } + div { + button(class="btn",id="get_tauri_version",on:click=get_current_monitor) { + "Get Current Monitor" + } + button(class="btn",id="get_tauri_version",on:click=get_primary_monitor) { + "Get Primary Monitor" + } + button(class="btn",id="get_tauri_version",on:click=get_all_monitors) { + "Get All Monitors" + } + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 20d4fbd..83deaf5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,6 +10,8 @@ pub mod event; pub mod mocks; #[cfg(feature = "tauri")] pub mod tauri; +#[cfg(feature = "window")] +pub mod window; #[derive(Debug, thiserror::Error)] pub enum Error { diff --git a/src/window.rs b/src/window.rs new file mode 100644 index 0000000..7ca5385 --- /dev/null +++ b/src/window.rs @@ -0,0 +1,744 @@ +use crate::event::Event; +use serde::{de::DeserializeOwned, Serialize}; +use std::fmt::Display; +use wasm_bindgen::{prelude::Closure, JsCast, JsValue}; + +#[derive(Debug, Clone, PartialEq)] +pub enum Theme { + Light, + Dark, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum TitleBarStyle { + Visible, + Transparent, + Overlay, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum UserAttentionType { + Critical = 1, + Informational, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum Position { + Physical(PhysicalPosition), + Logical(LogicalPosition), +} + +#[derive(Debug, Clone, PartialEq)] +pub enum Size { + Physical(PhysicalSize), + Logical(LogicalSize), +} + +#[derive(Debug, Clone, PartialEq)] +pub enum 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, +} + +impl Display for CursorIcon { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + CursorIcon::Default => write!(f, "default"), + CursorIcon::Crosshair => write!(f, "crosshair"), + CursorIcon::Hand => write!(f, "hand"), + CursorIcon::Arrow => write!(f, "arrow"), + CursorIcon::Move => write!(f, "move"), + CursorIcon::Text => write!(f, "text"), + CursorIcon::Wait => write!(f, "wait"), + CursorIcon::Help => write!(f, "help"), + CursorIcon::Progress => write!(f, "progress"), + CursorIcon::NotAllowed => write!(f, "notAllowed"), + CursorIcon::ContextMenu => write!(f, "contextMenu"), + CursorIcon::Cell => write!(f, "cell"), + CursorIcon::VerticalText => write!(f, "verticalText"), + CursorIcon::Alias => write!(f, "alias"), + CursorIcon::Copy => write!(f, "copy"), + CursorIcon::NoDrop => write!(f, "noDrop"), + CursorIcon::Grab => write!(f, "grab"), + CursorIcon::Grabbing => write!(f, "grabbing"), + CursorIcon::AllScroll => write!(f, "allScroll"), + CursorIcon::ZoomIn => write!(f, "zoomIn"), + CursorIcon::ZoomOut => write!(f, "zoomOut"), + CursorIcon::EResize => write!(f, "eResize"), + CursorIcon::NResize => write!(f, "nResize"), + CursorIcon::NeResize => write!(f, "neResize"), + CursorIcon::NwResize => write!(f, "nwResize"), + CursorIcon::SResize => write!(f, "sResize"), + CursorIcon::SeResize => write!(f, "seResize"), + CursorIcon::SwResize => write!(f, "swResize"), + CursorIcon::WResize => write!(f, "wResize"), + CursorIcon::EwResize => write!(f, "ewResize"), + CursorIcon::NsResize => write!(f, "nsResize"), + CursorIcon::NeswResize => write!(f, "neswResize"), + CursorIcon::NwseResize => write!(f, "nwseResize"), + CursorIcon::ColResize => write!(f, "colResize"), + CursorIcon::RowResize => write!(f, "rowResize"), + } + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct WebviewWindow(inner::WebviewWindow); + +impl WebviewWindow { + pub fn new(label: &str, options: ()) -> Self { + Self(inner::WebviewWindow::new(label, options)) + } + + pub fn get_by_label(label: &str) -> Option { + inner::WebviewWindow::getByLabel(label).map(Self) + } + + pub async fn scale_factor(&self) -> f64 { + self.0.scaleFactor().await.as_f64().unwrap() + } + + pub async fn inner_position(&self) -> PhysicalPosition { + PhysicalPosition(self.0.innerPosition().await.unchecked_into()) + } + + pub async fn outer_position(&self) -> PhysicalPosition { + PhysicalPosition(self.0.outerPosition().await.unchecked_into()) + } + + pub async fn inner_size(&self) -> PhysicalSize { + PhysicalSize(self.0.innerSize().await.unchecked_into()) + } + + pub async fn outer_size(&self) -> PhysicalSize { + PhysicalSize(self.0.outerSize().await.unchecked_into()) + } + + pub async fn is_fullscreen(&self) -> bool { + self.0.isFullscreen().await.as_bool().unwrap() + } + + pub async fn is_maximized(&self) -> bool { + self.0.isMaximized().await.as_bool().unwrap() + } + + pub async fn is_decorated(&self) -> bool { + self.0.isDecorated().await.as_bool().unwrap() + } + + pub async fn is_resizable(&self) -> bool { + self.0.isResizable().await.as_bool().unwrap() + } + + pub async fn is_visible(&self) -> bool { + self.0.isVisible().await.as_bool().unwrap() + } + + pub async fn theme(&self) -> Theme { + match self.0.theme().await.as_string().unwrap().as_str() { + "light" => Theme::Light, + "dark" => Theme::Dark, + _ => panic!("Unknown theme"), + } + } + + pub async fn center(&self) { + self.0.center().await; + } + + pub async fn request_user_attention(&self, request_type: UserAttentionType) { + self.0.requestUserAttention(request_type as u32).await; + } + + pub async fn set_resizable(&self, resizable: bool) { + self.0.setResizable(resizable).await; + } + + pub async fn set_title(&self, title: impl AsRef) { + self.0.setTitle(title.as_ref()).await; + } + + pub async fn maximize(&self) { + self.0.maximize().await; + } + + pub async fn unmaximize(&self) { + self.0.unmaximize().await; + } + + pub async fn toggle_maximize(&self) { + self.0.toggleMaximize().await; + } + + pub async fn minimize(&self) { + self.0.minimize().await; + } + + pub async fn unminimize(&self) { + self.0.unminimize().await; + } + + pub async fn show(&self) { + self.0.show().await; + } + + pub async fn hide(&self) { + self.0.hide().await; + } + + pub async fn close(&self) { + self.0.close().await; + } + + pub async fn set_decorations(&self, decorations: bool) { + self.0.setDecorations(decorations).await; + } + + pub async fn set_always_on_top(&self, always_on_top: bool) { + self.0.setAlwaysOnTop(always_on_top).await; + } + + pub async fn set_size(&self, size: Size) { + match size { + Size::Physical(size) => self.0.setSizePhysical(size.0).await, + Size::Logical(size) => self.0.setSizeLogical(size.0).await, + }; + } + + pub async fn set_min_size(&self, size: Option) { + match size { + None => self.0.setMinSizePhysical(None).await, + Some(Size::Physical(size)) => self.0.setMinSizePhysical(Some(size.0)).await, + Some(Size::Logical(size)) => self.0.setMinSizeLogical(Some(size.0)).await, + } + } + + pub async fn set_max_size(&self, size: Option) { + match size { + None => self.0.setMaxSizePhysical(None).await, + Some(Size::Physical(size)) => self.0.setMaxSizePhysical(Some(size.0)).await, + Some(Size::Logical(size)) => self.0.setMaxSizeLogical(Some(size.0)).await, + } + } + + pub async fn set_position(&self, position: Position) { + match position { + Position::Physical(pos) => self.0.setPositionPhysical(pos.0).await, + Position::Logical(pos) => self.0.setPositionLogical(pos.0).await, + } + } + + pub async fn set_fullscreen(&self, fullscreen: bool) { + self.0.setFullscreen(fullscreen).await; + } + + pub async fn set_focus(&self) { + self.0.setFocus().await; + } + + pub async fn set_icon(&self, icon: &[u8]) { + self.0.setIcon(icon).await; + } + + pub async fn set_skip_taskbar(&self, skip: bool) { + self.0.setSkipTaskbar(skip).await; + } + + pub async fn set_cursor_grab(&self, grab: bool) { + self.0.setCursorGrab(grab).await; + } + + pub async fn set_cursor_visible(&self, visible: bool) { + self.0.setCursorVisible(visible).await; + } + + pub async fn set_cursor_icon(&self, icon: CursorIcon) { + self.0.setCursorIcon(&icon.to_string()).await; + } + + pub async fn set_cursor_position(&self, position: Position) { + match position { + Position::Physical(pos) => self.0.setCursorPositionPhysical(pos.0).await, + Position::Logical(pos) => self.0.setCursorPositionLogical(pos.0).await, + } + } + + pub async fn set_ignore_cursor_events(&self, ignore: bool) { + self.0.setIgnoreCursorEvents(ignore).await; + } + + pub async fn start_dragging(&self) { + self.0.startDragging().await; + } + + #[inline(always)] + pub async fn emit(&self, event: &str, payload: &T) { + self.0 + .emit(event, serde_wasm_bindgen::to_value(payload).unwrap()) + .await; + } + + #[inline(always)] + pub async fn listen(&self, event: &str, mut handler: H) -> impl FnOnce() + where + T: DeserializeOwned, + H: FnMut(Event) + 'static, + { + let closure = Closure::::new(move |raw| { + (handler)(serde_wasm_bindgen::from_value(raw).unwrap()) + }); + + let unlisten = self.0.listen(event, &closure).await; + + closure.forget(); + + let unlisten = js_sys::Function::from(unlisten); + move || { + unlisten.call0(&wasm_bindgen::JsValue::NULL).unwrap(); + } + } + + #[inline(always)] + pub async fn once(&self, event: &str, mut handler: H) -> impl FnOnce() + where + T: DeserializeOwned, + H: FnMut(Event) + 'static, + { + let closure = Closure::::new(move |raw| { + (handler)(serde_wasm_bindgen::from_value(raw).unwrap()) + }); + + let unlisten = self.0.once(event, &closure).await; + + closure.forget(); + + let unlisten = js_sys::Function::from(unlisten); + move || { + unlisten.call0(&wasm_bindgen::JsValue::NULL).unwrap(); + } + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct LogicalPosition(inner::LogicalPosition); + +impl LogicalPosition { + pub fn new(x: u32, y: u32) -> Self { + Self(inner::LogicalPosition::new(x, y)) + } + + pub fn x(&self) -> u32 { + self.0.x() + } + pub fn set_x(&self, x: u32) { + self.0.set_x(x) + } + pub fn y(&self) -> u32 { + self.0.y() + } + pub fn set_y(&self, y: u32) { + self.0.set_y(y) + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct PhysicalPosition(inner::PhysicalPosition); + +impl PhysicalPosition { + pub fn new(x: u32, y: u32) -> Self { + Self(inner::PhysicalPosition::new(x, y)) + } + + pub fn to_logical(self, scale_factor: u32) -> LogicalPosition { + LogicalPosition(self.0.toLogical(scale_factor)) + } + + pub fn x(&self) -> u32 { + self.0.x() + } + pub fn set_x(&self, x: u32) { + self.0.set_x(x) + } + pub fn y(&self) -> u32 { + self.0.y() + } + pub fn set_y(&self, y: u32) { + self.0.set_y(y) + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct LogicalSize(inner::LogicalSize); + +impl LogicalSize { + pub fn new(x: u32, y: u32) -> Self { + Self(inner::LogicalSize::new(x, y)) + } + + pub fn width(&self) -> u32 { + self.0.width() + } + pub fn set_width(&self, x: u32) { + self.0.set_width(x) + } + pub fn height(&self) -> u32 { + self.0.height() + } + pub fn set_height(&self, y: u32) { + self.0.set_height(y) + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct PhysicalSize(inner::PhysicalSize); + +impl PhysicalSize { + pub fn new(x: u32, y: u32) -> Self { + Self(inner::PhysicalSize::new(x, y)) + } + + pub fn to_logical(self, scale_factor: u32) -> LogicalSize { + LogicalSize(self.0.toLogical(scale_factor)) + } + + pub fn width(&self) -> u32 { + self.0.width() + } + pub fn set_width(&self, x: u32) { + self.0.set_width(x) + } + pub fn height(&self) -> u32 { + self.0.height() + } + pub fn set_height(&self, y: u32) { + self.0.set_height(y) + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct Monitor(JsValue); + +impl Monitor { + pub fn name(&self) -> Option { + let raw = js_sys::Reflect::get(&self.0, &JsValue::from_str("name")).unwrap(); + + raw.as_string() + } + + pub fn size(&self) -> PhysicalSize { + let raw = js_sys::Reflect::get(&self.0, &JsValue::from_str("size")).unwrap(); + + PhysicalSize(raw.unchecked_into()) + } + + pub fn position(&self) -> PhysicalPosition { + let raw = js_sys::Reflect::get(&self.0, &JsValue::from_str("position")).unwrap(); + + PhysicalPosition(raw.unchecked_into()) + } + + pub fn scale_factor(&self) -> u32 { + let raw = js_sys::Reflect::get(&self.0, &JsValue::from_str("size")) + .unwrap() + .as_f64() + .unwrap(); + + raw as u32 + } +} + +pub fn current_window() -> WebviewWindow { + WebviewWindow(inner::getCurrent()) +} + +pub fn all_windows() -> Vec { + inner::getAll().into_iter().map(WebviewWindow).collect() +} + +pub async fn current_monitor() -> Monitor { + Monitor(inner::currentMonitor().await) +} +pub async fn primary_monitor() -> Monitor { + Monitor(inner::primaryMonitor().await) +} + +#[derive(Debug, Clone)] +pub struct AvailableMonitors { + idx: u32, + array: js_sys::Array, +} + +impl Iterator for AvailableMonitors { + type Item = Monitor; + + fn next(&mut self) -> Option { + let raw = self.array.get(self.idx); + + if raw.is_undefined() { + None + } else { + let monitor = Monitor(raw); + self.idx += 1; + + Some(monitor) + } + } +} + +pub async fn available_monitors() -> AvailableMonitors { + AvailableMonitors { + idx: 0, + array: inner::availableMonitors().await.unchecked_into() + } +} + +mod inner { + use wasm_bindgen::{ + prelude::{wasm_bindgen, Closure}, + JsValue, + }; + + #[wasm_bindgen(module = "/dist/window.js")] + extern "C" { + #[derive(Debug, Clone, PartialEq)] + pub type LogicalPosition; + #[wasm_bindgen(constructor)] + pub fn new(x: u32, y: u32) -> LogicalPosition; + #[wasm_bindgen(method, getter)] + pub fn x(this: &LogicalPosition) -> u32; + #[wasm_bindgen(method, setter)] + pub fn set_x(this: &LogicalPosition, x: u32); + #[wasm_bindgen(method, getter)] + pub fn y(this: &LogicalPosition) -> u32; + #[wasm_bindgen(method, setter)] + pub fn set_y(this: &LogicalPosition, y: u32); + } + + #[wasm_bindgen(module = "/dist/window.js")] + extern "C" { + #[derive(Debug, Clone, PartialEq)] + pub type PhysicalPosition; + #[wasm_bindgen(constructor)] + pub fn new(x: u32, y: u32) -> PhysicalPosition; + #[wasm_bindgen(method)] + pub fn toLogical(this: &PhysicalPosition, scaleFactor: u32) -> LogicalPosition; + #[wasm_bindgen(method, getter)] + pub fn x(this: &PhysicalPosition) -> u32; + #[wasm_bindgen(method, setter)] + pub fn set_x(this: &PhysicalPosition, x: u32); + #[wasm_bindgen(method, getter)] + pub fn y(this: &PhysicalPosition) -> u32; + #[wasm_bindgen(method, setter)] + pub fn set_y(this: &PhysicalPosition, y: u32); + } + + #[wasm_bindgen(module = "/dist/window.js")] + extern "C" { + #[derive(Debug, Clone, PartialEq)] + pub type LogicalSize; + #[wasm_bindgen(constructor)] + pub fn new(width: u32, height: u32) -> LogicalSize; + #[wasm_bindgen(method, getter)] + pub fn width(this: &LogicalSize) -> u32; + #[wasm_bindgen(method, setter)] + pub fn set_width(this: &LogicalSize, width: u32); + #[wasm_bindgen(method, getter)] + pub fn height(this: &LogicalSize) -> u32; + #[wasm_bindgen(method, setter)] + pub fn set_height(this: &LogicalSize, height: u32); + } + + #[wasm_bindgen(module = "/dist/window.js")] + extern "C" { + #[derive(Debug, Clone, PartialEq)] + pub type PhysicalSize; + #[wasm_bindgen(constructor)] + pub fn new(width: u32, height: u32) -> PhysicalSize; + #[wasm_bindgen(method)] + pub fn toLogical(this: &PhysicalSize, scaleFactor: u32) -> LogicalSize; + #[wasm_bindgen(method, getter)] + pub fn width(this: &PhysicalSize) -> u32; + #[wasm_bindgen(method, setter)] + pub fn set_width(this: &PhysicalSize, width: u32); + #[wasm_bindgen(method, getter)] + pub fn height(this: &PhysicalSize) -> u32; + #[wasm_bindgen(method, setter)] + pub fn set_height(this: &PhysicalSize, height: u32); + } + + #[wasm_bindgen(module = "/dist/window.js")] + extern "C" { + #[derive(Debug, Clone, PartialEq)] + pub type WebviewWindowHandle; + #[wasm_bindgen(constructor)] + pub fn new(label: &str) -> WebviewWindowHandle; + #[wasm_bindgen(method)] + pub async fn listen( + this: &WebviewWindowHandle, + event: &str, + handler: &Closure, + ) -> JsValue; + #[wasm_bindgen(method)] + pub async fn once( + this: &WebviewWindowHandle, + event: &str, + handler: &Closure, + ) -> JsValue; + #[wasm_bindgen(method)] + pub async fn emit(this: &WebviewWindowHandle, event: &str, payload: JsValue); + } + + #[wasm_bindgen(module = "/dist/window.js")] + extern "C" { + #[wasm_bindgen(extends = WebviewWindowHandle)] + #[derive(Debug, Clone, PartialEq)] + pub type WindowManager; + #[wasm_bindgen(constructor)] + pub fn new(label: &str) -> WindowManager; + #[wasm_bindgen(method)] + pub async fn scaleFactor(this: &WindowManager) -> JsValue; + #[wasm_bindgen(method)] + pub async fn innerPosition(this: &WindowManager) -> JsValue; + #[wasm_bindgen(method)] + pub async fn outerPosition(this: &WindowManager) -> JsValue; + #[wasm_bindgen(method)] + pub async fn innerSize(this: &WindowManager) -> JsValue; + #[wasm_bindgen(method)] + pub async fn outerSize(this: &WindowManager) -> JsValue; + #[wasm_bindgen(method)] + pub async fn isFullscreen(this: &WindowManager) -> JsValue; + #[wasm_bindgen(method)] + pub async fn isMaximized(this: &WindowManager) -> JsValue; + #[wasm_bindgen(method)] + pub async fn isDecorated(this: &WindowManager) -> JsValue; + #[wasm_bindgen(method)] + pub async fn isResizable(this: &WindowManager) -> JsValue; + #[wasm_bindgen(method)] + pub async fn isVisible(this: &WindowManager) -> JsValue; + #[wasm_bindgen(method)] + pub async fn theme(this: &WindowManager) -> JsValue; + #[wasm_bindgen(method)] + pub async fn center(this: &WindowManager); + #[wasm_bindgen(method)] + pub async fn requestUserAttention(this: &WindowManager, requestType: u32); + #[wasm_bindgen(method)] + pub async fn setResizable(this: &WindowManager, resizable: bool); + #[wasm_bindgen(method)] + pub async fn setTitle(this: &WindowManager, title: &str); + #[wasm_bindgen(method)] + pub async fn maximize(this: &WindowManager); + #[wasm_bindgen(method)] + pub async fn unmaximize(this: &WindowManager); + #[wasm_bindgen(method)] + pub async fn toggleMaximize(this: &WindowManager); + #[wasm_bindgen(method)] + pub async fn minimize(this: &WindowManager); + #[wasm_bindgen(method)] + pub async fn unminimize(this: &WindowManager); + #[wasm_bindgen(method)] + pub async fn show(this: &WindowManager); + #[wasm_bindgen(method)] + pub async fn hide(this: &WindowManager); + #[wasm_bindgen(method)] + pub async fn close(this: &WindowManager); + #[wasm_bindgen(method)] + pub async fn setDecorations(this: &WindowManager, decorations: bool); + #[wasm_bindgen(method)] + pub async fn setAlwaysOnTop(this: &WindowManager, alwaysOnTop: bool); + #[wasm_bindgen(method, js_name = setSize)] + pub async fn setSizePhysical(this: &WindowManager, size: PhysicalSize); + #[wasm_bindgen(method, js_name = setSize)] + pub async fn setSizeLogical(this: &WindowManager, size: LogicalSize); + #[wasm_bindgen(method, js_name = setMinSize)] + pub async fn setMinSizePhysical(this: &WindowManager, size: Option); + #[wasm_bindgen(method, js_name = setMinSize)] + pub async fn setMinSizeLogical(this: &WindowManager, size: Option); + #[wasm_bindgen(method, js_name = setMaxSize)] + pub async fn setMaxSizePhysical(this: &WindowManager, size: Option); + #[wasm_bindgen(method, js_name = setMinSize)] + pub async fn setMaxSizeLogical(this: &WindowManager, size: Option); + #[wasm_bindgen(method, js_name = setPosition)] + pub async fn setPositionPhysical(this: &WindowManager, position: PhysicalPosition); + #[wasm_bindgen(method, js_name = setPosition)] + pub async fn setPositionLogical(this: &WindowManager, position: LogicalPosition); + #[wasm_bindgen(method)] + pub async fn setFullscreen(this: &WindowManager, fullscreen: bool); + #[wasm_bindgen(method)] + pub async fn setFocus(this: &WindowManager); + #[wasm_bindgen(method)] + pub async fn setIcon(this: &WindowManager, icon: &[u8]); + #[wasm_bindgen(method)] + pub async fn setSkipTaskbar(this: &WindowManager, skip: bool); + #[wasm_bindgen(method)] + pub async fn setCursorGrab(this: &WindowManager, grab: bool); + #[wasm_bindgen(method)] + pub async fn setCursorVisible(this: &WindowManager, visible: bool); + #[wasm_bindgen(method)] + pub async fn setCursorIcon(this: &WindowManager, icon: &str); + #[wasm_bindgen(method, js_name = setCursorPosition)] + pub async fn setCursorPositionPhysical(this: &WindowManager, position: PhysicalPosition); + #[wasm_bindgen(method, js_name = setCursorPosition)] + pub async fn setCursorPositionLogical(this: &WindowManager, position: LogicalPosition); + #[wasm_bindgen(method)] + pub async fn setIgnoreCursorEvents(this: &WindowManager, ignore: bool); + #[wasm_bindgen(method)] + pub async fn startDragging(this: &WindowManager); + } + + #[wasm_bindgen(module = "/dist/window.js")] + extern "C" { + #[wasm_bindgen(extends = WindowManager)] + #[derive(Debug, Clone, PartialEq)] + pub type WebviewWindow; + #[wasm_bindgen(constructor)] + pub fn new(label: &str, options: ()) -> WebviewWindow; + #[wasm_bindgen(static_method_of = WebviewWindow)] + pub fn getByLabel(label: &str) -> Option; + } + + #[wasm_bindgen(module = "/dist/window.js")] + extern "C" { + pub fn getCurrent() -> WebviewWindow; + pub fn getAll() -> Vec; + pub async fn currentMonitor() -> JsValue; + pub async fn primaryMonitor() -> JsValue; + pub async fn availableMonitors() -> JsValue; + } +}