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

Make resize area larger or configurable for undecorated windows #4574

Closed
mgroth0 opened this issue Apr 4, 2024 · 10 comments · Fixed by JetBrains/compose-multiplatform-core#1505
Assignees
Labels
desktop enhancement New feature or request undecorated window Issue with `Window(undecorated = true)` window management

Comments

@mgroth0
Copy link
Contributor

mgroth0 commented Apr 4, 2024

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

  • note that an undecorated window also shows different icons when resizing compared to a decorated window, at least on my platform
@mgroth0 mgroth0 added enhancement New feature or request submitted labels Apr 4, 2024
@elijah-semyonov elijah-semyonov added window management desktop undecorated window Issue with `Window(undecorated = true)` and removed submitted labels Apr 4, 2024
@MatkovIvan MatkovIvan removed their assignment Apr 4, 2024
@m-sasha
Copy link
Member

m-sasha commented Apr 4, 2024

We'll consider it.

In the meanwhile, take a look at UndecoratedWindowResizer.

You could:

  1. Make the (Compose) Window unresizable
  2. Make the (Swing) Window resizable
  3. Copy UndecoratedWindowResizer into your code and add it to the content of the window at the top level.

@mgroth0
Copy link
Contributor Author

mgroth0 commented Apr 4, 2024

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:

  1. Copied UndecoratedWindowResizer to my MyUndecoratedWindowResizer
  2. 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:
  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
}
  1. I tried to do the same thing that ComposeWindow does in my content:
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:

  • I had to use reflection
  • 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. 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).
  • Having to copy the entire resizer class is not great either

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:

  • I changed borderThickness to be larger.
  • I added a 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:

  • Redesign this in a way that makes it so refleciton is not required. This might be as simple as exposing the default UndecoratedWindowResizer property and class.
  • Redesign this in a way that doesn't use the unstable "layoutId" method for the 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.
  • Redesign this in a way so that I don't have to copy the UndecoratedWindowResizer class. Maybe make that class open and allow users to subclass it?

Thanks again.

@m-sasha
Copy link
Member

m-sasha commented Apr 4, 2024

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 ?

@mgroth0
Copy link
Contributor Author

mgroth0 commented Apr 4, 2024

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.

@m-sasha
Copy link
Member

m-sasha commented Apr 5, 2024

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).

@mgroth0
Copy link
Contributor Author

mgroth0 commented Apr 5, 2024

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).

@m-sasha
Copy link
Member

m-sasha commented Apr 5, 2024

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.

@mgroth0
Copy link
Contributor Author

mgroth0 commented Apr 5, 2024

Just tested your idea, and it seems to work.

Specifically, I remmoved the code that:

  • Calls window.setResizable(true)
  • Uses reflection to disable the default 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.

@m-sasha
Copy link
Member

m-sasha commented Apr 5, 2024

Window.setResizable is useful for decorated windows.

@okushnikov
Copy link
Collaborator

Please check the following ticket on YouTrack for follow-ups to this issue. GitHub issues will be closed in the coming weeks.

@JetBrains JetBrains locked and limited conversation to collaborators Dec 18, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
desktop enhancement New feature or request undecorated window Issue with `Window(undecorated = true)` window management
Projects
None yet
5 participants