Skip to content

Commit 8fc66e8

Browse files
committed
feat: Tao-only TrayApp with transparent standalone popup panel (Windows)
Drops the AWT path entirely — TrayApp now requires the Nucleus Tao backend (nucleusApplication) and consumes the published 2.0.0-alpha-202607120006 from Maven Central (mavenLocal wiring removed). TrayApp: - Windows: the popup renders in TaoStandalonePopup — an ownerless, topmost, per-pixel transparent native panel. No backing window in the taskbar/Alt-Tab/task view; slide+fade animations composite over the desktop; outside-click dismissal works over other applications and the desktop; full keyboard support (text input incl. AltGr, Backspace, arrows) and per-control cursors. - macOS/Linux: opaque DecoratedWindow with focus-based dismissal, same API. Porting plan for the transparent panel lives in docs/PLAN_MACOS_LINUX_PANEL.md. - All public overloads moved to NucleusApplicationScope receivers; screen geometry is AWT-free (TrayScreenGeometry/TrayPosition on Nucleus TaoScreenGeometry). Removed: AwtTrayInitializer, AwtTrayMenuBuilderImpl, the three per-platform OutsideClickWatchers, WindowVisibilityMonitor, WindowRaise. Demo: all entry points converted to nucleusApplication; TrayAppDemo uses MaterialDecoratedWindow/MaterialTitleBar, single instance enabled with SingleInstanceRestoreEffect re-opening the tray popup on second launch. Validated on Windows 11 on JVM and GraalVM native image.
1 parent 5f72598 commit 8fc66e8

33 files changed

Lines changed: 974 additions & 1942 deletions

.idea/vcs.xml

Lines changed: 0 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

build.gradle.kts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,10 @@ kotlin {
4747
implementation(compose.ui)
4848
implementation(compose.components.resources)
4949
implementation(libs.kotlinx.coroutines.core)
50-
implementation(libs.kotlinx.coroutines.swing)
5150
implementation(libs.nucleus.core.runtime)
5251
implementation(libs.nucleus.darkmode.detector)
52+
api(libs.nucleus.application)
53+
implementation(libs.nucleus.decorated.window.tao)
5354
}
5455
jvmTest.dependencies {
5556
implementation(kotlin("test"))

demo/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ kotlin {
2424
implementation(libs.nucleus.core.runtime)
2525
implementation(libs.nucleus.darkmode.detector)
2626
implementation(libs.nucleus.graalvm.runtime)
27+
implementation(libs.nucleus.decorated.window.tao)
28+
implementation(libs.nucleus.decorated.window.material3)
2729
}
2830
}
2931
}

demo/src/jvmMain/kotlin/dev/nucleusframework/composenativetray/demo/DemoAdaptivePositionWindows.kt

Lines changed: 31 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -4,33 +4,35 @@ import androidx.compose.foundation.layout.fillMaxSize
44
import androidx.compose.material.Icon
55
import androidx.compose.material.icons.Icons
66
import androidx.compose.material.icons.filled.Notifications
7+
import androidx.compose.material3.Text
78
import androidx.compose.runtime.getValue
89
import androidx.compose.runtime.mutableStateOf
910
import androidx.compose.runtime.remember
1011
import androidx.compose.runtime.setValue
1112
import androidx.compose.ui.Modifier
1213
import androidx.compose.ui.graphics.Color
1314
import androidx.compose.ui.unit.dp
14-
import androidx.compose.ui.window.Window
1515
import androidx.compose.ui.window.WindowState
16-
import androidx.compose.ui.window.application
17-
import androidx.compose.ui.window.rememberWindowState
16+
import dev.nucleusframework.application.DecoratedWindow
17+
import dev.nucleusframework.application.nucleusApplication
1818
import dev.nucleusframework.composenativetray.tray.api.Tray
1919
import dev.nucleusframework.composenativetray.utils.ComposeNativeTrayLoggingLevel
20-
import dev.nucleusframework.core.runtime.SingleInstanceManager
2120
import dev.nucleusframework.composenativetray.utils.allowComposeNativeTrayLogging
2221
import dev.nucleusframework.composenativetray.utils.composeNativeTrayLoggingLevel
2322
import dev.nucleusframework.composenativetray.utils.getTrayPosition
2423
import dev.nucleusframework.composenativetray.utils.getTrayWindowPosition
24+
import dev.nucleusframework.darkmodedetector.isSystemInDarkMode
25+
import dev.nucleusframework.window.NucleusDecoratedWindowTheme
26+
import dev.nucleusframework.window.TitleBar
2527
import composenativetray.demo.generated.resources.Res
2628
import composenativetray.demo.generated.resources.icon
2729
import org.jetbrains.compose.resources.painterResource
2830

