
Host Compose UI in a GLFW window, enabling smooth resizing, HiDPI/fractional scaling, native file pickers, custom cursors, file drops, and direct GPU context access for advanced rendering.
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:
The following features are not yet supported:
SwingPanel. Advanced users can instead use the host
graphics context for integrating custom components.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:
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.
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:
The following features are not yet supported:
SwingPanel. Advanced users can instead use the host
graphics context for integrating custom components.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:
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.