This commit is contained in:
NianChen
2023-04-13 18:06:05 +08:00
commit e5873ae6fe
4063 changed files with 267552 additions and 0 deletions

View File

@ -0,0 +1,32 @@
// Suppress annotation is a workaround for a bug.
@Suppress("DSL_SCOPE_VIOLATION")
plugins {
kotlin("multiplatform")
id("org.jetbrains.compose")
}
group = extra["GROUP"] as String
version = extra["VERSION_NAME"] as String
kotlin {
jvm {
compilations.all {
kotlinOptions.jvmTarget = "11"
}
withJava()
}
sourceSets {
named("jvmMain") {
dependencies {
implementation(compose.runtime)
implementation(compose.ui)
api("net.java.dev.jna:jna-platform:latest.release")
}
}
}
}

View File

@ -0,0 +1,27 @@
#
# Publishing Configuration
#
SONATYPE_HOST=S01
RELEASE_SIGNING_ENABLED=true
GROUP=com.mayakapps.compose
POM_ARTIFACT_ID=window-styler
VERSION_NAME=0.3.3-SNAPSHOT
POM_NAME=Compose Window Styler
POM_DESCRIPTION=A library that lets you style your Compose Desktop application window
POM_INCEPTION_YEAR=2022
POM_URL=https://github.com/MayakaApps/ComposeWindowStyler
POM_SCM_URL=https://github.com/MayakaApps/ComposeWindowStyler
POM_SCM_CONNECTION=scm:git:git://github.com/MayakaApps/ComposeWindowStyler.git
POM_SCM_DEV_CONNECTION=scm:git:ssh://git@github.com/MayakaApps/ComposeWindowStyler.git
POM_LICENSE_NAME=MIT License
POM_LICENSE_URL=https://raw.githubusercontent.com/MayakaApps/ComposeWindowStyler/main/LICENSE
POM_LICENSE_DIST=repo
POM_DEVELOPER_ID=MayakaApps
POM_DEVELOPER_NAME=MayakaApps
POM_DEVELOPER_URL=https://github.com/MayakaApps/

View File

@ -0,0 +1,26 @@
package com.mayakapps.compose.windowstyler
import java.awt.AlphaComposite
import java.awt.Graphics
import java.awt.Graphics2D
import javax.swing.JPanel
internal class HackedContentPane : JPanel() {
override fun paint(g: Graphics) {
if (background.alpha != 255) {
val gg = g.create()
try {
if (gg is Graphics2D) {
gg.setColor(background)
gg.composite = AlphaComposite.getInstance(AlphaComposite.SRC)
gg.fillRect(0, 0, width, height)
}
} finally {
gg.dispose()
}
}
super.paint(g)
}
}

View File

@ -0,0 +1,66 @@
package com.mayakapps.compose.windowstyler
import androidx.compose.ui.awt.ComposeWindow
import org.jetbrains.skiko.SkiaLayer
import java.awt.*
import javax.swing.JComponent
import javax.swing.JDialog
import javax.swing.JWindow
internal fun ComposeWindow.setComposeLayerTransparency(isTransparent: Boolean) {
skiaLayer.transparency = isTransparent
}
internal fun Window.hackContentPane() {
val oldContentPane = contentPane ?: return
// Create hacked content pane the same way of AWT
val newContentPane: JComponent = HackedContentPane()
newContentPane.name = "$name.contentPane"
newContentPane.layout = object : BorderLayout() {
override fun addLayoutComponent(comp: Component, constraints: Any?) {
super.addLayoutComponent(comp, constraints ?: CENTER)
}
}
newContentPane.background = Color(0, 0, 0, 0)
oldContentPane.components.forEach { newContentPane.add(it) }
contentPane = newContentPane
}
internal val ComposeWindow.skiaLayer: SkiaLayer
get() {
val delegate = delegateField.get(this)
val layer = getLayerMethod.invoke(delegate)
return getComponentMethod.invoke(layer) as SkiaLayer
}
internal val Window.isTransparent
get() = when (this) {
is ComposeWindow -> skiaLayer.transparency
else -> background.alpha != 255
}
internal val Window.isUndecorated
get() = when (this) {
is Frame -> isUndecorated
is JDialog -> isUndecorated
is JWindow -> true
else -> false
}
private val delegateField by lazy {
ComposeWindow::class.java.getDeclaredField("delegate").apply { isAccessible = true }
}
private val getLayerMethod by lazy {
delegateField.type.getDeclaredMethod("getLayer").apply { isAccessible = true }
}
private val getComponentMethod by lazy {
getLayerMethod.returnType.getDeclaredMethod("getComponent")
}

View File

@ -0,0 +1,21 @@
package com.mayakapps.compose.windowstyler
import java.awt.Window
import javax.swing.JDialog
import javax.swing.JFrame
import javax.swing.JWindow
// Try hard to get the contentPane
internal var Window.contentPane
get() = when (this) {
is JFrame -> contentPane
is JDialog -> contentPane
is JWindow -> contentPane
else -> null
}
set(value) = when (this) {
is JFrame -> contentPane = value
is JDialog -> contentPane = value
is JWindow -> contentPane = value
else -> throw IllegalStateException()
}

View File

