From 58f8b2e98dcc08893ba5737b06a6bd3bc06028ad Mon Sep 17 00:00:00 2001 From: Igor Demin Date: Thu, 25 Nov 2021 17:15:53 +0300 Subject: [PATCH] Fix consuming events of undecorated resizable window Fixes https://github.com/JetBrains/compose-jb/issues/1432 Fixes https://github.com/JetBrains/compose-jb/issues/1444 Rewrote to Compose, so Compose can handle all events by itself. I haven't managed to write code properly with AWT (branch feature/fix_drag_undecorated) --- .../desktop/examples/example1/Main.jvm.kt | 24 ++- .../compose/ui/awt/ComposeDialog.desktop.kt | 27 +-- .../compose/ui/awt/ComposeWindow.desktop.kt | 27 +-- .../ui/awt/ComposeWindowDelegate.desktop.kt | 32 ++- .../UndecoratedWindowResizer.desktop.kt | 186 ++++++++++-------- 5 files changed, 156 insertions(+), 140 deletions(-) diff --git a/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/example1/Main.jvm.kt b/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/example1/Main.jvm.kt index d69e5da14a85d..2a7064d809cb2 100644 --- a/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/example1/Main.jvm.kt +++ b/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/example1/Main.jvm.kt @@ -49,6 +49,7 @@ import androidx.compose.foundation.text.InlineTextContent import androidx.compose.foundation.text.appendInlineContent import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.foundation.verticalScroll +import androidx.compose.foundation.window.WindowDraggableArea import androidx.compose.material.BottomAppBar import androidx.compose.material.Button import androidx.compose.material.ButtonDefaults @@ -113,6 +114,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextDecoration.Companion.Underline import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.em @@ -153,17 +155,19 @@ private fun FrameWindowScope.App() { MaterialTheme { Scaffold( topBar = { - TopAppBar( - title = { - Row(verticalAlignment = Alignment.CenterVertically) { - Image( - painterResource("androidx/compose/desktop/example/star.svg"), - contentDescription = "Star" - ) - Text(title) + WindowDraggableArea { + TopAppBar( + title = { + Row(verticalAlignment = Alignment.CenterVertically) { + Image( + painterResource("androidx/compose/desktop/example/star.svg"), + contentDescription = "Star" + ) + Text(title) + } } - } - ) + ) + } }, floatingActionButton = { ExtendedFloatingActionButton( diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeDialog.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeDialog.desktop.kt index 26e51df110640..5ed059d395cf3 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeDialog.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeDialog.desktop.kt @@ -41,7 +41,7 @@ class ComposeDialog( owner: Window? = null, modalityType: ModalityType = ModalityType.MODELESS ) : JDialog(owner, modalityType) { - private val delegate = ComposeWindowDelegate(this) + private val delegate = ComposeWindowDelegate(this, ::isUndecorated) internal val layer get() = delegate.layer init { @@ -115,16 +115,14 @@ class ComposeDialog( super.dispose() } - private val undecoratedWindowResizer = UndecoratedWindowResizer(this, layer) - override fun setUndecorated(value: Boolean) { super.setUndecorated(value) - undecoratedWindowResizer.enabled = isUndecorated && isResizable + delegate.undecoratedWindowResizer.enabled = isUndecorated && isResizable } override fun setResizable(value: Boolean) { super.setResizable(value) - undecoratedWindowResizer.enabled = isUndecorated && isResizable + delegate.undecoratedWindowResizer.enabled = isUndecorated && isResizable } /** @@ -132,24 +130,7 @@ class ComposeDialog( * Transparency should be set only if window is not showing and `isUndecorated` is set to * `true`, otherwise AWT will throw an exception. */ - var isTransparent: Boolean - get() = layer.component.transparency - set(value) { - if (value != layer.component.transparency) { - check(isUndecorated) { "Transparent window should be undecorated!" } - check(!isDisplayable) { - "Cannot change transparency if window is already displayable." - } - layer.component.transparency = value - if (value) { - if (hostOs != OS.Windows) { - background = Color(0, 0, 0, 0) - } - } else { - background = null - } - } - } + var isTransparent: Boolean by delegate::isTransparent /** * Registers a task to run when the rendering API changes. diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindow.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindow.desktop.kt index faf1b0c44f41f..cb4aead2d3fb6 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindow.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindow.desktop.kt @@ -38,7 +38,7 @@ import javax.swing.JFrame * ComposeWindow inherits javax.swing.JFrame. */ class ComposeWindow : JFrame() { - private val delegate = ComposeWindowDelegate(this) + private val delegate = ComposeWindowDelegate(this, ::isUndecorated) internal val layer get() = delegate.layer init { @@ -112,16 +112,14 @@ class ComposeWindow : JFrame() { super.dispose() } - private val undecoratedWindowResizer = UndecoratedWindowResizer(this, layer) - override fun setUndecorated(value: Boolean) { super.setUndecorated(value) - undecoratedWindowResizer.enabled = isUndecorated && isResizable + delegate.undecoratedWindowResizer.enabled = isUndecorated && isResizable } override fun setResizable(value: Boolean) { super.setResizable(value) - undecoratedWindowResizer.enabled = isUndecorated && isResizable + delegate.undecoratedWindowResizer.enabled = isUndecorated && isResizable } /** @@ -129,24 +127,7 @@ class ComposeWindow : JFrame() { * Transparency should be set only if window is not showing and `isUndecorated` is set to * `true`, otherwise AWT will throw an exception. */ - var isTransparent: Boolean - get() = layer.component.transparency - set(value) { - if (value != layer.component.transparency) { - check(isUndecorated) { "Transparent window should be undecorated!" } - check(!isDisplayable) { - "Cannot change transparency if window is already displayable." - } - layer.component.transparency = value - if (value) { - if (hostOs != OS.Windows) { - background = Color(0, 0, 0, 0) - } - } else { - background = null - } - } - } + var isTransparent: Boolean by delegate::isTransparent var placement: WindowPlacement get() = when { diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindowDelegate.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindowDelegate.desktop.kt index 3e26547d4205a..51ba0884de923 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindowDelegate.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindowDelegate.desktop.kt @@ -21,9 +21,13 @@ import androidx.compose.runtime.CompositionLocalContext import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.input.key.KeyEvent import androidx.compose.ui.window.LocalWindow +import androidx.compose.ui.window.UndecoratedWindowResizer import org.jetbrains.skiko.ClipComponent import org.jetbrains.skiko.GraphicsApi +import org.jetbrains.skiko.OS import org.jetbrains.skiko.SkiaLayer +import org.jetbrains.skiko.hostOs +import java.awt.Color import java.awt.Component import java.awt.Window import java.awt.event.MouseListener @@ -31,10 +35,15 @@ import java.awt.event.MouseMotionListener import java.awt.event.MouseWheelListener import javax.swing.JLayeredPane -internal class ComposeWindowDelegate(private val window: Window) { +internal class ComposeWindowDelegate( + private val window: Window, + private val isUndecorated: () -> Boolean +) { private var isDisposed = false val layer = ComposeLayer() + val undecoratedWindowResizer = UndecoratedWindowResizer(window) + val pane = object : JLayeredPane() { override fun setBounds(x: Int, y: Int, width: Int, height: Int) { layer.component.setSize(width, height) @@ -67,6 +76,7 @@ internal class ComposeWindowDelegate(private val window: Window) { init { pane.layout = null pane.add(layer.component, Integer.valueOf(1)) + setContent {} } fun add(component: Component): Component { @@ -93,6 +103,7 @@ internal class ComposeWindowDelegate(private val window: Window) { LocalLayerContainer provides pane ) { content() + undecoratedWindowResizer.Content() } } } @@ -116,6 +127,25 @@ internal class ComposeWindowDelegate(private val window: Window) { val renderApi: GraphicsApi get() = layer.component.renderApi + var isTransparent: Boolean + get() = layer.component.transparency + set(value) { + if (value != layer.component.transparency) { + check(isUndecorated()) { "Transparent window should be undecorated!" } + check(!window.isDisplayable) { + "Cannot change transparency if window is already displayable." + } + layer.component.transparency = value + if (value) { + if (hostOs != OS.Windows) { + window.background = Color(0, 0, 0, 0) + } + } else { + window.background = null + } + } + } + fun addMouseListener(listener: MouseListener) { layer.component.addMouseListener(listener) } diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/UndecoratedWindowResizer.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/UndecoratedWindowResizer.desktop.kt index 2b320b4aa6afb..8c059fad551be 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/UndecoratedWindowResizer.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/UndecoratedWindowResizer.desktop.kt @@ -16,98 +16,125 @@ package androidx.compose.ui.window -import androidx.compose.ui.awt.ComposeLayer -import java.awt.Dimension +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.pointer.PointerEventType +import androidx.compose.ui.input.pointer.PointerIcon +import androidx.compose.ui.input.pointer.isPrimaryPressed +import androidx.compose.ui.input.pointer.pointerHoverIcon +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.layout.Layout +import androidx.compose.ui.layout.Measurable +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp import java.awt.Cursor -import java.awt.event.MouseAdapter -import java.awt.event.MouseEvent -import java.awt.event.MouseMotionAdapter +import java.awt.Dimension import java.awt.MouseInfo import java.awt.Point import java.awt.Window -internal const val DefaultBorderThickness = 8 +internal val DefaultBorderThickness = 8.dp internal class UndecoratedWindowResizer( private val window: Window, - private val layer: ComposeLayer, - var enabled: Boolean = false, - var borderThickness: Int = DefaultBorderThickness + var borderThickness: Dp = DefaultBorderThickness ) { + var enabled: Boolean by mutableStateOf(false) + private var initialPointPos = Point() private var initialWindowPos = Point() private var initialWindowSize = Dimension() - private var sides = 0 - private var isResizing = false - private val motionListener = object : MouseMotionAdapter() { - override fun mouseDragged(event: MouseEvent) = resize() - override fun mouseMoved(event: MouseEvent) = changeCursor(event) + @Composable + fun Content() { + if (enabled) { + Layout( + { + Side(Cursor.W_RESIZE_CURSOR, Side.Left) + Side(Cursor.E_RESIZE_CURSOR, Side.Right) + Side(Cursor.N_RESIZE_CURSOR, Side.Top) + Side(Cursor.S_RESIZE_CURSOR, Side.Bottom) + Side(Cursor.NW_RESIZE_CURSOR, Side.Left or Side.Top) + Side(Cursor.NE_RESIZE_CURSOR, Side.Right or Side.Top) + Side(Cursor.SW_RESIZE_CURSOR, Side.Left or Side.Bottom) + Side(Cursor.SE_RESIZE_CURSOR, Side.Right or Side.Bottom) + }, + Modifier, + measurePolicy = { measurables, constraints -> + val b = borderThickness.roundToPx() + fun Measurable.measureSide(width: Int, height: Int) = measure( + Constraints.fixed(width.coerceAtLeast(0), height.coerceAtLeast(0)) + ) + + val left = measurables[0].measureSide(b, constraints.maxHeight - 2 * b) + val right = measurables[1].measureSide(b, constraints.maxHeight - 2 * b) + val top = measurables[2].measureSide(constraints.maxWidth - 2 * b, b) + val bottom = measurables[3].measureSide(constraints.maxWidth - 2 * b, b) + val leftTop = measurables[4].measureSide(b, b) + val rightTop = measurables[5].measureSide(b, b) + val leftBottom = measurables[6].measureSide(b, b) + val rightBottom = measurables[7].measureSide(b, b) + layout(constraints.maxWidth, constraints.maxHeight) { + left.place(0, b) + right.place(constraints.maxWidth - b, b) + top.place(b, 0) + bottom.place(0, constraints.maxHeight - b) + leftTop.place(0, 0) + rightTop.place(constraints.maxWidth - b, 0) + leftBottom.place(0, constraints.maxHeight - b) + rightBottom.place(constraints.maxWidth - b, constraints.maxHeight - b) + } + } + ) + } } - private val mouseListener = object : MouseAdapter() { - override fun mousePressed(event: MouseEvent) { - if (sides != 0) { + private fun Modifier.resizeOnDrag(sides: Int) = pointerInput(Unit) { + var isResizing = false + + while (true) { + val event = awaitPointerEventScope { awaitPointerEvent() } + val change = event.changes.first() + val changedToPressed = !change.previousPressed && change.pressed + + if (event.buttons.isPrimaryPressed && changedToPressed) { + initialPointPos = MouseInfo.getPointerInfo().location + initialWindowPos = Point(window.x, window.y) + initialWindowSize = Dimension(window.width, window.height) isResizing = true } - initialPointPos = MouseInfo.getPointerInfo().location - initialWindowPos = Point(window.x, window.y) - initialWindowSize = Dimension(window.width, window.height) - } - override fun mouseReleased(event: MouseEvent) { - isResizing = false - } - } - init { - layer.component.addMouseListener(mouseListener) - layer.component.addMouseMotionListener(motionListener) - } + if (!event.buttons.isPrimaryPressed) { + isResizing = false + } - private fun changeCursor(event: MouseEvent) { - if (!enabled || isResizing) { - return - } - val point = event.getPoint() - sides = getSides(point) - fun setCursor(cursorType: Int) { - layer.scene.component.desiredCursor = Cursor(cursorType) - } - when (sides) { - Side.Left.value -> setCursor(Cursor.W_RESIZE_CURSOR) - Side.Top.value -> setCursor(Cursor.N_RESIZE_CURSOR) - Side.Right.value -> setCursor(Cursor.E_RESIZE_CURSOR) - Side.Bottom.value -> setCursor(Cursor.S_RESIZE_CURSOR) - Corner.LeftTop.value -> setCursor(Cursor.NW_RESIZE_CURSOR) - Corner.LeftBottom.value -> setCursor(Cursor.SW_RESIZE_CURSOR) - Corner.RightTop.value -> setCursor(Cursor.NE_RESIZE_CURSOR) - Corner.RightBottom.value -> setCursor(Cursor.SE_RESIZE_CURSOR) + if (event.type == PointerEventType.Move) { + if (isResizing) { + resize(sides) + } + } } } - private fun getSides(point: Point): Int { - var sides = 0 - val tolerance = borderThickness - if (point.x <= tolerance) { - sides += Side.Left.value - } - if (point.x >= window.width - tolerance) { - sides += Side.Right.value - } - if (point.y <= tolerance) { - sides += Side.Top.value + @Composable + private fun Side(cursorId: Int, sides: Int) = Layout( + {}, + Modifier.cursor(cursorId).resizeOnDrag(sides), + measurePolicy = { _, constraints -> + layout(constraints.maxWidth, constraints.maxHeight) {} } - if (point.y >= window.height - tolerance) { - sides += Side.Bottom.value - } - return sides - } + ) - private fun resize() { - if (!enabled || sides == 0) { - return - } + @OptIn(ExperimentalComposeUiApi::class) + private fun Modifier.cursor(awtCursorId: Int) = + pointerHoverIcon(PointerIcon(Cursor(awtCursorId))) + private fun resize(sides: Int) { val pointPos = MouseInfo.getPointerInfo().location val diffX = pointPos.x - initialPointPos.x val diffY = pointPos.y - initialPointPos.y @@ -116,18 +143,18 @@ internal class UndecoratedWindowResizer( var newWidth = window.width var newHeight = window.height - if (contains(sides, Side.Left.value)) { + if (contains(sides, Side.Left)) { newWidth = initialWindowSize.width - diffX newWidth = newWidth.coerceAtLeast(window.minimumSize.width) newXPos = initialWindowPos.x + initialWindowSize.width - newWidth - } else if (contains(sides, Side.Right.value)) { + } else if (contains(sides, Side.Right)) { newWidth = initialWindowSize.width + diffX } - if (contains(sides, Side.Top.value)) { + if (contains(sides, Side.Top)) { newHeight = initialWindowSize.height - diffY newHeight = newHeight.coerceAtLeast(window.minimumSize.height) newYPos = initialWindowPos.y + initialWindowSize.height - newHeight - } else if (contains(sides, Side.Bottom.value)) { + } else if (contains(sides, Side.Bottom)) { newHeight = initialWindowSize.height + diffY } window.setLocation(newXPos, newYPos) @@ -141,17 +168,10 @@ internal class UndecoratedWindowResizer( return false } - private enum class Side(val value: Int) { - Left(0x0001), - Top(0x0010), - Right(0x0100), - Bottom(0x1000) - } - - private enum class Corner(val value: Int) { - LeftTop(0x0011), - LeftBottom(0x1001), - RightTop(0x0110), - RightBottom(0x1100) + private object Side { + val Left = 0x0001 + val Top = 0x0010 + val Right = 0x0100 + val Bottom = 0x1000 } } \ No newline at end of file