# Desktop Applications

Graphical applications arose in the early 80s as we moved from text-based terminals to more technically capable systems. This was part of the Personal Computer (PC) movement of that time, which aimed to put a “computer in every home”. Introduced in 1984, the Apple Macintosh introduced the first successful commercial graphical operating system; other vendors (e.g. Microsoft, Commodore, Sun) quickly followed suit with their own graphical operating systems. The conventions that were introduced on the Mac quickly became standard on other platforms.

Graphical User Interfaces (GUIs) were based on keyboard and mouse-input, and a graphical output (typically a CRT monitor). Such systems were seen as more "approachable" and "easy-to-use" for novice users, and were a major driver to the modern PC era.

Early Mac desktop{

Desktop applications refers to graphical applications designed for a notebook or desktop computer, typically running Windows, macOS or Linux. Users interact with these applications using a mouse and keyboard, although other devices may also be supported (e.g. camera, trackpad).

Modern graphical desktop applications closely resemble their earlier counterparts, and still rely heavily on point-and-click interaction. Modern operating systems have mostly been reduced to Linux, macOS and Windows.

Windows 11
Windows 11

There are obvious benefits to a graphical display being able to display rich colours, graphics and multimedia. However, point-and-click interfaces also provide other benefits:

  • The interface provides affordances: visual suggestions on how you might interact with the system. This can include hints like tooltips, or a graphical design that makes use of controls obvious (e.g. handles to show where to "grab" a window corner).
  • Systems provide continuous feedback to users. This includes obvious feedback (e.g. dialog boxes, status lines) and more subtle, continuous feedback (e.g. widgets animating when pressed).
  • Interfaces are explorable: users can use menus, and cues to discover new features.
  • Low cost of errors: undo-redo, and the ability to rollback to previous state makes exploration low-risk.
  • These environments encouraged developers to use consistent widgets and interactive elements. Standardization led to a common look-and-feel, and placement of common controls - which made software easier to learn, especially for novices. Many of the standard features that we take for granted are a direct result of this design standardization in the 80s1.

# Getting started

# Create a new project

To create a Compose Multiplatform project, use the project wizard in IntelliJ IDEA, and select Compose for Desktop as your project.

Create a new Compose project in IntelliJ IDEA
Create a new Compose project in IntelliJ IDEA

# Add Compose to a project

You can add also add Compose libraries to an existing project by extending your build.gradle.kts file to include the appropriate plugin and dependencies.

import org.jetbrains.compose.desktop.application.dsl.TargetFormat

plugins {
    kotlin("jvm")
    id("org.jetbrains.compose")
}

group = "com.example"
version = "1.0-SNAPSHOT"

repositories {
    mavenCentral()
    maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
    google()
}

dependencies {
    // Note, if you develop a library, you should use compose.desktop.common.
    // compose.desktop.currentOs should be used in launcher-sourceSet
    // (in a separate module for demo project and in testMain).
    // With compose.desktop.common you will also lose @Preview functionality
    implementation(compose.desktop.currentOs)
}

compose.desktop {
    application {
        mainClass = "MainKt"

        nativeDistributions {
            targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
            packageName = "scratch-pad"
            packageVersion = "1.0.0"
        }
    }
}

# Create a main method

Your entry point for a desktop application is the main method. For a Compose application, you need to:

  • use a main method as its entry point,
  • declare a top-level application scope,
  • declare one or more windows within that application scope.

This is a simple application to display a window (in fact, it's the simplest possible way to do this).

import androidx.compose.ui.window.singleWindowApplication

fun main() = singleWindowApplication(title = "Window Title", exitProcessOnExit = true) {
}

Empty application window
Empty application window

# Compile and package

Use the Gradle menu (View > Tool Windows > Gradle).

Command What does it do?
Tasks > build > clean Removes temp files (deletes the /build directory)
Tasks > build > build Compiles your application
Tasks > compose desktop > run Executes your application (builds it first if necessary)
Tasks > compose desktop > package Create an installer for your platform!

macOS installer
macOS installer

# Window support

The example above limits us to a single windows, which isn't always desireable. Graphical desktop applications also need to suuport the following:

  • Multiple application windows. Most applications will often present their interface within a single, interactive window, but it can sometimes be useful to have multiple simultaneous windows controlled by a single application2.
  • Full-screen and windowed interaction: although graphical applications tend to run windowed, they should usable full-screen as well. The window contents should scale or reposition themselves as the window size changes.
  • Window decorations: Each window should have a titlebar, minimize/maximize/restore buttons (that work as expected).

A more flexible solution is to use an expanded syntax which allows us to create and manage multiple windows. Compose will create them with min/max/restore buttons based on the defaults for the existing operating system.

import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application

fun main() =
    application {
        Window(title = "Window Title", onCloseRequest = ::exitApplication) { }
    }

application and Window are top-level composables.

Window accepts a number of parameters, including title and onCloseRequest, which is an event handler that fires when the window closes (we're calling the built-in exitApplication method to quit when the window closes).

# Create multiple windows

Using this expanded syntax, you can add multiple windows using the Window() composable!

fun main() {
    application{
        Window(
            title = "Window 1",
            state = WindowState(
              position = WindowPosition(0.dp, 0.dp),
              size = DpSize(300.dp, 200.dp)),
            onCloseRequest = ::exitApplication
        )
        {
            Text("This is the first window")
        }

        Window(
            title = "Window 2",
            state = WindowState(
              position = WindowPosition(50.dp, 50.dp),
              size = DpSize(300.dp, 200.dp)),
            onCloseRequest = ::exitApplication
        )
        {
            Text("This is the second window")
        }
    }
}

Windows
Windows

# Minimize to the system tray

fun main() = application {
    var isVisible by remember { mutableStateOf(true) }

    Window(
        onCloseRequest = { isVisible = false },
        state = WindowState(size = DpSize(250.dp, 150.dp)),
        visible = isVisible,
        title = "Counter",
    ) {
        var counter by remember { mutableStateOf(0) }
        LaunchedEffect(Unit) {
            while (true) {
                counter++
                delay(1000)
            }
        }
        Column(modifier = Modifier.fillMaxSize(),
            verticalArrangement = Arrangement.Center,
            horizontalAlignment = Alignment.CenterHorizontally)
        {
            Text(counter.toString())
        }
    }

    if (!isVisible) {
        Tray(
            TrayIcon,
            tooltip = "Counter",
            onAction = { isVisible = true },
            menu = {
                Item("Exit", onClick = ::exitApplication)
            },
        )
    }
}

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

    override fun DrawScope.onDraw() {
        drawOval(Color(0xFFFFA500))
    }
}

Counter window
Counter window

# Keyboard and mouse input

We should be able to handle:

  • Point-and-click interaction using a mouse.
  • Keyboard input, leveraging OS support for different keyboards.
  • Keyboard shortcuts, appropriate to the operating system 3. This is often combined with menubars and menu items. For example,
    • Ctrl-N for File-New, Ctrl-O for File-Open, Ctrl-Q for Quit.
    • Ctrl-X for Cut, Ctrl-C for Copy, Ctrl-V for Paste.
    • F1 for Help.

# Handle keyboard Input

fun main() = application {
    Window(
        title = "Key Events",
        state = WindowState(width = 500.dp, height = 100.dp),
        onCloseRequest = ::exitApplication,
        onKeyEvent = {
            if (it.type == KeyEventType.KeyUp) {
                println("Window handler: " + it.key.toString())
            }
            false
        }
    ) {
        MaterialTheme {
            Frame()
        }
    }
}

@Composable
fun Frame() {
    Row(modifier = Modifier.fillMaxSize(),
        horizontalArrangement = Arrangement.SpaceEvenly,
        verticalAlignment = Alignment.CenterVertically
    ) {
        KeyboardButton("1")
        KeyboardButton("2")
        KeyboardButton("3")
    }
}

@Composable
fun KeyboardButton(caption: String) {
    Button(
        onClick = { println("Button $caption click") },
        modifier = Modifier.onKeyEvent {
            if (it.type == KeyEventType.KeyUp) {
                println("Button $caption handler: " + it.key.toString());
            }
            false
        }
    ) {
        Text("Button $caption")
    }
}

Handling keyboard input
Handling keyboard input

# Handle mouse input

Composables have handlers for onClick (single-click), onDoubleClick and onLongClick (press). You can assign functions to each of these handlers, which are executed when the input is detected.

fun main() = singleWindowApplication (
    title = "Mouse Events",
    resizable = false,
    state = WindowState(
        position = WindowPosition(0.dp, 0.dp),
        size = DpSize(500.dp, 250.dp)
    )
)
{
    var count by remember { mutableStateOf(0) }
    Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxWidth()) {
        var text by remember { mutableStateOf("Click magenta box!") }
        Column(
            modifier = Modifier.fillMaxHeight(),
            verticalArrangement = Arrangement.SpaceEvenly,
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            @OptIn(ExperimentalFoundationApi::class)
            Box(
                modifier = Modifier
                    .background(Color.Magenta)
                    .fillMaxWidth(0.9f)
                    .fillMaxHeight(0.2f)
                    .combinedClickable(
                        onClick = {
                            text = "Click! ${count++}"
                        },
                        onDoubleClick = {
                            text = "Double click! ${count++}"
                        },
                        onLongClick = {
                            text = "Long click! ${count++}"
                        }
                    )
            )
            Text(text = text,
                 modifier = Modifier.fillMaxWidth(0.9f),
                 fontSize = 24.sp)
            Text("You can single-click, double-click or long-press the mouse button.",
                 modifier = Modifier.fillMaxWidth(0.9f),
                 fontSize = 24.sp
                )
        }
    }
}

mouse-events
mouse-events

The following code demonstrates how to track mouse movement through x, y positions.

@OptIn(ExperimentalComposeUiApi::class)
fun main() = singleWindowApplication(
    title = "Random window colour",
    resizable = false
) {
    var color by remember { mutableStateOf(Color(0, 0, 0)) }
    Box(
        modifier = Modifier
            .wrapContentSize(Alignment.Center)
            .fillMaxSize()
            .background(color = color)
            .onPointerEvent(PointerEventType.Move) {
                val position = it.changes.first().position
                color = Color(position.x.toInt() % 256, position.y.toInt() % 256, 0)
            }
    )
}

Mouse move
Mouse move

# Create menus

fun main() = application {
    Window(onCloseRequest = ::exitApplication) {
        App(this, this@application)
    }
}

@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun App(
    windowScope: FrameWindowScope,
    applicationScope: ApplicationScope
) {
    windowScope.MenuBar {
        Menu("File", mnemonic = 'F') {
            val nextWindowState = rememberWindowState()
            Item(
                "Exit",
                onClick = { applicationScope.exitApplication() },
                shortcut = KeyShortcut(
                    Key.X, ctrl = false
                )
            )
        }
    }
}

menus
menus

# Advanced desktop

We've already reviewed how Composables are interactive elements that we can customize (see Compose). The section below details functionality which is specific to desktop.

# Set the application icon

To change the application icon, you need to include an icon image file in your project resources, and then tell to Compose Desktop to use it when building installer images. For example:

compose.desktop {
    application {
        mainClass = "MainKt"

        nativeDistributions {
            targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
            packageName = "ApplicationName"
            packageVersion = "1.0.0"

            macOS {
                iconFile.set(project.file("src/main/resources/logo.icns"))
            }

            windows {
                iconFile.set(project.file("src/main/resources/logo.ico"))
            }

            linux {
                iconFile.set(project.file("src/main/resources/logo.png"))
            }
        }
    }
}

# Draw on a canvas

We can draw primitive graphics on a canvas.

fun main() = application {
    Window(
        onCloseRequest = ::exitApplication,
        state = WindowState(width = 250.dp, height = 300.dp),
        title = "Drawing Canvas",
        resizable = true
    ) {
        Surface(color = Color.White) {
            Canvas(modifier = Modifier.fillMaxSize()) {
                drawCircle(
                    color = Color.Yellow,
                    radius = 125.0f,
                    center = Offset(250.0f, 250.0f)
                )
                drawRect(
                    color = Color.Blue,
                    style = Fill,
                    size = Size(150.0f, 150.0f),
                    topLeft = Offset(100.0f, 100.0f)
                )
            }
        }
    }
}

Drawing canvas
Drawing canvas

# Show a dialog

We can show standard OS dialogs using the Dialog composable.

fun main() = application {
    var isOpen by remember { mutableStateOf(true) }
    var isAskingToClose by remember { mutableStateOf(false) }

    if (isOpen) {
        Window(
            onCloseRequest = { isAskingToClose = true }
        ) {
            if (isAskingToClose) {
                Dialog(
                    onCloseRequest = { isAskingToClose = false },
                    title = "Close the document without saving?",
                ) {
                    Button(
                        onClick = { isOpen = false }
                    ) {
                        Text("Yes")
                    }
                }
            }
        }
    }
}

  1. Notice that Windows, macOS, Linux all share a very common interaction paradigm, and a common look-and-feel! You can move between operating systems and be quite comfortable because of this.

  2. Photoshop, for instance, famously has multiple windows tiled over the desktop. It's also a very, very complex program, so it needs to split up functionality like this.

  3. Ctrl on Windows and Linux, CMD on Mac.