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
16.01k stars 1.17k forks source link

ComposePanel flashes default light grey for the first frame #2237

Closed kirill-grouchnikov closed 1 year ago

kirill-grouchnikov commented 2 years ago
fun main() = application {
    Window(
        onCloseRequest = ::exitApplication,
        state = rememberWindowState(
            width = 400.dp,
            height = 200.dp,
            position = WindowPosition.Aligned(Alignment.Center)
        ),
        title = "ComposePanel Demo",
        resizable = false
    ) {
        Box(
            modifier = Modifier.fillMaxSize(1.0f).background(Color.Yellow)
                .clickable {
                    val swingFrame = JFrame()
                    swingFrame.isUndecorated = true;
                    swingFrame.size = Dimension(500, 200)
                    swingFrame.setLocation(0, 0)

                    swingFrame.contentPane.layout = BorderLayout()
                    swingFrame.contentPane.background = java.awt.Color.BLACK

                    val composePanel = ComposePanel()
                    // The next line has no effect. The internal compose layer and skia layer ignore this
                    composePanel.background = java.awt.Color.RED
                    // So we need to do this instead
                    UIManager.put("Panel.background", java.awt.Color.RED)
                    swingFrame.contentPane.add(composePanel)

                    swingFrame.isVisible = true
                })
    }
}

Record the screen and then replay it frame by frame. When the JFrame is shown with the internal ComposePanel, it flashes the default light grey for just a frame or two before switching to the intended red.

Also, there should be a way to set the initial color of a ComposePanel without having to resort to the UIManager.put("Panel.background") call

kirill-grouchnikov commented 2 years ago

Maybe related to https://github.com/JetBrains/compose-jb/issues/1794 that was fixed some time ago

igordmn commented 2 years ago

For Composable Window we use this code to draw the first frame in the background:

swingFrame.preferredSize = swingFrame.size
swingFrame.pack()
swingFrame.contentPane.paint(swingFrame.graphics)

Full code in your case:

import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.awt.ComposePanel
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.WindowPosition
import androidx.compose.ui.window.application
import androidx.compose.ui.window.rememberWindowState
import java.awt.BorderLayout
import java.awt.Dimension
import javax.swing.JFrame

fun main() = application {
    Window(
        onCloseRequest = ::exitApplication,
        state = rememberWindowState(
            width = 400.dp,
            height = 200.dp,
            position = WindowPosition.Aligned(Alignment.Center)
        ),
        title = "ComposePanel Demo",
        resizable = false
    ) {
        Box(
            modifier = Modifier.fillMaxSize(1.0f).background(Color.Yellow)
                .clickable {
                    val swingFrame = JFrame()
                    swingFrame.isUndecorated = true;
                    swingFrame.size = Dimension(500, 200)
                    swingFrame.setLocation(0, 0)

                    swingFrame.contentPane.layout = BorderLayout()
                    swingFrame.contentPane.background = java.awt.Color.BLACK

                    val composePanel = ComposePanel()
                    composePanel.setContent {
                        Box(Modifier.fillMaxSize().background(Color.Blue))
                    }
                    swingFrame.contentPane.add(composePanel)

                    swingFrame.preferredSize = swingFrame.size
                    swingFrame.pack()
                    swingFrame.contentPane.paint(swingFrame.graphics)

                    swingFrame.isVisible = true
                })
    }
}

Will it work for you?

kirill-grouchnikov commented 2 years ago

Thanks, will check this snippet tomorrow

kirill-grouchnikov commented 1 year ago

Doesn't look like this is going to work for the scenario where I'm using ComposePanel in Aurora.

Due to how Swing's JPopupMenu.show works, I don't see how I can access the underlying popup window to set its size, pack it and then explicitly paint its content before making it visible.

kirill-grouchnikov commented 1 year ago

The reasons I can't use JWindow directly, and have to go through JPopupMenu for displaying popup content are over at https://github.com/kirill-grouchnikov/aurora/issues/22 - due to internal APIs to grab / ungrab windows during user interaction that are not exposed as Swing / AWT APIs.

igordmn commented 1 year ago

I didn't figure out how to fix it for JPopupMenu to the 1.2.0. Will try to figure to the 1.2.1 release

P.S. This issue also affects the case when we embed ComposePanel into IDEA.

