-
Notifications
You must be signed in to change notification settings - Fork 118
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
Conversation
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 When a panel changes its bounds, `doLayout` isn't called because the content itself wasn't changed. But we still need to update the bounds of the underlying layer. Docs: ``` > doLayout Causes this container to lay out its components > ``` ## Testing https://youtrack.jetbrains.com/issue/CMP-5856/Desktop-ComposePanel-size-breaks-with-.fillMax-modifiers#focus=Comments-27-10632441.0-0 isn't reproducible after the fix ## Release Notes ### Fixes - Fix bounds of ComposePanel in IntelliJ
… a deprecated version of `actions/upload-artifact: v2` Learn more: https://github.blog/changelog/2024-02-13-deprecation-notice-v1-and-v2-of-the-artifact-actions/
38c22fb
to
547ac25
Compare
@@ -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() |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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()
}
}
}
There was a problem hiding this comment.
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)
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() |
There was a problem hiding this comment.
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:
- Use
floor(scaled)
rather thanscaled.toInt()
. - Pass
contentScale
as an argument rather than access it inside, to avoid accessing it twice.
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
revalidate
sounds more expensive...
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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() }
?
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 wasinvalidated
- 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 performeddoLayout
yet, and sosyncBounds
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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed the comment
This reverts commit b036927.
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 asancestorMoved
)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