DEV Community

Cover image for How I put real macOS Liquid Glass inside a Java/Swing IDE
lilyaan
lilyaan

Posted on AI-assisted

How I put real macOS Liquid Glass inside a Java/Swing IDE

macOS 26 made the whole system out of glass. Finder, Xcode and System Settings got frosted sidebars, translucent toolbars and Liquid Glass buttons. Rider, my .NET IDE, still looked like a flat grey slab sitting on top of all that.

So I built Glass Islands, a free and open-source Rider plugin that turns the new Islands UI into real macOS glass:

Glass Islands demo

The key word is real. This is not a colour scheme and not a blur painted in Swing. Every translucent layer is an actual AppKit view (NSVisualEffectView, NSGlassEffectView), and the macOS compositor renders it live, the same way it renders Finder. This post explains how a Java/Swing application ends up with native AppKit glass behind it, and what broke along the way.

Why not just paint a blur?

JetBrains IDEs are Swing applications running on the JetBrains Runtime (a JDK fork). You could grab what's behind the window, blur it and paint it as a background. That falls apart quickly:

  • Swing can't see what's behind the window, so you'd need screenshots, refreshed constantly.
  • It would never match the system: vibrancy, the dimming of inactive windows, Liquid Glass refraction and accessibility settings all come from AppKit.
  • It would cost a lot of CPU for a worse result.

The only way to get the real thing is to let macOS do it, which means putting native views under Swing's content and making Swing's content see-through.

The view hierarchy

Here is what the plugin builds inside Rider's window:

NSWindow (Rider project frame, made non-opaque)
└─ NSThemeFrame
   ├─ backdrop        NSVisualEffectView, whole window, tinted          ┐ added by the plugin,
   ├─ islands         shadows │ frosted panes │ Liquid Glass pills       ┘ below Rider's content
   ├─ AWTView         Rider's Swing content (Metal layer, non-opaque)
   └─ titlebar
Enter fullscreen mode Exit fullscreen mode

Rider's Swing content still lives in its AWTView, rendered by Metal. The plugin slides a whole-window backdrop and one view per Islands pane underneath it, and makes everything above them transparent where the glass should show.

Step 1: making a decorated AWT window translucent

AWT lets an undecorated window be translucent with Window.setBackground(Color(0, 0, 0, 0)). A normal, decorated IDE frame is not allowed to. So the plugin reproduces what setBackground does internally:

  • a transparent Component.background on the frame,
  • LWWindowPeer.setOpaque(false), which makes the NSWindow and its Metal layer non-opaque,
  • classic double buffering for the root pane. IntelliJ's own per-window buffer is flipped onto the window with SrcOver, which is fine on an opaque window but accumulates translucent pixels on a translucent one: every repaint would get a little darker.

Then every Swing surface that the glass replaces becomes transparent: panels through UIManager overrides, editors through their own colour scheme delegate. The editor keeps most of its opacity (at least 86% by default), because readable code matters more than pretty glass.

Step 2: calling AppKit from Kotlin with FFM

The native side is a small Objective-C library, libliquidglass.dylib, with a deliberately boring C ABI: integers, pointers and arrays of doubles. No JNI glue. The plugin calls it through Java 25's Foreign Function & Memory API:

private val lookup = SymbolLookup.libraryLookup(libraryPath, libraryArena)
private val linker = Linker.nativeLinker()

private fun function(name: String, descriptor: FunctionDescriptor): MethodHandle =
    linker.downcallHandle(lookup.find(name).orElseThrow(), descriptor)

private val applyBackdrop = function("rlg_apply_backdrop", FunctionDescriptor.ofVoid(JAVA_LONG, ADDRESS))
private val setIslands = function("rlg_set_islands", FunctionDescriptor.ofVoid(JAVA_LONG, ADDRESS, JAVA_INT, ADDRESS))
Enter fullscreen mode Exit fullscreen mode