@ -0,0 +1,103 @@
package com.mayakapps.compose.windowstyler
import androidx.compose.ui.graphics.Color
/**
* The type of the window backdrop/background.
*
* **Fallback Strategy**
*
* In case of unsupported effect the library tries to fall back to the nearest supported effect as follows:
*
* [Tabbed] -> [Mica] -> [Acrylic] -> [Transparent]
*
* [Aero] is dropped from the fallback as it is much more transparent than [Tabbed] or [Mica] and not customizable as
* [Acrylic]. If [Tabbed] or [Mica] falls back to [Acrylic] or [Transparent], high alpha is used with white or black
* color according to `isDarkTheme` to emulate these effects.
*/
sealed interface WindowBackdrop {
/**
* This effect provides a simple solid backdrop colored as white or black according to isDarkTheme. This allows the
* backdrop to blend with the title bar as well. Though its name may imply that the window will be left unchanged,
* this is not the case as once the transparency is hacked into the window, it can't be reverted.
*/
object Default : WindowBackdrop
/**
* This applies [color] as a solid background which means that any alpha component is ignored and the color is
* rendered as opaque.
*/
open class Solid(override val color: Color) : WindowBackdrop, ColorableWindowBackdrop {
override fun equals(other: Any?): Boolean = equalsImpl(other)
override fun hashCode(): Int = hashCodeImpl()
}
/**
* Same as [Solid] but allows transparency taking into account the alpha value. If the passed [color] is fully
* opaque, the alpha is set to 0.5F.
*/
open class Transparent(color: Color) : WindowBackdrop, ColorableWindowBackdrop {
// If you really want the color to be fully opaque, just use Solid which is simpler and more stable
override val color: Color =
if (color.alpha != 1F) color else color.copy(alpha = 0.5F)
override fun equals(other: Any?): Boolean = equalsImpl(other)
override fun hashCode(): Int = hashCodeImpl()
/**
* This makes the window fully transparent.
*/
companion object : Transparent(Color.Transparent)
}
/**
* This applies [Aero](https://en.wikipedia.org/wiki/Windows_Aero) backdrop which is Windows Vista and Windows 7
* version of blur.
*
* This effect doesn't allow any customization.
*/
object Aero : WindowBackdrop
/**
* This applies [Acrylic](https://docs.microsoft.com/en-us/windows/apps/design/style/acrylic) backdrop blended with
* the supplied [color]. If the backdrop is rendered opaque, double check that [color] has reasonable alpha value.
*
* **Supported on Windows 10 version 1803 or greater.**
*/
open class Acrylic(override val color: Color) : WindowBackdrop, ColorableWindowBackdrop {
override fun equals(other: Any?): Boolean = equalsImpl(other)
override fun hashCode(): Int = hashCodeImpl()
}
/**
* This applies [Mica](https://docs.microsoft.com/en-us/windows/apps/design/style/mica) backdrop themed according
* to `isDarkTheme` value.
*
* **Supported on Windows 11 21H2 or greater.**
*/
object Mica : WindowBackdrop
/**
* This applies Tabbed backdrop themed according to `isDarkTheme` value. This is a backdrop that is similar to
* [Mica] but targeted at tabbed windows.
*
* **Supported on Windows 11 22H2 or greater.**
*/
object Tabbed : WindowBackdrop
}
internal sealed interface ColorableWindowBackdrop {
val color: Color
fun equalsImpl(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as ColorableWindowBackdrop
return color == other.color
}
fun hashCodeImpl(): Int = color.hashCode()
}

View File

@ -0,0 +1,34 @@
package com.mayakapps.compose.windowstyler
import androidx.compose.ui.graphics.Color
/**
* Styles for the window frame which includes the title bar and window border.
*
* All these styles are only supported on Windows 11 or greater and has no effect on other OSes.
*
* @property borderColor Specifies the color of the window border that is running around the window if the window is
* decorated. This property doesn't support transparency.
* @property titleBarColor Specifies the color of the window title bar (caption bar) if the window is decorated. This
* property doesn't support transparency.
* @property captionColor Specifies the color of the window caption (title) text if the window is decorated. This
* property doesn't support transparency.
* @property cornerPreference Specifies the shape of the corners you want. For example, you can use this property to
* avoid rounded corners in a decorated window or get the corners rounded in an undecorated window.
*/
data class WindowFrameStyle(
val borderColor: Color = Color.Unspecified,
val titleBarColor: Color = Color.Unspecified,
val captionColor: Color = Color.Unspecified,
val cornerPreference: WindowCornerPreference = WindowCornerPreference.DEFAULT
)
/**
* The preferred corner shape of the window.
*/
enum class WindowCornerPreference {
DEFAULT,
NOT_ROUNDED,
ROUNDED,
SMALL_ROUNDED,
}

View File

@ -0,0 +1,28 @@
package com.mayakapps.compose.windowstyler
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.window.WindowScope
/**
* Applies the provided styles to the current window.
*
* See [WindowStyleManager.isDarkTheme], [WindowBackdrop], [WindowFrameStyle].
*/
@Composable
fun WindowScope.WindowBackdropStyle(
isDarkTheme: Boolean = false,
backdropType: WindowBackdrop = WindowBackdrop.Default,
frameStyle: WindowFrameStyle = WindowFrameStyle(),
) {
val manager = remember { WindowStyleManager(window, isDarkTheme, backdropType, frameStyle) }
LaunchedEffect(isDarkTheme) {
manager.isDarkTheme = isDarkTheme
}
LaunchedEffect(backdropType) {
manager.backdropType = backdropType
}
}

View File

