Skip to content

Commit 6010e6b

Browse files
committed
docs: add System Tray section (ComposeNativeTray) with 5 pages
- Overview: framework positioning, AWT comparison, quick example - Tray API: icon types, render properties, primary action, reactive icons - Menu DSL: items, checkable items, submenus, dividers, reactive menus - TrayApp: popup window anchored to tray, state management - Utilities: single instance, tray position, dark mode detection
1 parent ac47012 commit 6010e6b

6 files changed

Lines changed: 507 additions & 0 deletions

File tree

docs/runtime/system-tray/index.md

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# System Tray
2+
3+
!!! note "Separate repository"
4+
System Tray is maintained in its own repository with its own release cycle: [**kdroidFilter/ComposeNativeTray**](https://github.com/kdroidFilter/ComposeNativeTray). The artifact is `io.github.kdroidfilter:composenativetray`.
5+
6+
**ComposeNativeTray** is not just a tray icon library — it is a **complete system tray framework** for Compose Desktop. It is the simplest and most powerful way to create system tray applications today, on any platform.
7+
8+
Where the standard Java `SystemTray` gives you a pixelated icon and a crude AWT menu that looks like Windows 95, ComposeNativeTray gives you **native menus**, **reactive state**, **HiDPI icons**, **dark mode awareness**, and a **pure Compose DSL** — on macOS, Windows, and Linux.
9+
10+
## Why not just use AWT SystemTray?
11+
12+
| | AWT SystemTray | ComposeNativeTray |
13+
|---|---|---|
14+
| Menu appearance | AWT popup (looks broken on modern OS) | Native platform menu |
15+
| Icon quality | Pixelated, no HiDPI | Crisp on every display |
16+
| Dark mode | No support | Automatic theme adaptation |
17+
| Menu updates | Rebuild the entire menu manually | Compose recomposition — just change state |
18+
| Icon types | `BufferedImage` only | `ImageVector`, `Painter`, `DrawableResource`, any `@Composable` |
19+
| Submenus | Clunky | Native nested submenus with icons |
20+
| Checkable items | Not available | Native checkable items |
21+
| Primary action (left-click) | Not available | Per-platform behavior |
22+
| Tray position detection | Not available | Built-in, for window positioning |
23+
| Single instance | Not available | Built-in with restore callback |
24+
| Popup window | Not available | `TrayApp` — Compose window anchored to tray |
25+
26+
## Quick example
27+
28+
```kotlin
29+
var isDarkMode by remember { mutableStateOf(false) }
30+
31+
Tray(
32+
icon = if (isDarkMode) Icons.Default.DarkMode else Icons.Default.LightMode,
33+
tooltip = "My App",
34+
primaryAction = { showWindow() },
35+
) {
36+
Item(label = "Show Window") { showWindow() }
37+
Divider()
38+
CheckableItem(label = "Dark Mode", checked = isDarkMode) {
39+
isDarkMode = !isDarkMode
40+
}
41+
SubMenu(label = "Options") {
42+
Item(label = "Settings") { openSettings() }
43+
Item(label = "About") { openAbout() }
44+
}
45+
Divider()
46+
Item(label = "Quit") { exitProcess(0) }
47+
}
48+
```
49+
50+
The menu is fully reactive — change `isDarkMode` and the icon, label, and checkmark all update instantly. No manual menu rebuild.
51+
52+
## Installation
53+
54+
```kotlin
55+
dependencies {
56+
implementation("io.github.kdroidfilter:composenativetray:<version>")
57+
}
58+
```
59+
60+
## Next steps
61+
62+
- [Tray API](tray-api.md)`Tray()` composable, icon types, primary actions, platform-specific icons
63+
- [Menu DSL](menu-dsl.md) — Items, checkable items, submenus, dividers, icons in menus
64+
- [TrayApp](tray-app.md) — Popup window anchored to the tray icon
65+
- [Utilities](utilities.md) — Single instance management, tray position detection, dark mode detection
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# Menu DSL
2+
3+
The tray menu is built with a Kotlin DSL inside the `Tray()` trailing lambda. Menus are fully reactive — change state, and items update automatically.
4+
5+
## Items
6+
7+
```kotlin
8+
Tray(icon = Icons.Default.Favorite, tooltip = "App") {
9+
Item(label = "Open Settings") { openSettings() }
10+
Item(label = "Disabled", isEnabled = false) { }
11+
}
12+
```
13+
14+
### Items with icons
15+
16+
Every item type supports icons — `ImageVector`, `Painter`, `DrawableResource`, or a custom `@Composable`:
17+
18+
```kotlin
19+
Item(label = "Settings", icon = Icons.Default.Settings) { openSettings() }
20+
Item(label = "Export", icon = painterResource("export.png")) { export() }
21+
Item(label = "Help", icon = Res.drawable.help) { openHelp() }
22+
```
23+
24+
## Checkable items
25+
26+
Native checkmark appearance on each platform:
27+
28+
```kotlin
29+
var notifications by remember { mutableStateOf(true) }
30+
var darkMode by remember { mutableStateOf(false) }
31+
32+
Tray(icon = Icons.Default.Favorite, tooltip = "App") {
33+
CheckableItem(label = "Notifications", checked = notifications) {
34+
notifications = it
35+
}
36+
CheckableItem(label = "Dark Mode", icon = Icons.Default.DarkMode, checked = darkMode) {
37+
darkMode = it
38+
}
39+
}
40+
```
41+
42+
## Submenus
43+
44+
Nested submenus with optional icons, unlimited depth:
45+
46+
```kotlin
47+
Tray(icon = Icons.Default.Favorite, tooltip = "App") {
48+
SubMenu(label = "Tools", icon = Icons.Default.Build) {
49+
Item(label = "Terminal") { openTerminal() }
50+
Item(label = "File Manager") { openFiles() }
51+
SubMenu(label = "More") {
52+
Item(label = "Calculator") { openCalc() }
53+
}
54+
}
55+
}
56+
```
57+
58+
## Dividers
59+
60+
Visual separators between groups of items:
61+
62+
```kotlin
63+
Tray(icon = Icons.Default.Favorite, tooltip = "App") {
64+
Item(label = "Show Window") { show() }
65+
Divider()
66+
Item(label = "Settings") { settings() }
67+
Item(label = "About") { about() }
68+
Divider()
69+
Item(label = "Quit") { exitProcess(0) }
70+
}
71+
```
72+
73+
## Reactive menus
74+
75+
The entire menu tree participates in Compose recomposition. Conditionally show items, update labels, toggle states — the menu rebuilds natively:
76+
77+
```kotlin
78+
var isConnected by remember { mutableStateOf(false) }
79+
var showAdvanced by remember { mutableStateOf(false) }
80+
81+
Tray(icon = Icons.Default.Wifi, tooltip = "Network") {
82+
Item(label = if (isConnected) "Disconnect" else "Connect") {
83+
isConnected = !isConnected
84+
}
85+
86+
CheckableItem(label = "Advanced Options", checked = showAdvanced) {
87+
showAdvanced = it
88+
}
89+
90+
if (showAdvanced) {
91+
Divider()
92+
SubMenu(label = "Advanced") {
93+
Item(label = "DNS Settings") { }
94+
Item(label = "Proxy") { }
95+
}
96+
}
97+
98+
Divider()
99+
Item(label = "Quit") { exitProcess(0) }
100+
}
101+
```
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
# Tray API
2+
3+
The `Tray()` composable is the main entry point. It creates a native system tray icon with an optional context menu.
4+
5+
## Basic usage
6+
7+
```kotlin
8+
Tray(
9+
icon = Icons.Default.Favorite,
10+
tooltip = "My App",
11+
) {
12+
Item(label = "Show") { showWindow() }
13+
Item(label = "Quit") { exitProcess(0) }
14+
}
15+
```
16+
17+
## Icon types
18+
19+
`Tray()` accepts multiple icon types — use whichever fits your project:
20+
21+
### ImageVector
22+
23+
```kotlin
24+
Tray(
25+
icon = Icons.Default.Notifications,
26+
tint = Color.White, // optional tint
27+
tooltip = "My App",
28+
) { /* menu */ }
29+
```
30+
31+
### Painter
32+
33+
```kotlin
34+
Tray(
35+
icon = painterResource("icon.png"),
36+
tooltip = "My App",
37+
) { /* menu */ }
38+
```
39+
40+
### DrawableResource (Compose Multiplatform)
41+
42+
```kotlin
43+
Tray(
44+
icon = Res.drawable.app_icon,
45+
tooltip = "My App",
46+
) { /* menu */ }
47+
```
48+
49+
### Custom Composable
50+
51+
Render any `@Composable` as the tray icon — animated icons, dynamic badges, anything Compose can draw:
52+
53+
```kotlin
54+
Tray(
55+
iconContent = {
56+
Box(Modifier.size(24.dp).background(Color.Red, CircleShape)) {
57+
Text("3", color = Color.White, modifier = Modifier.align(Alignment.Center))
58+
}
59+
},
60+
tooltip = "3 notifications",
61+
) { /* menu */ }
62+
```
63+
64+
### Platform-specific icons
65+
66+
Windows uses `.ico` format natively while macOS and Linux work best with vector icons. Use platform-specific overloads to provide the optimal format for each:
67+
68+
```kotlin
69+
Tray(
70+
windowsIcon = painterResource("icon.ico"), // ICO for Windows
71+
macLinuxIcon = Icons.Default.Notifications, // Vector for macOS/Linux
72+
tint = Color.White,
73+
tooltip = "My App",
74+
) { /* menu */ }
75+
```
76+
77+
## Icon render properties
78+
79+
Control how Compose icons are rasterized for the tray:
80+
81+
```kotlin
82+
Tray(
83+
icon = Icons.Default.Favorite,
84+
iconRenderProperties = IconRenderProperties(
85+
sceneWidth = 192,
86+
sceneHeight = 192,
87+
sceneDensity = Density(2f),
88+
targetWidth = 44,
89+
targetHeight = 44,
90+
),
91+
tooltip = "My App",
92+
) { /* menu */ }
93+
```
94+
95+
Preconfigured presets are available:
96+
97+
| Preset | Size | Use case |
98+
|--------|------|----------|
99+
| `IconRenderProperties.forCurrentOperatingSystem()` | 32x32 Win, 44x44 Mac, 24x24 Linux | Tray icon (default) |
100+
| `IconRenderProperties.forMenuItem()` | 16x16 all platforms | Menu item icons |
101+
| `IconRenderProperties.withoutScalingAndAliasing()` | No forced scaling | When you handle sizing yourself |
102+
103+
## Primary action
104+
105+
The `primaryAction` callback fires on left-click (Windows/macOS) or single-click (Linux, desktop-dependent):
106+
107+
```kotlin
108+
Tray(
109+
icon = Icons.Default.Favorite,
110+
tooltip = "My App",
111+
primaryAction = { showWindow() },
112+
) {
113+
// Right-click menu
114+
Item(label = "Quit") { exitProcess(0) }
115+
}
116+
```
117+
118+
Without a `primaryAction`, left-click opens the context menu on all platforms.
119+
120+
## Reactive icons
121+
122+
The icon updates automatically when state changes — no manual refresh:
123+
124+
```kotlin
125+
var unreadCount by remember { mutableStateOf(0) }
126+
127+
Tray(
128+
icon = if (unreadCount > 0) Icons.Default.MarkEmailUnread else Icons.Default.Email,
129+
tooltip = "$unreadCount unread messages",
130+
) {
131+
Item(label = "Mark all read") { unreadCount = 0 }
132+
}
133+
```

0 commit comments

Comments
 (0)