tray.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370
  1. // Copyright 2019-2024 Tauri Programme within The Commons Conservancy
  2. // SPDX-License-Identifier: Apache-2.0
  3. // SPDX-License-Identifier: MIT
  4. import type { Menu, Submenu } from './menu'
  5. import { Channel, invoke, Resource } from './core'
  6. import { Image, transformImage } from './image'
  7. import { PhysicalPosition, PhysicalSize } from './dpi'
  8. export type MouseButtonState = 'Up' | 'Down'
  9. export type MouseButton = 'Left' | 'Right' | 'Middle'
  10. /** A click happened on the tray icon. */
  11. export interface TrayIconClickEvent {
  12. /** Id of the tray icon which triggered this event. */
  13. id: string
  14. /** Physical X Position of the click the triggered this event. */
  15. x: number
  16. /** Physical Y Position of the click the triggered this event. */
  17. y: number
  18. /** Position and size of the tray icon. */
  19. rect: {
  20. position: PhysicalPosition
  21. size: PhysicalSize
  22. }
  23. /** Mouse button that triggered this event. */
  24. button: MouseButton
  25. /** Mouse button state when this event was triggered. */
  26. button_state: MouseButtonState
  27. }
  28. /** A double click happened on the tray icon. **Windows Only** */
  29. export interface TrayIconDoubleClickEvent {
  30. /** Id of the tray icon which triggered this event. */
  31. id: string
  32. /** Physical X Position of the click the triggered this event. */
  33. x: number
  34. /** Physical Y Position of the click the triggered this event. */
  35. y: number
  36. /** Position and size of the tray icon. */
  37. rect: {
  38. position: PhysicalPosition
  39. size: PhysicalSize
  40. }
  41. /** Mouse button that triggered this event. */
  42. button: MouseButton
  43. }
  44. /** The mouse entered the tray icon region. */
  45. export interface TrayIconEnterEvent {
  46. /** Id of the tray icon which triggered this event. */
  47. id: string
  48. /** Physical X Position of the click the triggered this event. */
  49. x: number
  50. /** Physical Y Position of the click the triggered this event. */
  51. y: number
  52. /** Position and size of the tray icon. */
  53. rect: {
  54. position: PhysicalPosition
  55. size: PhysicalSize
  56. }
  57. }
  58. /** The mouse moved over the tray icon region. */
  59. export interface TrayIconMoveEvent {
  60. /** Id of the tray icon which triggered this event. */
  61. id: string
  62. /** Physical X Position of the click the triggered this event. */
  63. x: number
  64. /** Physical Y Position of the click the triggered this event. */
  65. y: number
  66. /** Position and size of the tray icon. */
  67. rect: {
  68. position: PhysicalPosition
  69. size: PhysicalSize
  70. }
  71. }
  72. /** The mouse left the tray icon region. */
  73. export interface TrayIconLeaveEvent {
  74. /** Id of the tray icon which triggered this event. */
  75. id: string
  76. /** Physical X Position of the click the triggered this event. */
  77. x: number
  78. /** Physical Y Position of the click the triggered this event. */
  79. y: number
  80. /** Position and size of the tray icon. */
  81. rect: {
  82. position: PhysicalPosition
  83. size: PhysicalSize
  84. }
  85. }
  86. /**
  87. * Describes a tray icon event.
  88. *
  89. * #### Platform-specific:
  90. *
  91. * - **Linux**: Unsupported. The event is not emitted even though the icon is shown,
  92. * the icon will still show a context menu on right click.
  93. */
  94. export type TrayIconEvent =
  95. | { click: TrayIconClickEvent }
  96. | { doubleClick: TrayIconDoubleClickEvent }
  97. | { enter: TrayIconEnterEvent }
  98. | { move: TrayIconMoveEvent }
  99. | { leave: TrayIconLeaveEvent }
  100. /**
  101. * Tray icon types and utilities.
  102. *
  103. * This package is also accessible with `window.__TAURI__.tray` when [`app.withGlobalTauri`](https://tauri.app/v1/api/config/#appconfig.withglobaltauri) in `tauri.conf.json` is set to `true`.
  104. * @module
  105. */
  106. /** {@link TrayIcon.new|`TrayIcon`} creation options */
  107. export interface TrayIconOptions {
  108. /** The tray icon id. If undefined, a random one will be assigned */
  109. id?: string
  110. /** The tray icon menu */
  111. menu?: Menu | Submenu
  112. /**
  113. * The tray icon which could be icon bytes or path to the icon file.
  114. *
  115. * Note that you need the `image-ico` or `image-png` Cargo features to use this API.
  116. * To enable it, change your Cargo.toml file:
  117. * ```toml
  118. * [dependencies]
  119. * tauri = { version = "...", features = ["...", "image-png"] }
  120. * ```
  121. */
  122. icon?: string | Uint8Array | ArrayBuffer | number[] | Image
  123. /** The tray icon tooltip */
  124. tooltip?: string
  125. /**
  126. * The tray title
  127. *
  128. * #### Platform-specific
  129. *
  130. * - **Linux:** The title will not be shown unless there is an icon
  131. * as well. The title is useful for numerical and other frequently
  132. * updated information. In general, it shouldn't be shown unless a
  133. * user requests it as it can take up a significant amount of space
  134. * on the user's panel. This may not be shown in all visualizations.
  135. * - **Windows:** Unsupported.
  136. */
  137. title?: string
  138. /**
  139. * The tray icon temp dir path. **Linux only**.
  140. *
  141. * On Linux, we need to write the icon to the disk and usually it will
  142. * be `$XDG_RUNTIME_DIR/tray-icon` or `$TEMP/tray-icon`.
  143. */
  144. tempDirPath?: string
  145. /**
  146. * Use the icon as a [template](https://developer.apple.com/documentation/appkit/nsimage/1520017-template?language=objc). **macOS only**.
  147. */
  148. iconAsTemplate?: boolean
  149. /** Whether to show the tray menu on left click or not, default is `true`. **macOS only**. */
  150. menuOnLeftClick?: boolean
  151. /** A handler for an event on the tray icon. */
  152. action?: (event: TrayIconEvent) => void
  153. }
  154. /**
  155. * Tray icon class and associated methods. This type constructor is private,
  156. * instead, you should use the static method {@linkcode TrayIcon.new}.
  157. *
  158. * #### Warning
  159. *
  160. * Unlike Rust, javascript does not have any way to run cleanup code
  161. * when an object is being removed by garbage collection, but this tray icon
  162. * will be cleaned up when the tauri app exists, however if you want to cleanup
  163. * this object early, you need to call {@linkcode TrayIcon.close}.
  164. *
  165. * @example
  166. * ```ts
  167. * import { TrayIcon } from '@tauri-apps/api/tray';
  168. * const tray = await TrayIcon.new({ tooltip: 'awesome tray tooltip' });
  169. * tray.set_tooltip('new tooltip');
  170. * ```
  171. */
  172. export class TrayIcon extends Resource {
  173. /** The id associated with this tray icon. */
  174. public id: string
  175. private constructor(rid: number, id: string) {
  176. super(rid)
  177. this.id = id
  178. }
  179. /** Gets a tray icon using the provided id. */
  180. static async getById(id: string): Promise<TrayIcon | null> {
  181. return invoke<number>('plugin:tray|get_by_id', { id }).then((rid) =>
  182. rid ? new TrayIcon(rid, id) : null
  183. )
  184. }
  185. /**
  186. * Removes a tray icon using the provided id from tauri's internal state.
  187. *
  188. * Note that this may cause the tray icon to disappear
  189. * if it wasn't cloned somewhere else or referenced by JS.
  190. */
  191. static async removeById(id: string): Promise<void> {
  192. return invoke('plugin:tray|remove_by_id', { id })
  193. }
  194. /**
  195. * Creates a new {@linkcode TrayIcon}
  196. *
  197. * #### Platform-specific:
  198. *
  199. * - **Linux:** Sometimes the icon won't be visible unless a menu is set.
  200. * Setting an empty {@linkcode Menu} is enough.
  201. */
  202. static async new(options?: TrayIconOptions): Promise<TrayIcon> {
  203. if (options?.menu) {
  204. // @ts-expect-error we only need the rid and kind
  205. options.menu = [options.menu.rid, options.menu.kind]
  206. }
  207. if (options?.icon) {
  208. options.icon = transformImage(options.icon)
  209. }
  210. const handler = new Channel<TrayIconEvent>()
  211. if (options?.action) {
  212. const action = options.action
  213. handler.onmessage = (e) => {
  214. if ('click' in e) {
  215. // @ts-expect-error `TrayIconEvent` doesn't quite match the value yet so we reconstruct the incorrect fields
  216. e.click.rect.position = mapPosition(e.click.rect.position)
  217. // @ts-expect-error `TrayIconEvent` doesn't quite match the value yet so we reconstruct the incorrect fields
  218. e.click.rect.size = mapSize(e.click.rect.size)
  219. } else if ('doubleClick' in e) {
  220. // @ts-expect-error `TrayIconEvent` doesn't quite match the value yet so we reconstruct the incorrect fields
  221. e.doubleClick.rect.position = mapPosition(e.doubleClick.rect.position)
  222. // @ts-expect-error `TrayIconEvent` doesn't quite match the value yet so we reconstruct the incorrect fields
  223. e.doubleClick.rect.size = mapSize(e.doubleClick.rect.size)
  224. } else if ('enter' in e) {
  225. // @ts-expect-error `TrayIconEvent` doesn't quite match the value yet so we reconstruct the incorrect fields
  226. e.enter.rect.position = mapPosition(e.enter.rect.position)
  227. // @ts-expect-error `TrayIconEvent` doesn't quite match the value yet so we reconstruct the incorrect fields
  228. e.enter.rect.size = mapSize(e.enter.rect.size)
  229. } else if ('move' in e) {
  230. // @ts-expect-error `TrayIconEvent` doesn't quite match the value yet so we reconstruct the incorrect fields
  231. e.move.rect.position = mapPosition(e.move.rect.position)
  232. // @ts-expect-error `TrayIconEvent` doesn't quite match the value yet so we reconstruct the incorrect fields
  233. e.move.rect.size = mapSize(e.move.rect.size)
  234. } else if ('leave' in e) {
  235. // @ts-expect-error `TrayIconEvent` doesn't quite match the value yet so we reconstruct the incorrect fields
  236. e.leave.rect.position = mapPosition(e.leave.rect.position)
  237. // @ts-expect-error `TrayIconEvent` doesn't quite match the value yet so we reconstruct the incorrect fields
  238. e.leave.rect.size = mapSize(e.leave.rect.size)
  239. }
  240. action(e)
  241. }
  242. delete options.action
  243. }
  244. return invoke<[number, string]>('plugin:tray|new', {
  245. options: options ?? {},
  246. handler
  247. }).then(([rid, id]) => new TrayIcon(rid, id))
  248. }
  249. /**
  250. * Sets a new tray icon. If `null` is provided, it will remove the icon.
  251. *
  252. * Note that you need the `image-ico` or `image-png` Cargo features to use this API.
  253. * To enable it, change your Cargo.toml file:
  254. * ```toml
  255. * [dependencies]
  256. * tauri = { version = "...", features = ["...", "image-png"] }
  257. * ```
  258. */
  259. async setIcon(
  260. icon: string | Image | Uint8Array | ArrayBuffer | number[] | null
  261. ): Promise<void> {
  262. let trayIcon = null
  263. if (icon) {
  264. trayIcon = transformImage(icon)
  265. }
  266. return invoke('plugin:tray|set_icon', { rid: this.rid, icon: trayIcon })
  267. }
  268. /**
  269. * Sets a new tray menu.
  270. *
  271. * #### Platform-specific:
  272. *
  273. * - **Linux**: once a menu is set it cannot be removed so `null` has no effect
  274. */
  275. async setMenu(menu: Menu | Submenu | null): Promise<void> {
  276. if (menu) {
  277. // @ts-expect-error we only need the rid and kind
  278. menu = [menu.rid, menu.kind]
  279. }
  280. return invoke('plugin:tray|set_menu', { rid: this.rid, menu })
  281. }
  282. /**
  283. * Sets the tooltip for this tray icon.
  284. *
  285. * ## Platform-specific:
  286. *
  287. * - **Linux:** Unsupported
  288. */
  289. async setTooltip(tooltip: string | null): Promise<void> {
  290. return invoke('plugin:tray|set_tooltip', { rid: this.rid, tooltip })
  291. }
  292. /**
  293. * Sets the tooltip for this tray icon.
  294. *
  295. * ## Platform-specific:
  296. *
  297. * - **Linux:** The title will not be shown unless there is an icon
  298. * as well. The title is useful for numerical and other frequently
  299. * updated information. In general, it shouldn't be shown unless a
  300. * user requests it as it can take up a significant amount of space
  301. * on the user's panel. This may not be shown in all visualizations.
  302. * - **Windows:** Unsupported
  303. */
  304. async setTitle(title: string | null): Promise<void> {
  305. return invoke('plugin:tray|set_title', { rid: this.rid, title })
  306. }
  307. /** Show or hide this tray icon. */
  308. async setVisible(visible: boolean): Promise<void> {
  309. return invoke('plugin:tray|set_visible', { rid: this.rid, visible })
  310. }
  311. /**
  312. * Sets the tray icon temp dir path. **Linux only**.
  313. *
  314. * On Linux, we need to write the icon to the disk and usually it will
  315. * be `$XDG_RUNTIME_DIR/tray-icon` or `$TEMP/tray-icon`.
  316. */
  317. async setTempDirPath(path: string | null): Promise<void> {
  318. return invoke('plugin:tray|set_temp_dir_path', { rid: this.rid, path })
  319. }
  320. /** Sets the current icon as a [template](https://developer.apple.com/documentation/appkit/nsimage/1520017-template?language=objc). **macOS only** */
  321. async setIconAsTemplate(asTemplate: boolean): Promise<void> {
  322. return invoke('plugin:tray|set_icon_as_template', {
  323. rid: this.rid,
  324. asTemplate
  325. })
  326. }
  327. /** Disable or enable showing the tray menu on left click. **macOS only**. */
  328. async setMenuOnLeftClick(onLeft: boolean): Promise<void> {
  329. return invoke('plugin:tray|set_show_menu_on_left_click', {
  330. rid: this.rid,
  331. onLeft
  332. })
  333. }
  334. }
  335. function mapPosition(pos: {
  336. Physical: { x: number; y: number }
  337. }): PhysicalPosition {
  338. return new PhysicalPosition(pos.Physical.x, pos.Physical.y)
  339. }
  340. function mapSize(pos: {
  341. Physical: { width: number; height: number }
  342. }): PhysicalSize {
  343. return new PhysicalSize(pos.Physical.width, pos.Physical.height)
  344. }