@ -0,0 +1,55 @@
package com.mayakapps.compose.windowstyler
import com.mayakapps.compose.windowstyler.windows.WindowsWindowStyleManager
import org.jetbrains.skiko.OS
import org.jetbrains.skiko.hostOs
import java.awt.Window
/**
* Creates a suitable [WindowStyleManager] for [window] or a stub manager if the OS is not supported.
*
* The created manager is initialized by the supplied parameters.
* See [WindowStyleManager.isDarkTheme], [WindowBackdrop], [WindowFrameStyle].
*/
fun WindowStyleManager(
window: Window,
isDarkTheme: Boolean = false,
backdropType: WindowBackdrop = WindowBackdrop.Default,
frameStyle: WindowFrameStyle = WindowFrameStyle(),
) = when (hostOs) {
OS.Windows -> WindowsWindowStyleManager(window, isDarkTheme, backdropType, frameStyle)
else -> StubWindowStyleManager(isDarkTheme, backdropType, frameStyle)
}
/**
* Style manager which lets you update the style of the provided window using the exposed properties.
*
* Only use this manager if you can't use the `@Composable` method [WindowStyle]
*/
interface WindowStyleManager {
/**
* This property should match the theming system used in your application. It's effect depends on the used backdrop
* as follows:
* * If the [backdropType] is [WindowBackdrop.Default], [WindowBackdrop.Mica] or [WindowBackdrop.Tabbed], it is
* used to manage the color of the background whether it is light or dark.
* * Otherwise, it is used to control the color of the title bar of the window white/black.
*/
var isDarkTheme: Boolean
/**
* The type of the window backdrop/background. See [WindowBackdrop] and its implementations.
*/
var backdropType: WindowBackdrop
/**
* The style of the window frame which includes the title bar and window border. See [WindowFrameStyle].
*/
var frameStyle: WindowFrameStyle
}
internal class StubWindowStyleManager(
override var isDarkTheme: Boolean,
override var backdropType: WindowBackdrop,
override var frameStyle: WindowFrameStyle,
) : WindowStyleManager

View File

@ -0,0 +1,35 @@
package com.mayakapps.compose.windowstyler.windows
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.colorspace.connect
internal fun Color.toBgr(): Int {
val colorSpace = colorSpace
val color = floatArrayOf(red, green, blue)
// The transformation saturates the output
colorSpace.connect().transform(color)
return ((color[2] * 255.0f + 0.5f).toInt() shl 16) or
((color[1] * 255.0f + 0.5f).toInt() shl 8) or
(color[0] * 255.0f + 0.5f).toInt()
}
// Modified version of toArgb
internal fun Color.toAbgr(): Int {
val colorSpace = colorSpace
val color = floatArrayOf(red, green, blue, alpha)
// The transformation saturates the output
colorSpace.connect().transform(color)
return (color[3] * 255.0f + 0.5f).toInt() shl 24 or
((color[2] * 255.0f + 0.5f).toInt() shl 16) or
((color[1] * 255.0f + 0.5f).toInt() shl 8) or
(color[0] * 255.0f + 0.5f).toInt()
}
// For some reason, passing 0 (fully transparent black) to the setAccentPolicy with
// transparent accent policy results in solid red color. As a workaround, we pass
// fully transparent white which has the same visual effect.
internal fun Color.toAbgrForTransparent() = if (alpha == 0F) 0x00FFFFFF else toAbgr()

View File

@ -0,0 +1,50 @@
package com.mayakapps.compose.windowstyler.windows
import androidx.compose.ui.awt.ComposeWindow
import com.mayakapps.compose.windowstyler.WindowBackdrop
import com.mayakapps.compose.windowstyler.WindowCornerPreference
import com.mayakapps.compose.windowstyler.windows.jna.Nt
import com.mayakapps.compose.windowstyler.windows.jna.enums.AccentState
import com.mayakapps.compose.windowstyler.windows.jna.enums.DwmSystemBackdrop
import com.mayakapps.compose.windowstyler.windows.jna.enums.DwmWindowCornerPreference
import com.sun.jna.Native
import com.sun.jna.Pointer
import com.sun.jna.platform.win32.WinDef
import java.awt.Window
val Window.hwnd
get() =
if (this is ComposeWindow) WinDef.HWND(Pointer(windowHandle))
else WinDef.HWND(Native.getWindowPointer(this))
internal val windowsBuild by lazy {
val osVersionInfo = Nt.getVersion()
val buildNumber = osVersionInfo.buildNumber
osVersionInfo.dispose()
buildNumber
}
internal fun WindowBackdrop.toDwmSystemBackdrop(): DwmSystemBackdrop =
when (this) {
is WindowBackdrop.Mica -> DwmSystemBackdrop.DWMSBT_MAINWINDOW
is WindowBackdrop.Acrylic -> DwmSystemBackdrop.DWMSBT_TRANSIENTWINDOW
is WindowBackdrop.Tabbed -> DwmSystemBackdrop.DWMSBT_TABBEDWINDOW
else -> DwmSystemBackdrop.DWMSBT_DISABLE
}
internal fun WindowBackdrop.toAccentState(): AccentState =
when (this) {
is WindowBackdrop.Default, is WindowBackdrop.Solid -> AccentState.ACCENT_ENABLE_GRADIENT
is WindowBackdrop.Transparent -> AccentState.ACCENT_ENABLE_TRANSPARENTGRADIENT
is WindowBackdrop.Aero -> AccentState.ACCENT_ENABLE_BLURBEHIND
is WindowBackdrop.Acrylic -> AccentState.ACCENT_ENABLE_ACRYLICBLURBEHIND
else -> AccentState.ACCENT_DISABLED
}
internal fun WindowCornerPreference.toDwmWindowCornerPreference(): DwmWindowCornerPreference =
when (this) {
WindowCornerPreference.DEFAULT -> DwmWindowCornerPreference.DWMWCP_DEFAULT
WindowCornerPreference.NOT_ROUNDED -> DwmWindowCornerPreference.DWMWCP_DONOTROUND
WindowCornerPreference.ROUNDED -> DwmWindowCornerPreference.DWMWCP_ROUND
WindowCornerPreference.SMALL_ROUNDED -> DwmWindowCornerPreference.DWMWCP_ROUNDSMALL
}

