diff --git a/platform/jewel/decorated-window/src/main/kotlin/org/jetbrains/jewel/window/DecoratedWindow.kt b/platform/jewel/decorated-window/src/main/kotlin/org/jetbrains/jewel/window/DecoratedWindow.kt index 7549f8d5c12ac..d7ebb959a1782 100644 --- a/platform/jewel/decorated-window/src/main/kotlin/org/jetbrains/jewel/window/DecoratedWindow.kt +++ b/platform/jewel/decorated-window/src/main/kotlin/org/jetbrains/jewel/window/DecoratedWindow.kt @@ -44,6 +44,7 @@ import org.jetbrains.jewel.foundation.theme.JewelTheme import org.jetbrains.jewel.intui.standalone.window.Window import org.jetbrains.jewel.window.styling.DecoratedWindowStyle import org.jetbrains.jewel.window.utils.DesktopPlatform +import org.jetbrains.jewel.window.utils.ClientRegion @Suppress("ModifierMissing") @Composable @@ -279,7 +280,12 @@ public value class DecoratedWindowState(public val state: ULong) { } } -internal data class TitleBarInfo(val title: String, val icon: Painter?) +internal data class TitleBarInfo( + val title: String, + val icon: Painter? +) { + val clientRegions: HashMap = hashMapOf() +} internal val LocalTitleBarInfo: ProvidableCompositionLocal = compositionLocalOf { error("LocalTitleBarInfo not provided, TitleBar must be used in DecoratedWindow") diff --git a/platform/jewel/decorated-window/src/main/kotlin/org/jetbrains/jewel/window/TitleBar.MacOS.kt b/platform/jewel/decorated-window/src/main/kotlin/org/jetbrains/jewel/window/TitleBar.MacOS.kt index de2edcae0886c..14dcccbc5acdb 100644 --- a/platform/jewel/decorated-window/src/main/kotlin/org/jetbrains/jewel/window/TitleBar.MacOS.kt +++ b/platform/jewel/decorated-window/src/main/kotlin/org/jetbrains/jewel/window/TitleBar.MacOS.kt @@ -13,6 +13,7 @@ import androidx.compose.ui.unit.dp import com.jetbrains.JBR import org.jetbrains.jewel.foundation.theme.JewelTheme import org.jetbrains.jewel.window.styling.TitleBarStyle +import org.jetbrains.jewel.window.utils.WindowMouseEventEffect import org.jetbrains.jewel.window.utils.macos.MacUtil public fun Modifier.newFullscreenControls(newControls: Boolean = true): Modifier = @@ -78,8 +79,9 @@ internal fun DecoratedWindowScope.TitleBarOnMacOs( val titleBar = remember { JBR.getWindowDecorations().createCustomTitleBar() } + WindowMouseEventEffect(titleBar) + TitleBarImpl( - modifier = modifier.customTitleBarMouseEventHandler(titleBar), gradientStartColor = gradientStartColor, style = style, applyTitleBar = { height, state -> diff --git a/platform/jewel/decorated-window/src/main/kotlin/org/jetbrains/jewel/window/TitleBar.Windows.kt b/platform/jewel/decorated-window/src/main/kotlin/org/jetbrains/jewel/window/TitleBar.Windows.kt index be94b43f1f772..d3843d6d758f0 100644 --- a/platform/jewel/decorated-window/src/main/kotlin/org/jetbrains/jewel/window/TitleBar.Windows.kt +++ b/platform/jewel/decorated-window/src/main/kotlin/org/jetbrains/jewel/window/TitleBar.Windows.kt @@ -7,17 +7,12 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.input.pointer.PointerEventPass -import androidx.compose.ui.input.pointer.PointerEventType -import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.unit.dp import com.jetbrains.JBR -import com.jetbrains.WindowDecorations.CustomTitleBar -import kotlinx.coroutines.currentCoroutineContext -import kotlinx.coroutines.isActive import org.jetbrains.jewel.foundation.theme.JewelTheme import org.jetbrains.jewel.ui.util.isDark import org.jetbrains.jewel.window.styling.TitleBarStyle +import org.jetbrains.jewel.window.utils.WindowMouseEventEffect @Composable internal fun DecoratedWindowScope.TitleBarOnWindows( @@ -28,6 +23,8 @@ internal fun DecoratedWindowScope.TitleBarOnWindows( ) { val titleBar = remember { JBR.getWindowDecorations().createCustomTitleBar() } + WindowMouseEventEffect(titleBar) + TitleBarImpl( modifier = modifier, gradientStartColor = gradientStartColor, @@ -38,31 +35,7 @@ internal fun DecoratedWindowScope.TitleBarOnWindows( JBR.getWindowDecorations().setCustomTitleBar(window, titleBar) PaddingValues(start = titleBar.leftInset.dp, end = titleBar.rightInset.dp) }, - backgroundContent = { Spacer(modifier = Modifier.fillMaxSize().customTitleBarMouseEventHandler(titleBar)) }, + backgroundContent = { Spacer(modifier = Modifier.fillMaxSize()) }, content = content, ) } - -internal fun Modifier.customTitleBarMouseEventHandler(titleBar: CustomTitleBar): Modifier = - pointerInput(titleBar) { - val currentContext = currentCoroutineContext() - awaitPointerEventScope { - var inUserControl = false - while (currentContext.isActive) { - val event = awaitPointerEvent(PointerEventPass.Main) - event.changes.forEach { - if (!it.isConsumed && !inUserControl) { - titleBar.forceHitTest(false) - } else { - if (event.type == PointerEventType.Press) { - inUserControl = true - } - if (event.type == PointerEventType.Release) { - inUserControl = false - } - titleBar.forceHitTest(true) - } - } - } - } - } diff --git a/platform/jewel/decorated-window/src/main/kotlin/org/jetbrains/jewel/window/utils/ClientRegionHelper.kt b/platform/jewel/decorated-window/src/main/kotlin/org/jetbrains/jewel/window/utils/ClientRegionHelper.kt new file mode 100644 index 0000000000000..4e813e67808b5 --- /dev/null +++ b/platform/jewel/decorated-window/src/main/kotlin/org/jetbrains/jewel/window/utils/ClientRegionHelper.kt @@ -0,0 +1,115 @@ +package org.jetbrains.jewel.window.utils + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.LayoutCoordinates +import androidx.compose.ui.layout.positionInWindow +import androidx.compose.ui.node.CompositionLocalConsumerModifierNode +import androidx.compose.ui.node.GlobalPositionAwareModifierNode +import androidx.compose.ui.node.ModifierNodeElement +import androidx.compose.ui.node.currentValueOf +import androidx.compose.ui.platform.InspectorInfo +import com.jetbrains.WindowDecorations +import org.jetbrains.jewel.window.DecoratedWindowScope +import org.jetbrains.jewel.window.LocalTitleBarInfo +import org.jetbrains.jewel.window.TitleBarInfo +import java.awt.event.MouseAdapter +import java.awt.event.MouseEvent +import kotlin.collections.set + +internal data class ClientRegion( + val x: Float, + val y: Float, + val width: Int, + val height: Int, +) + +public fun Modifier.clientRegion(key: String): Modifier = then(RegisterClientRegionElement(key)) + +private data class RegisterClientRegionElement( + private val key: String +) : ModifierNodeElement() { + + override fun create() = RegisterClientRegionNode(key) + + override fun update(node: RegisterClientRegionNode) = node.updateKey(key) + + override fun InspectorInfo.inspectableProperties() { + name = "registerRegion" + properties["key"] = key + } +} + +private class RegisterClientRegionNode( + var key: String +) : Modifier.Node(), GlobalPositionAwareModifierNode, CompositionLocalConsumerModifierNode { + + private var titleBarInfo: TitleBarInfo? = null + + override fun onAttach() { + titleBarInfo = currentValueOf(LocalTitleBarInfo) + } + + override fun onGloballyPositioned(coordinates: LayoutCoordinates) { + val info = titleBarInfo ?: return + val position = coordinates.positionInWindow() + val size = coordinates.size + + info.clientRegions[key] = ClientRegion( + x = position.x, + y = position.y, + width = size.width, + height = size.height + ) + } + + override fun onDetach() { + titleBarInfo?.clientRegions?.remove(key) + titleBarInfo = null + } + + fun updateKey(newKey: String) { + if (key != newKey) { + titleBarInfo?.clientRegions?.remove(key) + key = newKey + } + } +} + +@Composable +internal fun DecoratedWindowScope.WindowMouseEventEffect(titleBar: WindowDecorations.CustomTitleBar) { + val titleBarInfo = LocalTitleBarInfo.current + + DisposableEffect(window, window.graphicsConfiguration, titleBar) { + val graphicsConfig = window.graphicsConfiguration + val scaleX = graphicsConfig?.defaultTransform?.scaleX ?: 1.0 + val scaleY = graphicsConfig?.defaultTransform?.scaleY ?: 1.0 + val listener = object : MouseAdapter() { + override fun mousePressed(e: MouseEvent) = updateHitTest(e) + override fun mouseReleased(e: MouseEvent) = updateHitTest(e) + override fun mouseDragged(e: MouseEvent) = updateHitTest(e) + override fun mouseMoved(e: MouseEvent) = updateHitTest(e) + + private fun updateHitTest(e: MouseEvent) { + // Convert from AWT raw pixels to Compose positionInWindow() coordinates + val x = e.x * scaleX + val y = e.y * scaleY + + val isClientRegion = titleBarInfo.clientRegions.any { region -> + (x >= region.value.x && x <= region.value.x + region.value.width) && + (y >= region.value.y && y <= region.value.y + region.value.height) + } + + titleBar.forceHitTest(isClientRegion) + } + } + window.addMouseListener(listener) + window.addMouseMotionListener(listener) + + onDispose { + window.removeMouseListener(listener) + window.removeMouseMotionListener(listener) + } + } +} diff --git a/platform/jewel/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/view/TitleBarView.kt b/platform/jewel/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/view/TitleBarView.kt index d541dc423cf39..6a78b75c87a62 100644 --- a/platform/jewel/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/view/TitleBarView.kt +++ b/platform/jewel/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/view/TitleBarView.kt @@ -25,6 +25,7 @@ import org.jetbrains.jewel.ui.painter.hints.Size import org.jetbrains.jewel.window.DecoratedWindowScope import org.jetbrains.jewel.window.TitleBar import org.jetbrains.jewel.window.newFullscreenControls +import org.jetbrains.jewel.window.utils.clientRegion @ExperimentalLayoutApi @Composable @@ -32,7 +33,7 @@ internal fun DecoratedWindowScope.TitleBarView() { TitleBar(Modifier.newFullscreenControls(), gradientStartColor = MainViewModel.projectColor) { Row(Modifier.align(Alignment.Start)) { Dropdown( - Modifier.height(30.dp), + Modifier.height(30.dp).clientRegion("dropdown"), menuContent = { MainViewModel.views.forEach { selectableItem( @@ -73,7 +74,7 @@ internal fun DecoratedWindowScope.TitleBarView() { val jewelGithubLink = "https://github.com/JetBrains/intellij-community/tree/master/platform/jewel" IconButton( { Desktop.getDesktop().browse(URI.create(jewelGithubLink)) }, - Modifier.size(40.dp).padding(5.dp), + Modifier.size(40.dp).padding(5.dp).clientRegion("github_link"), ) { Icon(ShowcaseIcons.gitHub, "Github") } @@ -99,7 +100,7 @@ internal fun DecoratedWindowScope.TitleBarView() { IntUiThemes.System -> IntUiThemes.Light } }, - Modifier.size(40.dp).padding(5.dp), + Modifier.size(40.dp).padding(5.dp).clientRegion("switch_theme"), ) { when (MainViewModel.theme) { IntUiThemes.Light ->