Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Feature/extend content to titlebar #36

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,8 @@ pluginManagement {
}

rootProject.name = "ComposeWindowStyler"
include("window-styler", "window-styler-demo")
include(
"window-styler",
"window-styler-demo",
"window-styler-demo-transparent",
)
39 changes: 39 additions & 0 deletions window-styler-demo-transparent/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
35 changes: 35 additions & 0 deletions window-styler-demo-transparent/src/jvmMain/kotlin/Main.kt
Original file line number Diff line number Diff line change
@@ -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()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

/**
Expand Down Expand Up @@ -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
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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) {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -193,6 +214,7 @@ class WindowsWindowStyleManager(
if (this is ThemedAcrylic) themedTransparent
else WindowBackdrop.Transparent(color)
}

else -> WindowBackdrop.Default
}.fallbackIfUnsupported()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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
}
Loading