View File

@ -0,0 +1,76 @@
package com.mayakapps.compose.windowstyler.windows
import com.mayakapps.compose.windowstyler.windows.jna.Dwm
import com.mayakapps.compose.windowstyler.windows.jna.User32
import com.mayakapps.compose.windowstyler.windows.jna.enums.AccentFlag
import com.mayakapps.compose.windowstyler.windows.jna.enums.AccentState
import com.mayakapps.compose.windowstyler.windows.jna.enums.DwmSystemBackdrop
import com.mayakapps.compose.windowstyler.windows.jna.enums.DwmWindowAttribute
import com.sun.jna.platform.win32.WinDef
internal class WindowsBackdropApis(private val hwnd: WinDef.HWND) {
private var isSystemBackdropSet = false
private var isMicaEnabled = false
private var isAccentPolicySet = false
private var isSheetOfGlassApplied = false
fun setSystemBackdrop(systemBackdrop: DwmSystemBackdrop) {
createSheetOfGlassEffect()
if (Dwm.setSystemBackdrop(hwnd, systemBackdrop)) {
isSystemBackdropSet = systemBackdrop == DwmSystemBackdrop.DWMSBT_DISABLE
if (isSystemBackdropSet) resetAccentPolicy()
}
}
fun setMicaEffectEnabled(enabled: Boolean) {
createSheetOfGlassEffect()
if (Dwm.setWindowAttribute(hwnd, DwmWindowAttribute.DWMWA_MICA_EFFECT, enabled)) {
isMicaEnabled = enabled
if (isMicaEnabled) resetAccentPolicy()
}
}
fun setAccentPolicy(
accentState: AccentState = AccentState.ACCENT_DISABLED,
accentFlags: Set<AccentFlag> = emptySet(),
color: Int = 0,
animationId: Int = 0,
) {
if (User32.setAccentPolicy(hwnd, accentState, accentFlags, color, animationId)) {
isAccentPolicySet = accentState != AccentState.ACCENT_DISABLED
if (isAccentPolicySet) {
resetSystemBackdrop()
resetMicaEffectEnabled()
resetWindowFrame()
}
}
}
fun createSheetOfGlassEffect() {
if (!isSheetOfGlassApplied && Dwm.extendFrameIntoClientArea(hwnd, -1)) isSheetOfGlassApplied = true
}
fun resetSystemBackdrop() {
if (isSystemBackdropSet) setSystemBackdrop(DwmSystemBackdrop.DWMSBT_DISABLE)
}
fun resetMicaEffectEnabled() {
if (isMicaEnabled) setMicaEffectEnabled(false)
}
fun resetAccentPolicy() {
if (isAccentPolicySet) setAccentPolicy(AccentState.ACCENT_DISABLED)
}
fun resetWindowFrame() {
// At least one margin should be non-negative in order to show the DWM
// window shadow created by handling [WM_NCCALCSIZE].
//
// Matching value with bitsdojo_window.
// https://github.com/bitsdojo/bitsdojo_window/blob/adad0cd40be3d3e12df11d864f18a96a2d0fb4fb/bitsdojo_window_windows/windows/bitsdojo_window.cpp#L149
if (isSheetOfGlassApplied && Dwm.extendFrameIntoClientArea(hwnd, 0, 0, 1, 0)) {
isSheetOfGlassApplied = false
}
}
}

View File