igordmn commented 1 year ago

I noticed, that even without ComposePanel it sometimes flashes with the gray color. And we can get rid of these flashes changing swingFrame.background instead of swingFrame.contentPane.background. But it doesn't work for JPopupMenu:

import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.WindowPosition
import androidx.compose.ui.window.application
import androidx.compose.ui.window.rememberWindowState
import java.awt.Dimension
import java.awt.Graphics
import javax.swing.JButton
import javax.swing.JPopupMenu

fun main() = application {
    Window(
        onCloseRequest = ::exitApplication,
        state = rememberWindowState(
            width = 400.dp,
            height = 200.dp,
            position = WindowPosition.Aligned(Alignment.Center)
        ),
        title = "ComposePanel Demo",
        resizable = false
    ) {
        Box(
            modifier = Modifier.fillMaxSize(1.0f).background(Color.Yellow)
                .clickable {
                    val swingFrame = JPopupMenu()
                    swingFrame.size = Dimension(500, 200)
                    swingFrame.background = java.awt.Color.RED
                    swingFrame.setLocation(0, 0)

//                    val composePanel = ComposePanel()
//                    composePanel.size = Dimension(200, 200)
//                    composePanel.preferredSize = Dimension(200, 200)
//                    composePanel.setContent {
//                        Box(Modifier.fillMaxSize().background(Color.Green))
//                        Canvas(Modifier.fillMaxSize()) {
//                            Thread.sleep(500)
//                        }
//                    }
//                    composePanel.background = java.awt.Color.RED

                    swingFrame.add(object : JButton() {
                        override fun paint(g: Graphics?) {
                            Thread.sleep(1000)
                            super.paint(g)
                        }
                    })

                    swingFrame.isVisible = true
                })
    }
}

There is another issue besides that - composePanel.background isn't applied to ComposePanel. But we should never see this color if we set content for ComposePanel, because the content of ComposePanel is painted synchronously (I verified this adding sleep into setContent and in the draw method). The only color that can be useful is a transparent color (there is an issue about ComposePanel transparency).

I am not sure, but is the main feature of JPopupMenu is an internal window pool? In this case workaround could be using JFrame with your own pool.

Another workaround can be finding similar to "Panel.background" property, that will affect JPopupMenu background.

In any case, it seems it is related to Swing, not to Compose, if I investigated correctly.

kirill-grouchnikov commented 1 year ago

Can't use JFrame or JWindow as those don't have built-in access to the internal APIs to track window grab and ungrab events that are needed to dismiss open popup menus when the user interacts with the windows. The two scenarios in Aurora and Radiance are:

  1. Click on the popup area of any button to bring the popup content, and then click in another window or somewhere on the desktop. The main demo window loses focus, but the popup is still hanging around.
  2. Change any demo with popup content to run in OS-decorated mode. Click on the popup area of any button to bring the popup content, and then "grab" the window title pane with the mouse. The popup is still showing (it shouldn't). Now start dragging the window around and observe the popup. It is dismissed, but a few frames later, which is quite noticeable.

There's probably more, and they are all down to the same private APIs to grab / ungrab top-level windows that are only wired in JPopupMenu

igordmn commented 1 year ago

Can you check, if the issue is with flashes remains, if you replace ComposePanel by this?

object : JButton() {
  override fun paint(g: Graphics?) {
      Thread.sleep(1000)
      super.paint(g)
  }
}

grab / ungrab

Can we close popups, when they just lose focus?

kirill-grouchnikov commented 1 year ago

That's going to break cascading popups

igordmn commented 1 year ago

That's going to break cascading popups

Maybe we can close all popups, when the parent window receives focus, not when an individual popup loses focus.

But if we definitely need to react on window grab, can we just call this?

Toolkit.getDefaultToolkit().addAWTEventListener(..., 0x80000000); // sun.awt.SunToolkit.GRAB_EVENT_MASK

and call grab/ungrab via reflection? I am not sure that we can access Sun API via reflection though. And It usually isn't safe, but the Swing API in a stable state for many years.

The alternative will be to use native API's (Compose exposes windowHandle, so it is possible)

Closing this issue, as from the current point of view it isn't obvious what can't be done on Compose side. This can help to further understand, if it is truly unrelated to Compose.

okushnikov commented 2 months ago

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