Code Review Videos > Kotlin > Compose Multiplatform Desktop Linux [First Try]

Compose Multiplatform Desktop Linux [First Try]

All week now I’ve been wanting to have a try with JetBrain’s Compose Multiplatform Desktop. The demos I’ve found looked great. As a heads up though, those demos are all showing OSX as the run target. I am aware that Compose for Desktop can run against Windows, and (interesting to me) Linux.

What I was most interested in was the “Menu, tray, notifications” demo:

jetbrains compose for desktop tray demo

You can find the code for this (sort of) over on GitHub.

Whilst “basic”, I think it looks quite nice. Especially the slide in notification.

In my head, I had it that it would look kinda similar on Linux (specifically Ubuntu, which I use day-to-day), and that it would use the big notifications area:

ubuntu notifications area on desktop

I should definitely give a heavy caveat to all of this by saying I am an absolute newb at Java / Kotlin. What follows is literally me following tutorials and not truly understanding much of anything.

New Project Problems

Right, the first issue I faced was selecting the wrong project type.

I went with Kotlin Multiplatform, and I should have selected Compose Multiplatform.

kotlin multiplatform vs compose multiplatform

I’m still not entirely sure what the difference is, if I’m honest.

What I do know is the resulting Gradle files are different, and migrating from one to the other was confusing enough that I simply created a new project and thought… this seems quite confusing.

But onwards.

What The Java?

Next up, the docs tell you that you need to update build.gradle.kts as follows:

plugins {
   kotlin("jvm") version "1.8.20"
   id("org.jetbrains.compose") version "1.4.0"
}Code language: Kotlin (kotlin)

The confusing part to this is that, on a fresh creation of the project using the JetBrains IntelliJ IDE, the build.gradle.kts file looks like this:

plugins {
    kotlin("multiplatform")
    id("org.jetbrains.compose")
}Code language: Kotlin (kotlin)

And the actual versions themselves are declared in the gradle.properties file:

kotlin.code.style=official
kotlin.version=1.8.20
agp.version=7.3.0
compose.version=1.4.0

Fortunately I figured this part out myself.

But this wouldn’t actually work.

When I built the project and tried to run the main function in Main.kt, I got this wild error:

large compose multiplatform desktop error

This literally takes up the full width of the screen, pretty nuts.

And Googling this error comes up… well, short.