@ -0,0 +1,243 @@
package com.mayakapps.compose.windowstyler.windows
import androidx.compose.ui.awt.ComposeWindow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.isSpecified
import com.mayakapps.compose.windowstyler.*
import com.mayakapps.compose.windowstyler.windows.jna.Dwm
import com.mayakapps.compose.windowstyler.windows.jna.enums.AccentFlag
import com.mayakapps.compose.windowstyler.windows.jna.enums.DwmWindowAttribute
import com.sun.jna.platform.win32.WinDef.HWND
import java.awt.Window
import java.awt.event.WindowAdapter
import java.awt.event.WindowEvent
import javax.swing.SwingUtilities
/**
* Windows implementation of [WindowStyleManager]. It is not recommended to use this class directly.
*
* If used on an OS other than Windows, it'll crash.
*/
class WindowsWindowStyleManager(
window: Window,
isDarkTheme: Boolean = false,
backdropType: WindowBackdrop = WindowBackdrop.Default,
frameStyle: WindowFrameStyle = WindowFrameStyle(),
) : WindowStyleManager {
private val hwnd: HWND = window.hwnd
private val isUndecorated = window.isUndecorated
private var wasAero = false
private val backdropApis = WindowsBackdropApis(hwnd)
override var isDarkTheme: Boolean = isDarkTheme
set(value) {
if (field != value) {
field = value
updateTheme()
}
}
override var backdropType: WindowBackdrop = backdropType
set(value) {
val finalValue = value.fallbackIfUnsupported()
if (field != finalValue) {
wasAero = field is WindowBackdrop.Aero
field = finalValue
updateBackdrop()
}
}
override var frameStyle: WindowFrameStyle = frameStyle
set(value) {
if (field != value) {
val oldValue = field
field = value
updateFrameStyle(oldValue)
}
}
init {
// invokeLater is called to make sure that ComposeLayer was initialized first
SwingUtilities.invokeLater {
// If the window is not already transparent, hack it to be transparent
if (!window.isTransparent) {
// For some reason, reversing the order of these two calls doesn't work.
if (window is ComposeWindow) window.setComposeLayerTransparency(true)
window.hackContentPane()
}
updateTheme()
updateBackdrop()
updateFrameStyle()
}
}
private fun updateTheme() {
val attribute =
when {
windowsBuild < 17763 -> return
windowsBuild >= 18985 -> DwmWindowAttribute.DWMWA_USE_IMMERSIVE_DARK_MODE
else -> DwmWindowAttribute.DWMWA_USE_IMMERSIVE_DARK_MODE_BEFORE_20H1
}
if (windowsBuild >= 17763 && Dwm.setWindowAttribute(hwnd, attribute, isDarkTheme)) {
// Default: This is done to update the background color between white or black
// ThemedAcrylic: Update the acrylic effect if it is themed
// Transparent:
// For some reason, using setImmersiveDarkModeEnabled after setting accent policy to transparent
// results in solid red backdrop. So, we have to reset the transparent backdrop after using it.
// This is also required for updating emulated transparent effect
if (backdropType is WindowBackdrop.Default || backdropType is ThemedAcrylic ||
backdropType is WindowBackdrop.Transparent
) updateBackdrop()
// This is necessary for window buttons to change color correctly
else if (backdropType is WindowBackdrop.Mica && !isUndecorated) {
backdropApis.resetWindowFrame()
backdropApis.createSheetOfGlassEffect()
}
}
}
private fun updateBackdrop() {
// This is done to make sure that the window has become visible
// If the window isn't shown yet, and we try to apply Default, Solid, Aero,
// or Acrylic, the effect will be applied to the title bar background
// leaving the caption with awkward background box.
// Unfortunately, even with this method, mica has this background box.
SwingUtilities.invokeLater {
// Only on later Windows 11 versions and if effect is WindowEffect.mica,
// WindowEffect.acrylic or WindowEffect.tabbed, otherwise fallback to old
// approach.
if (
windowsBuild >= 22523 &&
(backdropType is WindowBackdrop.Acrylic || backdropType is WindowBackdrop.Mica || backdropType is WindowBackdrop.Tabbed)
) {
backdropApis.setSystemBackdrop(backdropType.toDwmSystemBackdrop())
} else {
if (backdropType is WindowBackdrop.Mica) {
// Check for Windows 11.
if (windowsBuild >= 22000) {
backdropApis.setMicaEffectEnabled(true)
}
} else {
val color = when (val backdropType = backdropType) {
// As the transparency hack is irreversible, the default effect is applied by solid backdrop.
// The default color is white or black depending on the theme
is WindowBackdrop.Default -> (if (isDarkTheme) Color.Black else Color.White).toAbgr()
is WindowBackdrop.Transparent -> backdropType.color.toAbgrForTransparent()
is ColorableWindowBackdrop -> backdropType.color.toAbgr()
else -> 0x7FFFFFFF
}
// wasAero: This is required as sometimes the window gets stuck at aero
// Transparent: In many cases, if this is not done, red opaque background is shown
if (wasAero || backdropType is WindowBackdrop.Transparent) backdropApis.resetAccentPolicy()
// Another red opaque background case :'(
// Resetting these values needs to be done before applying transparency
if (backdropType is WindowBackdrop.Transparent) {
backdropApis.resetMicaEffectEnabled()
backdropApis.resetSystemBackdrop()
}
backdropApis.setAccentPolicy(
accentState = backdropType.toAccentState(),
accentFlags = setOf(AccentFlag.DRAW_ALL_BORDERS),
color = color,
)
}
}
}
}
/*
* Frame Style
*/
private fun updateFrameStyle(oldStyle: WindowFrameStyle? = null) {
if (windowsBuild >= 22000) {
if ((oldStyle?.cornerPreference ?: WindowCornerPreference.DEFAULT) != frameStyle.cornerPreference) {
Dwm.setWindowCornerPreference(hwnd, frameStyle.cornerPreference.toDwmWindowCornerPreference())
}
if (frameStyle.borderColor.isSpecified && oldStyle?.borderColor != frameStyle.borderColor) {
Dwm.setWindowAttribute(hwnd, DwmWindowAttribute.DWMWA_BORDER_COLOR, frameStyle.borderColor.toBgr())
}
if (frameStyle.titleBarColor.isSpecified && oldStyle?.titleBarColor != frameStyle.titleBarColor) {
Dwm.setWindowAttribute(hwnd, DwmWindowAttribute.DWMWA_CAPTION_COLOR, frameStyle.titleBarColor.toBgr())
}
if (frameStyle.captionColor.isSpecified && oldStyle?.captionColor != frameStyle.captionColor) {
Dwm.setWindowAttribute(hwnd, DwmWindowAttribute.DWMWA_TEXT_COLOR, frameStyle.captionColor.toBgr())
}
}
}
/*
* Fallback Strategy
*/
private fun WindowBackdrop.fallbackIfUnsupported(): WindowBackdrop {
if (windowsBuild >= supportedSince) return this
return when (this) {
is WindowBackdrop.Tabbed -> WindowBackdrop.Mica
is WindowBackdrop.Mica -> themedAcrylic
is WindowBackdrop.Acrylic -> {
// Aero isn't customizable and too transparent for background
// Manual mapping of themedAcrylic is to keep the theming working as expected
if (this is ThemedAcrylic) themedTransparent
else WindowBackdrop.Transparent(color)
}
else -> WindowBackdrop.Default
}.fallbackIfUnsupported()
}
private val themedTransparent = ThemedTransparent()
private val themedAcrylic = ThemedAcrylic()
private val themedFallbackColor
get() = if (isDarkTheme) Color(0xEF000000L) else Color(0xEFFFFFFFL)
private inner class ThemedAcrylic : WindowBackdrop.Acrylic(Color.Unspecified) {
override val color: Color
get() = themedFallbackColor
}
private inner class ThemedTransparent : WindowBackdrop.Transparent(Color.Unspecified) {
override val color: Color
get() = themedFallbackColor
}
private val WindowBackdrop.supportedSince
get() = when (this) {
is WindowBackdrop.Acrylic -> 17063
is WindowBackdrop.Mica -> 22000
is WindowBackdrop.Tabbed -> 22523
else -> 0
}
/*
* Focus Listener for transparency workaround
*/
// This is a workaround for transparency getting replaced by red opaque color for decorated windows on focus
// changes. This workaround doesn't appear to be efficient, and there may be red flashes on losing/gaining focus.
// Yet, it seems to be enough for the limited use cases of transparent decorated
private val windowAdapter = object : WindowAdapter() {
override fun windowGainedFocus(e: WindowEvent?) = resetTransparent()
override fun windowLostFocus(e: WindowEvent?) = resetTransparent()
private fun resetTransparent() {
if (!isUndecorated && this@WindowsWindowStyleManager.backdropType is WindowBackdrop.Transparent) updateBackdrop()
}
}
init {
window.addWindowFocusListener(windowAdapter)
}
}

