diff --git a/settings.gradle.kts b/settings.gradle.kts index e6bc341..0ea6268 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -8,4 +8,8 @@ pluginManagement { } rootProject.name = "ComposeWindowStyler" -include("window-styler", "window-styler-demo") \ No newline at end of file +include( + "window-styler", + "window-styler-demo", + "window-styler-demo-transparent", +) \ No newline at end of file diff --git a/window-styler-demo-transparent/build.gradle.kts b/window-styler-demo-transparent/build.gradle.kts new file mode 100644 index 0000000..658383c --- /dev/null +++ b/window-styler-demo-transparent/build.gradle.kts @@ -0,0 +1,39 @@ +import org.jetbrains.compose.desktop.application.dsl.TargetFormat + +// Suppress annotation is a workaround for a bug. +@Suppress("DSL_SCOPE_VIOLATION") +plugins { + alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.compose) +} + +kotlin { + jvmToolchain(17) + + jvm { + withJava() + } + + sourceSets { + named("jvmMain") { + dependencies { + implementation(compose.desktop.currentOs) + + implementation(project(":window-styler")) + } + } + } +} + +compose.desktop { + application { + mainClass = "MainKt" + + nativeDistributions { + targetFormats(TargetFormat.Msi) + + packageName = "window-styler-demo-transparent" + packageVersion = "1.0.0" + } + } +} diff --git a/window-styler-demo-transparent/src/jvmMain/kotlin/Main.kt b/window-styler-demo-transparent/src/jvmMain/kotlin/Main.kt new file mode 100644 index 0000000..809454e --- /dev/null +++ b/window-styler-demo-transparent/src/jvmMain/kotlin/Main.kt @@ -0,0 +1,35 @@ +import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material.Button +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.application +import com.mayakapps.compose.windowstyler.WindowBackdrop +import com.mayakapps.compose.windowstyler.WindowStyle + +@Composable +@Preview +fun App() { + Button(onClick = {}) { + Text("Button") + } +} + +fun main() = application { + Window( + onCloseRequest = ::exitApplication, + title = "Compose Window Styler Demo", + ) { + WindowStyle( + isDarkTheme = isSystemInDarkTheme(), + backdropType = WindowBackdrop.Mica, + manageTitlebar = true + ) + + MaterialTheme { + App() + } + } +} diff --git a/window-styler/src/jvmMain/kotlin/com/mayakapps/compose/windowstyler/WindowStyle.kt b/window-styler/src/jvmMain/kotlin/com/mayakapps/compose/windowstyler/WindowStyle.kt index a39c5f9..d01f3cb 100644 --- a/window-styler/src/jvmMain/kotlin/com/mayakapps/compose/windowstyler/WindowStyle.kt +++ b/window-styler/src/jvmMain/kotlin/com/mayakapps/compose/windowstyler/WindowStyle.kt @@ -15,8 +15,9 @@ fun WindowScope.WindowStyle( isDarkTheme: Boolean = false, backdropType: WindowBackdrop = WindowBackdrop.Default, frameStyle: WindowFrameStyle = WindowFrameStyle(), + manageTitlebar: Boolean = false, ) { - val manager = remember { WindowStyleManager(window, isDarkTheme, backdropType, frameStyle) } + val manager = remember { WindowStyleManager(window, isDarkTheme, backdropType, frameStyle, manageTitlebar) } LaunchedEffect(isDarkTheme) { manager.isDarkTheme = isDarkTheme diff --git a/window-styler/src/jvmMain/kotlin/com/mayakapps/compose/windowstyler/WindowStyleManager.kt b/window-styler/src/jvmMain/kotlin/com/mayakapps/compose/windowstyler/WindowStyleManager.kt index 91f2361..0a3d904 100644 --- a/window-styler/src/jvmMain/kotlin/com/mayakapps/compose/windowstyler/WindowStyleManager.kt +++ b/window-styler/src/jvmMain/kotlin/com/mayakapps/compose/windowstyler/WindowStyleManager.kt @@ -13,12 +13,13 @@ import java.awt.Window */ fun WindowStyleManager( window: Window, - isDarkTheme: Boolean = false, - backdropType: WindowBackdrop = WindowBackdrop.Default, - frameStyle: WindowFrameStyle = WindowFrameStyle(), + isDarkTheme: Boolean, + backdropType: WindowBackdrop, + frameStyle: WindowFrameStyle, + manageTitlebar: Boolean, ) = when (hostOs) { - OS.Windows -> WindowsWindowStyleManager(window, isDarkTheme, backdropType, frameStyle) - else -> StubWindowStyleManager(isDarkTheme, backdropType, frameStyle) + OS.Windows -> WindowsWindowStyleManager(window, isDarkTheme, backdropType, frameStyle, manageTitlebar) + else -> StubWindowStyleManager(isDarkTheme, backdropType, frameStyle, manageTitlebar) } /** @@ -46,10 +47,17 @@ interface WindowStyleManager { * The style of the window frame which includes the title bar and window border. See [WindowFrameStyle]. */ var frameStyle: WindowFrameStyle + + /** + * Whether to manage the title bar of the window. If set to `true`, the title bar will be hidden and the window + * will need to manage its own title. + */ + var manageTitlebar: Boolean } internal class StubWindowStyleManager( override var isDarkTheme: Boolean, override var backdropType: WindowBackdrop, override var frameStyle: WindowFrameStyle, + override var manageTitlebar: Boolean, ) : WindowStyleManager \ No newline at end of file diff --git a/window-styler/src/jvmMain/kotlin/com/mayakapps/compose/windowstyler/windows/CustomDecorationWindowProc.kt b/window-styler/src/jvmMain/kotlin/com/mayakapps/compose/windowstyler/windows/CustomDecorationWindowProc.kt new file mode 100644 index 0000000..543e618 --- /dev/null +++ b/window-styler/src/jvmMain/kotlin/com/mayakapps/compose/windowstyler/windows/CustomDecorationWindowProc.kt @@ -0,0 +1,47 @@ +package com.mayakapps.compose.windowstyler.windows + +import com.mayakapps.compose.windowstyler.windows.jna.Dwm +import com.mayakapps.compose.windowstyler.windows.jna.User32 +import com.sun.jna.platform.win32.WinDef +import com.sun.jna.platform.win32.WinUser + +private const val WM_NCCALCSIZE = 0x0083 +private const val WM_NCHITTEST = 0x0084 + +class CustomDecorationWindowProc private constructor(private val hwnd: WinDef.HWND) : WinUser.WindowProc { + private val defWndProc = User32.setWindowProc(hwnd, this) + + override fun callback(hWnd: WinDef.HWND, uMsg: Int, wParam: WinDef.WPARAM, lParam: WinDef.LPARAM): WinDef.LRESULT { + if (Dwm.callDefaultWindowHitProc(hwnd, uMsg, wParam, lParam)) { + return WinDef.LRESULT(0) + } + + return when (uMsg) { + WM_NCCALCSIZE -> { + WinDef.LRESULT(0) + } + + WM_NCHITTEST -> { + User32.callWindowProc(defWndProc, hWnd, uMsg, wParam, lParam) + } + + WinUser.WM_DESTROY -> { + User32.setWindowProc(hWnd, defWndProc) + WinDef.LRESULT(-1) + } + + else -> { + User32.callWindowProc(defWndProc, hWnd, uMsg, wParam, lParam) + } + } + } + + companion object { + /** + * Installs a [CustomDecorationWindowProc] for the given window. + * + * @param hwnd The window handle. + */ + fun install(hwnd: WinDef.HWND) = CustomDecorationWindowProc(hwnd) + } +} \ No newline at end of file diff --git a/window-styler/src/jvmMain/kotlin/com/mayakapps/compose/windowstyler/windows/WindowsBackdropApis.kt b/window-styler/src/jvmMain/kotlin/com/mayakapps/compose/windowstyler/windows/WindowsBackdropApis.kt index f1d0a2d..46a5f67 100644 --- a/window-styler/src/jvmMain/kotlin/com/mayakapps/compose/windowstyler/windows/WindowsBackdropApis.kt +++ b/window-styler/src/jvmMain/kotlin/com/mayakapps/compose/windowstyler/windows/WindowsBackdropApis.kt @@ -8,12 +8,19 @@ 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) { +internal class WindowsBackdropApis private constructor(private val hwnd: WinDef.HWND) { private var isSystemBackdropSet = false private var isMicaEnabled = false private var isAccentPolicySet = false private var isSheetOfGlassApplied = false + companion object { + /** + * Instantiate [WindowsBackdropApis] for the given window and install it. + */ + fun install(hwnd: WinDef.HWND) = WindowsBackdropApis(hwnd) + } + fun setSystemBackdrop(systemBackdrop: DwmSystemBackdrop) { createSheetOfGlassEffect() if (Dwm.setSystemBackdrop(hwnd, systemBackdrop)) { diff --git a/window-styler/src/jvmMain/kotlin/com/mayakapps/compose/windowstyler/windows/WindowsWindowStyleManager.kt b/window-styler/src/jvmMain/kotlin/com/mayakapps/compose/windowstyler/windows/WindowsWindowStyleManager.kt index 76fa289..52e43c1 100644 --- a/window-styler/src/jvmMain/kotlin/com/mayakapps/compose/windowstyler/windows/WindowsWindowStyleManager.kt +++ b/window-styler/src/jvmMain/kotlin/com/mayakapps/compose/windowstyler/windows/WindowsWindowStyleManager.kt @@ -3,7 +3,15 @@ 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.ColorableWindowBackdrop +import com.mayakapps.compose.windowstyler.WindowBackdrop +import com.mayakapps.compose.windowstyler.WindowCornerPreference +import com.mayakapps.compose.windowstyler.WindowFrameStyle +import com.mayakapps.compose.windowstyler.WindowStyleManager +import com.mayakapps.compose.windowstyler.hackContentPane +import com.mayakapps.compose.windowstyler.isTransparent +import com.mayakapps.compose.windowstyler.isUndecorated +import com.mayakapps.compose.windowstyler.setComposeLayerTransparency 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 @@ -20,16 +28,18 @@ import javax.swing.SwingUtilities */ class WindowsWindowStyleManager( window: Window, - isDarkTheme: Boolean = false, - backdropType: WindowBackdrop = WindowBackdrop.Default, - frameStyle: WindowFrameStyle = WindowFrameStyle(), + isDarkTheme: Boolean, + backdropType: WindowBackdrop, + frameStyle: WindowFrameStyle, + manageTitlebar: Boolean, ) : WindowStyleManager { private val hwnd: HWND = window.hwnd private val isUndecorated = window.isUndecorated private var wasAero = false - private val backdropApis = WindowsBackdropApis(hwnd) + private val backdropApis = WindowsBackdropApis.install(hwnd) + private val customDecorationWindowProc = if (manageTitlebar) CustomDecorationWindowProc.install(hwnd) else null override var isDarkTheme: Boolean = isDarkTheme set(value) { @@ -59,6 +69,17 @@ class WindowsWindowStyleManager( } } + override var manageTitlebar: Boolean = manageTitlebar + get() = field + set(value) { + field = if (value) { + true + } else { + // TODO: reset the window proc to the default one + false + } + } + init { // invokeLater is called to make sure that ComposeLayer was initialized first SwingUtilities.invokeLater { @@ -193,6 +214,7 @@ class WindowsWindowStyleManager( if (this is ThemedAcrylic) themedTransparent else WindowBackdrop.Transparent(color) } + else -> WindowBackdrop.Default }.fallbackIfUnsupported() } diff --git a/window-styler/src/jvmMain/kotlin/com/mayakapps/compose/windowstyler/windows/jna/Dwm.kt b/window-styler/src/jvmMain/kotlin/com/mayakapps/compose/windowstyler/windows/jna/Dwm.kt index 4cf6c92..2daa42d 100644 --- a/window-styler/src/jvmMain/kotlin/com/mayakapps/compose/windowstyler/windows/jna/Dwm.kt +++ b/window-styler/src/jvmMain/kotlin/com/mayakapps/compose/windowstyler/windows/jna/Dwm.kt @@ -9,14 +9,17 @@ 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.WinDef.LPARAM +import com.sun.jna.platform.win32.WinDef.LRESULT +import com.sun.jna.platform.win32.WinDef.WPARAM 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 internal object Dwm { - fun extendFrameIntoClientArea(hwnd: HWND, margin: Int = 0) = - extendFrameIntoClientArea(hwnd, margin, margin, margin, margin) + fun extendFrameIntoClientArea(hwnd: HWND, allMargins: Int = 0) = + extendFrameIntoClientArea(hwnd, allMargins, allMargins, allMargins, allMargins) fun extendFrameIntoClientArea( hwnd: HWND, @@ -47,6 +50,11 @@ internal object Dwm { fun setWindowAttribute(hwnd: HWND, attribute: DwmWindowAttribute, value: Int) = setWindowAttribute(hwnd, attribute, IntByReference(value), INT_SIZE) + fun callDefaultWindowHitProc(hwnd: HWND, msg: Int, wParam: WPARAM, lParam: LPARAM): Boolean { + val dwmDefWindowProc = DwmImpl.DwmDefWindowProc(hwnd, msg, wParam, lParam) + return dwmDefWindowProc != LRESULT(0) + } + private fun setWindowAttribute( hwnd: HWND, attribute: DwmWindowAttribute, @@ -67,4 +75,5 @@ private object DwmImpl : DwmApi by Native.load("dwmapi", DwmApi::class.java, W32 private interface DwmApi : StdCallLibrary { fun DwmExtendFrameIntoClientArea(hwnd: HWND, margins: Margins): HRESULT fun DwmSetWindowAttribute(hwnd: HWND, attribute: Int, value: PointerType?, valueSize: Int): HRESULT + fun DwmDefWindowProc(hwnd: HWND, msg: Int, wParam: WPARAM, lParam: LPARAM): LRESULT } \ No newline at end of file diff --git a/window-styler/src/jvmMain/kotlin/com/mayakapps/compose/windowstyler/windows/jna/User32.kt b/window-styler/src/jvmMain/kotlin/com/mayakapps/compose/windowstyler/windows/jna/User32.kt index fa26236..e81a48d 100644 --- a/window-styler/src/jvmMain/kotlin/com/mayakapps/compose/windowstyler/windows/jna/User32.kt +++ b/window-styler/src/jvmMain/kotlin/com/mayakapps/compose/windowstyler/windows/jna/User32.kt @@ -6,13 +6,23 @@ import com.mayakapps.compose.windowstyler.windows.jna.enums.WindowCompositionAtt 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.Structure +import com.sun.jna.platform.win32.BaseTSD.LONG_PTR +import com.sun.jna.platform.win32.WinDef.HWND +import com.sun.jna.platform.win32.WinDef.LPARAM +import com.sun.jna.platform.win32.WinDef.LRESULT +import com.sun.jna.platform.win32.WinDef.RECT +import com.sun.jna.platform.win32.WinDef.WPARAM +import com.sun.jna.platform.win32.WinUser +import com.sun.jna.platform.win32.WinUser.WindowProc import com.sun.jna.win32.StdCallLibrary import com.sun.jna.win32.W32APIOptions + internal object User32 { + fun setAccentPolicy( - hwnd: WinDef.HWND, + hwnd: HWND, accentState: AccentState = AccentState.ACCENT_DISABLED, accentFlags: Set = emptySet(), color: Int = 0, @@ -30,8 +40,8 @@ internal object User32 { } private fun setWindowCompositionAttribute( - hwnd: WinDef.HWND, - attributeData: WindowCompositionAttributeData + hwnd: HWND, + attributeData: WindowCompositionAttributeData, ): Boolean { Native.setLastError(0) @@ -40,11 +50,56 @@ internal object User32 { if (!isSuccess) println("SetWindowCompositionAttribute(${attributeData.attribute}) failed with last error ${Native.getLastError()}") return isSuccess } + + fun setWindowLongAttr(hwnd: HWND, index: Int, long: LONG_PTR) = + User32Impl.SetWindowLongPtr(hwnd, index, long) + + fun getWindowLongAttr(hwnd: HWND, index: Int) = User32Impl.GetWindowLongPtr(hwnd, index) + + fun setWindowProc(hwnd: HWND, withWndProc: LONG_PTR) = setWindowLongAttr(hwnd, WinUser.GWL_WNDPROC, withWndProc) + fun setWindowProc(hwnd: HWND, withWndProc: WindowProc) = + User32Impl.SetWindowLongPtr(hwnd, WinUser.GWL_WNDPROC, withWndProc) + + fun callWindowProc( + defWndProc: LONG_PTR, + hwnd: HWND, + uMsg: Int, + wparam: WPARAM, + lparam: LPARAM, + ): LRESULT = + User32Impl.CallWindowProc(defWndProc, hwnd, uMsg, wparam, lparam) + + fun setWindowPos( + hwnd: HWND, + hWndInsertAfter: HWND? = null, + x: Int = 0, + y: Int = 0, + cx: Int = 0, + cy: Int = 0, + uFlags: Int = 0, + ): Boolean = User32Impl.SetWindowPos(hwnd, hWndInsertAfter, x, y, cx, cy, uFlags) + + fun getWindowRect(hwnd: HWND) = RECT().also { User32Impl.GetWindowRect(hwnd, it) } } +// See https://stackoverflow.com/q/62240901 +@Suppress("unused") +@Structure.FieldOrder( + "cxLeftWidth", + "cxRightWidth", + "cyTopHeight", + "cyBottomHeight" +) 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 + fun SetWindowCompositionAttribute(hwnd: HWND, attributeData: WindowCompositionAttributeData): Boolean + fun SetWindowPos(hWnd: HWND, hWndInsertAfter: HWND?, x: Int, y: Int, cx: Int, cy: Int, uFlags: Int): Boolean + fun GetWindowRect(hWnd: HWND, rect: RECT) + fun SetWindowLongPtr(hWnd: HWND, nIndex: Int, wndProc: WindowProc): LONG_PTR + fun SetWindowLongPtr(hWnd: HWND, nIndex: Int, wndProc: LONG_PTR): LONG_PTR + fun GetWindowLongPtr(hWnd: HWND, nIndex: Int): LONG_PTR + fun CallWindowProc(proc: LONG_PTR, hWnd: HWND, uMsg: Int, uParam: WPARAM, lParam: LPARAM): LRESULT } \ No newline at end of file