/home/chris/.sdkman/candidates/java/20-open/bin/java -Djava.library.path=/home/chris/Development/compose-multiplatform-demo/build/classes/kotlin/jvm/main -javaagent:/home/chris/.local/share/JetBrains/Toolbox/apps/IDEA-U/ch-0/231.8109.175/lib/idea_rt.jar=38217:/home/chris/.local/share/JetBrains/Toolbox/apps/IDEA-U/ch-0/231.8109.175/bin -Dfile.encoding=UTF-8 -Dsun.stdout.encoding=UTF-8 -Dsun.stderr.encoding=UTF-8 -classpath /home/chris/Development/compose-multiplatform-demo/build/classes/kotlin/jvm/main:/home/chris/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib-jdk8/1.8.0/ed04f49e186a116753ad70d34f0ac2925d1d8020/kotlin-stdlib-jdk8-1.8.0.jar:/home/chris/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib-jdk7/1.8.0/3c91271347f678c239607abb676d4032a7898427/kotlin-stdlib-jdk7-1.8.0.jar:/home/chris/.gradle/caches/modules-2/files-2.1/org.jetbrains.skiko/skiko-awt-runtime-linux-x64/0.7.50/c80b3ef8abdd4db24fa9905d6e5d821e2a1f7ab5/skiko-awt-runtime-linux-x64-0.7.50.jar:/home/chris/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib/1.8.0/1796921c7a3e2e2665a83e6c8d33399336cd39bc/kotlin-stdlib-1.8.0.jar:/home/chris/.gradle/caches/modules-2/files-2.1/org.jetbrains.compose.desktop/desktop-jvm/1.3.0/e2b2c3df64b02dab8673daba012294d9ef5efb49/desktop-jvm-1.3.0.jar:/home/chris/.gradle/caches/modules-2/files-2.1/org.jetbrains/annotations/13.0/919f0dfe192fb4e063e7dacadee7f8bb9a2672a9/annotations-13.0.jar:/home/chris/.gradle/caches/modules-2/files-2.1/org.jetbrains.compose.material/material-desktop/1.3.0/aee6d5f9630e14e42be059bb1e0a1a409aa8076/material-desktop-1.3.0.jar:/home/chris/.gradle/caches/modules-2/files-2.1/org.jetbrains.compose.foundation/foundation-desktop/1.3.0/22ab58187358e52e14714a61b0224317fc9af567/foundation-desktop-1.3.0.jar:/home/chris/.gradle/caches/modules-2/files-2.1/org.jetbrains.compose.ui/ui-desktop/1.3.0/33fe21065b205dc251cdee9d457e45658c26ca29/ui-desktop-1.3.0.jar:/home/chris/.gradle/caches/modules-2/files-2.1/org.jetbrains.compose.ui/ui-tooling-preview-desktop/1.3.0/9eaed02ecbf2a1be3ef5ff0481ce7691c5a4b399/ui-tooling-preview-desktop-1.3.0.jar:/home/chris/.gradle/caches/modules-2/files-2.1/org.jetbrains.compose.runtime/runtime-desktop/1.3.0/8b826174d5658059d552099a45ad33a964c79da9/runtime-desktop-1.3.0.jar:/home/chris/.gradle/caches/modules-2/files-2.1/org.jetbrains.compose.material/material-ripple-desktop/1.3.0/4544b518c966fb89e1d7d343646e8199f466ce56/material-ripple-desktop-1.3.0.jar:/home/chris/.gradle/caches/modules-2/files-2.1/org.jetbrains.compose.material/material-icons-core-desktop/1.3.0/587f4283e9874bc7c998a29c561d4ae4ae2165d4/material-icons-core-desktop-1.3.0.jar:/home/chris/.gradle/caches/modules-2/files-2.1/org.jetbrains.compose.ui/ui-text-desktop/1.3.0/b3fc15b133950effcc96a6c9d95daba6e4ae8ce2/ui-text-desktop-1.3.0.jar:/home/chris/.gradle/caches/modules-2/files-2.1/org.jetbrains.compose.animation/animation-core-desktop/1.3.0/b262c47b70599fd97b05afec477399e74749f64d/animation-core-desktop-1.3.0.jar:/home/chris/.gradle/caches/modules-2/files-2.1/org.jetbrains.compose.ui/ui-util-desktop/1.3.0/23411174bc22e0aa269121791c5814f0a57692c2/ui-util-desktop-1.3.0.jar:/home/chris/.gradle/caches/modules-2/files-2.1/org.jetbrains.compose.animation/animation-desktop/1.3.0/a821316519b968a53791011940dd216af36497aa/animation-desktop-1.3.0.jar:/home/chris/.gradle/caches/modules-2/files-2.1/org.jetbrains.skiko/skiko-awt/0.7.50/fa48f851a947ba3571133eedb28feecd3871d313/skiko-awt-0.7.50.jar:/home/chris/.gradle/caches/modules-2/files-2.1/org.jetbrains.compose.ui/ui-graphics-desktop/1.3.0/9f2375d3f9edc384590d3296c55bcf86eaee30ee/ui-graphics-desktop-1.3.0.jar:/home/chris/.gradle/caches/modules-2/files-2.1/org.jetbrains.compose.runtime/runtime-saveable-desktop/1.3.0/e07d563675823b53669d729eeaca1e0c5ccee30f/runtime-saveable-desktop-1.3.0.jar:/home/chris/.gradle/caches/modules-2/files-2.1/org.jetbrains.compose.ui/ui-unit-desktop/1.3.0/4211a519058e924e8b90f9658e0d0e9cf2088ae4/ui-unit-desktop-1.3.0.jar:/home/chris/.gradle/caches/modules-2/files-2.1/org.jetbrains.compose.ui/ui-geometry-desktop/1.3.0/339029b8e0b6396839297148b4ff7810c5326f54/ui-geometry-desktop-1.3.0.jar:/home/chris/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlinx/kotlinx-coroutines-core-jvm/1.6.4/2c997cd1c0ef33f3e751d3831929aeff1390cb30/kotlinx-coroutines-core-jvm-1.6.4.jar:/home/chris/.gradle/caches/modules-2/files-2.1/org.jetbrains.compose.foundation/foundation-layout-desktop/1.3.0/b7a7013707e561800bee39dc10e47079364ea561/foundation-layout-desktop-1.3.0.jar:/home/chris/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlinx/atomicfu-jvm/0.17.2/7f2eae498c00cd33ef2a8eba4be72b284270aa16/atomicfu-jvm-0.17.2.jar MainKt
Exception in thread "main" java.lang.NoSuchMethodError: 'void androidx.compose.material.TextKt.Text--4IGK_g(java.lang.String, androidx.compose.ui.Modifier, long, long, androidx.compose.ui.text.font.FontStyle, androidx.compose.ui.text.font.FontWeight, androidx.compose.ui.text.font.FontFamily, long, androidx.compose.ui.text.style.TextDecoration, androidx.compose.ui.text.style.TextAlign, long, int, boolean, int, int, kotlin.jvm.functions.Function1, androidx.compose.ui.text.TextStyle, androidx.compose.runtime.Composer, int, int, int)'
	at ComposableSingletons$MainKt$lambda-1$1$3.invoke(Main.kt:65)
	at ComposableSingletons$MainKt$lambda-1$1$3.invoke(Main.kt:59)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:116)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:34)
	at androidx.compose.ui.awt.ComposeWindow$setContent$5.invoke(ComposeWindow.desktop.kt:126)
	at androidx.compose.ui.awt.ComposeWindow$setContent$5.invoke(ComposeWindow.desktop.kt:125)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:107)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:34)
	at androidx.compose.ui.awt.ComposeWindowDelegate$setContent$3$1.invoke(ComposeWindowDelegate.desktop.kt:146)
	at androidx.compose.ui.awt.ComposeWindowDelegate$setContent$3$1.invoke(ComposeWindowDelegate.desktop.kt:145)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:107)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:34)
	at androidx.compose.runtime.CompositionLocalKt.CompositionLocalProvider(CompositionLocal.kt:228)
	at androidx.compose.ui.awt.ComposeWindowDelegate$setContent$3.invoke(ComposeWindowDelegate.desktop.kt:142)
	at androidx.compose.ui.awt.ComposeWindowDelegate$setContent$3.invoke(ComposeWindowDelegate.desktop.kt:141)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:107)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:34)
	at androidx.compose.runtime.CompositionLocalKt.CompositionLocalProvider(CompositionLocal.kt:228)
	at androidx.compose.ui.ComposeScene$setContent$5.invoke(ComposeScene.skiko.kt:371)
	at androidx.compose.ui.ComposeScene$setContent$5.invoke(ComposeScene.skiko.kt:370)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:107)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:34)
	at androidx.compose.runtime.CompositionLocalKt.CompositionLocalProvider(CompositionLocal.kt:228)
	at androidx.compose.ui.platform.CompositionLocalsKt.ProvideCommonCompositionLocals(CompositionLocals.kt:177)
	at androidx.compose.ui.platform.Wrapper_skikoKt$setContent$2$1.invoke(Wrapper.skiko.kt:47)
	at androidx.compose.ui.platform.Wrapper_skikoKt$setContent$2$1.invoke(Wrapper.skiko.kt:46)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:107)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:34)
	at androidx.compose.runtime.CompositionLocalKt.CompositionLocalProvider(CompositionLocal.kt:228)
	at androidx.compose.runtime.CompositionLocalKt.CompositionLocalProvider(CompositionLocal.kt:248)
	at androidx.compose.ui.platform.Wrapper_skikoKt.provide(Wrapper.skiko.kt:61)
	at androidx.compose.ui.platform.Wrapper_skikoKt.access$provide(Wrapper.skiko.kt:1)
	at androidx.compose.ui.platform.Wrapper_skikoKt$setContent$2.invoke(Wrapper.skiko.kt:46)
	at androidx.compose.ui.platform.Wrapper_skikoKt$setContent$2.invoke(Wrapper.skiko.kt:45)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:107)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:34)
	at androidx.compose.runtime.ActualJvm_jvmKt.invokeComposable(ActualJvm.jvm.kt:73)
	at androidx.compose.runtime.ComposerImpl$doCompose$2$5.invoke(Composer.kt:3248)
	at androidx.compose.runtime.ComposerImpl$doCompose$2$5.invoke(Composer.kt:3238)
	at androidx.compose.runtime.SnapshotStateKt__DerivedStateKt.observeDerivedStateRecalculations(DerivedState.kt:341)
	at androidx.compose.runtime.SnapshotStateKt.observeDerivedStateRecalculations(Unknown Source)
	at androidx.compose.runtime.ComposerImpl.doCompose(Composer.kt:3238)
	at androidx.compose.runtime.ComposerImpl.composeContent$runtime(Composer.kt:3173)
	at androidx.compose.runtime.CompositionImpl.composeContent(Composition.kt:587)
	at androidx.compose.runtime.Recomposer.composeInitial$runtime(Recomposer.kt:950)
	at androidx.compose.runtime.CompositionImpl.setContent(Composition.kt:519)
	at androidx.compose.ui.platform.Wrapper_skikoKt.setContent(Wrapper.skiko.kt:45)
	at androidx.compose.ui.ComposeScene.setContent$ui(ComposeScene.skiko.kt:367)
	at androidx.compose.ui.ComposeScene.setContent$ui$default(ComposeScene.skiko.kt:346)
	at androidx.compose.ui.awt.ComposeLayer$setContent$3$1.invoke(ComposeLayer.desktop.kt:435)
	at androidx.compose.ui.awt.ComposeLayer$setContent$3$1.invoke(ComposeLayer.desktop.kt:434)
	at androidx.compose.ui.awt.ComposeLayer.catchExceptions(ComposeLayer.desktop.kt:109)
	at androidx.compose.ui.awt.ComposeLayer.access$catchExceptions(ComposeLayer.desktop.kt:87)
	at androidx.compose.ui.awt.ComposeLayer$setContent$3.invoke(ComposeLayer.desktop.kt:434)
	at androidx.compose.ui.awt.ComposeLayer$setContent$3.invoke(ComposeLayer.desktop.kt:433)
	at androidx.compose.ui.awt.ComposeLayer.initContent(ComposeLayer.desktop.kt:449)
	at androidx.compose.ui.awt.ComposeLayer.access$initContent(ComposeLayer.desktop.kt:87)
	at androidx.compose.ui.awt.ComposeLayer$ComponentImpl.addNotify(ComposeLayer.desktop.kt:226)
	at java.desktop/java.awt.Container.addNotify(Container.java:2804)
	at java.desktop/javax.swing.JComponent.addNotify(JComponent.java:4846)
	at androidx.compose.ui.awt.ComposeWindowDelegate$_pane$1.addNotify(ComposeWindowDelegate.desktop.kt:80)
	at java.desktop/java.awt.Container.addNotify(Container.java:2804)
	at java.desktop/javax.swing.JComponent.addNotify(JComponent.java:4846)
	at java.desktop/java.awt.Container.addNotify(Container.java:2804)
	at java.desktop/javax.swing.JComponent.addNotify(JComponent.java:4846)
	at java.desktop/java.awt.Container.addNotify(Container.java:2804)
	at java.desktop/javax.swing.JComponent.addNotify(JComponent.java:4846)
	at java.desktop/javax.swing.JRootPane.addNotify(JRootPane.java:721)
	at java.desktop/java.awt.Container.addNotify(Container.java:2804)
	at java.desktop/java.awt.Window.addNotify(Window.java:791)
	at java.desktop/java.awt.Frame.addNotify(Frame.java:495)
	at java.desktop/java.awt.Window.pack(Window.java:829)
	at androidx.compose.ui.util.Windows_desktopKt.makeDisplayable(Windows.desktop.kt:160)
	at androidx.compose.ui.window.Window_desktopKt$Window$11$1.invoke(Window.desktop.kt:392)
	at androidx.compose.ui.window.Window_desktopKt$Window$11$1.invoke(Window.desktop.kt:385)
	at androidx.compose.ui.window.AwtWindow_desktopKt$AwtWindow$3.invoke(AwtWindow.desktop.kt:83)
	at androidx.compose.ui.window.AwtWindow_desktopKt$AwtWindow$3.invoke(AwtWindow.desktop.kt:81)
	at androidx.compose.ui.util.UpdateEffect_desktopKt$UpdateEffect$2$performUpdate$1.invoke(UpdateEffect.desktop.kt:59)
	at androidx.compose.ui.util.UpdateEffect_desktopKt$UpdateEffect$2$performUpdate$1.invoke(UpdateEffect.desktop.kt:55)
	at androidx.compose.runtime.snapshots.Snapshot$Companion.observe(Snapshot.kt:2140)
	at androidx.compose.runtime.snapshots.SnapshotStateObserver$observeReads$1$1.invoke(SnapshotStateObserver.kt:134)
	at androidx.compose.runtime.snapshots.SnapshotStateObserver$observeReads$1$1.invoke(SnapshotStateObserver.kt:130)
	at androidx.compose.runtime.SnapshotStateKt__DerivedStateKt.observeDerivedStateRecalculations(DerivedState.kt:341)
	at androidx.compose.runtime.SnapshotStateKt.observeDerivedStateRecalculations(Unknown Source)
	at androidx.compose.runtime.snapshots.SnapshotStateObserver.observeReads(SnapshotStateObserver.kt:130)
	at androidx.compose.ui.util.UpdateEffect_desktopKt$UpdateEffect$2.invoke$performUpdate(UpdateEffect.desktop.kt:55)
	at androidx.compose.ui.util.UpdateEffect_desktopKt$UpdateEffect$2.invoke(UpdateEffect.desktop.kt:64)
	at androidx.compose.ui.util.UpdateEffect_desktopKt$UpdateEffect$2.invoke(UpdateEffect.desktop.kt:47)
	at androidx.compose.runtime.DisposableEffectImpl.onRemembered(Effects.kt:81)
	at androidx.compose.runtime.CompositionImpl$RememberEventDispatcher.dispatchRememberObservers(Composition.kt:1091)
	at androidx.compose.runtime.CompositionImpl.applyChangesInLocked(Composition.kt:818)
	at androidx.compose.runtime.CompositionImpl.applyChanges(Composition.kt:839)
	at androidx.compose.runtime.Recomposer.composeInitial$runtime(Recomposer.kt:978)
	at androidx.compose.runtime.CompositionImpl.setContent(Composition.kt:519)
	at androidx.compose.ui.window.Application_desktopKt$awaitApplication$2$1$2.invokeSuspend(Application.desktop.kt:219)
	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
	at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
	at java.desktop/java.awt.event.InvocationEvent.dispatch(InvocationEvent.java:318)
	at java.desktop/java.awt.EventQueue.dispatchEventImpl(EventQueue.java:773)
	at java.desktop/java.awt.EventQueue$4.run(EventQueue.java:720)
	at java.desktop/java.awt.EventQueue$4.run(EventQueue.java:714)
	at java.base/java.security.AccessController.doPrivileged(AccessController.java:400)
	at java.base/java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:87)
	at java.desktop/java.awt.EventQueue.dispatchEvent(EventQueue.java:742)
	at java.desktop/java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:203)
	at java.desktop/java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:124)
	at java.desktop/java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:113)
	at java.desktop/java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:109)
	at java.desktop/java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:101)
	at java.desktop/java.awt.EventDispatchThread.run(EventDispatchThread.java:90)Code language: PHP (php)