View File

@ -0,0 +1,75 @@
package com.mayakapps.compose.windowstyler.windows.jna
import androidx.compose.ui.window.WindowScope
import com.mayakapps.compose.windowstyler.windows.hwnd
import com.mayakapps.compose.windowstyler.windows.jna.enums.DwmSystemBackdrop
import com.mayakapps.compose.windowstyler.windows.jna.enums.DwmWindowAttribute
import com.mayakapps.compose.windowstyler.windows.jna.enums.DwmWindowCornerPreference
import com.mayakapps.compose.windowstyler.windows.jna.structs.Margins
import com.sun.jna.Native
import com.sun.jna.PointerType
import com.sun.jna.platform.win32.W32Errors
import com.sun.jna.platform.win32.WinDef
import com.sun.jna.platform.win32.WinDef.HWND
import com.sun.jna.platform.win32.WinNT.HRESULT
import com.sun.jna.ptr.IntByReference
import com.sun.jna.win32.StdCallLibrary
import com.sun.jna.win32.W32APIOptions
object Dwm {
fun extendFrameIntoClientArea(hwnd: HWND, margin: Int = 0) =
extendFrameIntoClientArea(hwnd, margin, margin, margin, margin)
fun extendFrameIntoClientArea(
hwnd: HWND,
leftWidth: Int = 0,
rightWidth: Int = 0,
topHeight: Int = 0,
bottomHeight: Int = 0,
): Boolean {
val margins = Margins(leftWidth, rightWidth, topHeight, bottomHeight)
val result = DwmImpl.DwmExtendFrameIntoClientArea(hwnd, margins)
if (result != W32Errors.S_OK) println("DwmExtendFrameIntoClientArea failed with result $result")
margins.dispose()
return result == W32Errors.S_OK
}
fun setSystemBackdrop(hwnd: HWND, systemBackdrop: DwmSystemBackdrop): Boolean =
setWindowAttribute(hwnd, DwmWindowAttribute.DWMWA_SYSTEMBACKDROP_TYPE, systemBackdrop.value)
fun setWindowCornerPreference(hwnd: HWND, cornerPreference: DwmWindowCornerPreference): Boolean =
setWindowAttribute(hwnd, DwmWindowAttribute.DWMWA_WINDOW_CORNER_PREFERENCE, cornerPreference.value)
fun setWindowAttribute(hwnd: HWND, attribute: DwmWindowAttribute, value: Boolean) =
setWindowAttribute(hwnd, attribute, WinDef.BOOLByReference(WinDef.BOOL(value)), WinDef.BOOL.SIZE)
fun setWindowAttribute(hwnd: HWND, attribute: DwmWindowAttribute, value: Int) =
setWindowAttribute(hwnd, attribute, IntByReference(value), INT_SIZE)
fun WindowScope.setWindowAttribute(attribute: DwmWindowAttribute, value: Int) =
setWindowAttribute(this.window.hwnd, attribute, IntByReference(value), INT_SIZE)
private fun setWindowAttribute(
hwnd: HWND,
attribute: DwmWindowAttribute,
value: PointerType?,
valueSize: Int,
): Boolean {
val result = DwmImpl.DwmSetWindowAttribute(hwnd, attribute.value, value, valueSize)
if (result != W32Errors.S_OK) println("DwmSetWindowAttribute(${attribute.name}) failed with result $result")
return result == W32Errors.S_OK
}
}
@Suppress("SpellCheckingInspection")
private object DwmImpl : DwmApi by Native.load("dwmapi", DwmApi::class.java, W32APIOptions.DEFAULT_OPTIONS)
@Suppress("FunctionName")
private interface DwmApi : StdCallLibrary {
fun DwmExtendFrameIntoClientArea(hwnd: HWND, margins: Margins): HRESULT
fun DwmSetWindowAttribute(hwnd: HWND, attribute: Int, value: PointerType?, valueSize: Int): HRESULT
}

View File

@ -0,0 +1,18 @@
package com.mayakapps.compose.windowstyler.windows.jna
import com.mayakapps.compose.windowstyler.windows.jna.structs.OsVersionInfo
import com.sun.jna.Native
import com.sun.jna.win32.StdCallLibrary
import com.sun.jna.win32.W32APIOptions
internal object Nt {
fun getVersion() = OsVersionInfo().also { NtImpl.RtlGetVersion(it) }
}
@Suppress("SpellCheckingInspection")
private object NtImpl : NtApi by Native.load("Ntdll", NtApi::class.java, W32APIOptions.DEFAULT_OPTIONS)
@Suppress("FunctionName")
private interface NtApi : StdCallLibrary {
fun RtlGetVersion(osVersionInfo: OsVersionInfo): Int
}

