Skip to content

Commit e16e01d

Browse files
committed
refactor: move geometry/positioning to tray-app, distinct package, no screen fallback
Addresses the code-review findings on the module split. - Move TrayScreenGeometry + all popup positioning (getTrayWindowPosition / getTrayWindowPositionForInstance, corner detection, persistence) into :tray-app, where the Tao backend is always present. The core artifact no longer fabricates a 1920x1080 screen when no windowing backend is available: the native tray managers just record the raw tray-icon click, and tray-app resolves the corner + window position against real Tao geometry. macOS native status-item queries are reached through typed MacTrayInitializer wrappers so the JNI bridge stays internal. - Move TrayApp/TrayAppState/TrayWindowDismissMode and friends to package dev.nucleusframework.composenativetray.trayapp so no package is split across the two published JARs (unblocks JPMS module-path consumers). - tray-app: declare nucleus.application as api (it is TrayApp's receiver type). - Align the tray-app log timestamp format with the core formatter. - README: document the trayapp package and that getTrayWindowPosition lives in composenativetray-app; getTrayPosition stays in core. Consequence: getTrayPosition() on Linux, when used without composenativetray-app, returns the desktop-environment default corner instead of a click-derived one.
1 parent c212612 commit e16e01d

16 files changed

Lines changed: 547 additions & 654 deletions

File tree

README.md

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -463,28 +463,26 @@ nucleusApplication {
463463

464464
### 📍 Position Detection
465465

466-
Precisely position your windows relative to the system tray icon:
466+
`getTrayPosition()` (in the core artifact) tells you which screen corner the tray icon sits in:
467467

468468
```kotlin
469-
val windowWidth = 800
470-
val windowHeight = 600
471-
val windowPosition = getTrayWindowPosition(windowWidth, windowHeight)
472-
473-
Window(
474-
state = rememberWindowState(
475-
width = windowWidth.dp,
476-
height = windowHeight.dp,
477-
position = windowPosition
478-
)
479-
) { /* content */ }
469+
val corner: TrayPosition = getTrayPosition() // TOP_LEFT / TOP_RIGHT / BOTTOM_LEFT / BOTTOM_RIGHT
480470
```
481471

482-
**Implementation Details:**
483-
- **Windows**: Uses the Windows native API to get the exact position
484-
- **macOS**: Uses the Cocoa API for the position in the menu bar
485-
- **Linux**: Captures coordinates when clicking on the icon
472+
- **Windows / macOS**: resolved from the native tray/menu-bar region.
473+
- **Linux**: uses the desktop-environment convention (no reliable native tray-region API).
474+
475+
To compute a precise window position anchored to the tray icon, use `getTrayWindowPosition(...)`. Because
476+
it needs screen geometry (the Tao backend), it lives in the **`composenativetray-app`** artifact
477+
(package `dev.nucleusframework.composenativetray.trayapp`) — the same one that provides `TrayApp`,
478+
which uses it internally:
479+
480+
```kotlin
481+
val windowPosition = getTrayWindowPosition(windowWidth = 800, windowHeight = 600)
482+
```
486483

487-
The window is automatically horizontally centered on the icon and vertically positioned based on whether the system tray is at the top or bottom of the screen.
484+
The window is horizontally centered on the icon and vertically anchored to the top or bottom of the
485+
screen depending on where the tray lives. For a ready-made tray + popup window, prefer `TrayApp`.
488486

489487
### 🌓 Dark Mode Detection
490488

@@ -576,8 +574,10 @@ Add the following to your ProGuard rules file:
576574
> ```kotlin
577575
> implementation("dev.nucleusframework:composenativetray-app:<version>")
578576
> ```
579-
> It requires the Nucleus Tao backend — launch your app with `nucleusApplication { … }`. See the
580-
> `TrayAppDemo` in the `demo` module for a complete example.
577+
> It requires the Nucleus Tao backend — launch your app with `nucleusApplication { … }`. `TrayApp`,
578+
> `TrayAppState`, `rememberTrayAppState` and `TrayWindowDismissMode` live in the
579+
> `dev.nucleusframework.composenativetray.trayapp` package. See the `TrayAppDemo` in the `demo`
580+
> module for a complete example.
581581
582582
---
583583

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import dev.nucleusframework.composenativetray.utils.ComposeNativeTrayLoggingLeve
2020
import dev.nucleusframework.composenativetray.utils.allowComposeNativeTrayLogging
2121
import dev.nucleusframework.composenativetray.utils.composeNativeTrayLoggingLevel
2222
import dev.nucleusframework.composenativetray.utils.getTrayPosition
23-
import dev.nucleusframework.composenativetray.utils.getTrayWindowPosition
23+
import dev.nucleusframework.composenativetray.trayapp.getTrayWindowPosition
2424
import dev.nucleusframework.darkmodedetector.isSystemInDarkMode
2525
import dev.nucleusframework.window.NucleusDecoratedWindowTheme
2626
import dev.nucleusframework.window.TitleBar

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@ import composenativetray.demo.generated.resources.Res
1919
import composenativetray.demo.generated.resources.icon
2020
import dev.nucleusframework.application.SingleInstanceRestoreEffect
2121
import dev.nucleusframework.application.nucleusApplication
22-
import dev.nucleusframework.composenativetray.tray.api.TrayApp
23-
import dev.nucleusframework.composenativetray.tray.api.TrayWindowDismissMode
24-
import dev.nucleusframework.composenativetray.tray.api.rememberTrayAppState
22+
import dev.nucleusframework.composenativetray.trayapp.TrayApp
23+
import dev.nucleusframework.composenativetray.trayapp.TrayWindowDismissMode
24+
import dev.nucleusframework.composenativetray.trayapp.rememberTrayAppState
2525
import dev.nucleusframework.composenativetray.utils.allowComposeNativeTrayLogging
2626
import dev.nucleusframework.darkmodedetector.isSystemInDarkMode
2727
import dev.nucleusframework.window.material.MaterialDecoratedWindow

src/jvmMain/kotlin/dev/nucleusframework/composenativetray/lib/linux/LinuxTrayManager.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ internal class LinuxTrayManager(
170170
try {
171171
val xy = IntArray(2)
172172
native.nativeGetLastClickXY(trayHandle, xy)
173-
TrayClickTracker.updateClickPosition(xy[0], xy[1])
173+
TrayClickTracker.recordClick(xy[0], xy[1])
174174
} catch (_: Throwable) {
175175
}
176176
onLeftClick?.invoke()

src/jvmMain/kotlin/dev/nucleusframework/composenativetray/lib/windows/WindowsTrayManager.kt

Lines changed: 3 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
package dev.nucleusframework.composenativetray.lib.windows
22

33
import dev.nucleusframework.composenativetray.utils.TrayClickTracker
4-
import dev.nucleusframework.composenativetray.utils.TrayScreenGeometry
5-
import dev.nucleusframework.composenativetray.utils.convertPositionToCorner
64
import dev.nucleusframework.composenativetray.utils.debugln
75
import kotlinx.coroutines.CoroutineScope
86
import kotlinx.coroutines.Dispatchers
@@ -254,26 +252,9 @@ internal class WindowsTrayManager(
254252
val precise = WindowsNativeBridge.nativeGetNotificationIconsPosition(outXY) != 0
255253
log("nativeGetNotificationIconsPosition: precise=$precise, rawX=${outXY[0]}, rawY=${outXY[1]}")
256254
if (precise) {
257-
// Native coordinates are in physical pixels; window positioning
258-
// works in logical pixels. Convert via the primary monitor scale.
259-
val scale = TrayScreenGeometry.scale()
260-
val logicalX = (outXY[0] / scale).toInt()
261-
val logicalY = (outXY[1] / scale).toInt()
262-
263-
val screen = TrayScreenGeometry.workAreaLogical()
264-
log(
265-
"DPI scale=$scale, logicalX=$logicalX, logicalY=$logicalY, " +
266-
"screenW=${screen.width}, screenH=${screen.height}",
267-
)
268-
val corner =
269-
convertPositionToCorner(
270-
logicalX - screen.x,
271-
logicalY - screen.y,
272-
screen.width,
273-
screen.height,
274-
)
275-
log("Detected corner: $corner")
276-
TrayClickTracker.setClickPosition(instanceId, logicalX, logicalY, corner)
255+
// Record the raw physical click; composenativetray-app converts to logical and
256+
// resolves the corner against the Tao-backed screen geometry it owns.
257+
TrayClickTracker.recordClick(instanceId, outXY[0], outXY[1])
277258
true
278259
} else {
279260
false

src/jvmMain/kotlin/dev/nucleusframework/composenativetray/tray/impl/MacTrayInitializer.kt

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package dev.nucleusframework.composenativetray.tray.impl
22

3+
import dev.nucleusframework.composenativetray.lib.mac.MacNativeBridge
34
import dev.nucleusframework.composenativetray.lib.mac.MacTrayManager
45
import dev.nucleusframework.composenativetray.menu.api.TrayMenuBuilder
56
import dev.nucleusframework.composenativetray.menu.impl.MacTrayMenuBuilderImpl
@@ -18,6 +19,32 @@ object MacTrayInitializer {
1819
@Synchronized
1920
internal fun getNativeTrayHandle(id: String): Long = trayManagers[id]?.getNativeTrayHandle() ?: 0L
2021

22+
// Status-item queries used by the composenativetray-app module to place the TrayApp popup,
23+
// wrapping the internal JNI bridge so it stays out of the public surface.
24+
25+
/** Global status-item position in physical pixels; `false` if not precisely available. */
26+
fun statusItemPosition(outXY: IntArray): Boolean =
27+
runCatching { MacNativeBridge.nativeGetStatusItemPosition(outXY) != 0 }.getOrDefault(false)
28+
29+
/** Per-instance status-item position in physical pixels; `false` if the tray/handle isn't ready. */
30+
@Synchronized
31+
fun statusItemPositionFor(
32+
id: String,
33+
outXY: IntArray,
34+
): Boolean {
35+
val handle = getNativeTrayHandle(id)
36+
if (handle == 0L) return false
37+
return runCatching { MacNativeBridge.nativeGetStatusItemPositionFor(handle, outXY) != 0 }.getOrDefault(false)
38+
}
39+
40+
/** Per-instance status-item screen region ("top-left" | "top-right" | …), or `null` if unavailable. */
41+
@Synchronized
42+
fun statusItemRegionFor(id: String): String? {
43+
val handle = getNativeTrayHandle(id)
44+
if (handle == 0L) return null
45+
return runCatching { MacNativeBridge.nativeGetStatusItemRegionFor(handle) }.getOrNull()
46+
}
47+
2148
@Synchronized
2249
fun initialize(
2350
id: String,

0 commit comments

Comments
 (0)