Skip to content

Split TrayApp into a separate composenativetray-app module (#418)#419

Open
kdroidFilter wants to merge 5 commits into
masterfrom
feat/split-trayapp-module
Open

Split TrayApp into a separate composenativetray-app module (#418)#419
kdroidFilter wants to merge 5 commits into
masterfrom
feat/split-trayapp-module

Conversation

@kdroidFilter

Copy link
Copy Markdown
Collaborator

Fixes #418.

Splits the single composenativetray artifact so apps that only need a basic system tray icon no longer pull in the decorated-window-tao windowing backend. Public API and behaviour are unchanged.

What changed

  • New :tray-app module (dev.nucleusframework:composenativetray-app) holds TrayApp and its TrayAppState / TrayWindowDismissMode / animation helper. It owns the nucleus.decorated-window-tao dependency.
  • Core composenativetray keeps the Tray / menu DSL and native tray implementation unchanged, and drops its direct decorated-window-tao dependency (nucleus.application stays — it backs the NucleusApplicationScope.Tray receiver).
  • TrayScreenGeometry is now windowing-agnostic: it exposes injectable scale / work-area providers (with graceful fallbacks) instead of calling Tao directly. tray-app installs the Tao-backed providers at composition, so behaviour with TrayApp is identical.
  • NativeTray and TrayScreenGeometry are now public (shared engine used by both modules). No change to the Tray / TrayApp public API.
  • Bump Nucleus 2.0.32.0.5.
  • README updated: documents the two artifacts and corrects snippets to the current nucleusApplication / SingleInstanceRestoreEffect API.

Consumer impact

  • Basic tray users: no change — same composenativetray coordinate, now lighter.
  • TrayApp users: add one dependency — implementation("dev.nucleusframework:composenativetray-app:<version>").

Verification

  • composenativetray, :tray-app and :demo all compile; ktlint + detekt + core test pass.
  • ./gradlew :dependencies confirms decorated-window-tao is absent from the core runtime classpath and present only in :tray-app; nucleus.application does not pull it transitively.
  • Generated POMs: composenativetray-app → core + nucleus.application + decorated-window-tao.
  • Ran TrayAppDemo end-to-end on macOS: tray created via the cross-module NativeTray, popup positioned from the injected Tao geometry (real screen bounds), shown/dismissed, clean exit.
  • Existing release workflow (publishAndReleaseToMavenCentral) publishes both modules with no change, since the unqualified task runs in the root and every subproject that registers it.

Move TrayApp and its state/dismiss-mode/animation helpers into a new
:tray-app module, published as composenativetray-app, so apps that only
need a basic tray icon no longer pull in the decorated-window-tao
windowing backend.

- Core (composenativetray) keeps the Tray/menu API and native tray impl
  unchanged, and drops its direct decorated-window-tao dependency.
- TrayScreenGeometry is now windowing-agnostic (injectable scale/work-area
  providers with fallbacks); tray-app installs the Tao-backed geometry.
- NativeTray and TrayScreenGeometry are now public (shared engine used by
  both modules); the public Tray / TrayApp API is unchanged.
- Bump Nucleus to 2.0.5.
- README: document the two artifacts and correct the snippets to the
  current nucleusApplication / SingleInstanceRestoreEffect API.
… 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.
getTrayPosition() and the TrayPosition enum now live in composenativetray-app
(package ...trayapp), next to the popup positioning they feed, so the core
artifact exposes no tray-corner / positioning API at all. The native
tray-region queries are reached through typed wrappers
(WindowsTrayInitializer.notificationIconsRegion, MacTrayInitializer.statusItemRegion)
so the JNI bridges stay internal to core.

Core's TrayPosition.kt is reduced to the raw click tracker (TrayClickTracker +
TrayClickPoint) and renamed TrayClickTracker.kt. Demos and README updated for
the new package. This also removes the Linux core-only degradation: getTrayPosition
now always has Tao geometry available in tray-app.
Comment thread build.gradle.kts
@@ -50,7 +50,7 @@ kotlin {
implementation(libs.nucleus.core.runtime)
implementation(libs.nucleus.darkmode.detector)
api(libs.nucleus.application)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can it be replaced with the core-runtime instead?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NucleusApplicationScope — the receiver of every Tray(...) overload — is defined in nucleus-application, so we can't type against core-runtime without a breaking API change. The dependency is also effectively free: you can't call Tray without already being inside nucleusApplication {}. The heavy part from #418 (decorated-window-tao) is not pulled transitively — it's compileOnly in nucleus-application and now lives only in composenativetray-app.

@amir1376 amir1376 Jul 19, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens for applications that are not using "NucleusApplication"?

Previously, this was based on the Compose application scope, which made it available to any Compose application. From what I can see, "NucleusApplication" does not provide any additional behavior here (the receiver is unused). Unlike the Compose default tray implementation, which requires access to "awt.Window", the creation logic in this library is already handled on the native side and does not require this receiver.

Using the Nucleus application scope seems to only introduce the dependency itself. This is not a small dependency either, as it pulls in a large part of the Nucleus framework (window decoration, styles, graalvm and other related components), even for applications that do not use those features.

Would it be possible to keep this independent from "NucleusApplication"? Otherwise, applications that do not use Nucleus would need to add a dependency that they cannot actually use.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The old implementation of TrayApp was based on a massive amount of AWT hacks—code that was highly burdensome to maintain—and despite that, there were still bugs that seemed impossible to resolve. The reason I created my own backend is that I could no longer stand all those AWT hacks I had to deal with on a daily basis. Today's TrayApp implementation has been entirely rewritten and can only function with the Tao backend from Nucleus. I am not forcing the use of my backend, but indeed, others of my libraries—like Compose Media Player—will eventually only work with my backend because it allows me to reduce code complexity by a factor of 10.

I prefer maintaining my own backend with a clean, modern implementation rather than tons of hacks, because AWT is an aging, low-performance framework that hasn't been updated in at least 15 years. The core, however, will continue to function, which is all that is expected from a tray library. TrayApp is a very specific implementation meant solely for creating JetBrains Toolbox-style applications.

@amir1376 amir1376 Jul 19, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the explanation. Just to make sure I understand correctly:

Does this mean the basic "@composable Tray" (not the TrayApp) in v2.0.0 implementation actually depends on the Tao backend at runtime? In other words, if "NucleusApplicationScope" (or the Tao backend) is not present, the basic "Tray" won't work and will fail at runtime. Is that why the "NucleusApplicationScope" receiver remains in the core module, even though it isn't used directly?

My original assumption was that "NucleusApplication" was just an unused receiver, so I was wondering whether it was really necessary to pull a dependency for an unused receiver.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh right, my bad, Yes, I haven't finished this PR yet, but once it's merged, even the simple tray won't use NucleusApplicationReceiver anymore and can be used without Nucleus."

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Happy to hear that. looking forward to the new version🔥

…sent

Align :tray-app with 9194bd0 (master): the unconditional signAllPublications()
predates the merge and broke publishToMavenLocal without a signatory.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Enhancement] Separate TrayApp from the core Compose Tray

2 participants