Closed mgroth0 closed 4 weeks ago
We'll consider it.
In the meanwhile, take a look at UndecoratedWindowResizer.
You could:
UndecoratedWindowResizer
into your code and add it to the content of the window at the top level.Thank you for suggesting this workaround.
I was able to get the exact behavior I want. However, I don't love the way I had to do it because I had to resort to reflection and I had make a copy of UndecoratedWindowResizer
.
Here is what I did:
UndecoratedWindowResizer
to my MyUndecoratedWindowResizer
window.setResizable
in such a way to only set the swing part resizable, but not the compose part. This was difficult because ComposeWindow
overrides setResizable
: override fun setResizable(value: Boolean) {
super.setResizable(value)
undecoratedWindowResizer.enabled = isUndecorated && isResizable
}
I wanted the super.setResizable(value)
part to run, but not to enable the default undecoratedWindowResizer
. The best workaround I could come up with was reflection:
LaunchedEffect(resizable) {
window.setResizable(resizable)
ComposeWindow::class
.declaredMembers
.single { it.name == "undecoratedWindowResizer" }
.run {
isAccessible = true
val undecoratedWindowResizer = call(window)!!
val enabled =
undecoratedWindowResizer::class.declaredMembers.single {
it.name == "enabled"
} as KMutableProperty
enabled.isAccessible = true
enabled.setter.call(undecoratedWindowResizer, false)
}
myUndecoratedWindowResizer.enabled = resizable
}
content()
myUndecoratedWindowResizer.Content(
modifier = Modifier.layoutId("UndecoratedWindowResizer")
)
The end result is that I was able to customize my resizer just the way I wanted, but the process of doing so required reflection and the code doesn't look very stable or pretty. The main things I dislike about the code are:
Modifier.layoutId
with the exact right string. Searching the compose code base I realized this string had some affect somewhere, so I had to keep it the same. Also, I'm not sure if this is working as intended because where it is used (WindowContentLayout
) gets this thing in a kind of unsafe way (measurables.lastOrNull
). I hope knowing my use case is helpful if your team ever decides to redesign some of this API. In the end, I needed to make two adjustments to my custom resizer:
borderThickness
to be larger.borderThicknessTop
and made sure that this was used in all the appropriate places, because I wanted the resize area at the top to be smaller so it didn't cover the window buttons (like close and minimize):/**
* See [[androidx.compose.ui.window.UndecoratedWindowResizer]]
* See https://github.com/JetBrains/compose-multiplatform/issues/4574#issuecomment-2037464649
* Purposes:
* I want to control the width of the resize areas
* I want the resize areas to be different for the title bar
*/
internal class MyUndecoratedWindowResizer(
private val window: Window,
private val borderThickness: Dp,
private val borderThicknessTop: Dp
) {
// Kept the implementation the same except for only replacing `borderThickness` with `borderThicknessTop` where appropriate
}
If I could suggest some possible goals here, they might be:
UndecoratedWindowResizer
property and class.WindowContentLayout
. It seems like if I ever placed another composable component below the resizer in my window content or changed the layoutId string of the resizer, this function would break which seems quite unstable.open
and allow users to subclass it?Thanks again.
I wanted to call window.setResizable in such a way to only set the swing part resizable, but not the compose part. This was difficult because ComposeWindow overrides setResizable:
Yes, that's unfortunate. We could avoid overriding setResizable
in ComposeWindow
. @MatkovIvan ?
I had to call Modifier.layoutId with the exact right string. Searching the compose code base I realized this string had some affect somewhere, so I had to keep it the same.
You didn't have to do that. Modifier.layoutId
is a way to tag children elements so that the MeasurePolicy
of the layout can identify them. In this case, the Layout in WindowContentLayout
identifies it and has separate code to size and position it. In your case, you aren't putting your resizer in WindowContentLayout
, so it meaningless.
I'm not sure we want to expose the resizer itself, but we'll see whether we can provide a smaller API surface to just define the border thickness. Or maybe we just need to select a better default value. Maybe check the sizes of the resizable areas of native windows and use that. @igordmn ?
Are you sure that I am not putting my content into a WindowContentLayout
?
I see that WindowContentLayout
is used inside of ComposeWindowPanel.setContent
, and ComposeWindowPanel
is used inside of ComposeWindow.setContent
. I am not subclassing ComposeWindow
, so it looks to me like my content and also my custom resizer are still inside of a ComposeWindowPanel
, and therefore also inside of a WindowContentLayout
.
If you put your resizer immediately in the window, as the last element, then yes, it looks like WindowContentLayout
will find it. But this is not guaranteed (in the sense that we could change this and not consider it a breaking change), and is more a bug than a feature.
I'd recommend not relying on this, and instead laying out your resizer by yourself (just put all your content in a Box
and set Modifier.matchParentSize
on the resizer).
That sounds like a sufficient workaround for now, thanks. If possible I'd like to keep this issue open, with the hope that maybe some redesigns in the future will make it so we don't need such heavy workarounds to set the resizer widths (and to set them to different widths for different sides).
Hmm, now that I think about it - do you actually need to make the AWT window resizable? The resizability there is "by the user". You can just call ComposeWindow.resizable = false
and that's it. The resizer should still be able to resize the window.
Just tested your idea, and it seems to work.
Specifically, I remmoved the code that:
window.setResizable(true)
undecoratedWindowResizer
(since I no longer called setResizable, this was never set to true in the first place I think)And confirmed it seems to behave exactly as before.
One of the concerns I have is that by not making the AWT window resizable, I don't know if this would have unintended side effects, some of which might be platform-specific. I am testing on a Mac. Looking at the implementation o Frame.setResizable
, java seems to do multiple things and some of them platform specific. For example, see the implementation of CPlatformWindow.setResizable
:
@Override
public void setResizable(final boolean resizable) {
setCanFullscreen(resizable);
setStyleBits(RESIZABLE, resizable);
setStyleBits(ZOOMABLE, resizable);
}
If the recommendation is that users who make a custom override for the undecorated resizer don't call the AWT setResizable
, I think the question then is why Compose would ever call the AWT setResizable
in the first place. Is it worth considering that Compose just ignore that method entirely? I would just hope that at the end of the day we have consistency - either AWT setResizable
is never called anywhere, or we are able to call it everywhere without having to use any reflection hacks.
Window.setResizable is useful for decorated windows.
Please check the following ticket on YouTrack for follow-ups to this issue. GitHub issues will be closed in the coming weeks.
When making a window undecorated, the area that you can click in the corners on macOS is sigificantly smaller than when the window is decorated. This leads to frustration as you are tying to get the mouse into the tiny little area that allows you to click and drag to resize an undecorated window.
I would like to make this area larger. I am not sure if this is more on the compose side or the swing side.
Platform: Desktop OS: macos 14.4.1 arm64 Compose Version: 1.6.1 Kotlin version: 2.0.0-Beta5