diff --git a/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/MacScrollbarHelper.kt b/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/MacScrollbarHelper.kt new file mode 100644 index 000000000..f3d3296f0 --- /dev/null +++ b/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/MacScrollbarHelper.kt @@ -0,0 +1,163 @@ +package org.jetbrains.jewel.bridge + +import com.intellij.openapi.util.SystemInfoRt +import com.intellij.ui.mac.foundation.Foundation +import com.intellij.ui.mac.foundation.Foundation.NSAutoreleasePool +import com.intellij.ui.mac.foundation.ID +import com.sun.jna.Callback +import com.sun.jna.Pointer +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import org.jetbrains.jewel.bridge.theme.defaults +import org.jetbrains.jewel.foundation.util.myLogger +import org.jetbrains.jewel.ui.component.styling.ScrollbarVisibility +import org.jetbrains.jewel.ui.component.styling.TrackClickBehavior + +internal object MacScrollbarHelper { + private val _scrollbarVisibilityStyleFlow = MutableStateFlow(scrollbarVisibility) + val scrollbarVisibilityStyleFlow: StateFlow = _scrollbarVisibilityStyleFlow + + private val _trackClickBehaviorFlow = MutableStateFlow(trackClickBehavior) + val trackClickBehaviorFlow: StateFlow = _trackClickBehaviorFlow + + init { + if (SystemInfoRt.isMac) { + initNotificationObserver() + } + } + + val trackClickBehavior: TrackClickBehavior + get() { + if (!SystemInfoRt.isMac) { + return TrackClickBehavior.JumpToSpot + } + + val pool = NSAutoreleasePool() + try { + return readMacScrollbarBehavior() + } finally { + pool.drain() + } + } + + val scrollbarVisibility: ScrollbarVisibility + get() { + if (!SystemInfoRt.isMac) { + return ScrollbarVisibility.AlwaysVisible + } + + val pool = NSAutoreleasePool() + try { + return readMacScrollbarStyle() + } catch (ignore: Throwable) { + } finally { + pool.drain() + } + return ScrollbarVisibility.AlwaysVisible + } + + private fun initNotificationObserver() { + val pool = NSAutoreleasePool() + + val delegateClass = + Foundation.allocateObjcClassPair(Foundation.getObjcClass("NSObject"), "NSScrollerChangesObserver") + if (ID.NIL != delegateClass) { + if (!addScrollbarVisibilityChangeListener(delegateClass)) { + myLogger().error("Cannot add observer method") + } + + if (!addTrackClickBehaviorChangeListener(delegateClass)) { + myLogger().error("Cannot add observer method") + } + Foundation.registerObjcClassPair(delegateClass) + } + val delegate = Foundation.invoke("NSScrollerChangesObserver", "new") + + try { + var center = Foundation.invoke("NSNotificationCenter", "defaultCenter") + Foundation.invoke( + center, + "addObserver:selector:name:object:", + delegate, + Foundation.createSelector("handleScrollerStyleChanged:"), + Foundation.nsString("NSPreferredScrollerStyleDidChangeNotification"), + ID.NIL, + ) + + center = Foundation.invoke("NSDistributedNotificationCenter", "defaultCenter") + Foundation.invoke( + center, + "addObserver:selector:name:object:", + delegate, + Foundation.createSelector("handleBehaviorChanged:"), + Foundation.nsString("AppleNoRedisplayAppearancePreferenceChanged"), + ID.NIL, + 2, // NSNotificationSuspensionBehaviorCoalesce + ) + } finally { + pool.drain() + } + } + + private val APPEARANCE_CALLBACK: Callback = + object : Callback { + @Suppress("UNUSED_PARAMETER", "unused") + @SuppressWarnings("UnusedDeclaration") + fun callback( + self: ID?, + selector: Pointer?, + event: ID?, + ) { + _scrollbarVisibilityStyleFlow.tryEmit(scrollbarVisibility) + } + } + + private val BEHAVIOR_CALLBACK: Callback = + object : Callback { + @Suppress("UNUSED_PARAMETER", "unused") + @SuppressWarnings("UnusedDeclaration") + fun callback( + self: ID?, + selector: Pointer?, + event: ID?, + ) { + _trackClickBehaviorFlow.tryEmit(trackClickBehavior) + } + } + + private fun readMacScrollbarBehavior(): TrackClickBehavior { + val defaults = Foundation.invoke("NSUserDefaults", "standardUserDefaults") + Foundation.invoke(defaults, "synchronize") + return Foundation + .invoke(defaults, "boolForKey:", Foundation.nsString("AppleScrollerPagingBehavior")) + .run { if (toInt() == 1) TrackClickBehavior.JumpToSpot else TrackClickBehavior.NextPage } + } + + private fun readMacScrollbarStyle(): ScrollbarVisibility { + val nsScroller = Foundation.invoke(Foundation.getObjcClass("NSScroller"), "preferredScrollerStyle") + + val visibility: ScrollbarVisibility = + if (1 == nsScroller.toInt()) { + ScrollbarVisibility.WhenScrolling.Companion.defaults() + } else { + ScrollbarVisibility.AlwaysVisible + } + return visibility + } + + private fun addScrollbarVisibilityChangeListener(delegateClass: ID?) = + Foundation.addMethod( + delegateClass, + Foundation.createSelector("handleScrollerStyleChanged:"), + APPEARANCE_CALLBACK, + "v@", + ) + + private fun addTrackClickBehaviorChangeListener(delegateClass: ID?) = + Foundation.addMethod( + delegateClass, + Foundation.createSelector("handleBehaviorChanged:"), + BEHAVIOR_CALLBACK, + "v@", + ) +} diff --git a/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/SwingBridgeService.kt b/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/SwingBridgeService.kt index 23843474d..b27c3455c 100644 --- a/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/SwingBridgeService.kt +++ b/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/SwingBridgeService.kt @@ -9,7 +9,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn import org.jetbrains.jewel.bridge.theme.createBridgeComponentStyling import org.jetbrains.jewel.bridge.theme.createBridgeThemeDefinition @@ -20,9 +20,13 @@ import kotlin.time.Duration.Companion.milliseconds @Service(Level.APP) internal class SwingBridgeService(scope: CoroutineScope) { internal val currentBridgeThemeData: StateFlow = - IntelliJApplication.lookAndFeelChangedFlow(scope) - .mapLatest { tryGettingThemeData() } - .stateIn(scope, SharingStarted.Eagerly, BridgeThemeData.DEFAULT) + combine( + IntelliJApplication.lookAndFeelChangedFlow(scope), + MacScrollbarHelper.scrollbarVisibilityStyleFlow, + MacScrollbarHelper.trackClickBehaviorFlow, + ) { _, _, _ -> + tryGettingThemeData() + }.stateIn(scope, SharingStarted.Eagerly, BridgeThemeData.DEFAULT) private suspend fun tryGettingThemeData(): BridgeThemeData { var counter = 0 diff --git a/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/theme/ScrollbarBridge.kt b/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/theme/ScrollbarBridge.kt index 0d816e457..1121348cf 100644 --- a/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/theme/ScrollbarBridge.kt +++ b/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/theme/ScrollbarBridge.kt @@ -5,7 +5,7 @@ import androidx.compose.foundation.shape.CornerSize import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.takeOrElse import androidx.compose.ui.unit.dp -import com.intellij.ui.mac.foundation.Foundation +import org.jetbrains.jewel.bridge.MacScrollbarHelper import org.jetbrains.jewel.bridge.retrieveColorOrUnspecified import org.jetbrains.jewel.ui.component.styling.ScrollbarColors import org.jetbrains.jewel.ui.component.styling.ScrollbarMetrics @@ -26,7 +26,7 @@ internal fun readScrollbarStyle(isDark: Boolean): ScrollbarStyle = private fun readScrollbarVisibility() = if (hostOs.isMacOS) { - readMacScrollbarStyle() + MacScrollbarHelper.scrollbarVisibility } else { ScrollbarVisibility.AlwaysVisible } @@ -40,7 +40,7 @@ private fun readScrollbarColors(isDark: Boolean) = private fun readTrackClickBehavior() = if (hostOs.isMacOS) { - readMacScrollbarBehavior() + MacScrollbarHelper.trackClickBehavior } else { TrackClickBehavior.JumpToSpot } @@ -50,7 +50,7 @@ private fun readScrollbarWinColors(isDark: Boolean): ScrollbarColors = thumbBackground = readScrollBarColorForKey( isDark, - "ScrollBar.Transparent.thumbColor", + "ScrollBar.thumbColor", 0x33737373, 0x47A6A6A6, ), @@ -110,9 +110,9 @@ private fun readScrollbarMacColors(isDark: Boolean): ScrollbarColors = thumbBackground = readScrollBarColorForKey( isDark, - "ScrollBar.Mac.Transparent.thumbColor", - 0x00000000, - 0x00808080, + "ScrollBar.Mac.thumbColor", + 0x33000000, + 0x59808080, ), thumbBackgroundHovered = readScrollBarColorForKey( @@ -152,16 +152,16 @@ private fun readScrollbarMacColors(isDark: Boolean): ScrollbarColors = trackBackground = readScrollBarColorForKey( isDark, - "ScrollBar.Mac.Transparent.trackColor", + "ScrollBar.Mac.trackColor", 0x00808080, 0x00808080, ), trackBackgroundHovered = readScrollBarColorForKey( isDark, - "ScrollBar.Mac.Transparent.hoverTrackColor", - 0x1A808080, - 0x1A808080, + "ScrollBar.Mac.hoverTrackColor", + 0x00808080, + 0x00808080, ), ) @@ -194,28 +194,6 @@ private fun readScrollbarMetrics(): ScrollbarMetrics = ) } -private fun readMacScrollbarStyle(): ScrollbarVisibility { - val nsScroller = - Foundation - .invoke(Foundation.getObjcClass("NSScroller"), "preferredScrollerStyle") - - val visibility: ScrollbarVisibility = - if (1 == nsScroller.toInt()) { - ScrollbarVisibility.WhenScrolling.Companion.defaults() - } else { - ScrollbarVisibility.AlwaysVisible - } - return visibility -} - -private fun readMacScrollbarBehavior(): TrackClickBehavior { - val defaults = Foundation.invoke("NSUserDefaults", "standardUserDefaults") - Foundation.invoke(defaults, "synchronize") - return Foundation - .invoke(defaults, "boolForKey:", Foundation.nsString("AppleScrollerPagingBehavior")) - .run { if (toInt() == 1) TrackClickBehavior.JumpToSpot else TrackClickBehavior.NextPage } -} - public fun ScrollbarVisibility.WhenScrolling.Companion.defaults( appearAnimationDuration: Duration = 125.milliseconds, disappearAnimationDuration: Duration = 125.milliseconds, diff --git a/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiScrollbarStyling.kt b/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiScrollbarStyling.kt index c406c34d3..078dcbaae 100644 --- a/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiScrollbarStyling.kt +++ b/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiScrollbarStyling.kt @@ -94,9 +94,9 @@ public fun ScrollbarVisibility.WhenScrolling.Companion.defaults( ) public fun ScrollbarColors.Companion.macOsLight( - thumbBackground: Color = Color(0x00000000), - thumbBackgroundHovered: Color = Color(0x80000000), - thumbBackgroundPressed: Color = thumbBackgroundHovered, + thumbBackground: Color = Color(0xFFBBBBBA), + thumbBackgroundHovered: Color = Color(0xFF7D7D7C), + thumbBackgroundPressed: Color = Color(0xFF7D7D7C), thumbBorder: Color = Color(0x33000000), thumbBorderHovered: Color = Color(0x80000000), thumbBorderPressed: Color = thumbBorderHovered, diff --git a/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/Scrollbars.kt b/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/Scrollbars.kt index 92998d211..0bab1a1d8 100644 --- a/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/Scrollbars.kt +++ b/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/Scrollbars.kt @@ -140,7 +140,6 @@ private fun MyScrollbar( // Visibility, hover and fade out var visible by remember { mutableStateOf(scrollState.canScrollBackward) } val hovered = interactionSource.collectIsHoveredAsState().value - var trackIsVisible by remember { mutableStateOf(false) } val animatedAlpha by animateFloatAsState( targetValue = if (visible) 1.0f else 0f, @@ -148,40 +147,13 @@ private fun MyScrollbar( ) LaunchedEffect(scrollState.isScrollInProgress, hovered, style.scrollbarVisibility) { - when (style.scrollbarVisibility) { - AlwaysVisible -> { - visible = true - trackIsVisible = true - } - - is WhenScrolling -> { - when { - scrollState.isScrollInProgress -> visible = true - hovered -> { - visible = true - trackIsVisible = true - } - - !hovered -> { - delay(style.scrollbarVisibility.lingerDuration) - trackIsVisible = false - visible = false - } - - !scrollState.isScrollInProgress && !hovered -> { - delay(style.scrollbarVisibility.lingerDuration) - visible = false - } - } - } + if(style.scrollbarVisibility is AlwaysVisible || scrollState.isScrollInProgress || hovered) { + visible = true } - when { - scrollState.isScrollInProgress -> visible = true - hovered -> { - visible = true - trackIsVisible = true - } + if (style.scrollbarVisibility is WhenScrolling && !hovered) { + delay(style.scrollbarVisibility.lingerDuration) + visible = false } } @@ -194,9 +166,9 @@ private fun MyScrollbar( else -> error("Unsupported scroll state type: ${scrollState::class}") } - val thumbWidth = if (trackIsVisible) style.metrics.thumbThicknessExpanded else style.metrics.thumbThickness - val trackBackground = if (trackIsVisible) style.colors.trackBackground else Color.Transparent - val trackPadding = if (trackIsVisible) style.metrics.trackPaddingExpanded else style.metrics.trackPadding + val thumbWidth = if (visible) style.metrics.thumbThicknessExpanded else style.metrics.thumbThickness + val trackBackground = if (visible) style.colors.trackBackground else Color.Transparent + val trackPadding = if (visible) style.metrics.trackPaddingExpanded else style.metrics.trackPadding ScrollbarImpl( adapter = adapter, modifier =