Always helpful to a newb, a huge stack trace and nothing on Google that directly answers the question.

At this point I ended up uninstalling the Temurin Java 17 (I think it was), and then installing Open JDK Java 20. I was fairly sure this was not the issue, and I was proven right in this, but I put it here as I did do this:

use sdkman to install java 20

For what it’s worth, I’m using the SDK Man tool there. I guess that’s like nvm for Node.

But as I say, that still errored.

What it took to fix this was to downgrade the versions I was using back in the gradle.properties file:

kotlin.code.style=official
kotlin.version=1.8.10
agp.version=7.3.0
compose.version=1.3.1

You have to downgrade both, as they appear to work in lockstep. For example if you try to run Kotlin 1.18.20 with compose version 1.3.1 you get this:

This version of Compose Multiplatform doesn't support Kotlin 1.8.20. Please see https://github.com/JetBrains/compose-jb/blob/master/VERSIONING.md#kotlin-compatibility to know the latest supported version of Kotlin.

No big deal.

Once they were both downgraded, the project did build and run! Great success.

How This Looks On Linux

How does this look on Ubuntu?

Not the prettiest:

desktop compose multiplatform app running on linux

So you’ve got the big empty window, which is fine enough. That’s how this is setup. I’m not sure why it’s the size it is, I guess that too is a default. Maybe that’s 640×480 or something.