The window handle comes from the platform (MacUtil.getWindowFromJavaWindow), and the native side only ever touches a pointer that is still one of NSApp.windows. A stale pointer is a crash, and a crash here takes the whole IDE down.

Step 3: the threading trap

AppKit must be called on the main thread. Swing paints on the AWT event thread. The obvious fix, dispatching to the main thread and waiting, deadlocks as soon as the main thread is itself waiting on the AWT thread, which happens all the time.

The JDK's own AWT natives solve this with a dedicated run-loop mode, AWTRunLoopMode, which the main thread keeps servicing while it is blocked on AWT. The bridge does exactly the same:

[call performSelectorOnMainThread:@selector(run)
                       withObject:nil
                    waitUntilDone:YES
                            modes:@[NSRunLoopCommonModes, @"AWTRunLoopMode"]];
Enter fullscreen mode Exit fullscreen mode

Waiting also means the native shapes change in the same frame as the Swing content: when you drag a splitter, the glass pane follows the panel instead of lagging behind it. And every call is wrapped in @try/@catch, because an Objective-C exception that reaches the JVM aborts the whole IDE.

Step 4: following the layout

The plugin tracks the Islands layout (tool window holders, editor splitters, toolbar and stripe buttons, editor tabs) with a single AWT event listener. Changes are coalesced to one pass per UI cycle, and only diffs are sent to the native side. Native views are keyed by a stable id and updated in place inside a CATransaction with implicit animations disabled, so nothing slides around.

Typing, scrolling and the caret blinking send nothing to AppKit. A full layout scan takes about 0.2 ms, and a native update 2 to 5 ms. On my M4 Pro, idle Rider uses about the same CPU with or without the glass.

Frosted panes vs Liquid Glass

Using NSGlassEffectView (Liquid Glass) for the big panes looks like the obvious choice, but it doesn't survive switching windows: Liquid Glass goes flat or drops its blur when a window loses key status, so the entire IDE would flash on every focus change.

The fix was to do what macOS itself does: large surfaces use the frosted NSVisualEffectView material, and Liquid Glass is reserved for small controls such as buttons, pills and tabs. On macOS versions before 26, those controls fall back to frosted pills.

Flicker, the hard part

A translucent Swing window flickers for two different reasons:

  1. Opaque components that don't paint their whole background. In a translucent window, Swing clears a repainted region with the background of the first opaque component above it. A component that claims to be opaque but leaves gaps shows a blank flash. The plugin paints each opaque component once offscreen and marks the ones that don't actually cover their bounds as non-opaque.
  2. Frames presented before they're complete. JBR's Metal pipeline presents frames from the display link, so a frame caught mid-update shows a panel blank for one frame. In screen recordings of a scrolled diff, -Dsun.java2d.metal.displaySync=false takes this from 2 or 3 blank frames per 12 seconds to none. JBR only reads that flag at startup, so the plugin offers to add it to your VM options and removes it when you uninstall.

Being a good citizen

  • Accessibility: Reduce Transparency turns the glass off, and Increase Contrast makes every layer more opaque and outlined. Presentation Mode stays opaque.
  • No restart, clean unload: everything the plugin changes (window opacity, UIManager keys, editor colours, native views, listeners) is recorded and restored. Uninstall it and Rider looks exactly as before.
  • Safe mode: if Rider ever stops while the glass is being applied, the plugin turns itself off at the next start and tells you.
  • Fragile internals in one place: everything that depends on JDK or IntelliJ implementation details is listed and checked in a single file. If a future Rider changes one of them, only the feature built on it stops, and the settings page says why.

Try it

It's Rider-only for now, but most of it is IntelliJ Platform code. If you'd like it in IntelliJ IDEA, WebStorm or PyCharm, let me know in the comments. I'd also love to hear from anyone on Intel Macs or macOS 12 to 15, which I can test less.

Top comments (0)