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
Missing animations when minimizing/maximizing/restoring undecorated window #3388
Comments
Relates to #3166 |
demo-windows-10.mp4Fixed the problem in Windows (don't know if the problem applies to Linux or macOS). The animation solution was adopted from https://github.com/kalibetre/CustomDecoratedJFrame which was for Swing (Jframe) windows. With this, native animations for window appearance (open), window disappearance (close), window minimize, window restore, and (possibly) window maximize work correctly. The repo above also implemented Aero snap feature (and possibly, shaking window title to hide other windows) but I omitted it. It also provides native window shadows and border (and rounded corners in Windows 11). The only downside is that the window cannot be made transparent (otherwise, it loses animations and shadows). To be honest, I don't know how the Update: 2024-05-07implementation("net.java.dev.jna:jna-jpms:5.14.0")
implementation("net.java.dev.jna:jna-platform-jpms:5.14.0")
// ... import com.sun.jna.Native
import com.sun.jna.Pointer
import com.sun.jna.platform.win32.BaseTSD.LONG_PTR
import com.sun.jna.platform.win32.User32
import com.sun.jna.platform.win32.WinDef.*
import com.sun.jna.platform.win32.WinUser.WM_DESTROY
import com.sun.jna.platform.win32.WinUser.WindowProc
import com.sun.jna.win32.W32APIOptions
// ...
fun main() = application {
Window(
undecorated = true, // This should be true
transparent = false,
// ...
) {
val windowHandle = remember(this.window) {
val windowPointer = (this.window as? ComposeWindow)
?.windowHandle
?.let(::Pointer)
?: Native.getWindowPointer(this.window)
HWND(windowPointer)
}
remember(windowHandle) { CustomWindowProcedure(windowHandle) }
MyAppContent()
}
} @Suppress("FunctionName")
private interface User32Ex : User32 {
fun SetWindowLong(hWnd: HWND, nIndex: Int, wndProc: WindowProc): LONG_PTR
fun SetWindowLongPtr(hWnd: HWND, nIndex: Int, wndProc: WindowProc): LONG_PTR
fun CallWindowProc(proc: LONG_PTR, hWnd: HWND, uMsg: Int, uParam: WPARAM, lParam: LPARAM): LRESULT
}
// See https://stackoverflow.com/q/62240901
@Structure.FieldOrder(
"leftBorderWidth",
"rightBorderWidth",
"topBorderHeight",
"bottomBorderHeight"
)
data class WindowMargins(
@JvmField var leftBorderWidth: Int,
@JvmField var rightBorderWidth: Int,
@JvmField var topBorderHeight: Int,
@JvmField var bottomBorderHeight: Int
) : Structure(), Structure.ByReference
private class CustomWindowProcedure(private val windowHandle: HWND) : WindowProc {
// See https://learn.microsoft.com/en-us/windows/win32/winmsg/about-messages-and-message-queues#system-defined-messages
private val WM_NCCALCSIZE = 0x0083
private val WM_NCHITTEST = 0x0084
private val GWLP_WNDPROC = -4
private val margins = WindowMargins(
leftBorderWidth = 0,
topBorderHeight = 0,
rightBorderWidth = -1,
bottomBorderHeight = -1
)
private val USER32EX =
runCatching { Native.load("user32", User32Ex::class.java, W32APIOptions.DEFAULT_OPTIONS) }
.onFailure { println("Could not load user32 library") }
.getOrNull()
// The default window procedure to call its methods when the default method behaviour is desired/sufficient
private var defaultWndProc = if (is64Bit()) {
USER32EX?.SetWindowLongPtr(windowHandle, GWLP_WNDPROC, this) ?: LONG_PTR(-1)
} else {
USER32EX?.SetWindowLong(windowHandle, GWLP_WNDPROC, this) ?: LONG_PTR(-1)
}
init {
enableResizability()
enableBorderAndShadow()
// enableTransparency(alpha = 255.toByte())
}
override fun callback(hWnd: HWND, uMsg: Int, wParam: WPARAM, lParam: LPARAM): LRESULT {
return when (uMsg) {
// Returns 0 to make the window not draw the non-client area (title bar and border)
// thus effectively making all the window our client area
WM_NCCALCSIZE -> { LRESULT(0) }
// The CallWindowProc function is used to pass messages that
// are not handled by our custom callback function to the default windows procedure
WM_NCHITTEST -> { USER32EX?.CallWindowProc(defaultWndProc, hWnd, uMsg, wParam, lParam) ?: LRESULT(0) }
WM_DESTROY -> { USER32EX?.CallWindowProc(defaultWndProc, hWnd, uMsg, wParam, lParam) ?: LRESULT(0) }
else -> { USER32EX?.CallWindowProc(defaultWndProc, hWnd, uMsg, wParam, lParam) ?: LRESULT(0) }
}
}
/**
* For this to take effect, also set `resizable` argument of Compose Window to `true`.
*/
private fun enableResizability() {
val style = USER32EX?.GetWindowLong(windowHandle, GWL_STYLE) ?: return
val newStyle = style or WS_CAPTION
USER32EX.SetWindowLong(windowHandle, GWL_STYLE, newStyle)
}
/**
* To disable window border and shadow, pass (0, 0, 0, 0) as window margins
* (or, simply, don't call this function).
*/
private fun enableBorderAndShadow() {
val dwmApi = "dwmapi"
.runCatching(NativeLibrary::getInstance)
.onFailure { println("Could not load dwmapi library") }
.getOrNull()
dwmApi
?.runCatching { getFunction("DwmExtendFrameIntoClientArea") }
?.onFailure { println("Could not enable window native decorations (border/shadow/rounded corners)") }
?.getOrNull()
?.invoke(arrayOf(windowHandle, margins))
}
/**
* See https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setlayeredwindowattributes
* @param alpha 0 for transparent, 255 for opaque
*/
private fun enableTransparency(alpha: Byte) {
val defaultStyle = User32.INSTANCE.GetWindowLong(windowHandle, GWL_EXSTYLE)
val newStyle = defaultStyle or User32.WS_EX_LAYERED
USER32EX?.SetWindowLong(windowHandle, User32.GWL_EXSTYLE, newStyle)
USER32EX?.SetLayeredWindowAttributes(windowHandle, 0, alpha, LWA_ALPHA)
}
private fun is64Bit(): Boolean {
val bitMode = System.getProperty("com.ibm.vm.bitmode")
val model = System.getProperty("sun.arch.data.model", bitMode)
return model == "64"
}
} To minimize the window, instead of using @Composable
fun WindowScope.MyCustomMinimizeButton() {
val windowHandle = remember(this.window) {
val windowPointer = (this.window as? ComposeWindow)
?.windowHandle
?.let(::Pointer)
?: Native.getWindowPointer(this.window)
HWND(windowPointer)
}
Button(onclick = { User32.INSTANCE.CloseWindow(windowHandle) }) {
Text("Minimize")
}
} I also added the following rule to my Proguard rules.pro file for the app release version to work correctly:
|
@pjBooms Fixed the problem with native animations and shadows as described in the above comment. Can they be incorporated into Compose Multiplatform code? |
@mahozad currently we are busy with other tasks |
OK, no problem. |
@mahozad |
@sheng-ri |
I found a solution for resizable window base your code. You need make window style has WS_CAPTION. |
how to set this style? |
See win32 api final var style = (long)GetWindowLongA.invoke(hWnd, GWL_STYLE);
SetWindowLongA.invoke(hWnd, GWL_STYLE, style | WS_CAPTION); |
I tried to do this, but it didn't work. WS_SIZEBOX too |
@sleinexxx |
it works! thanks a lot |
Describe the bug
I want to have custom title bar for my Desktop application (especially important to implement dark theme).
So, I set
undecorated = true
.With undecorated in Windows OS, when maximizing or minimizing a window or restoring a window already minimized to taskbar, the window does not animate. Decorated windows, in contrast, have proper animations:
decorated-window.mp4
undecorated.mp4
I tried the workaround in #3166 but it does not seem to work for undecorated windows (window minimizes but with no animation):
Electron seems to have had this problem too in the past:
This is the code they use to set proper windows decoration and preserve animations:
https://github.com/electron/electron/blob/3a5e2dd90c099b10679364642a0f7fa398dce875/shell/browser/native_window_views.cc#L367-L390
So, then tried setting just the flags that Electron sets like this for the window:
OR modifying the existing window flags:
The default close, maximize, minimize buttons are removed but cannot get rid of the title bar.
The IntelliJ IDEA 2023.1 is undecorated and has proper animations.
Maybe you could consider getting a little help from them?
Fixing this problem could probably resolve or help resolve all the following issues:
Affected platforms
Versions
To Reproduce
Compare app minimize animation in these two:
decorated (default title bar):
undecorated (custom title bar):
Expected behavior
Undecorated windows should have animations when minimizing/maximizing/restoring them.
The text was updated successfully, but these errors were encountered: