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

Fix bounds of ComposePanel in IntelliJ on macOs #988

Merged
merged 16 commits into from
Sep 17, 2024

Conversation

igordmn
Copy link
Collaborator

@igordmn igordmn commented Sep 16, 2024

Fixes https://youtrack.jetbrains.com/issue/CMP-5856/Desktop-ComposePanel-size-breaks-with-.fillMax-modifiers#focus=Comments-27-10632441.0-0

Fixes https://youtrack.jetbrains.com/issue/CMP-5968/Compose-content-is-rendered-in-the-wrong-place-in-IJ-when-using-AWT-compositing

Regression after https://github.com/JetBrains/skiko/pull/661/files#diff-910a6e28fda20a00bc98c6a8a04f74ab701d79e841b9baddf146b810e610572fR363 (setBounds is called more often, but not enough as ancestorMoved)

When a panel changes its position without changing its size, doLayout isn't called because the content itself wasn't changed. But we still need to update the bounds of the underlying layer.

Testing

Release Notes

Fixes - Desktop

  • Fix bounds of ComposePanel in IntelliJ on macOs

@igordmn igordmn force-pushed the igor.demin/fix-macos-syncSize branch from 38c22fb to 547ac25 Compare September 16, 2024 18:02
@igordmn igordmn marked this pull request as draft September 16, 2024 18:24
@@ -143,7 +155,7 @@ actual open class SkiaLayer internal constructor(
addPropertyChangeListener("graphicsContextScaleTransform") {
Logger.debug { "graphicsContextScaleTransform changed for $this" }
latestReceivedGraphicsContextScaleTransform = it.newValue as AffineTransform
redrawer?.syncSize()
revalidate()
Copy link
Collaborator Author

@igordmn igordmn Sep 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed because layout depends on the scale inside adjustSizeToContentScale

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds like it could bring back https://youtrack.jetbrains.com/issue/CMP-6550/Resizing-the-window-using-WindowState-breaks-the-UI

Is there a pure Compose reproducer I can try out?

Copy link
Collaborator Author

@igordmn igordmn Sep 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A pure Compose reproducer is:

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.ui.Modifier
import androidx.compose.ui.awt.ComposePanel
import androidx.compose.ui.graphics.Color
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import org.jetbrains.skiko.MainUIDispatcher
import java.awt.Dimension
import javax.swing.JFrame
import javax.swing.JLayeredPane
import javax.swing.WindowConstants

fun main() {
    runBlocking(MainUIDispatcher) {
        val window = JFrame()
        val panel = ComposePanel()
        panel.setContent {
            Box(Modifier.fillMaxSize().background(Color.Red))
        }

        panel.size = Dimension(100, 100)
        val box = javax.swing.Box.createVerticalBox().apply {
            add(panel)
        }
        box.setBounds(0, 0, 100, 100)

        try {
            val container = JLayeredPane()
            container.add(box)
            window.contentPane.add(container)
            window.setLocation(200, 200)
            window.size = Dimension(200, 200)
            window.defaultCloseOperation = WindowConstants.DISPOSE_ON_CLOSE
            window.isUndecorated = true
            window.isVisible = true

            delay(1000)

            box.setBounds(100, 0, 100, 100)
            delay(10000)
        } finally {
            window.dispose()
        }
    }
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I checked https://youtrack.jetbrains.com/issue/CMP-6550/Resizing-the-window-using-WindowState-breaks-the-UI with this Skiko, it doesn't reproduce (it reproduces in 1.7.0-beta02 on my machine)

@igordmn igordmn marked this pull request as ready for review September 16, 2024 20:08
Comment on lines 582 to 591
private fun roundSize(value: Int): Int {
var rounded = value * contentScale
val diff = rounded - rounded.toInt()
private fun adjustSizeToContentScale(value: Int): Int {
val scaled = value * contentScale
val diff = scaled - scaled.toInt()
// We check values close to 0.5 and edit the size to avoid white lines glitch
if (diff > 0.4f && diff < 0.6f) {
rounded = value + 1f
return if (diff > 0.4f && diff < 0.6f) {
(value + 1f).toInt()
} else {
rounded = value.toFloat()
value.toFloat().toInt()
}
return rounded.toInt()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The change looks valid (i.e. doesn't change semantics), but this is a really weird function. I understand neither what it does nor why it does it.

Just purely mechanical, minor, suggestions:

  1. Use floor(scaled) rather than scaled.toInt().
  2. Pass contentScale as an argument rather than access it inside, to avoid accessing it twice.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

override fun ancestorRemoved(event: AncestorEvent?) = Unit

override fun ancestorMoved(event: AncestorEvent?) {
revalidate()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not redrawer?.syncBounds() here?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

revalidate sounds more expensive...

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried, but we have glitches on resize. It seems we need to schedule it to the layout phase, to keep layouts in sync.

I checked the performance when we move the window, it doesn't decrease. When we call revalidate, it schedules relayout of only SkiaLayer + one child java.awt.Canvas. They don't have other children (a SwingPanel in Compose is added to a sibling container)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess I'm just not sure I understand why revalidation is needed here... Only the (absolute) position of the layer changes, not the size, so layout should not change.

Maybe SwingUtilities.invokeLater { redrawer?.syncBounds() } ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can't say that invokeLater is in sync with layout.

Actually I figured why there were resize glitches. addAncestorListener receives events asynchronously. I replaced it by addHierarchyBoundsListener which is called synchronously on the parent layout. We don't need revalidate then.

Also, I looked at the revalidate doc, and you are right, it is in't performant, as it actually revalidates the whole tree back to the root.

So, I removed it from the other place, I am not confident now to fix another very small issue (recalling adjustSizeToContentScale on content scale change).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, wait a minute. I tested it with a wrong version. Glitches are back. Investigating

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I returned to revalidate.

I looked at the documentation:
https://docs.oracle.com/javase/7/docs/api/javax/swing/JComponent.html#revalidate()

  • it notify the validateRoot, but it doesn't validate all children, just what was invalidated
  • it seems that only validate is expensive, but it is deferred, and actually is already called on every parent size change or when a component is added/removed

I don't want to user invokeLater because:

  • we need to handle a proper cancelation during dispose
  • we need to handle double invokeLater
  • we are not sure that it will be called the same time as the layout. I investigated resize glitches, and it appeared that we called syncBounds too often - we called it even if it haven't performed doLayout yet, and so syncBounds was based on incorrect size.

// TODO Recheck this method validity in 2 cases - full Window content, and a Panel content
// https://youtrack.jetbrains.com/issue/CMP-5447/Window-white-line-on-the-bottom-before-resizing
/**
* Change [value] size to avoid rounding errors, when converting AWT Int coordinates to pixels.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does it mean "avoid rounding errors"? What rounding errors? The link to the ticket does help though.

But why 0.4 and 0.6 specifically?

Should we maybe instead use the same function that determines the real content size and compare it to the window/container size? If it's smaller, then increase the unscaled size.

e.g.

return if ((value * contentScale).roundToInt() < windowSize)
    value + 1
else
    value

(assuming the scaled/real content size is (value * contentScale).roundToInt()).

Anyway, this is not a blocker for this PR in any way.

Copy link
Collaborator Author

@igordmn igordmn Sep 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rounding errors when converting Int points (10) to pixels (12) with float scale (1.25)

I will change the comment, but I will not change the logic in this PR, as it isn't relevant to it, and I also don't know the whole context of the fix itself, just knowledge of the issue.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The link to the issue is in the comment above, this is directly related.

As far as I understand, with this code there was an attempt to increase the size of the component, to avoid white line (but instead overlapped one meaningful content line)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed the comment

@igordmn igordmn merged commit 1ca83c1 into master Sep 17, 2024
6 checks passed
@igordmn igordmn deleted the igor.demin/fix-macos-syncSize branch September 17, 2024 22:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants