This library provides a JVM Compose host that runs Compose UI in a GLFW window instead of the default AWT/Swing desktop host.
I built this because the default Compose Desktop AWT/Swing host is a bad user experience, especially on Linux. Resize is slow, Wayland isn't supported, fractional scaling isn't supported, and the GPU context isn't readily available for advanced rendering. Here I fix all of that with a more robust windowing toolkit.
Warning
This project uses internal Compose APIs and may break with future Compose versions; pay attention to the Compose version listed in the release notes.
Add the core library and the runtime modules you want to ship:
dependencies {
implementation("dev.sargunv:compose-glfw:<version>")
runtimeOnly("dev.sargunv:compose-glfw-opengl-linux-arm64:<version>")
runtimeOnly("dev.sargunv:compose-glfw-opengl-linux-x64:<version>")
runtimeOnly("dev.sargunv:compose-glfw-metal-macos-arm64:<version>")
runtimeOnly("dev.sargunv:compose-glfw-metal-macos-x64:<version>")
runtimeOnly("dev.sargunv:compose-glfw-direct3d-windows-x64:<version>")
runtimeOnly("dev.sargunv:compose-glfw-direct3d-windows-arm64:<version>")
}On recent JDKs, we also require native access to be enabled:
--enable-native-access=ALL-UNNAMEDThe following features are supported:
- App composition and window state
- HiDPI and fractional scaling
- Pointer, scroll, keyboard, text, and IME input
- Not yet touch/stylus input
- Clipboard integration
- Custom cursors
- Dark/light theme detection
- Native file and folder pickers
- Dragged file drops
- Just the final drop event; not events like enter, exit, drag
- Preferred frame rates
- Graphics context access for advanced rendering integrations
The following features are not yet supported:
- Screen reader, native menus, or the system tray
- Keep screen on and frame rate voting
- Interop views like
SwingPanel. Advanced users can instead use the host graphics context for integrating custom components. - Draggable window area (custom title bar)
See ROADMAP.md for a detailed list of known gaps. Please
contribute!
On Linux, by default, the host prefers Wayland when WAYLAND_DISPLAY is set.
You can force the GLFW display server backend with:
-Dcompose.glfw.platform=wayland
-Dcompose.glfw.platform=x11Certain features are unavailable under Wayland:
- Window positioning and always-on-top are not supported due to limitations in the Wayland protocol.
- Native file dialogs are supported but not parented to the window due to a limitation of lwjgl-nfd
- Dragged file drops are not supported on Wayland (unclear why; it should work)
On macOS, apps must be launched on AppKit's first thread:
-XstartOnFirstThreadRun your Compose content with glfwApplication:
fun main() = glfwApplication {
Window(
onCloseRequest = ::exitApplication,
title = "Example",
state = rememberWindowState(size = DpSize(960.dp, 640.dp)),
) {
App()
}
}
@Composable
fun App() {
// Your Compose UI.
}Configure the GLFW window further with WindowOptions:
Window(
onCloseRequest = ::exitApplication,
title = "Example",
undecorated = true,
transparent = true,
resizable = true,
focusOnShow = false,
options = WindowOptions {
textToolbar = { state, actions ->
// Draw a custom text context menu / toolbar.
}
},
) {
App()
}Use cursorImagePointerIcon with the pointerHoverIcon modifier to use a
custom ImageBitmap cursor:
Modifier.pointerHoverIcon(
cursorImagePointerIcon(image, imageScale = 1f, hotSpot = hotSpot),
)Use the fileDropTarget modifier to receive file drops delivered by the host:
Modifier.fileDropTarget { files ->
files.paths.forEach { path ->
// Handle dropped file.
}
}Use LocalWindow to show native file and folder pickers from composables:
@Composable
fun OpenImageButton() {
val filePicker = LocalWindow.current.filePicker
Button(
onClick = {
val image =
filePicker.openFile(
filters = listOf(FileDialogFilter("Images", listOf("png", "jpg", "jpeg"))),
)
// Handle selected path, or null if canceled.
}
) {
Text("Open image")
}
}On Linux, file pickers use xdg-desktop-portal so they work with your desktop
environment's file picker.
Advanced renderers can access the host graphics context from composables:
@Composable
fun RendererHost() {
val renderContext = LocalWindow.current.renderContext
val openGl = renderContext as? OpenGlRenderContext
// or
val metal = renderContext as? MetalRenderContext
// or
val direct3d = renderContext as? Direct3DRenderContext
App()
}Compose GLFW does not expose GLFW's glfwSetWindowIcon API. Modern Linux
Wayland and macOS ignore that per-window API; app icons should come from
platform app metadata instead. Use Freedesktop desktop-entry/icon-theme metadata
on Linux, app bundle Info.plist icon metadata on macOS, and executable icon
resources on Windows.