29-
fun main() = application {
31+
fun main() = nucleusApplication(enableSingleInstance = false) {
3032
allowComposeNativeTrayLogging = false
3133
composeNativeTrayLoggingLevel = ComposeNativeTrayLoggingLevel.DEBUG
3234
val logTag = "NativeTray"
33-
35+
3436
println("$logTag: TrayPosition: ${getTrayPosition()}")
3537

3638
var isWindowVisible by remember { mutableStateOf(true) }
@@ -40,16 +42,6 @@ fun main() = application {
4042
var notificationsEnabled by remember { mutableStateOf(false) }
4143
var initialChecked by remember { mutableStateOf(true) }
4244

43-
val isSingleInstance = SingleInstanceManager.isSingleInstance(onRestoreRequest = {
44-
isWindowVisible = true
45-
})
46-
47-
if (!isSingleInstance) {
48-
exitApplication()
49-
return@application
50-
}
51-
52-
5345
// Always create the Tray composable, but make it conditional on visibility
5446
// This ensures it's recomposed when alwaysShowTray changes
5547
val showTray = alwaysShowTray || !isWindowVisible
@@ -146,26 +138,29 @@ fun main() = application {
146138
val windowHeight = 600
147139
val windowPosition = getTrayWindowPosition(windowWidth, windowHeight)
148140

149-
Window(
150-
onCloseRequest = {
151-
if (hideOnClose) {
152-
isWindowVisible = false
153-
} else {
154-
exitApplication()
141+
NucleusDecoratedWindowTheme(isDark = isSystemInDarkMode()) {
142+
DecoratedWindow(
143+
onCloseRequest = {
144+
if (hideOnClose) {
145+
isWindowVisible = false
146+
} else {
147+
exitApplication()
148+
}
149+
},
150+
state = WindowState(
151+
width = windowWidth.dp,
152+
height = windowHeight.dp,
153+
position = windowPosition
154+
),
155+
title = "Compose Desktop Application with Two Screens",
156+
visible = isWindowVisible,
157+
icon = painterResource(Res.drawable.icon) // Optional: Set window icon
158+
) {
159+
TitleBar { Text("Compose Desktop Application with Two Screens") }
160+
App(textVisible, alwaysShowTray, hideOnClose) { alwaysShow, hideOnCloseState ->
161+
alwaysShowTray = alwaysShow
162+
hideOnClose = hideOnCloseState
155163
}
156-
},
157-
state = WindowState(
158-
width = windowWidth.dp,
159-
height = windowHeight.dp,
160-
position = windowPosition
161-
),
162-
title = "Compose Desktop Application with Two Screens",
163-
visible = isWindowVisible,
164-
icon = painterResource(Res.drawable.icon) // Optional: Set window icon
165-
) {
166-
App(textVisible, alwaysShowTray, hideOnClose) { alwaysShow, hideOnCloseState ->
167-
alwaysShowTray = alwaysShow
168-
hideOnClose = hideOnCloseState
169164
}
170165
}
171-
}
166+
}

demo/src/jvmMain/kotlin/dev/nucleusframework/composenativetray/demo/DemoComposableMenu.kt

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,30 @@
11
package dev.nucleusframework.composenativetray.demo
22

3+
import androidx.compose.material3.Text
4+
import androidx.compose.runtime.LaunchedEffect
35
import androidx.compose.runtime.getValue
46
import androidx.compose.runtime.mutableStateOf
57
import androidx.compose.runtime.remember
68
import androidx.compose.runtime.setValue
7-
import androidx.compose.ui.window.Window
8-
import androidx.compose.ui.window.application
9+
import dev.nucleusframework.application.DecoratedWindow
10+
import dev.nucleusframework.application.nucleusApplication
911
import dev.nucleusframework.composenativetray.tray.api.Tray
10-
import dev.nucleusframework.core.runtime.SingleInstanceManager
12+
import dev.nucleusframework.darkmodedetector.isSystemInDarkMode
13+
import dev.nucleusframework.window.NucleusDecoratedWindowTheme
14+
import dev.nucleusframework.window.TitleBar
1115
import composenativetray.demo.generated.resources.Res
1216
import composenativetray.demo.generated.resources.icon
1317
import composenativetray.demo.generated.resources.icon2
1418
import org.jetbrains.compose.resources.painterResource
1519

1620
/**
1721
* Showcases the @Composable menu DSL. No need to hoist `painterResource(...)` above
18-
* `application { … }` — every menu/submenu lambda is composable.
22+
* `nucleusApplication { … }` — every menu/submenu lambda is composable.
1923
*/
20-
fun main() = application {
24+
fun main() = nucleusApplication(enableSingleInstance = false) {
2125
var isWindowVisible by remember { mutableStateOf(true) }
2226
var enableHeavyMode by remember { mutableStateOf(false) }
2327

24-
val isSingleInstance = SingleInstanceManager.isSingleInstance(onRestoreRequest = {
25-
isWindowVisible = true
26-
})
27-
if (!isSingleInstance) {
28-
exitApplication()
29-
return@application
30-
}
31-
3228
Tray(
3329
icon = painterResource(Res.drawable.icon),
3430
tooltip = "Composable Menu Demo",
@@ -68,12 +64,20 @@ fun main() = application {
6864
},
6965
)
7066

71-
Window(
72-
onCloseRequest = { isWindowVisible = false },
73-
title = "Composable Menu Demo",
74-
visible = isWindowVisible,
75-
icon = painterResource(Res.drawable.icon),
76-
) {
77-
window.toFront()
67+
NucleusDecoratedWindowTheme(isDark = isSystemInDarkMode()) {
68+
DecoratedWindow(
69+
onCloseRequest = { isWindowVisible = false },
70+
title = "Composable Menu Demo",
71+
visible = isWindowVisible,
72+
icon = painterResource(Res.drawable.icon),
73+
) {
74+
TitleBar { Text("Composable Menu Demo") }
75+
val window = nucleusWindow
76+
LaunchedEffect(isWindowVisible) {
77+
if (isWindowVisible) {
78+
window.toFront()
79+
}
80+
}
81+
}
7882
}
7983
}

demo/src/jvmMain/kotlin/dev/nucleusframework/composenativetray/demo/DemoToggleEmptyMenu.kt

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,12 @@ import androidx.compose.runtime.mutableStateOf
88
import androidx.compose.runtime.remember
99
import androidx.compose.runtime.setValue
1010
import androidx.compose.ui.Modifier
11-
import androidx.compose.ui.window.Window
12-
import androidx.compose.ui.window.application
11+
import dev.nucleusframework.application.DecoratedWindow
12+
import dev.nucleusframework.application.nucleusApplication
1313
import dev.nucleusframework.composenativetray.tray.api.Tray
14+
import dev.nucleusframework.darkmodedetector.isSystemInDarkMode
15+
import dev.nucleusframework.window.NucleusDecoratedWindowTheme
16+
import dev.nucleusframework.window.TitleBar
1417
import dev.nucleusframework.composenativetray.utils.ComposeNativeTrayLoggingLevel
1518
import dev.nucleusframework.composenativetray.utils.allowComposeNativeTrayLogging
1619
import dev.nucleusframework.composenativetray.utils.composeNativeTrayLoggingLevel
@@ -27,7 +30,7 @@ import org.jetbrains.compose.resources.painterResource
2730
* the presence of a single menu item. When the flag is false, the menuContent
2831
* is intentionally left empty.
2932
*/
30-
fun main() = application {
33+
fun main() = nucleusApplication(enableSingleInstance = false) {
3134
// Enable logging to help diagnose behavior on Linux
3235
allowComposeNativeTrayLogging = false
3336
composeNativeTrayLoggingLevel = ComposeNativeTrayLoggingLevel.DEBUG
@@ -61,7 +64,13 @@ fun main() = application {
6164

6265

6366
}
64-
Window( onCloseRequest = ::exitApplication) {
65-
Text("Compose Native Tray Demo")
67+
NucleusDecoratedWindowTheme(isDark = isSystemInDarkMode()) {
68+
DecoratedWindow(
69+
onCloseRequest = ::exitApplication,
70+
title = "Compose Native Tray Demo"
71+
) {
72+
TitleBar { Text("Compose Native Tray Demo") }
73+
Text("Compose Native Tray Demo")
74+
}
6675
}
6776
}

demo/src/jvmMain/kotlin/dev/nucleusframework/composenativetray/demo/DemoTransparentWindow.kt

Lines changed: 48 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -23,24 +23,27 @@ import androidx.compose.ui.Modifier
2323
import androidx.compose.ui.draw.clip
2424
import androidx.compose.ui.draw.shadow
2525
import androidx.compose.ui.unit.dp
26-
import androidx.compose.ui.window.Window
27-
import androidx.compose.ui.window.application
26+
import dev.nucleusframework.application.DecoratedWindow
27+
import dev.nucleusframework.application.nucleusApplication
2828
import dev.nucleusframework.composenativetray.tray.api.Tray
29+
import dev.nucleusframework.darkmodedetector.isSystemInDarkMode
30+
import dev.nucleusframework.window.NucleusDecoratedWindowTheme
2931
import composenativetray.demo.generated.resources.Res
3032
import composenativetray.demo.generated.resources.icon
3133
import org.jetbrains.compose.resources.painterResource
3234

3335
/**
3436
* DemoTransparentWindow
3537
*
36-
* A minimal example showing a simple system tray with a transparent, undecorated window.
38+
* A minimal example showing a simple system tray with an undecorated window.
3739
* Left click (primaryAction) on the tray icon shows the window. Use the tray menu to
38-
* hide or exit. The window content itself is a rounded card floating on a transparent background.
40+
* hide or exit. The window content itself is a rounded card. Note: per-pixel
41+
* transparency is not supported on the Tao backend.
3942
*/
40-
fun main() = application {
43+
fun main() = nucleusApplication(enableSingleInstance = false) {
4144
var isWindowVisible by remember { mutableStateOf(true) }
4245

43-
// Simple tray with primary action to show the transparent window
46+
// Simple tray with primary action to show the window
4447
Tray(
4548
icon = painterResource(Res.drawable.icon),
4649
tooltip = "Transparent Window Demo",
@@ -55,45 +58,48 @@ fun main() = application {
5558
}
5659
}
5760

58-
Window(
59-
onCloseRequest = { isWindowVisible = false },
60-
visible = isWindowVisible,
61-
undecorated = true,
62-
transparent = true,
63-
alwaysOnTop = false,
64-
resizable = false,
65-
title = "Transparent Window Demo",
66-
icon = painterResource(Res.drawable.icon)
67-
) {
68-
Box(
69-
modifier = Modifier
70-
.fillMaxSize()
71-
.padding(24.dp)
61+
NucleusDecoratedWindowTheme(isDark = isSystemInDarkMode()) {
62+
// Note: per-pixel transparency is not supported on the Tao backend,
63+
// so the window is only undecorated here.
64+
DecoratedWindow(
65+
onCloseRequest = { isWindowVisible = false },
66+
visible = isWindowVisible,
67+
undecorated = true,
68+
alwaysOnTop = false,
69+
resizable = false,
70+
title = "Transparent Window Demo",
71+
icon = painterResource(Res.drawable.icon)
7272
) {
73-
val shape = RoundedCornerShape(16.dp)
74-
Column(
73+
Box(
7574
modifier = Modifier
76-
.align(Alignment.Center)
77-
.shadow(24.dp, shape)
78-
.clip(shape)
79-
.border(1.dp, MaterialTheme.colorScheme.outlineVariant, shape)
80-
.background(MaterialTheme.colorScheme.surface.copy(alpha = 0.85f))
81-
.padding(20.dp),
82-
verticalArrangement = Arrangement.spacedBy(12.dp),
83-
horizontalAlignment = Alignment.CenterHorizontally
75+
.fillMaxSize()
76+
.padding(24.dp)
8477
) {
85-
Text(
86-
text = "Transparent window",
87-
color = MaterialTheme.colorScheme.onSurface,
88-
style = MaterialTheme.typography.headlineSmall
89-
)
90-
Text(
91-
text = "This window is undecorated and the background is transparent.",
92-
color = MaterialTheme.colorScheme.onSurface
93-
)
94-
Spacer(Modifier.height(8.dp))
95-
Button(onClick = { isWindowVisible = false }) {
96-
Text("Hide")
78+
val shape = RoundedCornerShape(16.dp)
79+
Column(
80+
modifier = Modifier
81+
.align(Alignment.Center)
82+
.shadow(24.dp, shape)
83+
.clip(shape)
84+
.border(1.dp, MaterialTheme.colorScheme.outlineVariant, shape)
85+
.background(MaterialTheme.colorScheme.surface.copy(alpha = 0.85f))
86+
.padding(20.dp),
87+
verticalArrangement = Arrangement.spacedBy(12.dp),
88+
horizontalAlignment = Alignment.CenterHorizontally
89+
) {
90+
Text(
91+
text = "Transparent window",
92+
color = MaterialTheme.colorScheme.onSurface,
93+
style = MaterialTheme.typography.headlineSmall
94+
)
95+
Text(
96+
text = "This window is undecorated. Per-pixel transparency is not supported on the Tao backend.",
97+
color = MaterialTheme.colorScheme.onSurface
98+
)
99+
Spacer(Modifier.height(8.dp))
100+
Button(onClick = { isWindowVisible = false }) {
101+
Text("Hide")
102+
}
97103
}
98104
}
99105
}

0 commit comments

Comments
 (0)