And then the little penguin icon (?) on the left is what you get in the taskbar. Notice it shows two little red balls next to it, indicating that there are two active windows for some reason:

kotlin compose desktop ubuntu two windows view

Here’s the code for reference – it’s literally a copy / paste from the example GitHub linked above:

import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.Text
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.window.Tray
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
import androidx.compose.ui.window.rememberNotification
import androidx.compose.ui.window.rememberTrayState

fun main() = application {
    var count by remember { mutableStateOf(0) }
    var isOpen by remember { mutableStateOf(true) }

    if (isOpen) {
        val trayState = rememberTrayState()
        val notification = rememberNotification("Notification", "Message from MyApp!")

        Tray(
            state = trayState,
            icon = TrayIcon,
            menu = {
                Item(
                    "Increment value",
                    onClick = {
                        count++
                    }
                )
                Item(
                    "Send notification",
                    onClick = {
                        trayState.sendNotification(notification)
                    }
                )
                Item(
                    "Exit",
                    onClick = {
                        isOpen = false
                    }
                )
            }
        )

        Window(
            onCloseRequest = {
                isOpen = false
            },
            icon = MyAppIcon
        ) {
            // content
            Box(
                modifier = Modifier.fillMaxSize(),
                contentAlignment = Alignment.Center
            ) {
                Text(text = "Value: $count")
            }
        }
    }
}

