feat(desktop): unify macOS window chrome

This commit is contained in:
2026-07-13 08:34:44 +02:00
parent b90148f6b8
commit 4059bce3e4
6 changed files with 246 additions and 67 deletions

View File

@@ -4,8 +4,10 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import java.awt.Color
import java.awt.EventQueue
import java.awt.Frame
import java.awt.Window
import javax.swing.JFrame
import javax.swing.JRootPane
@Composable
actual fun PlatformSystemAppearance(isDarkTheme: Boolean) {
@@ -16,10 +18,12 @@ actual fun PlatformSystemAppearance(isDarkTheme: Boolean) {
internal object DesktopSystemAppearance {
private const val MAC_APPEARANCE_PROPERTY = "apple.awt.application.appearance"
private const val FULL_WINDOW_CONTENT_PROPERTY = "apple.awt.fullWindowContent"
private const val TRANSPARENT_TITLE_BAR_PROPERTY = "apple.awt.transparentTitleBar"
private const val WINDOW_TITLE_VISIBLE_PROPERTY = "apple.awt.windowTitleVisible"
fun apply(isDarkTheme: Boolean) {
if (!isMacOs()) return
if (!DesktopAppearanceBridge.isMacOs()) return
System.setProperty(MAC_APPEARANCE_PROPERTY, macOsAppearanceName(isDarkTheme))
EventQueue.invokeLater {
DesktopAppearanceBridge.applyNativeAppearance?.invoke(isDarkTheme)
@@ -33,27 +37,50 @@ internal object DesktopSystemAppearance {
if (isDarkTheme) "NSAppearanceNameDarkAqua" else "NSAppearanceNameAqua"
internal fun usesTransparentTitlebar(): Boolean = true
internal fun usesFullWindowContent(): Boolean = true
internal fun showsNativeWindowTitle(): Boolean = false
internal fun titlebarBackground(isDarkTheme: Boolean): Color =
if (isDarkTheme) Color(0x12, 0x12, 0x12) else Color(0xF8, 0xF8, 0xF8)
if (isDarkTheme) Color(0x21, 0x21, 0x21) else Color(0xF3, 0xF3, 0xF3)
private fun applyWindowChrome(window: Window, isDarkTheme: Boolean) {
val background = titlebarBackground(isDarkTheme)
window.background = background
(window as? JFrame)?.rootPane?.let { rootPane ->
// The titlebar stays native, but AppKit receives the resolved app
// appearance so title text and controls switch contrast at runtime.
rootPane.putClientProperty(TRANSPARENT_TITLE_BAR_PROPERTY, usesTransparentTitlebar())
rootPane.background = background
rootPane.contentPane.background = background
}
(window as? JFrame)?.rootPane?.let { rootPane -> applyRootPaneChrome(rootPane, background) }
}
private fun isMacOs(): Boolean =
System.getProperty("os.name").startsWith("Mac", ignoreCase = true)
internal fun applyRootPaneChrome(rootPane: JRootPane, background: Color) {
// Extending the Compose surface beneath the native titlebar lets the
// window chrome and sidebar share one uninterrupted background.
rootPane.putClientProperty(FULL_WINDOW_CONTENT_PROPERTY, usesFullWindowContent())
rootPane.putClientProperty(TRANSPARENT_TITLE_BAR_PROPERTY, usesTransparentTitlebar())
rootPane.putClientProperty(WINDOW_TITLE_VISIBLE_PROPERTY, showsNativeWindowTitle())
rootPane.background = background
rootPane.contentPane.background = background
}
}
object DesktopAppearanceBridge {
@Volatile
var applyNativeAppearance: ((Boolean) -> Unit)? = null
fun isMacOs(): Boolean = isMacOs(System.getProperty("os.name"))
fun toggleMaximized(window: Window) {
if (!isMacOs()) return
val frame = window as? Frame ?: return
EventQueue.invokeLater {
frame.extendedState = toggledWindowState(frame.extendedState)
}
}
internal fun isMacOs(osName: String): Boolean =
osName.startsWith("Mac", ignoreCase = true)
internal fun toggledWindowState(currentState: Int): Int =
if (currentState and Frame.MAXIMIZED_BOTH == Frame.MAXIMIZED_BOTH) {
Frame.NORMAL
} else {
Frame.MAXIMIZED_BOTH
}
}