View File

@ -0,0 +1,50 @@
package com.mayakapps.compose.windowstyler.windows.jna
import com.mayakapps.compose.windowstyler.windows.jna.enums.AccentFlag
import com.mayakapps.compose.windowstyler.windows.jna.enums.AccentState
import com.mayakapps.compose.windowstyler.windows.jna.enums.WindowCompositionAttribute
import com.mayakapps.compose.windowstyler.windows.jna.structs.AccentPolicy
import com.mayakapps.compose.windowstyler.windows.jna.structs.WindowCompositionAttributeData
import com.sun.jna.Native
import com.sun.jna.platform.win32.WinDef
import com.sun.jna.win32.StdCallLibrary
import com.sun.jna.win32.W32APIOptions
internal object User32 {
fun setAccentPolicy(
hwnd: WinDef.HWND,
accentState: AccentState = AccentState.ACCENT_DISABLED,
accentFlags: Set<AccentFlag> = emptySet(),
color: Int = 0,
animationId: Int = 0,
): Boolean {
val data = WindowCompositionAttributeData(
WindowCompositionAttribute.WCA_ACCENT_POLICY,
AccentPolicy(accentState, accentFlags, color, animationId),
)
val isSuccess = setWindowCompositionAttribute(hwnd, data)
data.dispose()
return isSuccess
}
private fun setWindowCompositionAttribute(
hwnd: WinDef.HWND,
attributeData: WindowCompositionAttributeData
): Boolean {
Native.setLastError(0)
val isSuccess = User32Impl.SetWindowCompositionAttribute(hwnd, attributeData)
if (!isSuccess) println("SetWindowCompositionAttribute(${attributeData.attribute}) failed with last error ${Native.getLastError()}")
return isSuccess
}
}
private object User32Impl : User32Api by Native.load("user32", User32Api::class.java, W32APIOptions.DEFAULT_OPTIONS)
@Suppress("FunctionName")
private interface User32Api : StdCallLibrary {
fun SetWindowCompositionAttribute(hwnd: WinDef.HWND, attributeData: WindowCompositionAttributeData): Boolean
}

View File

@ -0,0 +1,9 @@
package com.mayakapps.compose.windowstyler.windows.jna
internal inline fun <T> Iterable<T>.orOf(selector: (T) -> Int): Int {
var result = 0
forEach { result = result or selector(it) }
return result
}
internal const val INT_SIZE = 4

View File

@ -0,0 +1,11 @@
package com.mayakapps.compose.windowstyler.windows.jna.enums
@Suppress("SpellCheckingInspection", "unused")
internal enum class AccentFlag(val value: Int) {
NONE(0),
DRAW_LEFT_BORDER(0x20),
DRAW_TOP_BORDER(0x40),
DRAW_RIGHT_BORDER(0x80),
DRAW_BOTTOM_BORDER(0x100),
DRAW_ALL_BORDERS(0x1E0), // OR result of all borders
}

View File

@ -0,0 +1,12 @@
package com.mayakapps.compose.windowstyler.windows.jna.enums
@Suppress("SpellCheckingInspection", "unused")
internal enum class AccentState(val value: Int) {
ACCENT_DISABLED(0),
ACCENT_ENABLE_GRADIENT(1),
ACCENT_ENABLE_TRANSPARENTGRADIENT(2),
ACCENT_ENABLE_BLURBEHIND(3),
ACCENT_ENABLE_ACRYLICBLURBEHIND(4),
ACCENT_ENABLE_HOSTBACKDROP(5),
ACCENT_INVALID_STATE(6),
}

View File

@ -0,0 +1,10 @@
package com.mayakapps.compose.windowstyler.windows.jna.enums
@Suppress("SpellCheckingInspection", "unused")
enum class DwmSystemBackdrop(val value: Int) {
DWMSBT_AUTO(0),
DWMSBT_DISABLE(1), // None
DWMSBT_MAINWINDOW(2), // Mica
DWMSBT_TRANSIENTWINDOW(3), // Acrylic
DWMSBT_TABBEDWINDOW(4), // Tabbed
}

View File

@ -0,0 +1,32 @@
package com.mayakapps.compose.windowstyler.windows.jna.enums
@Suppress("SpellCheckingInspection", "unused")
enum class DwmWindowAttribute(val value: Int) {
DWMWA_NCRENDERING_ENABLED(0),
DWMWA_NCRENDERING_POLICY(1),
DWMWA_TRANSITIONS_FORCEDISABLED(2),
DWMWA_ALLOW_NCPAINT(3),
DWMWA_CAPTION_BUTTON_BOUNDS(4),
DWMWA_NONCLIENT_RTL_LAYOUT(5),
DWMWA_FORCE_ICONIC_REPRESENTATION(6),
DWMWA_FLIP3D_POLICY(7),
DWMWA_EXTENDED_FRAME_BOUNDS(8),
DWMWA_HAS_ICONIC_BITMAP(9),
DWMWA_DISALLOW_PEEK(10),
DWMWA_EXCLUDED_FROM_PEEK(11),
DWMWA_CLOAK(12),
DWMWA_CLOAKED(13),
DWMWA_FREEZE_REPRESENTATION(14),
DWMWA_PASSIVE_UPDATE_MODE(15),
DWMWA_USE_HOSTBACKDROPBRUSH(16),
DWMWA_USE_IMMERSIVE_DARK_MODE_BEFORE_20H1(19),
DWMWA_USE_IMMERSIVE_DARK_MODE(20),
DWMWA_WINDOW_CORNER_PREFERENCE(33),
DWMWA_BORDER_COLOR(34),
DWMWA_CAPTION_COLOR(35),
DWMWA_TEXT_COLOR(36),
DWMWA_VISIBLE_FRAME_BORDER_THICKNESS(37),
DWMWA_SYSTEMBACKDROP_TYPE(38),
DWMWA_LAST(39),
DWMWA_MICA_EFFECT(1029),
}