object MyAppIcon : Painter() {
    override val intrinsicSize = Size(256f, 256f)

    override fun DrawScope.onDraw() {
        drawOval(Color.Green, Offset(size.width / 4, 0f), Size(size.width / 2f, size.height))
        drawOval(Color.Blue, Offset(0f, size.height / 4), Size(size.width, size.height / 2f))
        drawOval(Color.Red, Offset(size.width / 4, size.height / 4), Size(size.width / 2f, size.height / 2f))
    }
}

object TrayIcon : Painter() {
    override val intrinsicSize = Size(256f, 256f)

    override fun DrawScope.onDraw() {
        drawOval(Color(0xFFFFA500))
    }
}Code language: Kotlin (kotlin)

What’s more unusual is that in the very first gif in this post, the OSX version seems to have a large yellow icon in the System Tray, which doesn’t appear to be there on Linux.

This confused the hell out of me.

compose desktop osx big yellow ball tray icon

And on my running Linux desktop:

compose desktop linux no yellow ball tray icon

A bit confusing there. For reference the icons shown are the JetBrains toolbox, then the 3 standard Ubuntu icons for network, sound, and power.

No yellow ball, though.

Balls To It

After killing the app, trying some other demos, and then coming back to this one, it turns out that the system tray icon is actually there.

It’s just very hard to see:

ubuntu desktop compose tray icon hard to see

If you mouse over it, it is there!

Magic.

ubuntu desktop compose tray icon right click

And these menu items work as expected.

A couple of clicks on ‘Increment value’:

desktop compose multiplatform app incremented value

But we don’t quite get the nice OSX experience for the notification:

ubuntu desktop compose notification

Yikes.

Not only does it not show up in the main Ubuntu Notifications area, but it … looks ugly as all heck.

Though it does work. So that’s definitely a plus.

Honestly, I’m not sure where to go with this now. I got it working. It’s not what I expected, and it’s running an older version.

Small victories?

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.