JetBrains / compose-multiplatform

Compose Multiplatform, a modern UI framework for Kotlin that makes building performant and beautiful user interfaces easy and enjoyable.
https://jetbrains.com/lp/compose-multiplatform
Apache License 2.0
15.91k stars 1.16k forks source link

Maximized windows do not restore to non-maximized sizes correctly on Linux #3620

Open dzirbel opened 1 year ago

dzirbel commented 1 year ago

I've run into a few issues when trying to have my application switch between maximized and non-maximized states.

Affected platforms

Versions

To Reproduce

const val MAXIMIZED_INITIAL = true
fun main() {
    application {
        var maximized by remember { mutableStateOf(MAXIMIZED_INITIAL) }

        val windowState = if (maximized) {
            WindowState(
                placement = WindowPlacement.Maximized,
                size = DpSize.Unspecified,
                position = WindowPosition.Aligned(Alignment.Center),
            )
        } else {
            WindowState(
                placement = WindowPlacement.Floating,
                size = DpSize(300.dp, 800.dp),
                position = WindowPosition.Aligned(Alignment.Center),
            )
        }

        Window(onCloseRequest = ::exitApplication, state = windowState) {
            Box(
                Modifier
                    .fillMaxSize()
                    .layout { measurable, constraints ->
                        println("layout $constraints")
                        val placeable = measurable.measure(constraints)
                        layout(placeable.width, placeable.height) { placeable.place(0, 0) }
                    }
                    .clickable { maximized = !maximized }
            )
        }
    }
}

This creates a simple window which toggles its maximized state on click. Testing on Linux Mint, toggling to an un-maximized (floating) state, the window does not respect the given size; instead it still takes up most (but not all) of the screen. This works as expected (toggling between maximized and the 300x800 size) on Windows 10; I have not tested on macOS or other Linux desktops.

Interestingly, if the window is initialized in a non-maximized state (MAXIMIZED_INITIAL is false), then the issue is not present - the window can be toggled into and out of being maximized without issue.

One partial workaround is to use the same DpSize for the maximized WindowState, i.e.

WindowState(
    placement = WindowPlacement.Maximized,
    size = DpSize(300.dp, 800.dp),
    position = WindowPosition.Aligned(Alignment.Center),
)

for some reason, this does appear to make the window un-maximize down to the right size when the floating state is provided. However, with this size provided (instead of DpSize.Unspecified), the maximized window will compose twice - once with the provided size and once with the correct, full size. For my application, this causes problems with side panels which initialize their size as a percentage of the window size - so in the initial composition with the provided size they will obtain an incorrect window size. This is demonstrated by the printed constraint size in the sample code, and occurs on both Linux Mint and Windows 10.

There is a similar issue with the position - if it is not provided in the maximized WindowState, then un-maximizing will not respect the provided WindowPosition.

Expected behavior The window should resize between maximized and floating simply based on the given WindowState.

dzirbel commented 1 year ago

Actually, I'm getting unreliable results now - even with DpSize.Unspecified as the maximized state size, I still see a composition with the floating state size on Linux (but not Windows 10), throwing off my side panels.

Here is another method to reproduce, which uses WindowState as a proper state rather than recreating it each time:

const val MAXIMIZED_INITIAL = true
val FLOATING_SIZE = DpSize(300.dp, 800.dp)
val MAXIMIZED_SIZE = FLOATING_SIZE // DpSize.Unspecified
fun main() {
    application {
        var maximized by remember { mutableStateOf(MAXIMIZED_INITIAL) }

        val windowState = remember {
            WindowState(
                placement = if (maximized) WindowPlacement.Maximized else WindowPlacement.Floating,
                size = if (maximized) MAXIMIZED_SIZE else FLOATING_SIZE,
                position = WindowPosition.Aligned(Alignment.Center),
            )
        }

        LaunchedEffect(maximized) {
            windowState.placement = if (maximized) WindowPlacement.Maximized else WindowPlacement.Floating
            windowState.size = if (maximized) MAXIMIZED_SIZE else FLOATING_SIZE
        }

        Window(onCloseRequest = ::exitApplication, state = windowState) {
            Box(
                Modifier
                    .fillMaxSize()
                    .layout { measurable, constraints ->
                        println("layout $maximized : $constraints")
                        val placeable = measurable.measure(constraints)
                        layout(placeable.width, placeable.height) { placeable.place(0, 0) }
                    }
                    .clickable { maximized = !maximized }
            )
        }
    }
}

It's possible that the order of setting placement and size is significant, but I haven't found a solution playing around with either.

m-sasha commented 1 year ago

Thanks for the report, we'll look into it.

okushnikov commented 3 weeks ago

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