View File

@ -0,0 +1,9 @@
package com.mayakapps.compose.windowstyler.windows.jna.enums
@Suppress("SpellCheckingInspection", "unused")
enum class DwmWindowCornerPreference(val value: Int) {
DWMWCP_DEFAULT(0),
DWMWCP_DONOTROUND(1),
DWMWCP_ROUND(2),
DWMWCP_ROUNDSMALL(3),
}

View File

@ -0,0 +1,33 @@
package com.mayakapps.compose.windowstyler.windows.jna.enums
@Suppress("SpellCheckingInspection", "unused")
internal enum class WindowCompositionAttribute(val value: Int) {
WCA_UNDEFINED(0),
WCA_NCRENDERING_ENABLED(1),
WCA_NCRENDERING_POLICY(2),
WCA_TRANSITIONS_FORCEDISABLED(3),
WCA_ALLOW_NCPAINT(4),
WCA_CAPTION_BUTTON_BOUNDS(5),
WCA_NONCLIENT_RTL_LAYOUT(6),
WCA_FORCE_ICONIC_REPRESENTATION(7),
WCA_EXTENDED_FRAME_BOUNDS(8),
WCA_HAS_ICONIC_BITMAP(9),
WCA_THEME_ATTRIBUTES(10),
WCA_NCRENDERING_EXILED(11),
WCA_NCADORNMENTINFO(12),
WCA_EXCLUDED_FROM_LIVEPREVIEW(13),
WCA_VIDEO_OVERLAY_ACTIVE(14),
WCA_FORCE_ACTIVEWINDOW_APPEARANCE(15),
WCA_DISALLOW_PEEK(16),
WCA_CLOAK(17),
WCA_CLOAKED(18),
WCA_ACCENT_POLICY(19),
WCA_FREEZE_REPRESENTATION(20),
WCA_EVER_UNCLOAKED(21),
WCA_VISUAL_OWNER(22),
WCA_HOLOGRAPHIC(23),
WCA_EXCLUDED_FROM_DDA(24),
WCA_PASSIVEUPDATEMODE(25),
WCA_USEDARKMODECOLORS(26),
WCA_LAST(27),
}

View File

@ -0,0 +1,27 @@
package com.mayakapps.compose.windowstyler.windows.jna.structs
import com.mayakapps.compose.windowstyler.windows.jna.enums.AccentFlag
import com.mayakapps.compose.windowstyler.windows.jna.enums.AccentState
import com.mayakapps.compose.windowstyler.windows.jna.orOf
import com.sun.jna.Structure.FieldOrder
@Suppress("unused")
@FieldOrder(
"accentState",
"accentFlags",
"color",
"animationId",
)
internal class AccentPolicy(
accentState: AccentState = AccentState.ACCENT_DISABLED,
accentFlags: Set<AccentFlag> = emptySet(),
@JvmField var color: Int = 0,
@JvmField var animationId: Int = 0,
) : BaseStructure() {
@JvmField
var accentState: Int = accentState.value
@JvmField
var accentFlags: Int = accentFlags.orOf { it.value }
}

View File

@ -0,0 +1,7 @@
package com.mayakapps.compose.windowstyler.windows.jna.structs
import com.sun.jna.Structure
internal open class BaseStructure : Structure(), Structure.ByReference {
open fun dispose() = clear()
}

View File

@ -0,0 +1,16 @@
package com.mayakapps.compose.windowstyler.windows.jna.structs
import com.sun.jna.Structure
@Structure.FieldOrder(
"leftWidth",
"rightWidth",
"topHeight",
"bottomHeight",
)
internal data class Margins(
@JvmField var leftWidth: Int = 0,
@JvmField var rightWidth: Int = 0,
@JvmField var topHeight: Int = 0,
@JvmField var bottomHeight: Int = 0,
) : BaseStructure()

View File

@ -0,0 +1,28 @@
package com.mayakapps.compose.windowstyler.windows.jna.structs
import com.sun.jna.Pointer
import com.sun.jna.Structure.FieldOrder
import com.sun.jna.platform.win32.WinDef.ULONG
@Suppress("unused")
@FieldOrder(
"osVersionInfoSize",
"majorVersion",
"minorVersion",
"buildNumber",
"platformId",
"csdVersion",
)
internal class OsVersionInfo(
@JvmField var majorVersion: Int = 0,
@JvmField var minorVersion: Int = 0,
@JvmField var buildNumber: Int = 0,
@JvmField var platformId: Int = 0,
) : BaseStructure() {
@JvmField
var osVersionInfoSize: Int = (ULONG.SIZE * 5) + 4
@JvmField
var csdVersion: Pointer? = null
}

View File

@ -0,0 +1,27 @@
package com.mayakapps.compose.windowstyler.windows.jna.structs
import com.mayakapps.compose.windowstyler.windows.jna.enums.WindowCompositionAttribute
import com.sun.jna.Structure.FieldOrder
@Suppress("unused")
@FieldOrder(
"attribute",
"data",
"sizeOfData",
)
internal class WindowCompositionAttributeData(
attribute: WindowCompositionAttribute = WindowCompositionAttribute.WCA_UNDEFINED,
@JvmField var data: AccentPolicy = AccentPolicy(),
) : BaseStructure() {
@JvmField
var attribute: Int = attribute.value
@JvmField
var sizeOfData: Int = data.size()
override fun dispose() {
data.dispose()
super.dispose()
}
}