From aadf5b4a83445efe73eb8a33c33631467aed8b4c Mon Sep 17 00:00:00 2001 From: Ivan Morgillo Date: Mon, 5 Aug 2024 17:07:21 +0200 Subject: [PATCH] New iteration on scrollbars (#515) * iterate on scrollbars * update API files * add Scrollbars demo to Standalone * fix popup border width regression Reference https://github.com/JetBrains/jewel/pull/515#discussion_r1704184163 Signed-off-by: Ivan Morgillo * remove unnecessary platform check in ScrollbarBridge Reference https://github.com/JetBrains/jewel/pull/515#discussion_r1704190394 Signed-off-by: Ivan Morgillo * fix wrong thumb background color Reference https://github.com/JetBrains/jewel/pull/515#discussion_r1704209751 Signed-off-by: Ivan Morgillo --------- Signed-off-by: Ivan Morgillo --- ide-laf-bridge/api/ide-laf-bridge.api | 5 + .../jewel/bridge/theme/IntUiBridge.kt | 41 +- .../jewel/bridge/theme/ScrollbarBridge.kt | 230 ++++++ .../api/int-ui-standalone.api | 42 +- .../styling/IntUiScrollbarStyling.kt | 255 +++++- .../standalone/styling/IntUiTabStyling.kt | 13 +- .../standalone/view/component/Scrollbars.kt | 190 +++++ .../samples/standalone/view/component/Tabs.kt | 33 +- .../resources/icons/components/scrollbar.svg | 12 + .../icons/components/scrollbar_dark.svg | 12 + ui/api/ui.api | 61 +- .../jewel/ui/component/Scrollbars.kt | 731 ++++++++++++++++-- .../jetbrains/jewel/ui/component/TabStrip.kt | 32 +- .../ui/component/styling/ScrollbarStyling.kt | 30 +- .../jewel/ui/component/styling/TabStyling.kt | 1 + 15 files changed, 1512 insertions(+), 176 deletions(-) create mode 100644 ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/theme/ScrollbarBridge.kt create mode 100644 samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/view/component/Scrollbars.kt create mode 100644 samples/standalone/src/main/resources/icons/components/scrollbar.svg create mode 100644 samples/standalone/src/main/resources/icons/components/scrollbar_dark.svg diff --git a/ide-laf-bridge/api/ide-laf-bridge.api b/ide-laf-bridge/api/ide-laf-bridge.api index 471e5622e..777f0d70f 100644 --- a/ide-laf-bridge/api/ide-laf-bridge.api +++ b/ide-laf-bridge/api/ide-laf-bridge.api @@ -125,6 +125,11 @@ public final class org/jetbrains/jewel/bridge/theme/IntUiBridgeKt { public static final fun retrieveEditorTextStyle ()Landroidx/compose/ui/text/TextStyle; } +public final class org/jetbrains/jewel/bridge/theme/ScrollbarBridgeKt { + public static final fun defaults-6ksGUsA (Lorg/jetbrains/jewel/ui/component/styling/ScrollbarVisibility$WhenScrolling$Companion;JJJJ)Lorg/jetbrains/jewel/ui/component/styling/ScrollbarVisibility$WhenScrolling; + public static synthetic fun defaults-6ksGUsA$default (Lorg/jetbrains/jewel/ui/component/styling/ScrollbarVisibility$WhenScrolling$Companion;JJJJILjava/lang/Object;)Lorg/jetbrains/jewel/ui/component/styling/ScrollbarVisibility$WhenScrolling; +} + public final class org/jetbrains/jewel/bridge/theme/SwingBridgeThemeKt { public static final fun SwingBridgeTheme (Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;I)V } diff --git a/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/theme/IntUiBridge.kt b/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/theme/IntUiBridge.kt index b1af9e5a7..da3841968 100644 --- a/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/theme/IntUiBridge.kt +++ b/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/theme/IntUiBridge.kt @@ -18,7 +18,6 @@ import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.takeOrElse import com.intellij.ide.ui.laf.darcula.DarculaUIUtil import com.intellij.ide.ui.laf.intellij.IdeaPopupMenuUI -import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.editor.colors.ColorKey import com.intellij.openapi.editor.colors.EditorFontType import com.intellij.openapi.util.registry.Registry @@ -49,6 +48,7 @@ import org.jetbrains.jewel.foundation.Stroke import org.jetbrains.jewel.foundation.theme.ThemeColorPalette import org.jetbrains.jewel.foundation.theme.ThemeDefinition import org.jetbrains.jewel.foundation.theme.ThemeIconData +import org.jetbrains.jewel.foundation.util.JewelLogger import org.jetbrains.jewel.ui.ComponentStyling import org.jetbrains.jewel.ui.DefaultComponentStyling import org.jetbrains.jewel.ui.component.styling.ButtonColors @@ -96,9 +96,6 @@ import org.jetbrains.jewel.ui.component.styling.RadioButtonColors import org.jetbrains.jewel.ui.component.styling.RadioButtonIcons import org.jetbrains.jewel.ui.component.styling.RadioButtonMetrics import org.jetbrains.jewel.ui.component.styling.RadioButtonStyle -import org.jetbrains.jewel.ui.component.styling.ScrollbarColors -import org.jetbrains.jewel.ui.component.styling.ScrollbarMetrics -import org.jetbrains.jewel.ui.component.styling.ScrollbarStyle import org.jetbrains.jewel.ui.component.styling.SegmentedControlButtonColors import org.jetbrains.jewel.ui.component.styling.SegmentedControlButtonMetrics import org.jetbrains.jewel.ui.component.styling.SegmentedControlButtonStyle @@ -128,7 +125,7 @@ import org.jetbrains.jewel.ui.icons.AllIconsKeys import javax.swing.UIManager import kotlin.time.Duration.Companion.milliseconds -private val logger = Logger.getInstance("JewelIntUiBridge") +private val logger = JewelLogger.getInstance("JewelIntUiBridge") internal val uiDefaults get() = UIManager.getDefaults() @@ -178,13 +175,14 @@ public fun retrieveConsoleTextStyle(): TextStyle { ) } +private val isDark: Boolean + get() = !JBColor.isBright() + internal fun createBridgeThemeDefinition( textStyle: TextStyle, editorTextStyle: TextStyle, consoleTextStyle: TextStyle, ): ThemeDefinition { - val isDark = !JBColor.isBright() - logger.debug("Obtaining theme definition from Swing...") return ThemeDefinition( @@ -697,7 +695,7 @@ private fun readMenuStyle(): MenuStyle { ) return MenuStyle( - isDark = !JBColor.isBright(), + isDark = isDark, colors = colors, metrics = MenuMetrics( @@ -810,31 +808,6 @@ private object NewUiRadioButtonMetrics : BridgeRadioButtonMetrics { override val iconContentGap = 4.dp } -private fun readScrollbarStyle(isDark: Boolean) = - ScrollbarStyle( - colors = - ScrollbarColors( - // See ScrollBarPainter.THUMB_OPAQUE_BACKGROUND - thumbBackground = - retrieveColorOrUnspecified("ScrollBar.Mac.Transparent.thumbColor") - .let { if (it.alpha == 0f) Color.Unspecified else it } // See https://github.com/JetBrains/jewel/issues/259 - .takeOrElse { if (isDark) Color(0x59808080) else Color(0x33000000) }, - // See ScrollBarPainter.THUMB_OPAQUE_HOVERED_BACKGROUND - thumbBackgroundHovered = - retrieveColorOrUnspecified("ScrollBar.Mac.Transparent.hoverThumbColor") - .let { if (it.alpha == 0f) Color.Unspecified else it } // See https://github.com/JetBrains/jewel/issues/259 - .takeOrElse { if (isDark) Color(0x8C808080) else Color(0x80000000) }, - ), - metrics = - ScrollbarMetrics( - thumbCornerSize = CornerSize(100), - thumbThickness = 8.dp, - minThumbLength = 16.dp, - trackPadding = PaddingValues(start = 7.dp, end = 3.dp), - ), - hoverDuration = 300.milliseconds, - ) - private fun readSegmentedControlButtonStyle(): SegmentedControlButtonStyle { val selectedBackground = SolidColor(JBUI.CurrentTheme.SegmentedButton.SELECTED_BUTTON_COLOR.toComposeColor()) @@ -1094,6 +1067,7 @@ private fun readDefaultTabStyle(): TabStyle { contentHovered = 1f, contentSelected = 1f, ), + scrollbarStyle = readScrollbarStyle(isDark), ) } @@ -1150,6 +1124,7 @@ private fun readEditorTabStyle(): TabStyle { contentHovered = 1f, contentSelected = 1f, ), + scrollbarStyle = readScrollbarStyle(isDark), ) } 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 new file mode 100644 index 000000000..0d816e457 --- /dev/null +++ b/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/theme/ScrollbarBridge.kt @@ -0,0 +1,230 @@ +package org.jetbrains.jewel.bridge.theme + +import androidx.compose.foundation.layout.PaddingValues +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.retrieveColorOrUnspecified +import org.jetbrains.jewel.ui.component.styling.ScrollbarColors +import org.jetbrains.jewel.ui.component.styling.ScrollbarMetrics +import org.jetbrains.jewel.ui.component.styling.ScrollbarStyle +import org.jetbrains.jewel.ui.component.styling.ScrollbarVisibility +import org.jetbrains.jewel.ui.component.styling.TrackClickBehavior +import org.jetbrains.skiko.hostOs +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds + +internal fun readScrollbarStyle(isDark: Boolean): ScrollbarStyle = + ScrollbarStyle( + colors = readScrollbarColors(isDark), + metrics = readScrollbarMetrics(), + trackClickBehavior = readTrackClickBehavior(), + scrollbarVisibility = readScrollbarVisibility(), + ) + +private fun readScrollbarVisibility() = + if (hostOs.isMacOS) { + readMacScrollbarStyle() + } else { + ScrollbarVisibility.AlwaysVisible + } + +private fun readScrollbarColors(isDark: Boolean) = + if (hostOs.isMacOS) { + readScrollbarMacColors(isDark) + } else { + readScrollbarWinColors(isDark) + } + +private fun readTrackClickBehavior() = + if (hostOs.isMacOS) { + readMacScrollbarBehavior() + } else { + TrackClickBehavior.JumpToSpot + } + +private fun readScrollbarWinColors(isDark: Boolean): ScrollbarColors = + ScrollbarColors( + thumbBackground = + readScrollBarColorForKey( + isDark, + "ScrollBar.Transparent.thumbColor", + 0x33737373, + 0x47A6A6A6, + ), + thumbBackgroundHovered = + readScrollBarColorForKey( + isDark, + "ScrollBar.hoverThumbColor", + 0x47737373, + 0x59A6A6A6, + ), + thumbBackgroundPressed = + readScrollBarColorForKey( + isDark, + "ScrollBar.hoverThumbColor", + 0x47737373, + 0x59A6A6A6, + ), + thumbBorder = + readScrollBarColorForKey( + isDark, + "ScrollBar.thumbBorderColor", + 0x33595959, + 0x47383838, + ), + thumbBorderHovered = + readScrollBarColorForKey( + isDark, + "ScrollBar.hoverThumbBorderColor", + 0x47595959, + 0x59383838, + ), + thumbBorderPressed = + readScrollBarColorForKey( + isDark, + "ScrollBar.hoverThumbBorderColor", + 0x47595959, + 0x59383838, + ), + trackBackground = + readScrollBarColorForKey( + isDark, + "ScrollBar.Transparent.trackColor", + 0x00808080, + 0x00808080, + ), + trackBackgroundHovered = + readScrollBarColorForKey( + isDark, + "ScrollBar.Transparent.hoverTrackColor", + 0x1A808080, + 0x1A808080, + ), + ) + +private fun readScrollbarMacColors(isDark: Boolean): ScrollbarColors = + ScrollbarColors( + thumbBackground = + readScrollBarColorForKey( + isDark, + "ScrollBar.Mac.Transparent.thumbColor", + 0x00000000, + 0x00808080, + ), + thumbBackgroundHovered = + readScrollBarColorForKey( + isDark, + "ScrollBar.Mac.hoverThumbColor", + 0x80000000, + 0x8C808080, + ), + thumbBackgroundPressed = + readScrollBarColorForKey( + isDark, + "ScrollBar.Mac.hoverThumbColor", + 0x80000000, + 0x8C808080, + ), + thumbBorder = + readScrollBarColorForKey( + isDark, + "ScrollBar.Mac.thumbBorderColor", + 0x33000000, + 0x59262626, + ), + thumbBorderHovered = + readScrollBarColorForKey( + isDark, + "ScrollBar.Mac.hoverThumbBorderColor", + 0x80000000, + 0x8C262626, + ), + thumbBorderPressed = + readScrollBarColorForKey( + isDark, + "ScrollBar.Mac.hoverThumbBorderColor", + 0x80000000, + 0x8C262626, + ), + trackBackground = + readScrollBarColorForKey( + isDark, + "ScrollBar.Mac.Transparent.trackColor", + 0x00808080, + 0x00808080, + ), + trackBackgroundHovered = + readScrollBarColorForKey( + isDark, + "ScrollBar.Mac.Transparent.hoverTrackColor", + 0x1A808080, + 0x1A808080, + ), + ) + +private fun readScrollBarColorForKey( + isDark: Boolean, + colorKey: String, + fallbackLight: Long, + fallbackDark: Long, +) = retrieveColorOrUnspecified(colorKey) + .takeOrElse { if (isDark) Color(fallbackDark) else Color(fallbackLight) } + +private fun readScrollbarMetrics(): ScrollbarMetrics = + if (hostOs.isMacOS) { + ScrollbarMetrics( + thumbCornerSize = CornerSize(percent = 100), + thumbThickness = 8.dp, + thumbThicknessExpanded = 14.dp, + minThumbLength = 20.dp, + trackPadding = PaddingValues(2.dp), + trackPaddingExpanded = PaddingValues(2.dp), + ) + } else { + ScrollbarMetrics( + thumbCornerSize = CornerSize(0), + thumbThickness = 8.dp, + thumbThicknessExpanded = 8.dp, + minThumbLength = 16.dp, + trackPadding = PaddingValues(), + trackPaddingExpanded = PaddingValues(), + ) + } + +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, + expandAnimationDuration: Duration = 125.milliseconds, + lingerDuration: Duration = 700.milliseconds, +): ScrollbarVisibility.WhenScrolling = + ScrollbarVisibility.WhenScrolling( + appearAnimationDuration = appearAnimationDuration, + disappearAnimationDuration = disappearAnimationDuration, + expandAnimationDuration = expandAnimationDuration, + lingerDuration = lingerDuration, + ) diff --git a/int-ui/int-ui-standalone/api/int-ui-standalone.api b/int-ui/int-ui-standalone/api/int-ui-standalone.api index 57d0a06f5..d562c495e 100644 --- a/int-ui/int-ui-standalone/api/int-ui-standalone.api +++ b/int-ui/int-ui-standalone/api/int-ui-standalone.api @@ -116,8 +116,8 @@ public final class org/jetbrains/jewel/intui/standalone/styling/IntUiDefaultTabC public final class org/jetbrains/jewel/intui/standalone/styling/IntUiDefaultTabStyleFactory { public static final field $stable I public static final field INSTANCE Lorg/jetbrains/jewel/intui/standalone/styling/IntUiDefaultTabStyleFactory; - public final fun dark (Lorg/jetbrains/jewel/ui/component/styling/TabColors;Lorg/jetbrains/jewel/ui/component/styling/TabMetrics;Lorg/jetbrains/jewel/ui/component/styling/TabIcons;Lorg/jetbrains/jewel/ui/component/styling/TabContentAlpha;Landroidx/compose/runtime/Composer;II)Lorg/jetbrains/jewel/ui/component/styling/TabStyle; - public final fun light (Lorg/jetbrains/jewel/ui/component/styling/TabColors;Lorg/jetbrains/jewel/ui/component/styling/TabMetrics;Lorg/jetbrains/jewel/ui/component/styling/TabIcons;Lorg/jetbrains/jewel/ui/component/styling/TabContentAlpha;Landroidx/compose/runtime/Composer;II)Lorg/jetbrains/jewel/ui/component/styling/TabStyle; + public final fun dark (Lorg/jetbrains/jewel/ui/component/styling/TabColors;Lorg/jetbrains/jewel/ui/component/styling/TabMetrics;Lorg/jetbrains/jewel/ui/component/styling/TabIcons;Lorg/jetbrains/jewel/ui/component/styling/TabContentAlpha;Lorg/jetbrains/jewel/ui/component/styling/ScrollbarStyle;Landroidx/compose/runtime/Composer;II)Lorg/jetbrains/jewel/ui/component/styling/TabStyle; + public final fun light (Lorg/jetbrains/jewel/ui/component/styling/TabColors;Lorg/jetbrains/jewel/ui/component/styling/TabMetrics;Lorg/jetbrains/jewel/ui/component/styling/TabIcons;Lorg/jetbrains/jewel/ui/component/styling/TabContentAlpha;Lorg/jetbrains/jewel/ui/component/styling/ScrollbarStyle;Landroidx/compose/runtime/Composer;II)Lorg/jetbrains/jewel/ui/component/styling/TabStyle; } public final class org/jetbrains/jewel/intui/standalone/styling/IntUiDividerStyleKt { @@ -150,8 +150,8 @@ public final class org/jetbrains/jewel/intui/standalone/styling/IntUiEditorTabCo public final class org/jetbrains/jewel/intui/standalone/styling/IntUiEditorTabStyleFactory { public static final field $stable I public static final field INSTANCE Lorg/jetbrains/jewel/intui/standalone/styling/IntUiEditorTabStyleFactory; - public final fun dark (Lorg/jetbrains/jewel/ui/component/styling/TabColors;Lorg/jetbrains/jewel/ui/component/styling/TabMetrics;Lorg/jetbrains/jewel/ui/component/styling/TabIcons;Lorg/jetbrains/jewel/ui/component/styling/TabContentAlpha;Landroidx/compose/runtime/Composer;II)Lorg/jetbrains/jewel/ui/component/styling/TabStyle; - public final fun light (Lorg/jetbrains/jewel/ui/component/styling/TabColors;Lorg/jetbrains/jewel/ui/component/styling/TabMetrics;Lorg/jetbrains/jewel/ui/component/styling/TabIcons;Lorg/jetbrains/jewel/ui/component/styling/TabContentAlpha;Landroidx/compose/runtime/Composer;II)Lorg/jetbrains/jewel/ui/component/styling/TabStyle; + public final fun dark (Lorg/jetbrains/jewel/ui/component/styling/TabColors;Lorg/jetbrains/jewel/ui/component/styling/TabMetrics;Lorg/jetbrains/jewel/ui/component/styling/TabIcons;Lorg/jetbrains/jewel/ui/component/styling/TabContentAlpha;Lorg/jetbrains/jewel/ui/component/styling/ScrollbarStyle;Landroidx/compose/runtime/Composer;II)Lorg/jetbrains/jewel/ui/component/styling/TabStyle; + public final fun light (Lorg/jetbrains/jewel/ui/component/styling/TabColors;Lorg/jetbrains/jewel/ui/component/styling/TabMetrics;Lorg/jetbrains/jewel/ui/component/styling/TabIcons;Lorg/jetbrains/jewel/ui/component/styling/TabContentAlpha;Lorg/jetbrains/jewel/ui/component/styling/ScrollbarStyle;Landroidx/compose/runtime/Composer;II)Lorg/jetbrains/jewel/ui/component/styling/TabStyle; } public final class org/jetbrains/jewel/intui/standalone/styling/IntUiGroupHeaderStylingKt { @@ -252,12 +252,34 @@ public final class org/jetbrains/jewel/intui/standalone/styling/IntUiRadioButton } public final class org/jetbrains/jewel/intui/standalone/styling/IntUiScrollbarStylingKt { - public static final fun dark-45ZY6uE (Lorg/jetbrains/jewel/ui/component/styling/ScrollbarStyle$Companion;Lorg/jetbrains/jewel/ui/component/styling/ScrollbarColors;Lorg/jetbrains/jewel/ui/component/styling/ScrollbarMetrics;JLandroidx/compose/runtime/Composer;II)Lorg/jetbrains/jewel/ui/component/styling/ScrollbarStyle; - public static final fun dark-RIQooxk (Lorg/jetbrains/jewel/ui/component/styling/ScrollbarColors$Companion;JJLandroidx/compose/runtime/Composer;II)Lorg/jetbrains/jewel/ui/component/styling/ScrollbarColors; - public static final fun defaults--JS8el8 (Lorg/jetbrains/jewel/ui/component/styling/ScrollbarMetrics$Companion;Landroidx/compose/foundation/shape/CornerSize;FFLandroidx/compose/foundation/layout/PaddingValues;)Lorg/jetbrains/jewel/ui/component/styling/ScrollbarMetrics; - public static synthetic fun defaults--JS8el8$default (Lorg/jetbrains/jewel/ui/component/styling/ScrollbarMetrics$Companion;Landroidx/compose/foundation/shape/CornerSize;FFLandroidx/compose/foundation/layout/PaddingValues;ILjava/lang/Object;)Lorg/jetbrains/jewel/ui/component/styling/ScrollbarMetrics; - public static final fun light-45ZY6uE (Lorg/jetbrains/jewel/ui/component/styling/ScrollbarStyle$Companion;Lorg/jetbrains/jewel/ui/component/styling/ScrollbarColors;Lorg/jetbrains/jewel/ui/component/styling/ScrollbarMetrics;JLandroidx/compose/runtime/Composer;II)Lorg/jetbrains/jewel/ui/component/styling/ScrollbarStyle; - public static final fun light-RIQooxk (Lorg/jetbrains/jewel/ui/component/styling/ScrollbarColors$Companion;JJLandroidx/compose/runtime/Composer;II)Lorg/jetbrains/jewel/ui/component/styling/ScrollbarColors; + public static final fun dark (Lorg/jetbrains/jewel/ui/component/styling/ScrollbarStyle$Companion;)Lorg/jetbrains/jewel/ui/component/styling/ScrollbarStyle; + public static final fun defaults-6ksGUsA (Lorg/jetbrains/jewel/ui/component/styling/ScrollbarVisibility$WhenScrolling$Companion;JJJJ)Lorg/jetbrains/jewel/ui/component/styling/ScrollbarVisibility$WhenScrolling; + public static synthetic fun defaults-6ksGUsA$default (Lorg/jetbrains/jewel/ui/component/styling/ScrollbarVisibility$WhenScrolling$Companion;JJJJILjava/lang/Object;)Lorg/jetbrains/jewel/ui/component/styling/ScrollbarVisibility$WhenScrolling; + public static final fun defaults-VkLD3kw (Lorg/jetbrains/jewel/ui/component/styling/ScrollbarMetrics$Companion;Landroidx/compose/foundation/shape/CornerSize;FFLandroidx/compose/foundation/layout/PaddingValues;FLandroidx/compose/foundation/layout/PaddingValues;)Lorg/jetbrains/jewel/ui/component/styling/ScrollbarMetrics; + public static synthetic fun defaults-VkLD3kw$default (Lorg/jetbrains/jewel/ui/component/styling/ScrollbarMetrics$Companion;Landroidx/compose/foundation/shape/CornerSize;FFLandroidx/compose/foundation/layout/PaddingValues;FLandroidx/compose/foundation/layout/PaddingValues;ILjava/lang/Object;)Lorg/jetbrains/jewel/ui/component/styling/ScrollbarMetrics; + public static final fun light (Lorg/jetbrains/jewel/ui/component/styling/ScrollbarStyle$Companion;)Lorg/jetbrains/jewel/ui/component/styling/ScrollbarStyle; + public static final fun linux-VkLD3kw (Lorg/jetbrains/jewel/ui/component/styling/ScrollbarMetrics$Companion;Landroidx/compose/foundation/shape/CornerSize;FFLandroidx/compose/foundation/layout/PaddingValues;FLandroidx/compose/foundation/layout/PaddingValues;)Lorg/jetbrains/jewel/ui/component/styling/ScrollbarMetrics; + public static synthetic fun linux-VkLD3kw$default (Lorg/jetbrains/jewel/ui/component/styling/ScrollbarMetrics$Companion;Landroidx/compose/foundation/shape/CornerSize;FFLandroidx/compose/foundation/layout/PaddingValues;FLandroidx/compose/foundation/layout/PaddingValues;ILjava/lang/Object;)Lorg/jetbrains/jewel/ui/component/styling/ScrollbarMetrics; + public static final fun macOs-VkLD3kw (Lorg/jetbrains/jewel/ui/component/styling/ScrollbarMetrics$Companion;Landroidx/compose/foundation/shape/CornerSize;FFLandroidx/compose/foundation/layout/PaddingValues;FLandroidx/compose/foundation/layout/PaddingValues;)Lorg/jetbrains/jewel/ui/component/styling/ScrollbarMetrics; + public static synthetic fun macOs-VkLD3kw$default (Lorg/jetbrains/jewel/ui/component/styling/ScrollbarMetrics$Companion;Landroidx/compose/foundation/shape/CornerSize;FFLandroidx/compose/foundation/layout/PaddingValues;FLandroidx/compose/foundation/layout/PaddingValues;ILjava/lang/Object;)Lorg/jetbrains/jewel/ui/component/styling/ScrollbarMetrics; + public static final fun macOsDark (Lorg/jetbrains/jewel/ui/component/styling/ScrollbarStyle$Companion;Lorg/jetbrains/jewel/ui/component/styling/ScrollbarColors;Lorg/jetbrains/jewel/ui/component/styling/ScrollbarMetrics;Lorg/jetbrains/jewel/ui/component/styling/TrackClickBehavior;Lorg/jetbrains/jewel/ui/component/styling/ScrollbarVisibility;)Lorg/jetbrains/jewel/ui/component/styling/ScrollbarStyle; + public static synthetic fun macOsDark$default (Lorg/jetbrains/jewel/ui/component/styling/ScrollbarStyle$Companion;Lorg/jetbrains/jewel/ui/component/styling/ScrollbarColors;Lorg/jetbrains/jewel/ui/component/styling/ScrollbarMetrics;Lorg/jetbrains/jewel/ui/component/styling/TrackClickBehavior;Lorg/jetbrains/jewel/ui/component/styling/ScrollbarVisibility;ILjava/lang/Object;)Lorg/jetbrains/jewel/ui/component/styling/ScrollbarStyle; + public static final fun macOsDark-iLRpYWo (Lorg/jetbrains/jewel/ui/component/styling/ScrollbarColors$Companion;JJJJJJJJ)Lorg/jetbrains/jewel/ui/component/styling/ScrollbarColors; + public static synthetic fun macOsDark-iLRpYWo$default (Lorg/jetbrains/jewel/ui/component/styling/ScrollbarColors$Companion;JJJJJJJJILjava/lang/Object;)Lorg/jetbrains/jewel/ui/component/styling/ScrollbarColors; + public static final fun macOsLight (Lorg/jetbrains/jewel/ui/component/styling/ScrollbarStyle$Companion;Lorg/jetbrains/jewel/ui/component/styling/ScrollbarColors;Lorg/jetbrains/jewel/ui/component/styling/ScrollbarMetrics;Lorg/jetbrains/jewel/ui/component/styling/TrackClickBehavior;Lorg/jetbrains/jewel/ui/component/styling/ScrollbarVisibility;)Lorg/jetbrains/jewel/ui/component/styling/ScrollbarStyle; + public static synthetic fun macOsLight$default (Lorg/jetbrains/jewel/ui/component/styling/ScrollbarStyle$Companion;Lorg/jetbrains/jewel/ui/component/styling/ScrollbarColors;Lorg/jetbrains/jewel/ui/component/styling/ScrollbarMetrics;Lorg/jetbrains/jewel/ui/component/styling/TrackClickBehavior;Lorg/jetbrains/jewel/ui/component/styling/ScrollbarVisibility;ILjava/lang/Object;)Lorg/jetbrains/jewel/ui/component/styling/ScrollbarStyle; + public static final fun macOsLight-iLRpYWo (Lorg/jetbrains/jewel/ui/component/styling/ScrollbarColors$Companion;JJJJJJJJ)Lorg/jetbrains/jewel/ui/component/styling/ScrollbarColors; + public static synthetic fun macOsLight-iLRpYWo$default (Lorg/jetbrains/jewel/ui/component/styling/ScrollbarColors$Companion;JJJJJJJJILjava/lang/Object;)Lorg/jetbrains/jewel/ui/component/styling/ScrollbarColors; + public static final fun winOsDark (Lorg/jetbrains/jewel/ui/component/styling/ScrollbarStyle$Companion;Lorg/jetbrains/jewel/ui/component/styling/ScrollbarColors;Lorg/jetbrains/jewel/ui/component/styling/ScrollbarMetrics;Lorg/jetbrains/jewel/ui/component/styling/TrackClickBehavior;Lorg/jetbrains/jewel/ui/component/styling/ScrollbarVisibility;)Lorg/jetbrains/jewel/ui/component/styling/ScrollbarStyle; + public static synthetic fun winOsDark$default (Lorg/jetbrains/jewel/ui/component/styling/ScrollbarStyle$Companion;Lorg/jetbrains/jewel/ui/component/styling/ScrollbarColors;Lorg/jetbrains/jewel/ui/component/styling/ScrollbarMetrics;Lorg/jetbrains/jewel/ui/component/styling/TrackClickBehavior;Lorg/jetbrains/jewel/ui/component/styling/ScrollbarVisibility;ILjava/lang/Object;)Lorg/jetbrains/jewel/ui/component/styling/ScrollbarStyle; + public static final fun winOsDark-iLRpYWo (Lorg/jetbrains/jewel/ui/component/styling/ScrollbarColors$Companion;JJJJJJJJ)Lorg/jetbrains/jewel/ui/component/styling/ScrollbarColors; + public static synthetic fun winOsDark-iLRpYWo$default (Lorg/jetbrains/jewel/ui/component/styling/ScrollbarColors$Companion;JJJJJJJJILjava/lang/Object;)Lorg/jetbrains/jewel/ui/component/styling/ScrollbarColors; + public static final fun winOsLight (Lorg/jetbrains/jewel/ui/component/styling/ScrollbarStyle$Companion;Lorg/jetbrains/jewel/ui/component/styling/ScrollbarColors;Lorg/jetbrains/jewel/ui/component/styling/ScrollbarMetrics;Lorg/jetbrains/jewel/ui/component/styling/TrackClickBehavior;Lorg/jetbrains/jewel/ui/component/styling/ScrollbarVisibility;)Lorg/jetbrains/jewel/ui/component/styling/ScrollbarStyle; + public static synthetic fun winOsLight$default (Lorg/jetbrains/jewel/ui/component/styling/ScrollbarStyle$Companion;Lorg/jetbrains/jewel/ui/component/styling/ScrollbarColors;Lorg/jetbrains/jewel/ui/component/styling/ScrollbarMetrics;Lorg/jetbrains/jewel/ui/component/styling/TrackClickBehavior;Lorg/jetbrains/jewel/ui/component/styling/ScrollbarVisibility;ILjava/lang/Object;)Lorg/jetbrains/jewel/ui/component/styling/ScrollbarStyle; + public static final fun winOsLight-iLRpYWo (Lorg/jetbrains/jewel/ui/component/styling/ScrollbarColors$Companion;JJJJJJJJ)Lorg/jetbrains/jewel/ui/component/styling/ScrollbarColors; + public static synthetic fun winOsLight-iLRpYWo$default (Lorg/jetbrains/jewel/ui/component/styling/ScrollbarColors$Companion;JJJJJJJJILjava/lang/Object;)Lorg/jetbrains/jewel/ui/component/styling/ScrollbarColors; + public static final fun windows-VkLD3kw (Lorg/jetbrains/jewel/ui/component/styling/ScrollbarMetrics$Companion;Landroidx/compose/foundation/shape/CornerSize;FFLandroidx/compose/foundation/layout/PaddingValues;FLandroidx/compose/foundation/layout/PaddingValues;)Lorg/jetbrains/jewel/ui/component/styling/ScrollbarMetrics; + public static synthetic fun windows-VkLD3kw$default (Lorg/jetbrains/jewel/ui/component/styling/ScrollbarMetrics$Companion;Landroidx/compose/foundation/shape/CornerSize;FFLandroidx/compose/foundation/layout/PaddingValues;FLandroidx/compose/foundation/layout/PaddingValues;ILjava/lang/Object;)Lorg/jetbrains/jewel/ui/component/styling/ScrollbarMetrics; } public final class org/jetbrains/jewel/intui/standalone/styling/IntUiSegmentedControlButtonStylingKt { 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 71c040779..9d91b16b6 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 @@ -2,45 +2,252 @@ package org.jetbrains.jewel.intui.standalone.styling import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.shape.CornerSize -import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import org.jetbrains.jewel.ui.component.styling.ScrollbarColors import org.jetbrains.jewel.ui.component.styling.ScrollbarMetrics import org.jetbrains.jewel.ui.component.styling.ScrollbarStyle +import org.jetbrains.jewel.ui.component.styling.ScrollbarVisibility +import org.jetbrains.jewel.ui.component.styling.TrackClickBehavior +import org.jetbrains.skiko.hostOs import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds -@Composable -public fun ScrollbarStyle.Companion.light( - colors: ScrollbarColors = ScrollbarColors.light(), - metrics: ScrollbarMetrics = ScrollbarMetrics.defaults(), - hoverDuration: Duration = 300.milliseconds, -): ScrollbarStyle = ScrollbarStyle(colors, metrics, hoverDuration) - -@Composable -public fun ScrollbarStyle.Companion.dark( - colors: ScrollbarColors = ScrollbarColors.dark(), - metrics: ScrollbarMetrics = ScrollbarMetrics.defaults(), - hoverDuration: Duration = 300.milliseconds, -): ScrollbarStyle = ScrollbarStyle(colors, metrics, hoverDuration) - -@Composable -public fun ScrollbarColors.Companion.light( - thumbBackground: Color = Color(0x33000000), +public fun ScrollbarStyle.Companion.macOsLight( + colors: ScrollbarColors = ScrollbarColors.macOsLight(), + metrics: ScrollbarMetrics = provideScrollbarMetrics(), + trackClickBehavior: TrackClickBehavior = TrackClickBehavior.NextPage, + scrollbarVisibility: ScrollbarVisibility = ScrollbarVisibility.WhenScrolling.defaults(), +): ScrollbarStyle = + ScrollbarStyle( + colors = colors, + metrics = metrics, + trackClickBehavior = trackClickBehavior, + scrollbarVisibility = scrollbarVisibility, + ) + +public fun ScrollbarStyle.Companion.macOsDark( + colors: ScrollbarColors = ScrollbarColors.macOsDark(), + metrics: ScrollbarMetrics = provideScrollbarMetrics(), + trackClickBehavior: TrackClickBehavior = TrackClickBehavior.NextPage, + scrollbarVisibility: ScrollbarVisibility = ScrollbarVisibility.WhenScrolling.defaults(), +): ScrollbarStyle = + ScrollbarStyle( + colors = colors, + metrics = metrics, + trackClickBehavior = trackClickBehavior, + scrollbarVisibility = scrollbarVisibility, + ) + +public fun ScrollbarStyle.Companion.winOsDark( + colors: ScrollbarColors = ScrollbarColors.winOsDark(), + metrics: ScrollbarMetrics = provideScrollbarMetrics(), + trackClickBehavior: TrackClickBehavior = TrackClickBehavior.JumpToSpot, + scrollbarVisibility: ScrollbarVisibility = ScrollbarVisibility.AlwaysVisible, +): ScrollbarStyle = + ScrollbarStyle( + colors = colors, + metrics = metrics, + trackClickBehavior = trackClickBehavior, + scrollbarVisibility = scrollbarVisibility, + ) + +public fun ScrollbarStyle.Companion.winOsLight( + colors: ScrollbarColors = ScrollbarColors.winOsLight(), + metrics: ScrollbarMetrics = provideScrollbarMetrics(), + trackClickBehavior: TrackClickBehavior = TrackClickBehavior.JumpToSpot, + scrollbarVisibility: ScrollbarVisibility = ScrollbarVisibility.AlwaysVisible, +): ScrollbarStyle = + ScrollbarStyle( + colors = colors, + metrics = metrics, + trackClickBehavior = trackClickBehavior, + scrollbarVisibility = scrollbarVisibility, + ) + +public fun ScrollbarStyle.Companion.dark(): ScrollbarStyle = + if (hostOs.isMacOS) { + ScrollbarStyle.macOsDark() + } else { + ScrollbarStyle.winOsDark() + } + +public fun ScrollbarStyle.Companion.light(): ScrollbarStyle = + if (hostOs.isMacOS) { + ScrollbarStyle.macOsLight() + } else { + ScrollbarStyle.winOsLight() + } + +public fun ScrollbarVisibility.WhenScrolling.Companion.defaults( + appearAnimationDuration: Duration = 125.milliseconds, + disappearAnimationDuration: Duration = 125.milliseconds, + expandAnimationDuration: Duration = 125.milliseconds, + lingerDuration: Duration = 700.milliseconds, +): ScrollbarVisibility.WhenScrolling = + ScrollbarVisibility.WhenScrolling( + appearAnimationDuration = appearAnimationDuration, + disappearAnimationDuration = disappearAnimationDuration, + expandAnimationDuration = expandAnimationDuration, + lingerDuration = lingerDuration, + ) + +public fun ScrollbarColors.Companion.macOsLight( + thumbBackground: Color = Color(0x00000000), thumbBackgroundHovered: Color = Color(0x80000000), -): ScrollbarColors = ScrollbarColors(thumbBackground, thumbBackgroundHovered) + thumbBackgroundPressed: Color = thumbBackgroundHovered, + thumbBorder: Color = Color(0x33000000), + thumbBorderHovered: Color = Color(0x80000000), + thumbBorderPressed: Color = thumbBorderHovered, + trackBackground: Color = Color(0x00808080), + trackBackgroundHovered: Color = Color(0x00808080), +): ScrollbarColors = + ScrollbarColors( + thumbBackground, + thumbBackgroundHovered, + thumbBackgroundPressed, + thumbBorder, + thumbBorderHovered, + thumbBorderPressed, + trackBackground, + trackBackgroundHovered, + ) -@Composable -public fun ScrollbarColors.Companion.dark( +public fun ScrollbarColors.Companion.winOsLight( + thumbBackground: Color = Color(0x33737373), + thumbBackgroundHovered: Color = Color(0x47737373), + thumbBackgroundPressed: Color = thumbBackgroundHovered, + thumbBorder: Color = Color(0x33595959), + thumbBorderHovered: Color = Color(0x47595959), + thumbBorderPressed: Color = thumbBorderHovered, + trackBackground: Color = Color(0x00808080), + trackBackgroundHovered: Color = Color(0x1A808080), +): ScrollbarColors = + ScrollbarColors( + thumbBackground, + thumbBackgroundHovered, + thumbBackgroundPressed, + thumbBorder, + thumbBorderHovered, + thumbBorderPressed, + trackBackground, + trackBackgroundHovered, + ) + +public fun ScrollbarColors.Companion.macOsDark( thumbBackground: Color = Color(0x59808080), thumbBackgroundHovered: Color = Color(0x8C808080), -): ScrollbarColors = ScrollbarColors(thumbBackground, thumbBackgroundHovered) + thumbBackgroundPressed: Color = Color(0x8C808080), + thumbBorder: Color = Color(0x59262626), + thumbBorderHovered: Color = Color(0x8C262626), + thumbBorderPressed: Color = Color(0x8C262626), + trackBackground: Color = Color(0x00808080), + trackBackgroundHovered: Color = Color(0x1A808080), +): ScrollbarColors = + ScrollbarColors( + thumbBackground, + thumbBackgroundHovered, + thumbBackgroundPressed, + thumbBorder, + thumbBorderHovered, + thumbBorderPressed, + trackBackground, + trackBackgroundHovered, + ) + +public fun ScrollbarColors.Companion.winOsDark( + thumbBackground: Color = Color(0x47A6A6A6), + thumbBackgroundHovered: Color = Color(0x59A6A6A6), + thumbBackgroundPressed: Color = Color(0x59A6A6A6), + thumbBorder: Color = Color(0x47383838), + thumbBorderHovered: Color = Color(0x59A6A6A6), + thumbBorderPressed: Color = Color(0x59A6A6A6), + trackBackground: Color = Color(0x00808080), + trackBackgroundHovered: Color = Color(0x1A808080), +): ScrollbarColors = + ScrollbarColors( + thumbBackground, + thumbBackgroundHovered, + thumbBackgroundPressed, + thumbBorder, + thumbBorderHovered, + thumbBorderPressed, + trackBackground, + trackBackgroundHovered, + ) public fun ScrollbarMetrics.Companion.defaults( thumbCornerSize: CornerSize = CornerSize(100), thumbThickness: Dp = 8.dp, + minThumbLength: Dp = 20.dp, + trackPadding: PaddingValues = PaddingValues(2.dp), + thumbThicknessExpanded: Dp = 14.dp, + trackPaddingExpanded: PaddingValues = PaddingValues(2.dp), +): ScrollbarMetrics = + ScrollbarMetrics( + thumbCornerSize, + thumbThickness, + thumbThicknessExpanded, + minThumbLength, + trackPadding, + trackPaddingExpanded, + ) + +private fun provideScrollbarMetrics(): ScrollbarMetrics = + when { + hostOs.isMacOS -> ScrollbarMetrics.macOs() + hostOs.isLinux -> ScrollbarMetrics.linux() + else -> ScrollbarMetrics.windows() + } + +public fun ScrollbarMetrics.Companion.macOs( + thumbCornerSize: CornerSize = CornerSize(100), + thumbThickness: Dp = 8.dp, + minThumbLength: Dp = 20.dp, + trackPadding: PaddingValues = PaddingValues(2.dp), + thumbThicknessExpanded: Dp = 14.dp, + trackPaddingExpanded: PaddingValues = PaddingValues(2.dp), +): ScrollbarMetrics = + ScrollbarMetrics( + thumbCornerSize, + thumbThickness, + thumbThicknessExpanded, + minThumbLength, + trackPadding, + trackPaddingExpanded, + ) + +public fun ScrollbarMetrics.Companion.windows( + thumbCornerSize: CornerSize = CornerSize(0), + thumbThickness: Dp = 8.dp, + minThumbLength: Dp = 16.dp, + trackPadding: PaddingValues = PaddingValues(horizontal = 0.dp), + thumbThicknessExpanded: Dp = 8.dp, + trackPaddingExpanded: PaddingValues = PaddingValues(horizontal = 0.dp), +): ScrollbarMetrics = + ScrollbarMetrics( + thumbCornerSize, + thumbThickness, + thumbThicknessExpanded, + minThumbLength, + trackPadding, + trackPaddingExpanded, + ) + +public fun ScrollbarMetrics.Companion.linux( + thumbCornerSize: CornerSize = CornerSize(0), + thumbThickness: Dp = 8.dp, minThumbLength: Dp = 16.dp, - trackPadding: PaddingValues = PaddingValues(start = 7.dp, end = 3.dp), -): ScrollbarMetrics = ScrollbarMetrics(thumbCornerSize, thumbThickness, minThumbLength, trackPadding) + trackPadding: PaddingValues = PaddingValues(horizontal = 0.dp), + thumbThicknessExpanded: Dp = 8.dp, + trackPaddingExpanded: PaddingValues = PaddingValues(horizontal = 0.dp), +): ScrollbarMetrics = + ScrollbarMetrics( + thumbCornerSize, + thumbThickness, + thumbThicknessExpanded, + minThumbLength, + trackPadding, + trackPaddingExpanded, + ) diff --git a/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiTabStyling.kt b/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiTabStyling.kt index ae862a0e0..8cdfa108b 100644 --- a/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiTabStyling.kt +++ b/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiTabStyling.kt @@ -7,6 +7,7 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import org.jetbrains.jewel.intui.core.theme.IntUiDarkTheme import org.jetbrains.jewel.intui.core.theme.IntUiLightTheme +import org.jetbrains.jewel.ui.component.styling.ScrollbarStyle import org.jetbrains.jewel.ui.component.styling.TabColors import org.jetbrains.jewel.ui.component.styling.TabContentAlpha import org.jetbrains.jewel.ui.component.styling.TabIcons @@ -25,7 +26,8 @@ public object IntUiDefaultTabStyleFactory { metrics: TabMetrics = TabMetrics.defaults(), icons: TabIcons = TabIcons.defaults(), contentAlpha: TabContentAlpha = TabContentAlpha.default(), - ): TabStyle = TabStyle(colors, metrics, icons, contentAlpha) + scrollbarStyle: ScrollbarStyle = ScrollbarStyle.macOsLight(), + ): TabStyle = TabStyle(colors, metrics, icons, contentAlpha, scrollbarStyle) @Composable public fun dark( @@ -33,7 +35,8 @@ public object IntUiDefaultTabStyleFactory { metrics: TabMetrics = TabMetrics.defaults(), icons: TabIcons = TabIcons.defaults(), contentAlpha: TabContentAlpha = TabContentAlpha.default(), - ): TabStyle = TabStyle(colors, metrics, icons, contentAlpha) + scrollbarStyle: ScrollbarStyle = ScrollbarStyle.dark(), + ): TabStyle = TabStyle(colors, metrics, icons, contentAlpha, scrollbarStyle) } public val TabStyle.Companion.Editor: IntUiEditorTabStyleFactory @@ -46,7 +49,8 @@ public object IntUiEditorTabStyleFactory { metrics: TabMetrics = TabMetrics.defaults(), icons: TabIcons = TabIcons.defaults(), contentAlpha: TabContentAlpha = TabContentAlpha.editor(), - ): TabStyle = TabStyle(colors, metrics, icons, contentAlpha) + scrollbarStyle: ScrollbarStyle = ScrollbarStyle.light(), + ): TabStyle = TabStyle(colors, metrics, icons, contentAlpha, scrollbarStyle) @Composable public fun dark( @@ -54,7 +58,8 @@ public object IntUiEditorTabStyleFactory { metrics: TabMetrics = TabMetrics.defaults(), icons: TabIcons = TabIcons.defaults(), contentAlpha: TabContentAlpha = TabContentAlpha.editor(), - ): TabStyle = TabStyle(colors, metrics, icons, contentAlpha) + scrollbarStyle: ScrollbarStyle = ScrollbarStyle.dark(), + ): TabStyle = TabStyle(colors, metrics, icons, contentAlpha, scrollbarStyle) } public val TabColors.Companion.Default: IntUiDefaultTabColorsFactory diff --git a/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/view/component/Scrollbars.kt b/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/view/component/Scrollbars.kt new file mode 100644 index 000000000..11ee80a44 --- /dev/null +++ b/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/view/component/Scrollbars.kt @@ -0,0 +1,190 @@ +package org.jetbrains.jewel.samples.standalone.view.component + +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import org.jetbrains.jewel.foundation.theme.JewelTheme +import org.jetbrains.jewel.intui.standalone.styling.defaults +import org.jetbrains.jewel.intui.standalone.styling.macOsDark +import org.jetbrains.jewel.intui.standalone.styling.macOsLight +import org.jetbrains.jewel.intui.standalone.styling.winOsDark +import org.jetbrains.jewel.intui.standalone.styling.winOsLight +import org.jetbrains.jewel.samples.standalone.viewmodel.View +import org.jetbrains.jewel.ui.Orientation +import org.jetbrains.jewel.ui.component.CheckboxRow +import org.jetbrains.jewel.ui.component.Divider +import org.jetbrains.jewel.ui.component.Text +import org.jetbrains.jewel.ui.component.VerticalScrollbar +import org.jetbrains.jewel.ui.component.styling.ScrollbarStyle +import org.jetbrains.jewel.ui.component.styling.ScrollbarVisibility +import org.jetbrains.jewel.ui.theme.scrollbarStyle +import org.jetbrains.skiko.hostOs +import java.util.Locale + +@Composable +@View(title = "Scrollbars", position = 14, icon = "icons/components/scrollbar.svg") +fun Scrollbars() { + Column { + val isDark = JewelTheme.isDark + var alwaysVisible by remember { mutableStateOf(false) } + val initialStyle by remember { mutableStateOf(readStyle(hostOs.isMacOS, isDark)) } + var style by remember { mutableStateOf(initialStyle) } + + LaunchedEffect(alwaysVisible) { + style = + if (alwaysVisible) { + ScrollbarStyle( + colors = style.colors, + metrics = style.metrics, + trackClickBehavior = style.trackClickBehavior, + scrollbarVisibility = ScrollbarVisibility.AlwaysVisible, + ) + } else { + ScrollbarStyle( + colors = style.colors, + metrics = style.metrics, + trackClickBehavior = style.trackClickBehavior, + scrollbarVisibility = ScrollbarVisibility.WhenScrolling.defaults(), + ) + } + } + + CheckboxRow( + checked = alwaysVisible, + onCheckedChange = { alwaysVisible = it }, + text = "Always visible", + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Row( + Modifier.padding(horizontal = 16.dp).height(200.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Column { + Text("LazyColumn", fontSize = 18.sp) + Spacer(Modifier.height(8.dp)) + + Box(Modifier.border(1.dp, JewelTheme.globalColors.borders.normal)) { + val scrollState = rememberLazyListState() + LazyColumn( + Modifier + .width(200.dp) + .padding(end = JewelTheme.scrollbarStyle.metrics.thumbThicknessExpanded) + .align(Alignment.CenterStart), + verticalArrangement = Arrangement.spacedBy(4.dp), + state = scrollState, + ) { + items(LIST_ITEMS) { item -> + Column { + Text( + modifier = Modifier.padding(horizontal = 8.dp), + text = item, + ) + Divider(orientation = Orientation.Horizontal, color = Color.Gray) + } + } + } + VerticalScrollbar( + scrollState = scrollState, + modifier = Modifier.align(Alignment.CenterEnd), + style = style, + ) + } + } + Column { + Text("Column", fontSize = 18.sp) + Spacer(Modifier.height(8.dp)) + + Box(Modifier.border(1.dp, JewelTheme.globalColors.borders.normal)) { + val scrollState = rememberScrollState() + Column( + modifier = + Modifier + .verticalScroll(scrollState) + .padding(end = JewelTheme.scrollbarStyle.metrics.thumbThicknessExpanded) + .align(Alignment.CenterStart), + ) { + LIST_ITEMS.forEach { + Text( + modifier = Modifier.padding(horizontal = 8.dp), + text = it, + ) + } + } + VerticalScrollbar( + scrollState = scrollState, + modifier = Modifier.align(Alignment.CenterEnd), + style = style, + ) + } + } + } + } +} + +fun readStyle( + isMac: Boolean, + isDark: Boolean, +): ScrollbarStyle = + if (isDark) { + if (isMac) { + ScrollbarStyle.macOsDark() + } else { + ScrollbarStyle.winOsDark() + } + } else { + if (isMac) { + ScrollbarStyle.macOsLight() + } else { + ScrollbarStyle.winOsLight() + } + } + +@Suppress("SpellCheckingInspection") +private const val LOREM_IPSUM = + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. \n" + + "Sed auctor, neque in accumsan vehicula, enim purus vestibulum odio, non tristique dolor quam vel ipsum. \n" + + "Proin egestas, orci id hendrerit bibendum, nisl neque imperdiet nisl, a euismod nibh diam nec lectus. \n" + + "Duis euismod, quam nec aliquam iaculis, dolor lorem bibendum turpis, vel malesuada augue sapien vel mi. \n" + + "Quisque ut facilisis nibh. Maecenas euismod hendrerit sem, ac scelerisque odio auctor nec. \n" + + "Sed sit amet consequat eros. Donec nisl tellus, accumsan nec ligula in, eleifend sodales sem. \n" + + "Sed malesuada, nulla ac eleifend fermentum, nibh mi consequat quam, quis convallis lacus nunc eu dui. \n" + + "Pellentesque eget enim quis orci porttitor consequat sed sed quam. \n" + + "Sed aliquam, nisl et lacinia lacinia, diam nunc laoreet nisi, sit amet consectetur dolor lorem et sem. \n" + + "Duis ultricies, mauris in aliquam interdum, orci nulla finibus massa, a tristique urna sapien vel quam. \n" + + "Sed nec sapien nec dui rhoncus bibendum. Sed blandit bibendum libero." + +private val LIST_ITEMS = + LOREM_IPSUM + .split(",") + .map { lorem -> + lorem + .trim() + .replaceFirstChar { + if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() + } + } diff --git a/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/view/component/Tabs.kt b/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/view/component/Tabs.kt index 05f98397f..af1980523 100644 --- a/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/view/component/Tabs.kt +++ b/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/view/component/Tabs.kt @@ -3,6 +3,7 @@ package org.jetbrains.jewel.samples.standalone.view.component import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth @@ -10,6 +11,7 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.size import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue @@ -27,27 +29,31 @@ import org.jetbrains.jewel.ui.component.SimpleTabContent import org.jetbrains.jewel.ui.component.TabData import org.jetbrains.jewel.ui.component.TabStrip import org.jetbrains.jewel.ui.component.Text +import org.jetbrains.jewel.ui.component.styling.TabStyle import org.jetbrains.jewel.ui.icons.AllIconsKeys import org.jetbrains.jewel.ui.painter.hints.Stateful import org.jetbrains.jewel.ui.painter.rememberResourcePainterProvider import org.jetbrains.jewel.ui.theme.defaultTabStyle +import org.jetbrains.jewel.ui.theme.editorTabStyle import org.jetbrains.jewel.ui.util.thenIf import kotlin.math.max @Composable @View(title = "Tabs", position = 7, icon = "icons/components/tabs.svg") fun Tabs() { - Text("Default tabs", Modifier.fillMaxWidth()) - DefaultTabShowcase() + Column { + Text("Default tabs", Modifier.fillMaxWidth()) + DefaultTabShowcase() - Spacer(Modifier.height(16.dp)) - Text("Editor tabs", Modifier.fillMaxWidth()) - EditorTabShowcase() + Spacer(Modifier.height(16.dp)) + Text("Editor tabs", Modifier.fillMaxWidth()) + EditorTabShowcase() + } } @Composable private fun DefaultTabShowcase() { - var selectedTabIndex by remember { mutableStateOf(0) } + var selectedTabIndex by remember { mutableIntStateOf(0) } var tabIds by remember { mutableStateOf((1..12).toList()) } val maxId = remember(tabIds) { tabIds.maxOrNull() ?: 0 } @@ -80,12 +86,13 @@ private fun DefaultTabShowcase() { } } - TabStripWithAddButton(tabs) { + TabStripWithAddButton(tabs, JewelTheme.defaultTabStyle) { val insertionIndex = (selectedTabIndex + 1).coerceIn(0..tabIds.size) val nextTabId = maxId + 1 tabIds = - tabIds.toMutableList() + tabIds + .toMutableList() .apply { add(insertionIndex, nextTabId) } selectedTabIndex = insertionIndex } @@ -93,7 +100,7 @@ private fun DefaultTabShowcase() { @Composable private fun EditorTabShowcase() { - var selectedTabIndex by remember { mutableStateOf(0) } + var selectedTabIndex by remember { mutableIntStateOf(0) } var tabIds by remember { mutableStateOf((1..12).toList()) } val maxId = remember(tabIds) { tabIds.maxOrNull() ?: 0 } @@ -145,12 +152,13 @@ private fun EditorTabShowcase() { } } - TabStripWithAddButton(tabs) { + TabStripWithAddButton(tabs, JewelTheme.editorTabStyle) { val insertionIndex = (selectedTabIndex + 1).coerceIn(0..tabIds.size) val nextTabId = maxId + 1 tabIds = - tabIds.toMutableList() + tabIds + .toMutableList() .apply { add(insertionIndex, nextTabId) } selectedTabIndex = insertionIndex } @@ -159,10 +167,11 @@ private fun EditorTabShowcase() { @Composable private fun TabStripWithAddButton( tabs: List, + style: TabStyle, onAddClick: () -> Unit, ) { Row(verticalAlignment = Alignment.CenterVertically) { - TabStrip(tabs, modifier = Modifier.weight(1f)) + TabStrip(tabs, style, modifier = Modifier.weight(1f)) IconButton( onClick = onAddClick, diff --git a/samples/standalone/src/main/resources/icons/components/scrollbar.svg b/samples/standalone/src/main/resources/icons/components/scrollbar.svg new file mode 100644 index 000000000..a6d33be5c --- /dev/null +++ b/samples/standalone/src/main/resources/icons/components/scrollbar.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/samples/standalone/src/main/resources/icons/components/scrollbar_dark.svg b/samples/standalone/src/main/resources/icons/components/scrollbar_dark.svg new file mode 100644 index 000000000..d7510a3b5 --- /dev/null +++ b/samples/standalone/src/main/resources/icons/components/scrollbar_dark.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/ui/api/ui.api b/ui/api/ui.api index 4cb51e655..7ea1c929b 100644 --- a/ui/api/ui.api +++ b/ui/api/ui.api @@ -578,8 +578,10 @@ public final class org/jetbrains/jewel/ui/component/RadioButtonState$Companion { } public final class org/jetbrains/jewel/ui/component/ScrollbarsKt { + public static final fun HorizontalScrollbar (Landroidx/compose/foundation/gestures/ScrollableState;Landroidx/compose/ui/Modifier;ZLandroidx/compose/foundation/interaction/MutableInteractionSource;Lorg/jetbrains/jewel/ui/component/styling/ScrollbarStyle;Landroidx/compose/runtime/Composer;II)V public static final fun HorizontalScrollbar (Landroidx/compose/foundation/v2/ScrollbarAdapter;Landroidx/compose/ui/Modifier;ZLandroidx/compose/foundation/interaction/MutableInteractionSource;Lorg/jetbrains/jewel/ui/component/styling/ScrollbarStyle;Landroidx/compose/runtime/Composer;II)V - public static final fun TabStripHorizontalScrollbar (Landroidx/compose/foundation/v2/ScrollbarAdapter;Landroidx/compose/ui/Modifier;ZLandroidx/compose/foundation/interaction/MutableInteractionSource;Lorg/jetbrains/jewel/ui/component/styling/ScrollbarStyle;Landroidx/compose/runtime/Composer;II)V + public static final fun TabStripHorizontalScrollbar (Landroidx/compose/foundation/v2/ScrollbarAdapter;Lorg/jetbrains/jewel/ui/component/styling/ScrollbarStyle;Landroidx/compose/ui/Modifier;ZLandroidx/compose/foundation/interaction/MutableInteractionSource;Landroidx/compose/runtime/Composer;II)V + public static final fun VerticalScrollbar (Landroidx/compose/foundation/gestures/ScrollableState;Landroidx/compose/ui/Modifier;ZLandroidx/compose/foundation/interaction/MutableInteractionSource;Lorg/jetbrains/jewel/ui/component/styling/ScrollbarStyle;Landroidx/compose/runtime/Composer;II)V public static final fun VerticalScrollbar (Landroidx/compose/foundation/v2/ScrollbarAdapter;Landroidx/compose/ui/Modifier;ZLandroidx/compose/foundation/interaction/MutableInteractionSource;Lorg/jetbrains/jewel/ui/component/styling/ScrollbarStyle;Landroidx/compose/runtime/Composer;II)V } @@ -824,7 +826,7 @@ public final class org/jetbrains/jewel/ui/component/TabState$Companion { } public final class org/jetbrains/jewel/ui/component/TabStripKt { - public static final fun TabStrip (Ljava/util/List;Landroidx/compose/ui/Modifier;ZLandroidx/compose/runtime/Composer;II)V + public static final fun TabStrip (Ljava/util/List;Lorg/jetbrains/jewel/ui/component/styling/TabStyle;Landroidx/compose/ui/Modifier;ZLandroidx/compose/runtime/Composer;II)V } public final class org/jetbrains/jewel/ui/component/TabStripState : org/jetbrains/jewel/foundation/state/FocusableComponentState { @@ -1858,10 +1860,16 @@ public final class org/jetbrains/jewel/ui/component/styling/RadioButtonStylingKt public final class org/jetbrains/jewel/ui/component/styling/ScrollbarColors { public static final field $stable I public static final field Companion Lorg/jetbrains/jewel/ui/component/styling/ScrollbarColors$Companion; - public synthetic fun (JJLkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (JJJJJJJJLkotlin/jvm/internal/DefaultConstructorMarker;)V public fun equals (Ljava/lang/Object;)Z public final fun getThumbBackground-0d7_KjU ()J public final fun getThumbBackgroundHovered-0d7_KjU ()J + public final fun getThumbBackgroundPressed-0d7_KjU ()J + public final fun getThumbBorder-0d7_KjU ()J + public final fun getThumbBorderHovered-0d7_KjU ()J + public final fun getThumbBorderPressed-0d7_KjU ()J + public final fun getTrackBackground-0d7_KjU ()J + public final fun getTrackBackgroundHovered-0d7_KjU ()J public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -1872,12 +1880,14 @@ public final class org/jetbrains/jewel/ui/component/styling/ScrollbarColors$Comp public final class org/jetbrains/jewel/ui/component/styling/ScrollbarMetrics { public static final field $stable I public static final field Companion Lorg/jetbrains/jewel/ui/component/styling/ScrollbarMetrics$Companion; - public synthetic fun (Landroidx/compose/foundation/shape/CornerSize;FFLandroidx/compose/foundation/layout/PaddingValues;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (Landroidx/compose/foundation/shape/CornerSize;FFFLandroidx/compose/foundation/layout/PaddingValues;Landroidx/compose/foundation/layout/PaddingValues;Lkotlin/jvm/internal/DefaultConstructorMarker;)V public fun equals (Ljava/lang/Object;)Z public final fun getMinThumbLength-D9Ej5fM ()F public final fun getThumbCornerSize ()Landroidx/compose/foundation/shape/CornerSize; public final fun getThumbThickness-D9Ej5fM ()F + public final fun getThumbThicknessExpanded-D9Ej5fM ()F public final fun getTrackPadding ()Landroidx/compose/foundation/layout/PaddingValues; + public final fun getTrackPaddingExpanded ()Landroidx/compose/foundation/layout/PaddingValues; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -1888,11 +1898,12 @@ public final class org/jetbrains/jewel/ui/component/styling/ScrollbarMetrics$Com public final class org/jetbrains/jewel/ui/component/styling/ScrollbarStyle { public static final field $stable I public static final field Companion Lorg/jetbrains/jewel/ui/component/styling/ScrollbarStyle$Companion; - public synthetic fun (Lorg/jetbrains/jewel/ui/component/styling/ScrollbarColors;Lorg/jetbrains/jewel/ui/component/styling/ScrollbarMetrics;JLkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lorg/jetbrains/jewel/ui/component/styling/ScrollbarColors;Lorg/jetbrains/jewel/ui/component/styling/ScrollbarMetrics;Lorg/jetbrains/jewel/ui/component/styling/TrackClickBehavior;Lorg/jetbrains/jewel/ui/component/styling/ScrollbarVisibility;)V public fun equals (Ljava/lang/Object;)Z public final fun getColors ()Lorg/jetbrains/jewel/ui/component/styling/ScrollbarColors; - public final fun getHoverDuration-UwyO8pc ()J public final fun getMetrics ()Lorg/jetbrains/jewel/ui/component/styling/ScrollbarMetrics; + public final fun getScrollbarVisibility ()Lorg/jetbrains/jewel/ui/component/styling/ScrollbarVisibility; + public final fun getTrackClickBehavior ()Lorg/jetbrains/jewel/ui/component/styling/TrackClickBehavior; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -1904,6 +1915,33 @@ public final class org/jetbrains/jewel/ui/component/styling/ScrollbarStylingKt { public static final fun getLocalScrollbarStyle ()Landroidx/compose/runtime/ProvidableCompositionLocal; } +public abstract interface class org/jetbrains/jewel/ui/component/styling/ScrollbarVisibility { +} + +public final class org/jetbrains/jewel/ui/component/styling/ScrollbarVisibility$AlwaysVisible : org/jetbrains/jewel/ui/component/styling/ScrollbarVisibility { + public static final field $stable I + public static final field INSTANCE Lorg/jetbrains/jewel/ui/component/styling/ScrollbarVisibility$AlwaysVisible; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class org/jetbrains/jewel/ui/component/styling/ScrollbarVisibility$WhenScrolling : org/jetbrains/jewel/ui/component/styling/ScrollbarVisibility { + public static final field $stable I + public static final field Companion Lorg/jetbrains/jewel/ui/component/styling/ScrollbarVisibility$WhenScrolling$Companion; + public synthetic fun (JJJJLkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun equals (Ljava/lang/Object;)Z + public final fun getAppearAnimationDuration-UwyO8pc ()J + public final fun getDisappearAnimationDuration-UwyO8pc ()J + public final fun getExpandAnimationDuration-UwyO8pc ()J + public final fun getLingerDuration-UwyO8pc ()J + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class org/jetbrains/jewel/ui/component/styling/ScrollbarVisibility$WhenScrolling$Companion { +} + public final class org/jetbrains/jewel/ui/component/styling/SegmentedControlButtonColors { public static final field $stable I public static final field Companion Lorg/jetbrains/jewel/ui/component/styling/SegmentedControlButtonColors$Companion; @@ -2177,12 +2215,13 @@ public final class org/jetbrains/jewel/ui/component/styling/TabMetrics$Companion public final class org/jetbrains/jewel/ui/component/styling/TabStyle { public static final field $stable I public static final field Companion Lorg/jetbrains/jewel/ui/component/styling/TabStyle$Companion; - public fun (Lorg/jetbrains/jewel/ui/component/styling/TabColors;Lorg/jetbrains/jewel/ui/component/styling/TabMetrics;Lorg/jetbrains/jewel/ui/component/styling/TabIcons;Lorg/jetbrains/jewel/ui/component/styling/TabContentAlpha;)V + public fun (Lorg/jetbrains/jewel/ui/component/styling/TabColors;Lorg/jetbrains/jewel/ui/component/styling/TabMetrics;Lorg/jetbrains/jewel/ui/component/styling/TabIcons;Lorg/jetbrains/jewel/ui/component/styling/TabContentAlpha;Lorg/jetbrains/jewel/ui/component/styling/ScrollbarStyle;)V public fun equals (Ljava/lang/Object;)Z public final fun getColors ()Lorg/jetbrains/jewel/ui/component/styling/TabColors; public final fun getContentAlpha ()Lorg/jetbrains/jewel/ui/component/styling/TabContentAlpha; public final fun getIcons ()Lorg/jetbrains/jewel/ui/component/styling/TabIcons; public final fun getMetrics ()Lorg/jetbrains/jewel/ui/component/styling/TabMetrics; + public final fun getScrollbarStyle ()Lorg/jetbrains/jewel/ui/component/styling/ScrollbarStyle; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -2395,6 +2434,14 @@ public final class org/jetbrains/jewel/ui/component/styling/TooltipStylingKt { public static final fun getLocalTooltipStyle ()Landroidx/compose/runtime/ProvidableCompositionLocal; } +public final class org/jetbrains/jewel/ui/component/styling/TrackClickBehavior : java/lang/Enum { + public static final field JumpToSpot Lorg/jetbrains/jewel/ui/component/styling/TrackClickBehavior; + public static final field NextPage Lorg/jetbrains/jewel/ui/component/styling/TrackClickBehavior; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public static fun valueOf (Ljava/lang/String;)Lorg/jetbrains/jewel/ui/component/styling/TrackClickBehavior; + public static fun values ()[Lorg/jetbrains/jewel/ui/component/styling/TrackClickBehavior; +} + public abstract interface class org/jetbrains/jewel/ui/icon/IconKey { public abstract fun getIconClass ()Ljava/lang/Class; public abstract fun path (Z)Ljava/lang/String; 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 f1f9e6e97..eead891dd 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 @@ -1,55 +1,246 @@ package org.jetbrains.jewel.ui.component -import androidx.compose.foundation.HorizontalScrollbar -import androidx.compose.foundation.LocalScrollbarStyle -import androidx.compose.foundation.VerticalScrollbar +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.animateContentSize +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.ScrollState +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.ScrollableState +import androidx.compose.foundation.gestures.awaitEachGesture +import androidx.compose.foundation.gestures.awaitFirstDown +import androidx.compose.foundation.gestures.awaitHorizontalDragOrCancellation +import androidx.compose.foundation.gestures.awaitVerticalDragOrCancellation +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.gestures.drag +import androidx.compose.foundation.gestures.scrollable +import androidx.compose.foundation.hoverable +import androidx.compose.foundation.interaction.DragInteraction import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsHoveredAsState +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.grid.LazyGridState +import androidx.compose.foundation.rememberScrollbarAdapter import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.TextFieldScrollState import androidx.compose.foundation.v2.ScrollbarAdapter +import androidx.compose.foundation.v2.maxScrollOffset import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.PointerInputScope +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.input.pointer.positionChange +import androidx.compose.ui.layout.Layout +import androidx.compose.ui.layout.MeasurePolicy +import androidx.compose.ui.layout.layoutId +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.constrainHeight +import androidx.compose.ui.unit.constrainWidth import androidx.compose.ui.unit.dp +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import org.jetbrains.jewel.foundation.theme.JewelTheme import org.jetbrains.jewel.ui.component.styling.ScrollbarStyle +import org.jetbrains.jewel.ui.component.styling.ScrollbarVisibility.AlwaysVisible +import org.jetbrains.jewel.ui.component.styling.ScrollbarVisibility.WhenScrolling +import org.jetbrains.jewel.ui.component.styling.TrackClickBehavior.JumpToSpot +import org.jetbrains.jewel.ui.component.styling.TrackClickBehavior.NextPage import org.jetbrains.jewel.ui.theme.scrollbarStyle -import kotlin.time.DurationUnit -import androidx.compose.foundation.ScrollbarStyle as ComposeScrollbarStyle +import org.jetbrains.jewel.ui.util.thenIf +import kotlin.math.roundToInt @Composable public fun VerticalScrollbar( - adapter: ScrollbarAdapter, + scrollState: ScrollableState, modifier: Modifier = Modifier, reverseLayout: Boolean = false, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, style: ScrollbarStyle = JewelTheme.scrollbarStyle, ) { - val shape by remember { mutableStateOf(RoundedCornerShape(style.metrics.thumbCornerSize)) } - val hoverDurationMillis by remember { mutableStateOf(style.hoverDuration.inWholeMilliseconds) } - - val composeScrollbarStyle = - ComposeScrollbarStyle( - minimalHeight = style.metrics.minThumbLength, - thickness = style.metrics.thumbThickness, - shape = shape, - hoverDurationMillis = hoverDurationMillis.toInt(), - unhoverColor = style.colors.thumbBackground, - hoverColor = style.colors.thumbBackgroundHovered, - ) + MyScrollbar( + scrollState = scrollState, + modifier = modifier, + reverseLayout = reverseLayout, + interactionSource = interactionSource, + isVertical = true, + style = style, + ) +} - CompositionLocalProvider(LocalScrollbarStyle provides composeScrollbarStyle) { - VerticalScrollbar( - adapter = adapter, - modifier = modifier.padding(style.metrics.trackPadding), - reverseLayout = reverseLayout, - style = LocalScrollbarStyle.current, - interactionSource = interactionSource, - ) +@Composable +public fun HorizontalScrollbar( + scrollState: ScrollableState, + modifier: Modifier = Modifier, + reverseLayout: Boolean = false, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + style: ScrollbarStyle = JewelTheme.scrollbarStyle, +) { + MyScrollbar( + scrollState = scrollState, + modifier = modifier, + reverseLayout = reverseLayout, + interactionSource = interactionSource, + isVertical = false, + style = style, + ) +} + +@Composable +private fun MyScrollbar( + scrollState: ScrollableState, + modifier: Modifier = Modifier, + reverseLayout: Boolean = false, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + isVertical: Boolean, + style: ScrollbarStyle, +) { + // Click to scroll + var clickPosition by remember { mutableIntStateOf(0) } + val scrollbarWidth = remember { mutableIntStateOf(0) } + val scrollbarHeight = remember { mutableIntStateOf(0) } + LaunchedEffect(clickPosition) { + if (scrollState is ScrollState) { + if (scrollbarHeight.value == 0) return@LaunchedEffect + + val jumpTo = when (style.trackClickBehavior) { + NextPage -> scrollbarHeight.value + scrollState.viewportSize + JumpToSpot -> (scrollState.maxValue * clickPosition) / scrollbarHeight.value + } + + scrollState.scrollTo(jumpTo) + } + } + + // 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, + label = "alpha", + ) + + 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 + } + } + } + } + + when { + scrollState.isScrollInProgress -> visible = true + hovered -> { + visible = true + trackIsVisible = true + } + } } + + val adapter = + when (scrollState) { + is LazyListState -> rememberScrollbarAdapter(scrollState) + is LazyGridState -> rememberScrollbarAdapter(scrollState) + is ScrollState -> rememberScrollbarAdapter(scrollState) + is TextFieldScrollState -> rememberScrollbarAdapter(scrollState) + 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 + ScrollbarImpl( + adapter = adapter, + modifier = + modifier + .alpha(animatedAlpha) + .animateContentSize() + .width(thumbWidth) + .background(trackBackground) + .padding(trackPadding) + .scrollable( + scrollState, + orientation = Orientation.Vertical, + reverseDirection = true, + ).pointerInput(Unit) { + detectTapGestures { offset -> + clickPosition = offset.y.toInt() + } + }.onSizeChanged { + scrollbarWidth.value = it.width + scrollbarHeight.value = it.height + }, + reverseLayout = reverseLayout, + style = style, + interactionSource = interactionSource, + isVertical = isVertical, + ) +} + +@Composable +public fun VerticalScrollbar( + adapter: ScrollbarAdapter, + modifier: Modifier = Modifier, + reverseLayout: Boolean = false, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + style: ScrollbarStyle = JewelTheme.scrollbarStyle, +) { + ScrollbarImpl( + adapter = adapter, + modifier = modifier, + reverseLayout = reverseLayout, + style = style, + interactionSource = interactionSource, + isVertical = true, + ) } @Composable @@ -60,60 +251,458 @@ public fun HorizontalScrollbar( interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, style: ScrollbarStyle = JewelTheme.scrollbarStyle, ) { - val shape by remember { mutableStateOf(RoundedCornerShape(style.metrics.thumbCornerSize)) } - val hoverDurationMillis by remember { - mutableStateOf(style.hoverDuration.toInt(DurationUnit.MILLISECONDS)) - } - - val composeScrollbarStyle = - ComposeScrollbarStyle( - minimalHeight = style.metrics.minThumbLength, - thickness = style.metrics.thumbThickness, - shape = shape, - hoverDurationMillis = hoverDurationMillis, - unhoverColor = style.colors.thumbBackground, - hoverColor = style.colors.thumbBackgroundHovered, - ) - CompositionLocalProvider(LocalScrollbarStyle provides composeScrollbarStyle) { - HorizontalScrollbar( - adapter = adapter, - modifier = modifier.padding(style.metrics.trackPadding), - reverseLayout = reverseLayout, - style = LocalScrollbarStyle.current, - interactionSource = interactionSource, - ) - } + ScrollbarImpl( + adapter = adapter, + modifier = modifier, + reverseLayout = reverseLayout, + style = style, + interactionSource = interactionSource, + isVertical = false, + ) } +@Deprecated("Use HorizontalScrollbar with an appropriate style.") @Composable public fun TabStripHorizontalScrollbar( adapter: ScrollbarAdapter, + style: ScrollbarStyle, modifier: Modifier = Modifier, reverseLayout: Boolean = false, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, - style: ScrollbarStyle = JewelTheme.scrollbarStyle, ) { - val shape by remember { mutableStateOf(RoundedCornerShape(style.metrics.thumbCornerSize)) } - val hoverDurationMillis by remember { - mutableStateOf(style.hoverDuration.inWholeMilliseconds.toInt()) - } - - val composeScrollbarStyle = - ComposeScrollbarStyle( - minimalHeight = style.metrics.minThumbLength, - thickness = 3.dp, - shape = shape, - hoverDurationMillis = hoverDurationMillis, - unhoverColor = style.colors.thumbBackground, - hoverColor = style.colors.thumbBackgroundHovered, + HorizontalScrollbar( + adapter = adapter, + modifier = modifier.padding(1.dp), + reverseLayout = reverseLayout, + style = style, + interactionSource = interactionSource, + ) +} + +// =========================================================================== +// Note: most of the code below is copied and adapted from the stock scrollbar +// =========================================================================== + +@Composable +private fun ScrollbarImpl( + adapter: ScrollbarAdapter, + reverseLayout: Boolean, + style: ScrollbarStyle, + interactionSource: MutableInteractionSource, + isVertical: Boolean, + modifier: Modifier = Modifier, +) { + with(LocalDensity.current) { + val dragInteraction = remember { mutableStateOf(null) } + DisposableEffect(interactionSource) { + onDispose { + dragInteraction.value?.let { interaction -> + interactionSource.tryEmit(DragInteraction.Cancel(interaction)) + dragInteraction.value = null + } + } + } + + var containerSize by remember { mutableIntStateOf(0) } + val isHovered by interactionSource.collectIsHoveredAsState() + + val isHighlighted by remember { + derivedStateOf { isHovered || dragInteraction.value is DragInteraction.Start } + } + + val thumbMinHeight = style.metrics.minThumbLength.toPx() + + val coroutineScope = rememberCoroutineScope() + val sliderAdapter = + remember( + adapter, + containerSize, + thumbMinHeight, + reverseLayout, + isVertical, + coroutineScope, + ) { + SliderAdapter(adapter, containerSize, thumbMinHeight, reverseLayout, isVertical, coroutineScope) + } + + val thumbThickness = style.metrics.thumbThickness.roundToPx() + val measurePolicy = + if (isVertical) { + remember(sliderAdapter, thumbThickness) { + verticalMeasurePolicy(sliderAdapter, { containerSize = it }, thumbThickness) + } + } else { + remember(sliderAdapter, thumbThickness) { + horizontalMeasurePolicy(sliderAdapter, { containerSize = it }, thumbThickness) + } + } + + val thumbBackgroundColor = if (isHighlighted) { + style.colors.thumbBackground + } else { + style.colors.thumbBackgroundHovered + } + val thumbColor = if (style.scrollbarVisibility is WhenScrolling) { + val durationMillis = style.scrollbarVisibility.expandAnimationDuration.inWholeMilliseconds.toInt() + animateColorAsState( + targetValue = thumbBackgroundColor, + animationSpec = tween(durationMillis), + ).value + } else { + thumbBackgroundColor + } + + val isVisible = sliderAdapter.thumbSize < containerSize + + Layout( + { + Box( + Modifier + .layoutId("thumb") + .thenIf(isVisible) { + background( + color = thumbColor, + shape = RoundedCornerShape(style.metrics.thumbCornerSize), + ) + }.scrollbarDrag( + interactionSource = interactionSource, + draggedInteraction = dragInteraction, + sliderAdapter = sliderAdapter, + ), + ) + }, + modifier + .hoverable(interactionSource = interactionSource) + .scrollOnPressTrack(isVertical, reverseLayout, sliderAdapter), + measurePolicy, ) - CompositionLocalProvider(LocalScrollbarStyle provides composeScrollbarStyle) { - HorizontalScrollbar( - adapter = adapter, - modifier = modifier.padding(1.dp), - reverseLayout = reverseLayout, - style = LocalScrollbarStyle.current, - interactionSource = interactionSource, + } +} + +private val SliderAdapter.thumbPixelRange: IntRange + get() { + val start = position.roundToInt() + val endExclusive = start + thumbSize.roundToInt() + + return (start until endExclusive) + } + +private val IntRange.size get() = last + 1 - first + +private fun verticalMeasurePolicy( + sliderAdapter: SliderAdapter, + setContainerSize: (Int) -> Unit, + scrollThickness: Int, +) = MeasurePolicy { measurables, constraints -> + setContainerSize(constraints.maxHeight) + val pixelRange = sliderAdapter.thumbPixelRange + val placeable = + measurables.first().measure( + Constraints.fixed( + constraints.constrainWidth(scrollThickness), + pixelRange.size, + ), + ) + layout(placeable.width, constraints.maxHeight) { + placeable.place(0, pixelRange.first) + } +} + +private fun horizontalMeasurePolicy( + sliderAdapter: SliderAdapter, + setContainerSize: (Int) -> Unit, + scrollThickness: Int, +) = MeasurePolicy { measurables, constraints -> + setContainerSize(constraints.maxWidth) + val pixelRange = sliderAdapter.thumbPixelRange + val placeable = + measurables.first().measure( + Constraints.fixed( + pixelRange.size, + constraints.constrainHeight(scrollThickness), + ), + ) + layout(constraints.maxWidth, placeable.height) { + placeable.place(pixelRange.first, 0) + } +} + +private fun Modifier.scrollbarDrag( + interactionSource: MutableInteractionSource, + draggedInteraction: MutableState, + sliderAdapter: SliderAdapter, +): Modifier = + composed { + val currentInteractionSource by rememberUpdatedState(interactionSource) + val currentDraggedInteraction by rememberUpdatedState(draggedInteraction) + val currentSliderAdapter by rememberUpdatedState(sliderAdapter) + + pointerInput(Unit) { + awaitEachGesture { + val down = awaitFirstDown(requireUnconsumed = false) + val interaction = DragInteraction.Start() + currentInteractionSource.tryEmit(interaction) + currentDraggedInteraction.value = interaction + currentSliderAdapter.onDragStarted() + val isSuccess = + drag(down.id) { change -> + currentSliderAdapter.onDragDelta(change.positionChange()) + change.consume() + } + val finishInteraction = + if (isSuccess) { + DragInteraction.Stop(interaction) + } else { + DragInteraction.Cancel(interaction) + } + currentInteractionSource.tryEmit(finishInteraction) + currentDraggedInteraction.value = null + } + } + } + +private fun Modifier.scrollOnPressTrack( + isVertical: Boolean, + reverseLayout: Boolean, + sliderAdapter: SliderAdapter, +) = composed { + val coroutineScope = rememberCoroutineScope() + val scroller = + remember(sliderAdapter, coroutineScope, reverseLayout) { + TrackPressScroller(coroutineScope, sliderAdapter, reverseLayout) + } + Modifier.pointerInput(scroller) { + detectScrollViaTrackGestures( + isVertical = isVertical, + scroller = scroller, ) } } + +/** + * Responsible for scrolling when the scrollbar track is pressed (outside + * the thumb). + */ +private class TrackPressScroller( + private val coroutineScope: CoroutineScope, + private val sliderAdapter: SliderAdapter, + private val reverseLayout: Boolean, +) { + /** + * The current direction of scroll (1: down/right, -1: up/left, 0: not + * scrolling) + */ + private var direction = 0 + + /** The currently pressed location (in pixels) on the scrollable axis. */ + private var offset: Float? = null + + /** The job that keeps scrolling while the track is pressed. */ + private var job: Job? = null + + /** + * Calculates the direction of scrolling towards the given offset (in + * pixels). + */ + private fun directionOfScrollTowards(offset: Float): Int { + val pixelRange = sliderAdapter.thumbPixelRange + return when { + offset < pixelRange.first -> if (reverseLayout) 1 else -1 + offset > pixelRange.last -> if (reverseLayout) -1 else 1 + else -> 0 + } + } + + /** + * Scrolls once towards the current offset, if it matches the direction of + * the current gesture. + */ + private suspend fun scrollTowardsCurrentOffset() { + offset?.let { + val currentDirection = directionOfScrollTowards(it) + if (currentDirection != direction) { + return + } + with(sliderAdapter.adapter) { + scrollTo(scrollOffset + currentDirection * viewportSize) + } + } + } + + /** Starts the job that scrolls continuously towards the current offset. */ + private fun startScrolling() { + job?.cancel() + job = + coroutineScope.launch { + scrollTowardsCurrentOffset() + delay(DELAY_BEFORE_SECOND_SCROLL_ON_TRACK_PRESS) + while (true) { + scrollTowardsCurrentOffset() + delay(DELAY_BETWEEN_SCROLLS_ON_TRACK_PRESS) + } + } + } + + /** Invoked on the first press for a gesture. */ + fun onPress(offset: Float) { + this.offset = offset + this.direction = directionOfScrollTowards(offset) + + if (direction != 0) { + startScrolling() + } + } + + /** Invoked when the pointer moves while pressed during the gesture. */ + fun onMovePressed(offset: Float) { + this.offset = offset + } + + /** Cleans up when the gesture finishes. */ + private fun cleanupAfterGesture() { + job?.cancel() + direction = 0 + offset = null + } + + /** Invoked when the button is released. */ + fun onRelease() { + cleanupAfterGesture() + } + + /** Invoked when the gesture is cancelled. */ + fun onGestureCancelled() { + cleanupAfterGesture() + // Maybe revert to the initial position? + } +} + +/** + * Detects the pointer events relevant for the "scroll by pressing on the + * track outside the thumb" gesture and calls the corresponding methods in + * the [scroller]. + */ +private suspend fun PointerInputScope.detectScrollViaTrackGestures( + isVertical: Boolean, + scroller: TrackPressScroller, +) { + fun Offset.onScrollAxis() = if (isVertical) y else x + + awaitEachGesture { + val down = awaitFirstDown() + scroller.onPress(down.position.onScrollAxis()) + + while (true) { + val drag = + if (isVertical) { + awaitVerticalDragOrCancellation(down.id) + } else { + awaitHorizontalDragOrCancellation(down.id) + } + + if (drag == null) { + scroller.onGestureCancelled() + break + } else if (!drag.pressed) { + scroller.onRelease() + break + } else { + scroller.onMovePressed(drag.position.onScrollAxis()) + } + } + } +} + +/** + * The delay between the 1st and 2nd scroll while the scrollbar track is + * pressed outside the thumb. + */ +internal const val DELAY_BEFORE_SECOND_SCROLL_ON_TRACK_PRESS: Long = 300L + +/** + * The delay between each subsequent (after the 2nd) scroll while the + * scrollbar track is pressed outside the thumb. + */ +internal const val DELAY_BETWEEN_SCROLLS_ON_TRACK_PRESS: Long = 100L + +internal class SliderAdapter( + val adapter: ScrollbarAdapter, + private val trackSize: Int, + private val minHeight: Float, + private val reverseLayout: Boolean, + private val isVertical: Boolean, + private val coroutineScope: CoroutineScope, +) { + private val contentSize get() = adapter.contentSize + private val visiblePart: Double + get() { + val contentSize = contentSize + return if (contentSize == 0.0) { + 1.0 + } else { + (adapter.viewportSize / contentSize).coerceAtMost(1.0) + } + } + + val thumbSize + get() = (trackSize * visiblePart).coerceAtLeast(minHeight.toDouble()) + + private val scrollScale: Double + get() { + val extraScrollbarSpace = trackSize - thumbSize + val extraContentSpace = adapter.maxScrollOffset // == contentSize - viewportSize + return if (extraContentSpace == 0.0) 1.0 else extraScrollbarSpace / extraContentSpace + } + + private val rawPosition: Double + get() = scrollScale * adapter.scrollOffset + + val position: Double + get() = if (reverseLayout) trackSize - thumbSize - rawPosition else rawPosition + + val bounds get() = position..position + thumbSize + + // How much of the current drag was ignored because we've reached the end of the scrollbar area + private var unscrolledDragDistance = 0.0 + + /** Called when the thumb dragging starts */ + fun onDragStarted() { + unscrolledDragDistance = 0.0 + } + + private suspend fun setPosition(value: Double) { + val rawPosition = + if (reverseLayout) { + trackSize - thumbSize - value + } else { + value + } + adapter.scrollTo(rawPosition / scrollScale) + } + + private val dragMutex = Mutex() + + /** Called on every movement while dragging the thumb */ + fun onDragDelta(offset: Offset) { + coroutineScope.launch(start = CoroutineStart.UNDISPATCHED) { + // Mutex is used to ensure that all earlier drag deltas were applied + // before calculating a new raw position + dragMutex.withLock { + val dragDelta = if (isVertical) offset.y else offset.x + val maxScrollPosition = adapter.maxScrollOffset * scrollScale + val currentPosition = position + val targetPosition = + (currentPosition + dragDelta + unscrolledDragDistance).coerceIn( + 0.0, + maxScrollPosition, + ) + val sliderDelta = targetPosition - currentPosition + + // Have to add to position for smooth content scroll if the items are of different size + val newPos = position + sliderDelta + setPosition(newPos) + unscrolledDragDistance += dragDelta - sliderDelta + } + } + } +} diff --git a/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/TabStrip.kt b/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/TabStrip.kt index 1f4f425f6..54c045901 100644 --- a/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/TabStrip.kt +++ b/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/TabStrip.kt @@ -29,10 +29,12 @@ import org.jetbrains.jewel.foundation.GenerateDataFunctions import org.jetbrains.jewel.foundation.modifier.onHover import org.jetbrains.jewel.foundation.state.CommonStateBitMask import org.jetbrains.jewel.foundation.state.FocusableComponentState +import org.jetbrains.jewel.ui.component.styling.TabStyle @Composable public fun TabStrip( tabs: List, + style: TabStyle, modifier: Modifier = Modifier, enabled: Boolean = true, ) { @@ -42,24 +44,25 @@ public fun TabStrip( val scrollState = rememberScrollState() Box( - modifier.focusable(true, remember { MutableInteractionSource() }) + modifier + .focusable(true, remember { MutableInteractionSource() }) .onHover { tabStripState = tabStripState.copy(hovered = it) }, ) { Row( modifier = - Modifier.horizontalScroll(scrollState) - .scrollable( - orientation = Orientation.Vertical, - reverseDirection = - ScrollableDefaults.reverseDirection( - LocalLayoutDirection.current, - Orientation.Vertical, - false, - ), - state = scrollState, - interactionSource = remember { MutableInteractionSource() }, - ) - .selectableGroup(), + Modifier + .horizontalScroll(scrollState) + .scrollable( + orientation = Orientation.Vertical, + reverseDirection = + ScrollableDefaults.reverseDirection( + LocalLayoutDirection.current, + Orientation.Vertical, + false, + ), + state = scrollState, + interactionSource = remember { MutableInteractionSource() }, + ).selectableGroup(), ) { tabs.forEach { TabImpl(isActive = tabStripState.isActive, tabData = it) } } @@ -71,6 +74,7 @@ public fun TabStrip( ) { TabStripHorizontalScrollbar( adapter = rememberScrollbarAdapter(scrollState), + style = style.scrollbarStyle, modifier = Modifier.fillMaxWidth(), ) } diff --git a/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/styling/ScrollbarStyling.kt b/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/styling/ScrollbarStyling.kt index 5e384cc8e..d447f95f9 100644 --- a/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/styling/ScrollbarStyling.kt +++ b/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/styling/ScrollbarStyling.kt @@ -16,7 +16,8 @@ import kotlin.time.Duration public class ScrollbarStyle( public val colors: ScrollbarColors, public val metrics: ScrollbarMetrics, - public val hoverDuration: Duration, + public val trackClickBehavior: TrackClickBehavior, + public val scrollbarVisibility: ScrollbarVisibility, ) { public companion object } @@ -26,6 +27,12 @@ public class ScrollbarStyle( public class ScrollbarColors( public val thumbBackground: Color, public val thumbBackgroundHovered: Color, + public val thumbBackgroundPressed: Color, + public val thumbBorder: Color, + public val thumbBorderHovered: Color, + public val thumbBorderPressed: Color, + public val trackBackground: Color, + public val trackBackgroundHovered: Color, ) { public companion object } @@ -35,12 +42,33 @@ public class ScrollbarColors( public class ScrollbarMetrics( public val thumbCornerSize: CornerSize, public val thumbThickness: Dp, + public val thumbThicknessExpanded: Dp, public val minThumbLength: Dp, public val trackPadding: PaddingValues, + public val trackPaddingExpanded: PaddingValues, ) { public companion object } +public sealed interface ScrollbarVisibility { + public data object AlwaysVisible : ScrollbarVisibility + + @GenerateDataFunctions + public class WhenScrolling( + public val appearAnimationDuration: Duration, + public val disappearAnimationDuration: Duration, + public val expandAnimationDuration: Duration, + public val lingerDuration: Duration, + ) : ScrollbarVisibility { + public companion object + } +} + +public enum class TrackClickBehavior { + NextPage, + JumpToSpot, +} + public val LocalScrollbarStyle: ProvidableCompositionLocal = staticCompositionLocalOf { error("No ScrollbarStyle provided. Have you forgotten the theme?") diff --git a/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/styling/TabStyling.kt b/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/styling/TabStyling.kt index 10a42bcf9..b63e45354 100644 --- a/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/styling/TabStyling.kt +++ b/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/styling/TabStyling.kt @@ -21,6 +21,7 @@ public class TabStyle( public val metrics: TabMetrics, public val icons: TabIcons, public val contentAlpha: TabContentAlpha, + public val scrollbarStyle: ScrollbarStyle, ) { public companion object }