# desktop-expert > Expert in Compose Multiplatform Desktop development for AmethystMultiplatform. Covers Desktop-specific APIs, OS conventions, navigation patterns, and UX principles. - Author: Vitor Pamplona - Repository: aco-linux/amethyst - Version: 20260126100202 - Stars: 0 - Forks: 0 - Last Updated: 2026-02-07 - Source: https://github.com/aco-linux/amethyst - Web: https://mule.run/skillshub/@@aco-linux/amethyst~desktop-expert:20260126100202 --- # Desktop Expert Expert in Compose Multiplatform Desktop development for AmethystMultiplatform. Covers Desktop-specific APIs, OS conventions, navigation patterns, and UX principles. ## When to Use This Skill **Auto-invoke when:** - Working with `desktopApp/` module files - Using Desktop-only APIs: `Window`, `Tray`, `MenuBar`, `Dialog` - Implementing keyboard shortcuts, menu systems - Desktop navigation (NavigationRail, multi-window) - File system operations (file pickers, drag-drop) - OS-specific behavior (macOS, Windows, Linux) - Desktop UX patterns (keyboard-first, tooltips) **Delegate to:** - **kotlin-multiplatform**: Shared code questions, `jvmMain` source set structure - **gradle-expert**: All `build.gradle.kts` issues, dependency conflicts - **compose-expert**: General Compose patterns, `@Composable` best practices, Material3 ## Scope **In scope:** - Desktop-only Compose APIs - Window management, positioning, state - MenuBar + keyboard shortcuts (OS-specific) - System Tray integration - Desktop navigation patterns (NavigationRail) - File dialogs, Desktop.getDesktop() - OS conventions (macOS vs Windows vs Linux) - Desktop UX principles **Out of scope:** - Build configuration → **gradle-expert** - Shared composables → **compose-expert** - KMP structure → **kotlin-multiplatform** --- ## 1. Desktop Entry Point ### application {} DSL Desktop apps start with the `application {}` block: ```kotlin // desktopApp/src/jvmMain/kotlin/Main.kt fun main() = application { val windowState = rememberWindowState( width = 1200.dp, height = 800.dp, position = WindowPosition.Aligned(Alignment.Center) ) Window( onCloseRequest = ::exitApplication, state = windowState, title = "Amethyst" ) { MenuBar { /* ... */ } App() } } ``` **Key points:** - `application {}` is the root composable (JVM-only) - `Window()` creates the main window - `rememberWindowState()` manages size/position - `onCloseRequest` handles window close **See:** `desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt:87-138` --- ## 2. Window Management ### WindowState ```kotlin val windowState = rememberWindowState( width = 1200.dp, height = 800.dp, position = WindowPosition.Aligned(Alignment.Center) ) Window( state = windowState, title = "My App", resizable = true, onCloseRequest = ::exitApplication ) { // Content } ``` ### Multiple Windows ```kotlin fun main() = application { var showSettings by remember { mutableStateOf(false) } Window(onCloseRequest = ::exitApplication, title = "Main") { Button(onClick = { showSettings = true }) { Text("Open Settings") } } if (showSettings) { Window( onCloseRequest = { showSettings = false }, title = "Settings" ) { // Settings UI } } } ``` **Pattern:** Use state to control window visibility conditionally. --- ## 3. MenuBar System ### Basic MenuBar ```kotlin Window(onCloseRequest = ::exitApplication, title = "App") { MenuBar { Menu("File") { Item("New Note", onClick = { /* ... */ }) Separator() Item("Quit", onClick = ::exitApplication) } Menu("Edit") { Item("Copy", onClick = { /* ... */ }) Item("Paste", onClick = { /* ... */ }) } } App() } ``` ### Keyboard Shortcuts (OS-Aware) **Current issue:** Main.kt hardcodes `ctrl = true` (Main.kt:105, 111, 117, 122, 123). **OS-specific shortcuts:** ```kotlin import androidx.compose.ui.input.key.Key import androidx.compose.ui.input.key.KeyShortcut // Detect OS val isMacOS = System.getProperty("os.name").lowercase().contains("mac") MenuBar { Menu("File") { Item( "New Note", shortcut = if (isMacOS) { KeyShortcut(Key.N, meta = true) // Cmd+N on macOS } else { KeyShortcut(Key.N, ctrl = true) // Ctrl+N on Win/Linux }, onClick = { /* ... */ } ) Item( "Settings", shortcut = if (isMacOS) { KeyShortcut(Key.Comma, meta = true) // Cmd+, on macOS } else { KeyShortcut(Key.Comma, ctrl = true) // Ctrl+, on Win/Linux }, onClick = { /* ... */ } ) Separator() Item( "Quit", shortcut = if (isMacOS) { KeyShortcut(Key.Q, meta = true) // Cmd+Q on macOS } else { KeyShortcut(Key.Q, ctrl = true) // Ctrl+Q on Win/Linux }, onClick = ::exitApplication ) } } ``` **Standard shortcuts:** | Action | macOS | Windows/Linux | |--------|-------|---------------| | New | Cmd+N | Ctrl+N | | Open | Cmd+O | Ctrl+O | | Save | Cmd+S | Ctrl+S | | Quit | Cmd+Q | Ctrl+Q (Alt+F4) | | Settings | Cmd+, | Ctrl+, | | Copy | Cmd+C | Ctrl+C | | Paste | Cmd+V | Ctrl+V | | Undo | Cmd+Z | Ctrl+Z | **See:** `references/keyboard-shortcuts.md` for full list. --- ## 4. System Tray ### Basic Tray ```kotlin application { var isVisible by remember { mutableStateOf(true) } Tray( icon = painterResource("icon.png"), onAction = { isVisible = true }, menu = { Item("Show", onClick = { isVisible = true }) Separator() Item("Quit", onClick = ::exitApplication) } ) if (isVisible) { Window( onCloseRequest = { isVisible = false }, // Minimize to tray title = "App" ) { // Content } } } ``` **Pattern:** Hide window to tray instead of closing. **Current status:** Not implemented in Main.kt. Planned feature. --- ## 5. Desktop Navigation Patterns ### NavigationRail (Current Pattern) Desktop uses **NavigationRail** (vertical sidebar) instead of Android's bottom navigation. ```kotlin Row(Modifier.fillMaxSize()) { // Sidebar NavigationRail( modifier = Modifier.width(80.dp).fillMaxHeight(), containerColor = MaterialTheme.colorScheme.surfaceVariant ) { NavigationRailItem( icon = { Icon(Icons.Default.Home, "Feed") }, label = { Text("Feed") }, selected = currentScreen == AppScreen.Feed, onClick = { currentScreen = AppScreen.Feed } ) // More items... } VerticalDivider() // Main content area Box(Modifier.weight(1f).fillMaxHeight()) { when (currentScreen) { AppScreen.Feed -> FeedScreen() // Other screens... } } } ``` **See:** Main.kt:191-264 **Why NavigationRail?** - Desktop has horizontal space (1200+ dp width) - Vertical sidebar is standard desktop pattern - Always visible (no tabs hidden) - Icon + label both visible **Android comparison:** - Android: `BottomNavigationBar` (horizontal, bottom) - Desktop: `NavigationRail` (vertical, left) ### Multi-Pane Layouts Desktop can leverage wide screens: ```kotlin Row { // Left: Navigation NavigationRail { /* ... */ } // Center: Main content Box(Modifier.weight(0.6f)) { FeedScreen() } // Right: Details pane (desktop only) if (selectedNote != null) { VerticalDivider() Box(Modifier.weight(0.4f)) { NoteDetailPane(selectedNote) } } } ``` **See:** `references/desktop-navigation.md` --- ## 6. File System Integration ### File Dialogs ```kotlin // File picker (load) val fileDialog = FileDialog(Frame(), "Select file", FileDialog.LOAD) fileDialog.isVisible = true val filePath = fileDialog.file?.let { "${fileDialog.directory}$it" } // File picker (save) val saveDialog = FileDialog(Frame(), "Save file", FileDialog.SAVE) saveDialog.isVisible = true val savePath = saveDialog.file?.let { "${saveDialog.directory}$it" } ``` **Note:** Compose Desktop doesn't have native file picker composable yet. Use AWT `FileDialog`. ### Open External URLs ```kotlin // jvmMain actual implementation actual fun openExternalUrl(url: String) { if (Desktop.isDesktopSupported()) { Desktop.getDesktop().browse(URI(url)) } } ``` **Pattern:** Define `expect` in `commonMain`, implement `actual` in `jvmMain`. ### Drag & Drop (Future) ```kotlin // Compose Desktop drag-drop (experimental) Box( modifier = Modifier .onExternalDrag( onDragStart = { /* ... */ }, onDrag = { /* ... */ }, onDragExit = { /* ... */ }, onDrop = { state -> val dragData = state.dragData // Handle dropped files } ) ) { Text("Drop files here") } ``` --- ## 7. OS-Specific Conventions ### Platform Detection ```kotlin val osName = System.getProperty("os.name").lowercase() val isMacOS = osName.contains("mac") val isWindows = osName.contains("win") val isLinux = osName.contains("nux") || osName.contains("nix") ``` ### Menu Bar Placement | OS | Behavior | |----|----------| | **macOS** | System-wide menu bar at top of screen | | **Windows** | In-window menu bar | | **Linux** | Varies by desktop environment | Compose Desktop `MenuBar` adapts automatically. ### Keyboard Modifier Keys | Modifier | macOS | Windows/Linux | |----------|-------|---------------| | Primary | `meta = true` (Cmd) | `ctrl = true` | | Secondary | `ctrl = true` | `alt = true` | | Shift | `shift = true` | `shift = true` | **Best practice:** Detect OS and use appropriate modifier. ### System Tray Behavior | OS | Tray Location | |----|---------------| | **macOS** | Top-right menu bar | | **Windows** | Bottom-right taskbar | | **Linux** | Top panel (varies) | --- ## 8. Desktop UX Principles ### Keyboard-First Design **Every action should have:** 1. Mouse/touch interaction 2. Keyboard shortcut (if frequent) 3. Tooltip showing shortcut ```kotlin IconButton( onClick = { /* refresh */ }, modifier = Modifier.tooltipArea( tooltip = { Text("Refresh (${if (isMacOS) "Cmd" else "Ctrl"}+R)") } ) ) { Icon(Icons.Default.Refresh, "Refresh") } ``` ### Tooltip Best Practices - Show keyboard shortcut in tooltip - Use native modifier name (Cmd vs Ctrl) - Brief description + shortcut ### Context Menus Right-click should show context menu: ```kotlin // Future: Compose Desktop context menu API Box( modifier = Modifier.contextMenuArea( items = { listOf( ContextMenuItem("Copy") { /* ... */ }, ContextMenuItem("Paste") { /* ... */ } ) } ) ) { // Content } ``` **Current:** Use popup or custom implementation. ### Window State Persistence Save/restore window size/position: ```kotlin // Save on close windowState.size // DpSize windowState.position // WindowPosition // Restore on launch val savedWidth = preferences.getInt("window.width", 1200) val savedHeight = preferences.getInt("window.height", 800) val windowState = rememberWindowState( width = savedWidth.dp, height = savedHeight.dp ) ``` --- ## 9. Desktop Module Structure ``` desktopApp/ ├── build.gradle.kts # Desktop-only build config └── src/ └── jvmMain/ ├── kotlin/ │ └── com/vitorpamplona/amethyst/desktop/ │ ├── Main.kt # Entry point, Window, MenuBar │ ├── network/ │ │ ├── DesktopHttpClient.kt │ │ └── DesktopRelayConnectionManager.kt │ └── ui/ │ ├── FeedScreen.kt # Desktop screen layouts │ └── LoginScreen.kt └── resources/ ├── icon.icns # macOS icon ├── icon.ico # Windows icon └── icon.png # Linux icon ``` **Key files:** - `Main.kt:87-138` - `application {}`, `Window`, `MenuBar` - `Main.kt:183-264` - NavigationRail pattern - `build.gradle.kts:45-73` - Desktop packaging config --- ## 10. Packaging & Distribution ### Build Configuration ```kotlin // desktopApp/build.gradle.kts compose.desktop { application { mainClass = "com.vitorpamplona.amethyst.desktop.MainKt" nativeDistributions { targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) packageName = "Amethyst" packageVersion = "1.0.0" macOS { bundleID = "com.vitorpamplona.amethyst.desktop" iconFile.set(project.file("src/jvmMain/resources/icon.icns")) } windows { iconFile.set(project.file("src/jvmMain/resources/icon.ico")) menuGroup = "Amethyst" } linux { iconFile.set(project.file("src/jvmMain/resources/icon.png")) } } } } ``` **See:** desktopApp/build.gradle.kts:45-73 ### Gradle Tasks ```bash # Run desktop app ./gradlew :desktopApp:run # Package for distribution ./gradlew :desktopApp:packageDmg # macOS ./gradlew :desktopApp:packageMsi # Windows ./gradlew :desktopApp:packageDeb # Linux ``` **Delegate packaging issues to gradle-expert.** --- ## Common Patterns ### Pattern: OS-Aware Shortcuts Helper ```kotlin // commons/src/jvmMain/kotlin/shortcuts/ShortcutUtils.kt object DesktopShortcuts { private val isMacOS = System.getProperty("os.name") .lowercase().contains("mac") fun primary(key: Key) = if (isMacOS) { KeyShortcut(key, meta = true) } else { KeyShortcut(key, ctrl = true) } fun primaryShift(key: Key) = if (isMacOS) { KeyShortcut(key, meta = true, shift = true) } else { KeyShortcut(key, ctrl = true, shift = true) } val modifierName = if (isMacOS) "Cmd" else "Ctrl" } // Usage in MenuBar Item( "New Note", shortcut = DesktopShortcuts.primary(Key.N), onClick = { /* ... */ } ) ``` ### Pattern: Shared Composables, Platform Layouts ```kotlin // commons/commonMain - Shared NoteCard @Composable fun NoteCard(note: NoteDisplayData) { // Business logic, UI component (shared) } // desktopApp/jvmMain - Desktop layout @Composable fun FeedScreen() { Column { FeedHeader(/* ... */) // Shared from commons LazyColumn { items(notes) { note -> NoteCard(note) // Shared composable } } } } // amethyst/androidMain - Android layout @Composable fun FeedScreen() { Scaffold( bottomBar = { BottomNavigationBar() } // Android-specific ) { LazyColumn { items(notes) { note -> NoteCard(note) // Same shared composable } } } } ``` **Philosophy:** Share UI components (cards, buttons), keep navigation/layout platform-specific. --- ## Resources ### Official Documentation - [Desktop-only API | Kotlin Multiplatform](https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-desktop-components.html) - [Top-level windows management](https://kotlinlang.org/docs/multiplatform/compose-desktop-top-level-windows-management.html) - [Tray/MenuBar Tutorial](https://github.com/JetBrains/compose-multiplatform/blob/master/tutorials/Tray_Notifications_MenuBar_new/README.md) ### Bundled References - `references/desktop-compose-apis.md` - Complete Desktop API catalog - `references/desktop-navigation.md` - NavigationRail vs BottomNav patterns - `references/keyboard-shortcuts.md` - Standard shortcuts by OS - `references/os-detection.md` - Platform detection patterns ### Codebase Examples - Main.kt:87-138 - Window, MenuBar entry point - Main.kt:183-264 - NavigationRail pattern - FeedScreen.kt:49-136 - Desktop screen layout - LoginScreen.kt:44-97 - Centered desktop login --- ## Questions to Ask When working on desktop features: 1. **Should this be shared or desktop-only?** - Business logic → Share in `commonMain` - Navigation/layout → Keep in `desktopApp/jvmMain` 2. **Does this need OS-specific behavior?** - Keyboard shortcuts → Yes (Cmd vs Ctrl) - File paths → Yes (separators) - Icons → Yes (per-OS formats) 3. **Is there a desktop UX convention?** - Check MenuBar standards - Consider keyboard-first design - Tooltips for all actions 4. **Does this need gradle-expert?** - Any `build.gradle.kts` changes → Delegate - Packaging/distribution issues → Delegate --- ## Anti-Patterns ❌ **Hardcoding Ctrl everywhere** ```kotlin // Main.kt:105 - Current issue shortcut = KeyShortcut(Key.N, ctrl = true) // Wrong on macOS ``` ✅ **OS-aware shortcuts** ```kotlin shortcut = DesktopShortcuts.primary(Key.N) ``` --- ❌ **Using Android navigation on Desktop** ```kotlin Scaffold(bottomBar = { BottomNavigationBar() }) // Wrong for desktop ``` ✅ **NavigationRail for desktop** ```kotlin Row { NavigationRail { /* ... */ } MainContent() } ``` --- ❌ **No keyboard shortcuts** ```kotlin IconButton(onClick = { refresh() }) { Icon(Icons.Default.Refresh, "Refresh") } ``` ✅ **Shortcuts + tooltips** ```kotlin IconButton( onClick = { refresh() }, modifier = Modifier.tooltipArea("Refresh (Cmd+R)") ) { Icon(Icons.Default.Refresh, "Refresh") } ``` --- ## Next Steps When implementing desktop features: 1. **Read** `references/desktop-compose-apis.md` for API catalog 2. **Check** `references/keyboard-shortcuts.md` for standard shortcuts 3. **Reference** Main.kt:87-264 for current patterns 4. **Test** on all 3 platforms (macOS, Windows, Linux) if possible 5. **Delegate** build issues to gradle-expert 6. **Share** UI components via compose-expert, not desktop-expert --- **Version:** 1.0.0 **Last Updated:** 2025-12-30 **Codebase Reference:** AmethystMultiplatform commit 258c4e011