From c45e9ea98668b1f7398c2a64110851f6e1802318 Mon Sep 17 00:00:00 2001 From: Sebastiano Poggi Date: Tue, 12 Dec 2023 00:26:33 -0800 Subject: [PATCH] Create Slider component (#270) Introduce Slider component following general IntUI style --- ide-laf-bridge/api/ide-laf-bridge.api | 9 + .../jewel/bridge/theme/BridgeSliderStyling.kt | 88 +++ .../jewel/bridge/theme/IntUiBridge.kt | 13 + .../api/int-ui-standalone.api | 12 +- .../standalone/styling/IntUiSliderStyling.kt | 109 +++ .../intui/standalone/theme/IntUiTheme.kt | 5 + .../samples/ideplugin/ComponentShowcaseTab.kt | 7 +- .../standalone/view/component/Slider.kt | 43 + ui/api/ui.api | 108 ++- .../jewel/ui/DefaultComponentStyling.kt | 4 + .../jetbrains/jewel/ui/component/Button.kt | 11 +- .../jetbrains/jewel/ui/component/Slider.kt | 735 ++++++++++++++++++ .../ui/component/styling/SliderStyling.kt | 109 +++ .../jetbrains/jewel/ui/theme/JewelTheme.kt | 7 + 14 files changed, 1250 insertions(+), 10 deletions(-) create mode 100644 ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/theme/BridgeSliderStyling.kt create mode 100644 int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiSliderStyling.kt create mode 100644 samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/view/component/Slider.kt create mode 100644 ui/src/main/kotlin/org/jetbrains/jewel/ui/component/Slider.kt create mode 100644 ui/src/main/kotlin/org/jetbrains/jewel/ui/component/styling/SliderStyling.kt diff --git a/ide-laf-bridge/api/ide-laf-bridge.api b/ide-laf-bridge/api/ide-laf-bridge.api index 548302112..2bf12ffed 100644 --- a/ide-laf-bridge/api/ide-laf-bridge.api +++ b/ide-laf-bridge/api/ide-laf-bridge.api @@ -73,6 +73,15 @@ public final class org/jetbrains/jewel/bridge/theme/BridgeGlobalMetricsKt { public static final fun readFromLaF (Lorg/jetbrains/jewel/foundation/GlobalMetrics$Companion;)Lorg/jetbrains/jewel/foundation/GlobalMetrics; } +public final class org/jetbrains/jewel/bridge/theme/BridgeSliderStylingKt { + public static final fun dark-7HESe_I (Lorg/jetbrains/jewel/ui/component/styling/SliderColors$Companion;JJJJJJJJJJJJJJJ)Lorg/jetbrains/jewel/ui/component/styling/SliderColors; + public static synthetic fun dark-7HESe_I$default (Lorg/jetbrains/jewel/ui/component/styling/SliderColors$Companion;JJJJJJJJJJJJJJJILjava/lang/Object;)Lorg/jetbrains/jewel/ui/component/styling/SliderColors; + public static final fun defaults-IDSuZpE (Lorg/jetbrains/jewel/ui/component/styling/SliderMetrics$Companion;FJFFFF)Lorg/jetbrains/jewel/ui/component/styling/SliderMetrics; + public static synthetic fun defaults-IDSuZpE$default (Lorg/jetbrains/jewel/ui/component/styling/SliderMetrics$Companion;FJFFFFILjava/lang/Object;)Lorg/jetbrains/jewel/ui/component/styling/SliderMetrics; + public static final fun light-7HESe_I (Lorg/jetbrains/jewel/ui/component/styling/SliderColors$Companion;JJJJJJJJJJJJJJJ)Lorg/jetbrains/jewel/ui/component/styling/SliderColors; + public static synthetic fun light-7HESe_I$default (Lorg/jetbrains/jewel/ui/component/styling/SliderColors$Companion;JJJJJJJJJJJJJJJILjava/lang/Object;)Lorg/jetbrains/jewel/ui/component/styling/SliderColors; +} + public final class org/jetbrains/jewel/bridge/theme/BridgeThemeColorPaletteKt { public static final fun getWindowsPopupBorder (Lorg/jetbrains/jewel/foundation/theme/ThemeColorPalette;)Landroidx/compose/ui/graphics/Color; public static final fun readFromLaF (Lorg/jetbrains/jewel/foundation/theme/ThemeColorPalette$Companion;)Lorg/jetbrains/jewel/foundation/theme/ThemeColorPalette; diff --git a/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/theme/BridgeSliderStyling.kt b/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/theme/BridgeSliderStyling.kt new file mode 100644 index 000000000..c7714d147 --- /dev/null +++ b/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/theme/BridgeSliderStyling.kt @@ -0,0 +1,88 @@ +package org.jetbrains.jewel.bridge.theme + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.takeOrElse +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import org.jetbrains.jewel.bridge.retrieveColorOrUnspecified +import org.jetbrains.jewel.ui.component.styling.SliderColors +import org.jetbrains.jewel.ui.component.styling.SliderMetrics + +public fun SliderColors.Companion.light( + track: Color = retrieveColorOrUnspecified("ColorPalette.Grey10").takeOrElse { Color(0xFFD3D5DB) }, + trackFilled: Color = retrieveColorOrUnspecified("ColorPalette.Blue6").takeOrElse { Color(0xFF588CF3) }, + trackDisabled: Color = retrieveColorOrUnspecified("ColorPalette.Grey12").takeOrElse { Color(0xFFEBECF0) }, + trackFilledDisabled: Color = retrieveColorOrUnspecified("ColorPalette.Grey11").takeOrElse { Color(0xFFDFE1E5) }, + stepMarker: Color = track, + thumbFill: Color = retrieveColorOrUnspecified("ColorPalette.Grey14").takeOrElse { Color(0xFFFFFFFF) }, + thumbFillDisabled: Color = thumbFill, + thumbFillFocused: Color = thumbFill, + thumbFillPressed: Color = thumbFill, + thumbFillHovered: Color = thumbFill, + thumbBorder: Color = retrieveColorOrUnspecified("ColorPalette.Grey8").takeOrElse { Color(0xFFA8ADBD) }, + thumbBorderFocused: Color = retrieveColorOrUnspecified("ColorPalette.Blue4").takeOrElse { Color(0xFF3574F0) }, + thumbBorderDisabled: Color = retrieveColorOrUnspecified("ColorPalette.Grey11").takeOrElse { Color(0xFFDFE1E5) }, + thumbBorderPressed: Color = retrieveColorOrUnspecified("ColorPalette.Grey7").takeOrElse { Color(0xFF818594) }, + thumbBorderHovered: Color = retrieveColorOrUnspecified("ColorPalette.Grey9").takeOrElse { Color(0xFFC9CCD6) }, +): SliderColors = SliderColors( + track, + trackFilled, + trackDisabled, + trackFilledDisabled, + stepMarker, + thumbFill, + thumbFillDisabled, + thumbFillFocused, + thumbFillPressed, + thumbFillHovered, + thumbBorder, + thumbBorderFocused, + thumbBorderDisabled, + thumbBorderPressed, + thumbBorderHovered, +) + +public fun SliderColors.Companion.dark( + track: Color = retrieveColorOrUnspecified("ColorPalette.Grey4").takeOrElse { Color(0xFF43454A) }, + trackFilled: Color = retrieveColorOrUnspecified("ColorPalette.Blue7").takeOrElse { Color(0xFF467FF2) }, + trackDisabled: Color = retrieveColorOrUnspecified("ColorPalette.Grey3").takeOrElse { Color(0xFF393B40) }, + trackFilledDisabled: Color = retrieveColorOrUnspecified("ColorPalette.Grey4").takeOrElse { Color(0xFF43454A) }, + stepMarker: Color = track, + thumbFill: Color = retrieveColorOrUnspecified("ColorPalette.Grey2").takeOrElse { Color(0xFF2B2D30) }, + thumbFillDisabled: Color = retrieveColorOrUnspecified("ColorPalette.Grey3").takeOrElse { Color(0xFF393B40) }, + thumbFillFocused: Color = thumbFill, + thumbFillPressed: Color = thumbFill, + thumbFillHovered: Color = thumbFill, + thumbBorder: Color = retrieveColorOrUnspecified("ColorPalette.Grey7").takeOrElse { Color(0xFF6F737A) }, + thumbBorderFocused: Color = retrieveColorOrUnspecified("ColorPalette.Blue6").takeOrElse { Color(0xFF3574F0) }, + thumbBorderDisabled: Color = retrieveColorOrUnspecified("ColorPalette.Grey5").takeOrElse { Color(0xFF4E5157) }, + thumbBorderPressed: Color = retrieveColorOrUnspecified("ColorPalette.Grey8").takeOrElse { Color(0xFF868A91) }, + thumbBorderHovered: Color = retrieveColorOrUnspecified("ColorPalette.Grey9").takeOrElse { Color(0xFF9DA0A8) }, +): SliderColors = SliderColors( + track, + trackFilled, + trackDisabled, + trackFilledDisabled, + stepMarker, + thumbFill, + thumbFillDisabled, + thumbFillFocused, + thumbFillPressed, + thumbFillHovered, + thumbBorder, + thumbBorderFocused, + thumbBorderDisabled, + thumbBorderPressed, + thumbBorderHovered, +) + +public fun SliderMetrics.Companion.defaults( + trackHeight: Dp = 4.dp, + thumbSize: DpSize = DpSize(14.dp, 14.dp), + thumbBorderWidth: Dp = 1.dp, + stepLineHeight: Dp = 8.dp, + stepLineWidth: Dp = 1.dp, + trackToStepSpacing: Dp = thumbSize.height / 2 + 4.dp, +): SliderMetrics = + SliderMetrics(trackHeight, thumbSize, thumbBorderWidth, stepLineHeight, stepLineWidth, trackToStepSpacing) 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 fd192a014..07a1a4d88 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 @@ -1,6 +1,7 @@ package org.jetbrains.jewel.bridge.theme import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CornerSize import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor @@ -87,6 +88,9 @@ 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.SliderColors +import org.jetbrains.jewel.ui.component.styling.SliderMetrics +import org.jetbrains.jewel.ui.component.styling.SliderStyle import org.jetbrains.jewel.ui.component.styling.SubmenuMetrics import org.jetbrains.jewel.ui.component.styling.TabColors import org.jetbrains.jewel.ui.component.styling.TabContentAlpha @@ -178,6 +182,7 @@ internal fun createBridgeComponentStyling( outlinedButtonStyle = readOutlinedButtonStyle(), radioButtonStyle = readRadioButtonStyle(), scrollbarStyle = readScrollbarStyle(theme.isDark), + sliderStyle = readSliderStyle(theme.isDark), textAreaStyle = readTextAreaStyle(textAreaTextStyle, textFieldStyle.metrics), textFieldStyle = textFieldStyle, tooltipStyle = readTooltipStyle(), @@ -627,6 +632,14 @@ private fun readScrollbarStyle(isDark: Boolean) = hoverDuration = 300.milliseconds, ) +private fun readSliderStyle(dark: Boolean): SliderStyle { + // There are no values for sliders in IntUi, so we're essentially reusing the + // standalone colors logic, reading the palette values (and falling back to + // hardcoded defaults). + val colors = if (dark) SliderColors.dark() else SliderColors.light() + return SliderStyle(colors, SliderMetrics.defaults(), CircleShape) +} + private fun readTextAreaStyle(textStyle: TextStyle, metrics: TextFieldMetrics): TextAreaStyle { val normalBackground = retrieveColorOrUnspecified("TextArea.background") val normalContent = retrieveColorOrUnspecified("TextArea.foreground") 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 c3461a432..0d48441c4 100644 --- a/int-ui/int-ui-standalone/api/int-ui-standalone.api +++ b/int-ui/int-ui-standalone/api/int-ui-standalone.api @@ -254,6 +254,14 @@ public final class org/jetbrains/jewel/intui/standalone/styling/IntUiScrollbarSt 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 final class org/jetbrains/jewel/intui/standalone/styling/IntUiSliderStylingKt { + public static final fun dark (Lorg/jetbrains/jewel/ui/component/styling/SliderStyle$Companion;Lorg/jetbrains/jewel/ui/component/styling/SliderColors;Lorg/jetbrains/jewel/ui/component/styling/SliderMetrics;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/runtime/Composer;II)Lorg/jetbrains/jewel/ui/component/styling/SliderStyle; + public static final fun dark-8v1krLo (Lorg/jetbrains/jewel/ui/component/styling/SliderColors$Companion;JJJJJJJJJJJJJJJLandroidx/compose/runtime/Composer;III)Lorg/jetbrains/jewel/ui/component/styling/SliderColors; + public static final fun defaults-nDjVmYc (Lorg/jetbrains/jewel/ui/component/styling/SliderMetrics$Companion;FJFFFFLandroidx/compose/runtime/Composer;II)Lorg/jetbrains/jewel/ui/component/styling/SliderMetrics; + public static final fun light (Lorg/jetbrains/jewel/ui/component/styling/SliderStyle$Companion;Lorg/jetbrains/jewel/ui/component/styling/SliderColors;Lorg/jetbrains/jewel/ui/component/styling/SliderMetrics;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/runtime/Composer;II)Lorg/jetbrains/jewel/ui/component/styling/SliderStyle; + public static final fun light-8v1krLo (Lorg/jetbrains/jewel/ui/component/styling/SliderColors$Companion;JJJJJJJJJJJJJJJLandroidx/compose/runtime/Composer;III)Lorg/jetbrains/jewel/ui/component/styling/SliderColors; +} + public final class org/jetbrains/jewel/intui/standalone/styling/IntUiTabStylingKt { public static final fun default (Lorg/jetbrains/jewel/ui/component/styling/TabContentAlpha$Companion;FFFFFFFFFF)Lorg/jetbrains/jewel/ui/component/styling/TabContentAlpha; public static synthetic fun default$default (Lorg/jetbrains/jewel/ui/component/styling/TabContentAlpha$Companion;FFFFFFFFFFILjava/lang/Object;)Lorg/jetbrains/jewel/ui/component/styling/TabContentAlpha; @@ -325,11 +333,11 @@ public final class org/jetbrains/jewel/intui/standalone/theme/IntUiGlobalMetrics public final class org/jetbrains/jewel/intui/standalone/theme/IntUiThemeKt { public static final fun IntUiTheme (Lorg/jetbrains/jewel/foundation/theme/ThemeDefinition;Lorg/jetbrains/jewel/ui/ComponentStyling;ZLkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)V public static final fun IntUiTheme (ZZLkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)V - public static final fun dark (Lorg/jetbrains/jewel/ui/ComponentStyling;Lorg/jetbrains/jewel/ui/component/styling/CheckboxStyle;Lorg/jetbrains/jewel/ui/component/styling/ChipStyle;Lorg/jetbrains/jewel/ui/component/styling/CircularProgressStyle;Lorg/jetbrains/jewel/ui/component/styling/ButtonStyle;Lorg/jetbrains/jewel/ui/component/styling/TabStyle;Lorg/jetbrains/jewel/ui/component/styling/DividerStyle;Lorg/jetbrains/jewel/ui/component/styling/DropdownStyle;Lorg/jetbrains/jewel/ui/component/styling/TabStyle;Lorg/jetbrains/jewel/ui/component/styling/GroupHeaderStyle;Lorg/jetbrains/jewel/ui/component/styling/HorizontalProgressBarStyle;Lorg/jetbrains/jewel/ui/component/styling/IconButtonStyle;Lorg/jetbrains/jewel/ui/component/styling/LazyTreeStyle;Lorg/jetbrains/jewel/ui/component/styling/LinkStyle;Lorg/jetbrains/jewel/ui/component/styling/MenuStyle;Lorg/jetbrains/jewel/ui/component/styling/ButtonStyle;Lorg/jetbrains/jewel/ui/component/styling/RadioButtonStyle;Lorg/jetbrains/jewel/ui/component/styling/ScrollbarStyle;Lorg/jetbrains/jewel/ui/component/styling/TextAreaStyle;Lorg/jetbrains/jewel/ui/component/styling/TextFieldStyle;Lorg/jetbrains/jewel/ui/component/styling/TooltipStyle;Lorg/jetbrains/jewel/ui/component/styling/DropdownStyle;Landroidx/compose/runtime/Composer;IIII)Lorg/jetbrains/jewel/ui/ComponentStyling; + public static final fun dark (Lorg/jetbrains/jewel/ui/ComponentStyling;Lorg/jetbrains/jewel/ui/component/styling/CheckboxStyle;Lorg/jetbrains/jewel/ui/component/styling/ChipStyle;Lorg/jetbrains/jewel/ui/component/styling/CircularProgressStyle;Lorg/jetbrains/jewel/ui/component/styling/ButtonStyle;Lorg/jetbrains/jewel/ui/component/styling/TabStyle;Lorg/jetbrains/jewel/ui/component/styling/DividerStyle;Lorg/jetbrains/jewel/ui/component/styling/DropdownStyle;Lorg/jetbrains/jewel/ui/component/styling/TabStyle;Lorg/jetbrains/jewel/ui/component/styling/GroupHeaderStyle;Lorg/jetbrains/jewel/ui/component/styling/HorizontalProgressBarStyle;Lorg/jetbrains/jewel/ui/component/styling/IconButtonStyle;Lorg/jetbrains/jewel/ui/component/styling/LazyTreeStyle;Lorg/jetbrains/jewel/ui/component/styling/LinkStyle;Lorg/jetbrains/jewel/ui/component/styling/MenuStyle;Lorg/jetbrains/jewel/ui/component/styling/ButtonStyle;Lorg/jetbrains/jewel/ui/component/styling/RadioButtonStyle;Lorg/jetbrains/jewel/ui/component/styling/ScrollbarStyle;Lorg/jetbrains/jewel/ui/component/styling/SliderStyle;Lorg/jetbrains/jewel/ui/component/styling/TextAreaStyle;Lorg/jetbrains/jewel/ui/component/styling/TextFieldStyle;Lorg/jetbrains/jewel/ui/component/styling/TooltipStyle;Lorg/jetbrains/jewel/ui/component/styling/DropdownStyle;Landroidx/compose/runtime/Composer;IIII)Lorg/jetbrains/jewel/ui/ComponentStyling; public static final fun darkThemeDefinition-RFMEUTM (Lorg/jetbrains/jewel/foundation/theme/JewelTheme$Companion;Lorg/jetbrains/jewel/foundation/GlobalColors;Lorg/jetbrains/jewel/foundation/GlobalMetrics;Lorg/jetbrains/jewel/foundation/theme/ThemeColorPalette;Lorg/jetbrains/jewel/foundation/theme/ThemeIconData;Landroidx/compose/ui/text/TextStyle;JLandroidx/compose/runtime/Composer;II)Lorg/jetbrains/jewel/foundation/theme/ThemeDefinition; public static final fun default (Lorg/jetbrains/jewel/ui/ComponentStyling;Landroidx/compose/runtime/Composer;I)Lorg/jetbrains/jewel/ui/ComponentStyling; public static final fun getDefaultTextStyle (Lorg/jetbrains/jewel/foundation/theme/JewelTheme$Companion;)Landroidx/compose/ui/text/TextStyle; - public static final fun light (Lorg/jetbrains/jewel/ui/ComponentStyling;Lorg/jetbrains/jewel/ui/component/styling/CheckboxStyle;Lorg/jetbrains/jewel/ui/component/styling/ChipStyle;Lorg/jetbrains/jewel/ui/component/styling/CircularProgressStyle;Lorg/jetbrains/jewel/ui/component/styling/ButtonStyle;Lorg/jetbrains/jewel/ui/component/styling/TabStyle;Lorg/jetbrains/jewel/ui/component/styling/DividerStyle;Lorg/jetbrains/jewel/ui/component/styling/DropdownStyle;Lorg/jetbrains/jewel/ui/component/styling/TabStyle;Lorg/jetbrains/jewel/ui/component/styling/GroupHeaderStyle;Lorg/jetbrains/jewel/ui/component/styling/HorizontalProgressBarStyle;Lorg/jetbrains/jewel/ui/component/styling/IconButtonStyle;Lorg/jetbrains/jewel/ui/component/styling/LazyTreeStyle;Lorg/jetbrains/jewel/ui/component/styling/LinkStyle;Lorg/jetbrains/jewel/ui/component/styling/MenuStyle;Lorg/jetbrains/jewel/ui/component/styling/ButtonStyle;Lorg/jetbrains/jewel/ui/component/styling/RadioButtonStyle;Lorg/jetbrains/jewel/ui/component/styling/ScrollbarStyle;Lorg/jetbrains/jewel/ui/component/styling/TextAreaStyle;Lorg/jetbrains/jewel/ui/component/styling/TextFieldStyle;Lorg/jetbrains/jewel/ui/component/styling/TooltipStyle;Lorg/jetbrains/jewel/ui/component/styling/DropdownStyle;Landroidx/compose/runtime/Composer;IIII)Lorg/jetbrains/jewel/ui/ComponentStyling; + public static final fun light (Lorg/jetbrains/jewel/ui/ComponentStyling;Lorg/jetbrains/jewel/ui/component/styling/CheckboxStyle;Lorg/jetbrains/jewel/ui/component/styling/ChipStyle;Lorg/jetbrains/jewel/ui/component/styling/CircularProgressStyle;Lorg/jetbrains/jewel/ui/component/styling/ButtonStyle;Lorg/jetbrains/jewel/ui/component/styling/TabStyle;Lorg/jetbrains/jewel/ui/component/styling/DividerStyle;Lorg/jetbrains/jewel/ui/component/styling/DropdownStyle;Lorg/jetbrains/jewel/ui/component/styling/TabStyle;Lorg/jetbrains/jewel/ui/component/styling/GroupHeaderStyle;Lorg/jetbrains/jewel/ui/component/styling/HorizontalProgressBarStyle;Lorg/jetbrains/jewel/ui/component/styling/IconButtonStyle;Lorg/jetbrains/jewel/ui/component/styling/LazyTreeStyle;Lorg/jetbrains/jewel/ui/component/styling/LinkStyle;Lorg/jetbrains/jewel/ui/component/styling/MenuStyle;Lorg/jetbrains/jewel/ui/component/styling/ButtonStyle;Lorg/jetbrains/jewel/ui/component/styling/RadioButtonStyle;Lorg/jetbrains/jewel/ui/component/styling/ScrollbarStyle;Lorg/jetbrains/jewel/ui/component/styling/SliderStyle;Lorg/jetbrains/jewel/ui/component/styling/TextAreaStyle;Lorg/jetbrains/jewel/ui/component/styling/TextFieldStyle;Lorg/jetbrains/jewel/ui/component/styling/TooltipStyle;Lorg/jetbrains/jewel/ui/component/styling/DropdownStyle;Landroidx/compose/runtime/Composer;IIII)Lorg/jetbrains/jewel/ui/ComponentStyling; public static final fun lightThemeDefinition-RFMEUTM (Lorg/jetbrains/jewel/foundation/theme/JewelTheme$Companion;Lorg/jetbrains/jewel/foundation/GlobalColors;Lorg/jetbrains/jewel/foundation/GlobalMetrics;Lorg/jetbrains/jewel/foundation/theme/ThemeColorPalette;Lorg/jetbrains/jewel/foundation/theme/ThemeIconData;Landroidx/compose/ui/text/TextStyle;JLandroidx/compose/runtime/Composer;II)Lorg/jetbrains/jewel/foundation/theme/ThemeDefinition; } diff --git a/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiSliderStyling.kt b/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiSliderStyling.kt new file mode 100644 index 000000000..f7b5f8a53 --- /dev/null +++ b/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiSliderStyling.kt @@ -0,0 +1,109 @@ +package org.jetbrains.jewel.intui.standalone.styling + +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpSize +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.SliderColors +import org.jetbrains.jewel.ui.component.styling.SliderMetrics +import org.jetbrains.jewel.ui.component.styling.SliderStyle + +@Composable +public fun SliderStyle.Companion.light( + colors: SliderColors = SliderColors.light(), + metrics: SliderMetrics = SliderMetrics.defaults(), + thumbShape: Shape = CircleShape, +): SliderStyle = SliderStyle(colors, metrics, thumbShape) + +@Composable +public fun SliderStyle.Companion.dark( + colors: SliderColors = SliderColors.dark(), + metrics: SliderMetrics = SliderMetrics.defaults(), + thumbShape: Shape = CircleShape, +): SliderStyle = SliderStyle(colors, metrics, thumbShape) + +@Composable +public fun SliderColors.Companion.light( + track: Color = IntUiLightTheme.colors.grey(10), + trackFilled: Color = IntUiLightTheme.colors.blue(6), + trackDisabled: Color = IntUiLightTheme.colors.grey(12), + trackFilledDisabled: Color = IntUiLightTheme.colors.grey(11), + stepMarker: Color = track, + thumbFill: Color = IntUiLightTheme.colors.grey(14), + thumbFillDisabled: Color = thumbFill, + thumbFillFocused: Color = thumbFill, + thumbFillPressed: Color = thumbFill, + thumbFillHovered: Color = thumbFill, + thumbBorder: Color = IntUiLightTheme.colors.grey(8), + thumbBorderFocused: Color = IntUiLightTheme.colors.blue(4), + thumbBorderDisabled: Color = IntUiLightTheme.colors.grey(11), + thumbBorderPressed: Color = IntUiLightTheme.colors.grey(7), + thumbBorderHovered: Color = IntUiLightTheme.colors.grey(9), +): SliderColors = SliderColors( + track, + trackFilled, + trackDisabled, + trackFilledDisabled, + stepMarker, + thumbFill, + thumbFillDisabled, + thumbFillFocused, + thumbFillPressed, + thumbFillHovered, + thumbBorder, + thumbBorderFocused, + thumbBorderDisabled, + thumbBorderPressed, + thumbBorderHovered, +) + +@Composable +public fun SliderColors.Companion.dark( + track: Color = IntUiDarkTheme.colors.grey(4), + trackFilled: Color = IntUiDarkTheme.colors.blue(7), + trackDisabled: Color = IntUiDarkTheme.colors.grey(3), + trackFilledDisabled: Color = IntUiDarkTheme.colors.grey(4), + stepMarker: Color = track, + thumbFill: Color = IntUiDarkTheme.colors.grey(2), + thumbFillDisabled: Color = IntUiDarkTheme.colors.grey(3), + thumbFillFocused: Color = thumbFill, + thumbFillPressed: Color = thumbFill, + thumbFillHovered: Color = thumbFill, + thumbBorder: Color = IntUiDarkTheme.colors.grey(7), + thumbBorderFocused: Color = IntUiDarkTheme.colors.blue(6), + thumbBorderDisabled: Color = IntUiDarkTheme.colors.grey(5), + thumbBorderPressed: Color = IntUiDarkTheme.colors.grey(8), + thumbBorderHovered: Color = IntUiDarkTheme.colors.grey(9), +): SliderColors = SliderColors( + track, + trackFilled, + trackDisabled, + trackFilledDisabled, + stepMarker, + thumbFill, + thumbFillDisabled, + thumbFillFocused, + thumbFillPressed, + thumbFillHovered, + thumbBorder, + thumbBorderFocused, + thumbBorderDisabled, + thumbBorderPressed, + thumbBorderHovered, +) + +@Composable +public fun SliderMetrics.Companion.defaults( + trackHeight: Dp = 4.dp, + thumbSize: DpSize = DpSize(14.dp, 14.dp), + thumbBorderWidth: Dp = 1.dp, + stepLineHeight: Dp = 8.dp, + stepLineWidth: Dp = 1.dp, + trackToStepSpacing: Dp = thumbSize.height / 2 + 4.dp, +): SliderMetrics = + SliderMetrics(trackHeight, thumbSize, thumbBorderWidth, stepLineHeight, stepLineWidth, trackToStepSpacing) diff --git a/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/theme/IntUiTheme.kt b/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/theme/IntUiTheme.kt index 554ed00ff..4fcecd135 100644 --- a/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/theme/IntUiTheme.kt +++ b/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/theme/IntUiTheme.kt @@ -40,6 +40,7 @@ import org.jetbrains.jewel.ui.component.styling.LinkStyle import org.jetbrains.jewel.ui.component.styling.MenuStyle import org.jetbrains.jewel.ui.component.styling.RadioButtonStyle import org.jetbrains.jewel.ui.component.styling.ScrollbarStyle +import org.jetbrains.jewel.ui.component.styling.SliderStyle import org.jetbrains.jewel.ui.component.styling.TabStyle import org.jetbrains.jewel.ui.component.styling.TextAreaStyle import org.jetbrains.jewel.ui.component.styling.TextFieldStyle @@ -102,6 +103,7 @@ public fun ComponentStyling.dark( outlinedButtonStyle: ButtonStyle = ButtonStyle.Outlined.dark(), radioButtonStyle: RadioButtonStyle = RadioButtonStyle.dark(), scrollbarStyle: ScrollbarStyle = ScrollbarStyle.dark(), + sliderStyle: SliderStyle = SliderStyle.dark(), textAreaStyle: TextAreaStyle = TextAreaStyle.dark(), textFieldStyle: TextFieldStyle = TextFieldStyle.dark(), tooltipStyle: TooltipStyle = TooltipStyle.dark(), @@ -126,6 +128,7 @@ public fun ComponentStyling.dark( outlinedButtonStyle = outlinedButtonStyle, radioButtonStyle = radioButtonStyle, scrollbarStyle = scrollbarStyle, + sliderStyle = sliderStyle, textAreaStyle = textAreaStyle, textFieldStyle = textFieldStyle, tooltipStyle = tooltipStyle, @@ -152,6 +155,7 @@ public fun ComponentStyling.light( outlinedButtonStyle: ButtonStyle = ButtonStyle.Outlined.light(), radioButtonStyle: RadioButtonStyle = RadioButtonStyle.light(), scrollbarStyle: ScrollbarStyle = ScrollbarStyle.light(), + sliderStyle: SliderStyle = SliderStyle.light(), textAreaStyle: TextAreaStyle = TextAreaStyle.light(), textFieldStyle: TextFieldStyle = TextFieldStyle.light(), tooltipStyle: TooltipStyle = TooltipStyle.light(), @@ -176,6 +180,7 @@ public fun ComponentStyling.light( outlinedButtonStyle = outlinedButtonStyle, radioButtonStyle = radioButtonStyle, scrollbarStyle = scrollbarStyle, + sliderStyle = sliderStyle, textAreaStyle = textAreaStyle, textFieldStyle = textFieldStyle, tooltipStyle = tooltipStyle, diff --git a/samples/ide-plugin/src/main/kotlin/org/jetbrains/jewel/samples/ideplugin/ComponentShowcaseTab.kt b/samples/ide-plugin/src/main/kotlin/org/jetbrains/jewel/samples/ideplugin/ComponentShowcaseTab.kt index 0d4ebefa4..37de7255e 100644 --- a/samples/ide-plugin/src/main/kotlin/org/jetbrains/jewel/samples/ideplugin/ComponentShowcaseTab.kt +++ b/samples/ide-plugin/src/main/kotlin/org/jetbrains/jewel/samples/ideplugin/ComponentShowcaseTab.kt @@ -27,7 +27,6 @@ import com.intellij.icons.AllIcons import com.intellij.ui.JBColor import icons.JewelIcons import org.jetbrains.jewel.bridge.LocalComponent -import org.jetbrains.jewel.bridge.ToolWindowScope import org.jetbrains.jewel.bridge.toComposeColor import org.jetbrains.jewel.foundation.lazy.tree.buildTree import org.jetbrains.jewel.foundation.modifier.onActivated @@ -43,12 +42,13 @@ import org.jetbrains.jewel.ui.component.IconButton import org.jetbrains.jewel.ui.component.LazyTree import org.jetbrains.jewel.ui.component.OutlinedButton import org.jetbrains.jewel.ui.component.RadioButtonRow +import org.jetbrains.jewel.ui.component.Slider import org.jetbrains.jewel.ui.component.Text import org.jetbrains.jewel.ui.component.TextField import org.jetbrains.jewel.ui.component.Tooltip @Composable -internal fun ToolWindowScope.ComponentShowcaseTab() { +internal fun ComponentShowcaseTab() { val bgColor by remember(JewelTheme.isDark) { mutableStateOf(JBColor.PanelBackground.toComposeColor()) } val scrollState = rememberScrollState() @@ -178,6 +178,9 @@ private fun RowScope.ColumnOne() { ) } } + + var sliderValue by remember { mutableStateOf(.15f) } + Slider(sliderValue, { sliderValue = it }, steps = 5) } } diff --git a/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/view/component/Slider.kt b/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/view/component/Slider.kt new file mode 100644 index 000000000..659ec4697 --- /dev/null +++ b/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/view/component/Slider.kt @@ -0,0 +1,43 @@ +package org.jetbrains.jewel.samples.standalone.view.component + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import org.jetbrains.jewel.samples.standalone.viewmodel.View +import org.jetbrains.jewel.ui.component.Slider + +@Composable +@View(title = "Sliders", position = 12) +fun Sliders() { + var value1 by remember { mutableStateOf(.45f) } + Slider( + value = value1, + onValueChange = { value1 = it }, + ) + + var value2 by remember { mutableStateOf(.7f) } + Slider( + value = value2, + onValueChange = { value2 = it }, + enabled = false, + ) + + var value3 by remember { mutableStateOf(33f) } + Slider( + value = value3, + onValueChange = { value3 = it }, + steps = 10, + valueRange = 0f..100f, + ) + + var value4 by remember { mutableStateOf(23f) } + Slider( + value = value4, + onValueChange = { value4 = it }, + steps = 10, + valueRange = 0f..100f, + enabled = false, + ) +} diff --git a/ui/api/ui.api b/ui/api/ui.api index 0d51705b7..670a8529e 100644 --- a/ui/api/ui.api +++ b/ui/api/ui.api @@ -25,7 +25,7 @@ public final class org/jetbrains/jewel/ui/ComponentStyling$DefaultImpls { public final class org/jetbrains/jewel/ui/DefaultComponentStyling : org/jetbrains/jewel/ui/ComponentStyling { public static final field $stable I - public fun (Lorg/jetbrains/jewel/ui/component/styling/CheckboxStyle;Lorg/jetbrains/jewel/ui/component/styling/ChipStyle;Lorg/jetbrains/jewel/ui/component/styling/CircularProgressStyle;Lorg/jetbrains/jewel/ui/component/styling/ButtonStyle;Lorg/jetbrains/jewel/ui/component/styling/DropdownStyle;Lorg/jetbrains/jewel/ui/component/styling/TabStyle;Lorg/jetbrains/jewel/ui/component/styling/DividerStyle;Lorg/jetbrains/jewel/ui/component/styling/TabStyle;Lorg/jetbrains/jewel/ui/component/styling/GroupHeaderStyle;Lorg/jetbrains/jewel/ui/component/styling/HorizontalProgressBarStyle;Lorg/jetbrains/jewel/ui/component/styling/IconButtonStyle;Lorg/jetbrains/jewel/ui/component/styling/LazyTreeStyle;Lorg/jetbrains/jewel/ui/component/styling/LinkStyle;Lorg/jetbrains/jewel/ui/component/styling/MenuStyle;Lorg/jetbrains/jewel/ui/component/styling/ButtonStyle;Lorg/jetbrains/jewel/ui/component/styling/RadioButtonStyle;Lorg/jetbrains/jewel/ui/component/styling/ScrollbarStyle;Lorg/jetbrains/jewel/ui/component/styling/TextAreaStyle;Lorg/jetbrains/jewel/ui/component/styling/TextFieldStyle;Lorg/jetbrains/jewel/ui/component/styling/TooltipStyle;Lorg/jetbrains/jewel/ui/component/styling/DropdownStyle;)V + public fun (Lorg/jetbrains/jewel/ui/component/styling/CheckboxStyle;Lorg/jetbrains/jewel/ui/component/styling/ChipStyle;Lorg/jetbrains/jewel/ui/component/styling/CircularProgressStyle;Lorg/jetbrains/jewel/ui/component/styling/ButtonStyle;Lorg/jetbrains/jewel/ui/component/styling/DropdownStyle;Lorg/jetbrains/jewel/ui/component/styling/TabStyle;Lorg/jetbrains/jewel/ui/component/styling/DividerStyle;Lorg/jetbrains/jewel/ui/component/styling/TabStyle;Lorg/jetbrains/jewel/ui/component/styling/GroupHeaderStyle;Lorg/jetbrains/jewel/ui/component/styling/HorizontalProgressBarStyle;Lorg/jetbrains/jewel/ui/component/styling/IconButtonStyle;Lorg/jetbrains/jewel/ui/component/styling/LazyTreeStyle;Lorg/jetbrains/jewel/ui/component/styling/LinkStyle;Lorg/jetbrains/jewel/ui/component/styling/MenuStyle;Lorg/jetbrains/jewel/ui/component/styling/ButtonStyle;Lorg/jetbrains/jewel/ui/component/styling/RadioButtonStyle;Lorg/jetbrains/jewel/ui/component/styling/ScrollbarStyle;Lorg/jetbrains/jewel/ui/component/styling/SliderStyle;Lorg/jetbrains/jewel/ui/component/styling/TextAreaStyle;Lorg/jetbrains/jewel/ui/component/styling/TextFieldStyle;Lorg/jetbrains/jewel/ui/component/styling/TooltipStyle;Lorg/jetbrains/jewel/ui/component/styling/DropdownStyle;)V public fun equals (Ljava/lang/Object;)Z public final fun getCheckboxStyle ()Lorg/jetbrains/jewel/ui/component/styling/CheckboxStyle; public final fun getChipStyle ()Lorg/jetbrains/jewel/ui/component/styling/ChipStyle; @@ -44,6 +44,7 @@ public final class org/jetbrains/jewel/ui/DefaultComponentStyling : org/jetbrain public final fun getOutlinedButtonStyle ()Lorg/jetbrains/jewel/ui/component/styling/ButtonStyle; public final fun getRadioButtonStyle ()Lorg/jetbrains/jewel/ui/component/styling/RadioButtonStyle; public final fun getScrollbarStyle ()Lorg/jetbrains/jewel/ui/component/styling/ScrollbarStyle; + public final fun getSliderStyle ()Lorg/jetbrains/jewel/ui/component/styling/SliderStyle; public final fun getTextAreaStyle ()Lorg/jetbrains/jewel/ui/component/styling/TextAreaStyle; public final fun getTextFieldStyle ()Lorg/jetbrains/jewel/ui/component/styling/TextFieldStyle; public final fun getTooltipStyle ()Lorg/jetbrains/jewel/ui/component/styling/TooltipStyle; @@ -551,6 +552,44 @@ public final class org/jetbrains/jewel/ui/component/ScrollbarsKt { 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 } +public final class org/jetbrains/jewel/ui/component/SliderKt { + public static final fun Slider (FLkotlin/jvm/functions/Function1;Landroidx/compose/ui/Modifier;ZLkotlin/ranges/ClosedFloatingPointRange;ILkotlin/jvm/functions/Function0;Landroidx/compose/foundation/interaction/MutableInteractionSource;Lorg/jetbrains/jewel/ui/component/styling/SliderStyle;Landroidx/compose/runtime/Composer;II)V +} + +public final class org/jetbrains/jewel/ui/component/SliderState : org/jetbrains/jewel/foundation/state/FocusableComponentState { + public static final field Companion Lorg/jetbrains/jewel/ui/component/SliderState$Companion; + public static final synthetic fun box-impl (J)Lorg/jetbrains/jewel/ui/component/SliderState; + public fun chooseValue (Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Landroidx/compose/runtime/Composer;I)Ljava/lang/Object; + public static fun chooseValue-impl (JLjava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Landroidx/compose/runtime/Composer;I)Ljava/lang/Object; + public static fun constructor-impl (J)J + public static final fun copy-GPhM_18 (JZZZZZ)J + public static synthetic fun copy-GPhM_18$default (JZZZZZILjava/lang/Object;)J + public fun equals (Ljava/lang/Object;)Z + public static fun equals-impl (JLjava/lang/Object;)Z + public static final fun equals-impl0 (JJ)Z + public final fun getState-s-VKNKU ()J + public fun hashCode ()I + public static fun hashCode-impl (J)I + public fun isActive ()Z + public static fun isActive-impl (J)Z + public fun isEnabled ()Z + public static fun isEnabled-impl (J)Z + public fun isFocused ()Z + public static fun isFocused-impl (J)Z + public fun isHovered ()Z + public static fun isHovered-impl (J)Z + public fun isPressed ()Z + public static fun isPressed-impl (J)Z + public fun toString ()Ljava/lang/String; + public static fun toString-impl (J)Ljava/lang/String; + public final synthetic fun unbox-impl ()J +} + +public final class org/jetbrains/jewel/ui/component/SliderState$Companion { + public final fun of-GPhM_18 (ZZZZZ)J + public static synthetic fun of-GPhM_18$default (Lorg/jetbrains/jewel/ui/component/SliderState$Companion;ZZZZZILjava/lang/Object;)J +} + public final class org/jetbrains/jewel/ui/component/SplitLayoutKt { public static final fun HorizontalSplitLayout-BssWTFQ (Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Landroidx/compose/ui/Modifier;JFFFFFFLandroidx/compose/runtime/Composer;II)V public static final fun VerticalSplitLayout-BssWTFQ (Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Landroidx/compose/ui/Modifier;JFFFFFFLandroidx/compose/runtime/Composer;II)V @@ -1637,6 +1676,72 @@ public final class org/jetbrains/jewel/ui/component/styling/ScrollbarStylingKt { public static final fun getLocalScrollbarStyle ()Landroidx/compose/runtime/ProvidableCompositionLocal; } +public final class org/jetbrains/jewel/ui/component/styling/SliderColors { + public static final field $stable I + public static final field Companion Lorg/jetbrains/jewel/ui/component/styling/SliderColors$Companion; + public synthetic fun (JJJJJJJJJJJJJJJLkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun equals (Ljava/lang/Object;)Z + public final fun getStepMarker-0d7_KjU ()J + public final fun getThumbBorder-0d7_KjU ()J + public final fun getThumbBorderDisabled-0d7_KjU ()J + public final fun getThumbBorderFocused-0d7_KjU ()J + public final fun getThumbBorderHovered-0d7_KjU ()J + public final fun getThumbBorderPressed-0d7_KjU ()J + public final fun getThumbFill-0d7_KjU ()J + public final fun getThumbFillDisabled-0d7_KjU ()J + public final fun getThumbFillFocused-0d7_KjU ()J + public final fun getThumbFillHovered-0d7_KjU ()J + public final fun getThumbFillPressed-0d7_KjU ()J + public final fun getTrack-0d7_KjU ()J + public final fun getTrackDisabled-0d7_KjU ()J + public final fun getTrackFilled-0d7_KjU ()J + public final fun getTrackFilledDisabled-0d7_KjU ()J + public fun hashCode ()I + public final fun thumbBorderFor-p6gmeQ4 (JLandroidx/compose/runtime/Composer;I)Landroidx/compose/runtime/State; + public final fun thumbFillFor-p6gmeQ4 (JLandroidx/compose/runtime/Composer;I)Landroidx/compose/runtime/State; + public fun toString ()Ljava/lang/String; +} + +public final class org/jetbrains/jewel/ui/component/styling/SliderColors$Companion { +} + +public final class org/jetbrains/jewel/ui/component/styling/SliderMetrics { + public static final field $stable I + public static final field Companion Lorg/jetbrains/jewel/ui/component/styling/SliderMetrics$Companion; + public synthetic fun (FJFFFFLkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun equals (Ljava/lang/Object;)Z + public final fun getStepLineHeight-D9Ej5fM ()F + public final fun getStepLineWidth-D9Ej5fM ()F + public final fun getThumbBorderWidth-D9Ej5fM ()F + public final fun getThumbSize-MYxV2XQ ()J + public final fun getTrackHeight-D9Ej5fM ()F + public final fun getTrackToStepSpacing-D9Ej5fM ()F + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class org/jetbrains/jewel/ui/component/styling/SliderMetrics$Companion { +} + +public final class org/jetbrains/jewel/ui/component/styling/SliderStyle { + public static final field $stable I + public static final field Companion Lorg/jetbrains/jewel/ui/component/styling/SliderStyle$Companion; + public fun (Lorg/jetbrains/jewel/ui/component/styling/SliderColors;Lorg/jetbrains/jewel/ui/component/styling/SliderMetrics;Landroidx/compose/ui/graphics/Shape;)V + public fun equals (Ljava/lang/Object;)Z + public final fun getColors ()Lorg/jetbrains/jewel/ui/component/styling/SliderColors; + public final fun getMetrics ()Lorg/jetbrains/jewel/ui/component/styling/SliderMetrics; + public final fun getThumbShape ()Landroidx/compose/ui/graphics/Shape; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class org/jetbrains/jewel/ui/component/styling/SliderStyle$Companion { +} + +public final class org/jetbrains/jewel/ui/component/styling/SliderStylingKt { + public static final fun getLocalSliderStyle ()Landroidx/compose/runtime/ProvidableCompositionLocal; +} + public final class org/jetbrains/jewel/ui/component/styling/SubmenuMetrics { public static final field $stable I public static final field Companion Lorg/jetbrains/jewel/ui/component/styling/SubmenuMetrics$Companion; @@ -2197,6 +2302,7 @@ public final class org/jetbrains/jewel/ui/theme/JewelThemeKt { public static final fun getOutlinedButtonStyle (Lorg/jetbrains/jewel/foundation/theme/JewelTheme$Companion;Landroidx/compose/runtime/Composer;I)Lorg/jetbrains/jewel/ui/component/styling/ButtonStyle; public static final fun getRadioButtonStyle (Lorg/jetbrains/jewel/foundation/theme/JewelTheme$Companion;Landroidx/compose/runtime/Composer;I)Lorg/jetbrains/jewel/ui/component/styling/RadioButtonStyle; public static final fun getScrollbarStyle (Lorg/jetbrains/jewel/foundation/theme/JewelTheme$Companion;Landroidx/compose/runtime/Composer;I)Lorg/jetbrains/jewel/ui/component/styling/ScrollbarStyle; + public static final fun getSliderStyle (Lorg/jetbrains/jewel/foundation/theme/JewelTheme$Companion;Landroidx/compose/runtime/Composer;I)Lorg/jetbrains/jewel/ui/component/styling/SliderStyle; public static final fun getTextAreaStyle (Lorg/jetbrains/jewel/foundation/theme/JewelTheme$Companion;Landroidx/compose/runtime/Composer;I)Lorg/jetbrains/jewel/ui/component/styling/TextAreaStyle; public static final fun getTextFieldStyle (Lorg/jetbrains/jewel/foundation/theme/JewelTheme$Companion;Landroidx/compose/runtime/Composer;I)Lorg/jetbrains/jewel/ui/component/styling/TextFieldStyle; public static final fun getTooltipStyle (Lorg/jetbrains/jewel/foundation/theme/JewelTheme$Companion;Landroidx/compose/runtime/Composer;I)Lorg/jetbrains/jewel/ui/component/styling/TooltipStyle; diff --git a/ui/src/main/kotlin/org/jetbrains/jewel/ui/DefaultComponentStyling.kt b/ui/src/main/kotlin/org/jetbrains/jewel/ui/DefaultComponentStyling.kt index 10b1d93a9..55adc5e05 100644 --- a/ui/src/main/kotlin/org/jetbrains/jewel/ui/DefaultComponentStyling.kt +++ b/ui/src/main/kotlin/org/jetbrains/jewel/ui/DefaultComponentStyling.kt @@ -34,6 +34,7 @@ import org.jetbrains.jewel.ui.component.styling.LocalMenuStyle import org.jetbrains.jewel.ui.component.styling.LocalOutlinedButtonStyle import org.jetbrains.jewel.ui.component.styling.LocalRadioButtonStyle import org.jetbrains.jewel.ui.component.styling.LocalScrollbarStyle +import org.jetbrains.jewel.ui.component.styling.LocalSliderStyle import org.jetbrains.jewel.ui.component.styling.LocalTextAreaStyle import org.jetbrains.jewel.ui.component.styling.LocalTextFieldStyle import org.jetbrains.jewel.ui.component.styling.LocalTooltipStyle @@ -41,6 +42,7 @@ import org.jetbrains.jewel.ui.component.styling.LocalUndecoratedDropdownStyle import org.jetbrains.jewel.ui.component.styling.MenuStyle import org.jetbrains.jewel.ui.component.styling.RadioButtonStyle import org.jetbrains.jewel.ui.component.styling.ScrollbarStyle +import org.jetbrains.jewel.ui.component.styling.SliderStyle import org.jetbrains.jewel.ui.component.styling.TabStyle import org.jetbrains.jewel.ui.component.styling.TextAreaStyle import org.jetbrains.jewel.ui.component.styling.TextFieldStyle @@ -66,6 +68,7 @@ public class DefaultComponentStyling( public val outlinedButtonStyle: ButtonStyle, public val radioButtonStyle: RadioButtonStyle, public val scrollbarStyle: ScrollbarStyle, + public val sliderStyle: SliderStyle, public val textAreaStyle: TextAreaStyle, public val textFieldStyle: TextFieldStyle, public val tooltipStyle: TooltipStyle, @@ -93,6 +96,7 @@ public class DefaultComponentStyling( LocalOutlinedButtonStyle provides outlinedButtonStyle, LocalRadioButtonStyle provides radioButtonStyle, LocalScrollbarStyle provides scrollbarStyle, + LocalSliderStyle provides sliderStyle, LocalTextAreaStyle provides textAreaStyle, LocalTextFieldStyle provides textFieldStyle, LocalTooltipStyle provides tooltipStyle, diff --git a/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/Button.kt b/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/Button.kt index 235ae3919..187b810df 100644 --- a/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/Button.kt +++ b/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/Button.kt @@ -94,8 +94,9 @@ private fun ButtonImpl( textStyle: TextStyle, content: @Composable RowScope.() -> Unit, ) { - var buttonState by - remember(interactionSource) { mutableStateOf(ButtonState.of(enabled = enabled)) } + var buttonState by remember(interactionSource) { + mutableStateOf(ButtonState.of(enabled = enabled)) + } remember(enabled) { buttonState = buttonState.copy(enabled = enabled) } @@ -103,9 +104,9 @@ private fun ButtonImpl( interactionSource.interactions.collect { interaction -> when (interaction) { is PressInteraction.Press -> buttonState = buttonState.copy(pressed = true) - is PressInteraction.Cancel, - is PressInteraction.Release, - -> buttonState = buttonState.copy(pressed = false) + is PressInteraction.Cancel, is PressInteraction.Release -> + buttonState = buttonState.copy(pressed = false) + is HoverInteraction.Enter -> buttonState = buttonState.copy(hovered = true) is HoverInteraction.Exit -> buttonState = buttonState.copy(hovered = false) is FocusInteraction.Focus -> buttonState = buttonState.copy(focused = true) diff --git a/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/Slider.kt b/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/Slider.kt new file mode 100644 index 000000000..2efbd66a7 --- /dev/null +++ b/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/Slider.kt @@ -0,0 +1,735 @@ +package org.jetbrains.jewel.ui.component + +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.TweenSpec +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.MutatePriority +import androidx.compose.foundation.MutatorMutex +import androidx.compose.foundation.background +import androidx.compose.foundation.focusable +import androidx.compose.foundation.gestures.DragScope +import androidx.compose.foundation.gestures.DraggableState +import androidx.compose.foundation.gestures.GestureCancellationException +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.gestures.draggable +import androidx.compose.foundation.hoverable +import androidx.compose.foundation.interaction.DragInteraction +import androidx.compose.foundation.interaction.FocusInteraction +import androidx.compose.foundation.interaction.HoverInteraction +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.PressInteraction +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.requiredSizeIn +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.progressSemantics +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.State +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +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.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.lerp +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.input.key.KeyEvent +import androidx.compose.ui.input.key.KeyEventType +import androidx.compose.ui.input.key.key +import androidx.compose.ui.input.key.nativeKeyCode +import androidx.compose.ui.input.key.onKeyEvent +import androidx.compose.ui.input.key.type +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.platform.debugInspectorInfo +import androidx.compose.ui.semantics.disabled +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.setProgress +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch +import org.jetbrains.jewel.foundation.Stroke +import org.jetbrains.jewel.foundation.modifier.border +import org.jetbrains.jewel.foundation.state.CommonStateBitMask.Active +import org.jetbrains.jewel.foundation.state.CommonStateBitMask.Enabled +import org.jetbrains.jewel.foundation.state.CommonStateBitMask.Focused +import org.jetbrains.jewel.foundation.state.CommonStateBitMask.Hovered +import org.jetbrains.jewel.foundation.state.CommonStateBitMask.Pressed +import org.jetbrains.jewel.foundation.state.FocusableComponentState +import org.jetbrains.jewel.foundation.theme.JewelTheme +import org.jetbrains.jewel.ui.component.styling.SliderStyle +import org.jetbrains.jewel.ui.focusOutline +import org.jetbrains.jewel.ui.theme.sliderStyle +import kotlin.math.abs +import kotlin.math.max +import kotlin.math.min + +// Note: a lot of code in this file is from the Material 2 implementation + +@Composable +public fun Slider( + value: Float, + onValueChange: (Float) -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + valueRange: ClosedFloatingPointRange = 0f..1f, + steps: Int = 0, + onValueChangeFinished: (() -> Unit)? = null, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + style: SliderStyle = JewelTheme.sliderStyle, +) { + require(steps >= 0) { "The number of steps must be >= 0" } + + val onValueChangeState = rememberUpdatedState(onValueChange) + val onValueChangeFinishedState = rememberUpdatedState(onValueChangeFinished) + val tickFractions = remember(steps) { + stepsToTickFractions(steps) + } + val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl + + val focusRequester = remember { FocusRequester() } + val thumbSize = style.metrics.thumbSize + val ticksHeight = if (tickFractions.isNotEmpty()) { + style.metrics.trackToStepSpacing + style.metrics.stepLineHeight + } else { + 0.dp + } + + val minHeight = thumbSize.height + style.metrics.thumbBorderWidth * 2 + ticksHeight + + BoxWithConstraints( + modifier + .requiredSizeIn(minWidth = thumbSize.height * 2, minHeight = minHeight) + .sliderSemantics(value, enabled, onValueChange, onValueChangeFinished, valueRange, steps) + .focusRequester(focusRequester) + .focusable(enabled, interactionSource) + .slideOnKeyEvents(enabled, steps, valueRange, value, isRtl, onValueChangeState, onValueChangeFinishedState), + ) { + val widthPx = constraints.maxWidth.toFloat() + val maxPx: Float + val minPx: Float + + with(LocalDensity.current) { + maxPx = max(widthPx - thumbSize.width.toPx(), 0f) + minPx = min(thumbSize.width.toPx(), maxPx) + } + + fun scaleToUserValue(offset: Float) = + scale(minPx, maxPx, offset, valueRange.start, valueRange.endInclusive) + + fun scaleToOffset(userValue: Float) = + scale(valueRange.start, valueRange.endInclusive, userValue, minPx, maxPx) + + val scope = rememberCoroutineScope() + val rawOffset = remember { mutableFloatStateOf(scaleToOffset(value)) } + val pressOffset = remember { mutableFloatStateOf(0f) } + + val draggableState = remember(minPx, maxPx, valueRange) { + SliderDraggableState { + rawOffset.floatValue = (rawOffset.floatValue + it + pressOffset.floatValue) + pressOffset.floatValue = 0f + val offsetInTrack = rawOffset.floatValue.coerceIn(minPx, maxPx) + onValueChangeState.value.invoke(scaleToUserValue(offsetInTrack)) + } + } + + CorrectValueSideEffect(::scaleToOffset, valueRange, minPx..maxPx, rawOffset, value) + + val canAnimate = !JewelTheme.isSwingCompatMode + val gestureEndAction = rememberUpdatedState<(Float) -> Unit> { velocity: Float -> + val current = rawOffset.floatValue + val target = snapValueToTick(current, tickFractions, minPx, maxPx) + focusRequester.requestFocus() + if (current != target) { + scope.launch { + if (canAnimate) { + animateToTarget(draggableState, current, target, velocity) + } else { + draggableState.drag { + dragBy(target - current) + } + } + onValueChangeFinished?.invoke() + } + } else if (!draggableState.isDragging) { + // check ifDragging in case the change is still in progress (touch -> drag case) + onValueChangeFinished?.invoke() + } + } + val press = Modifier.sliderTapModifier( + draggableState, + interactionSource, + widthPx, + isRtl, + rawOffset, + gestureEndAction, + pressOffset, + enabled, + ) + + val drag = Modifier.draggable( + orientation = Orientation.Horizontal, + reverseDirection = isRtl, + enabled = enabled, + interactionSource = interactionSource, + onDragStopped = { velocity -> gestureEndAction.value.invoke(velocity) }, + startDragImmediately = draggableState.isDragging, + state = draggableState, + ) + + val coerced = value.coerceIn(valueRange.start, valueRange.endInclusive) + val fraction = calcFraction(valueRange.start, valueRange.endInclusive, coerced) + SliderImpl( + enabled = enabled, + positionFraction = fraction, + tickFractions = tickFractions, + style = style, + minHeight = minHeight, + width = maxPx - minPx, + interactionSource = interactionSource, + modifier = press.then(drag), + ) + } +} + +private val SliderMinWidth = 100.dp // Completely arbitrary + +@Composable +private fun SliderImpl( + enabled: Boolean, + positionFraction: Float, + tickFractions: List, + style: SliderStyle, + width: Float, + minHeight: Dp, + interactionSource: MutableInteractionSource, + modifier: Modifier, +) { + Box( + modifier.then( + Modifier + .widthIn(min = SliderMinWidth) + .heightIn(min = minHeight), + ), + ) { + val widthDp: Dp + with(LocalDensity.current) { + widthDp = width.toDp() + } + + Track( + modifier = Modifier.fillMaxWidth(), + style = style, + enabled = enabled, + positionFractionEnd = positionFraction, + tickFractions = tickFractions, + ) + + val offset = (widthDp + style.metrics.thumbSize.width) * positionFraction + SliderThumb(offset, interactionSource, style, enabled) + } +} + +@Composable +private fun Track( + modifier: Modifier, + style: SliderStyle, + enabled: Boolean, + positionFractionEnd: Float, + tickFractions: List, +) { + val trackStrokeWidthPx: Float + val thumbWidthPx: Float + val trackYPx: Float + val stepLineHeightPx: Float + val stepLineWidthPx: Float + val trackToMarkersGapPx: Float + + with(LocalDensity.current) { + trackStrokeWidthPx = style.metrics.trackHeight.toPx() + thumbWidthPx = style.metrics.thumbSize.width.toPx() + trackYPx = style.metrics.thumbSize.width.toPx() / 2 + style.metrics.thumbBorderWidth.toPx() + stepLineHeightPx = style.metrics.stepLineHeight.toPx() + stepLineWidthPx = style.metrics.stepLineWidth.toPx() + trackToMarkersGapPx = style.metrics.trackToStepSpacing.toPx() + } + + Canvas(modifier) { + val isRtl = layoutDirection == LayoutDirection.Rtl + + val trackLeft = Offset(thumbWidthPx / 2, trackYPx) + val trackRight = Offset(size.width - thumbWidthPx / 2, trackYPx) + val trackStart = if (isRtl) trackRight else trackLeft + val trackEnd = if (isRtl) trackLeft else trackRight + + drawLine( + color = if (enabled) style.colors.track else style.colors.trackDisabled, + start = trackStart, + end = trackEnd, + strokeWidth = trackStrokeWidthPx, + cap = StrokeCap.Round, + ) + + val activeTrackStart = Offset(trackStart.x, trackYPx) + val activeTrackEnd = Offset( + x = trackStart.x + (trackEnd.x - trackStart.x) * positionFractionEnd - thumbWidthPx / 2, + y = trackYPx, + ) + + drawLine( + color = if (enabled) style.colors.trackFilled else style.colors.trackFilledDisabled, + start = activeTrackStart, + end = activeTrackEnd, + strokeWidth = trackStrokeWidthPx, + cap = StrokeCap.Round, + ) + + val stepMarkerY = trackStrokeWidthPx + trackToMarkersGapPx + + tickFractions.forEach { fraction -> + drawLine( + color = style.colors.stepMarker, + start = Offset(lerp(trackStart, trackEnd, fraction).x, stepMarkerY), + end = Offset(lerp(trackStart, trackEnd, fraction).x, stepMarkerY + stepLineHeightPx), + strokeWidth = stepLineWidthPx, + cap = StrokeCap.Round, + ) + } + } +} + +@Composable +private fun BoxScope.SliderThumb( + offset: Dp, + interactionSource: MutableInteractionSource, + style: SliderStyle, + enabled: Boolean, + modifier: Modifier = Modifier, +) { + Box( + Modifier + .padding(start = offset, top = style.metrics.thumbBorderWidth) + .align(Alignment.TopStart), + ) { + var state by remember { mutableStateOf(SliderState.of(enabled)) } + remember(enabled) { state = state.copy(enabled = enabled) } + + LaunchedEffect(interactionSource) { + interactionSource.interactions.collect { interaction -> + when (interaction) { + is PressInteraction.Press, is DragInteraction.Start -> + state = state.copy(pressed = true) + + is PressInteraction.Release, is PressInteraction.Cancel, + is DragInteraction.Stop, is DragInteraction.Cancel, + -> + state = state.copy(pressed = false) + + is HoverInteraction.Enter -> state = state.copy(hovered = true) + is HoverInteraction.Exit -> state = state.copy(hovered = false) + is FocusInteraction.Focus -> state = state.copy(focused = true) + is FocusInteraction.Unfocus -> state = state.copy(focused = false) + } + } + } + + val thumbSize = style.metrics.thumbSize + Spacer( + modifier + .size(thumbSize) + .hoverable(interactionSource, enabled) + .background(style.colors.thumbFillFor(state).value, style.thumbShape) + .border( + alignment = Stroke.Alignment.Outside, + width = style.metrics.thumbBorderWidth, + color = style.colors.thumbBorderFor(state).value, + shape = style.thumbShape, + ) + .focusOutline(state, style.thumbShape), + ) + } +} + +private fun snapValueToTick( + current: Float, + tickFractions: List, + minPx: Float, + maxPx: Float, +): Float = + // target is a closest anchor to the `current`, if exists + tickFractions + .minByOrNull { abs(lerp(minPx, maxPx, it) - current) } + ?.run { lerp(minPx, maxPx, this) } + ?: current + +private fun stepsToTickFractions(steps: Int): List = + if (steps == 0) emptyList() else List(steps + 2) { it.toFloat() / (steps + 1) } + +// Scale x1 from a1..b1 range to a2..b2 range +private fun scale(a1: Float, b1: Float, x1: Float, a2: Float, b2: Float) = + lerp(a2, b2, calcFraction(a1, b1, x1)) + +// Calculate the 0..1 fraction that `pos` value represents between `a` and `b` +private fun calcFraction(a: Float, b: Float, pos: Float) = + (if (b - a == 0f) 0f else (pos - a) / (b - a)).coerceIn(0f, 1f) + +@Composable +private fun CorrectValueSideEffect( + scaleToOffset: (Float) -> Float, + valueRange: ClosedFloatingPointRange, + trackRange: ClosedFloatingPointRange, + valueState: MutableState, + value: Float, +) { + SideEffect { + val error = (valueRange.endInclusive - valueRange.start) / 1000 + val newOffset = scaleToOffset(value) + if (abs(newOffset - valueState.value) > error && valueState.value in trackRange) { + valueState.value = newOffset + } + } +} + +private fun Modifier.sliderSemantics( + value: Float, + enabled: Boolean, + onValueChange: (Float) -> Unit, + onValueChangeFinished: (() -> Unit)? = null, + valueRange: ClosedFloatingPointRange = 0f..1f, + steps: Int = 0, +): Modifier { + val coerced = value.coerceIn(valueRange.start, valueRange.endInclusive) + + return semantics { + if (!enabled) disabled() + + setProgress( + action = { targetValue -> + var newValue = targetValue.coerceIn(valueRange.start, valueRange.endInclusive) + val originalVal = newValue + val resolvedValue = if (steps > 0) { + var distance: Float = newValue + for (i in 0..steps + 1) { + val stepValue = lerp( + valueRange.start, + valueRange.endInclusive, + i.toFloat() / (steps + 1), + ) + if (abs(stepValue - originalVal) <= distance) { + distance = abs(stepValue - originalVal) + newValue = stepValue + } + } + newValue + } else { + newValue + } + // This is to keep it consistent with AbsSeekbar.java: return false if no + // change from current. + if (resolvedValue == coerced) { + false + } else { + onValueChange(resolvedValue) + onValueChangeFinished?.invoke() + true + } + }, + ) + }.progressSemantics(value, valueRange, steps) +} + +private fun Modifier.sliderTapModifier( + draggableState: DraggableState, + interactionSource: MutableInteractionSource, + maxPx: Float, + isRtl: Boolean, + rawOffset: State, + gestureEndAction: State<(Float) -> Unit>, + pressOffset: MutableState, + enabled: Boolean, +) = composed( + factory = { + if (enabled) { + val scope = rememberCoroutineScope() + pointerInput(draggableState, interactionSource, maxPx, isRtl) { + detectTapGestures( + onPress = { pos -> + val to = if (isRtl) maxPx - pos.x else pos.x + pressOffset.value = to - rawOffset.value + try { + awaitRelease() + } catch (_: GestureCancellationException) { + pressOffset.value = 0f + } + }, + onTap = { + scope.launch { + draggableState.drag(MutatePriority.UserInput) { + // just trigger animation, press offset will be applied + dragBy(0f) + } + gestureEndAction.value.invoke(0f) + } + }, + ) + } + } else { + this + } + }, + inspectorInfo = debugInspectorInfo { + name = "sliderTapModifier" + properties["draggableState"] = draggableState + properties["interactionSource"] = interactionSource + properties["maxPx"] = maxPx + properties["isRtl"] = isRtl + properties["rawOffset"] = rawOffset + properties["gestureEndAction"] = gestureEndAction + properties["pressOffset"] = pressOffset + properties["enabled"] = enabled + }, +) + +private val SliderToTickAnimation = TweenSpec(durationMillis = 100) + +private suspend fun animateToTarget( + draggableState: DraggableState, + current: Float, + target: Float, + velocity: Float, +) { + draggableState.drag { + var latestValue = current + Animatable(initialValue = current).animateTo(target, SliderToTickAnimation, velocity) { + dragBy(this.value - latestValue) + latestValue = this.value + } + } +} + +// TODO: Edge case - losing focus on slider while key is pressed will end up with onValueChangeFinished not being invoked +@OptIn(ExperimentalComposeUiApi::class) +private fun Modifier.slideOnKeyEvents( + enabled: Boolean, + steps: Int, + valueRange: ClosedFloatingPointRange, + value: Float, + isRtl: Boolean, + onValueChangeState: State<(Float) -> Unit>, + onValueChangeFinishedState: State<(() -> Unit)?>, +): Modifier { + require(steps >= 0) { "steps should be >= 0" } + + return this.onKeyEvent { + if (!enabled) return@onKeyEvent false + + when (it.type) { + KeyEventType.KeyDown -> { + val rangeLength = abs(valueRange.endInclusive - valueRange.start) + // When steps == 0, it means that a user is not limited by a step length (delta) when using touch or mouse. + // But it is not possible to adjust the value continuously when using keyboard buttons - + // the delta has to be discrete. In this case, 1% of the valueRange seems to make sense. + val actualSteps = if (steps > 0) steps + 1 else 100 + val delta = rangeLength / actualSteps + when { + it.isDirectionUp -> { + onValueChangeState.value((value + delta).coerceIn(valueRange)) + true + } + + it.isDirectionDown -> { + onValueChangeState.value((value - delta).coerceIn(valueRange)) + true + } + + it.isDirectionRight -> { + val sign = if (isRtl) -1 else 1 + onValueChangeState.value((value + sign * delta).coerceIn(valueRange)) + true + } + + it.isDirectionLeft -> { + val sign = if (isRtl) -1 else 1 + onValueChangeState.value((value - sign * delta).coerceIn(valueRange)) + true + } + + it.isHome -> { + onValueChangeState.value(valueRange.start) + true + } + + it.isMoveEnd -> { + onValueChangeState.value(valueRange.endInclusive) + true + } + + it.isPgUp -> { + val page = (actualSteps / 10).coerceIn(1, 10) + onValueChangeState.value((value - page * delta).coerceIn(valueRange)) + true + } + + it.isPgDn -> { + val page = (actualSteps / 10).coerceIn(1, 10) + onValueChangeState.value((value + page * delta).coerceIn(valueRange)) + true + } + + else -> false + } + } + + KeyEventType.KeyUp -> { + @Suppress("ComplexCondition") // In original m2 code + if (it.isDirectionDown || it.isDirectionUp || it.isDirectionRight || + it.isDirectionLeft || it.isHome || it.isMoveEnd || it.isPgUp || it.isPgDn + ) { + onValueChangeFinishedState.value?.invoke() + true + } else { + false + } + } + + else -> false + } + } +} + +internal val KeyEvent.isDirectionUp: Boolean + get() = key.nativeKeyCode == java.awt.event.KeyEvent.VK_UP + +internal val KeyEvent.isDirectionDown: Boolean + get() = key.nativeKeyCode == java.awt.event.KeyEvent.VK_DOWN + +internal val KeyEvent.isDirectionRight: Boolean + get() = key.nativeKeyCode == java.awt.event.KeyEvent.VK_RIGHT + +internal val KeyEvent.isDirectionLeft: Boolean + get() = key.nativeKeyCode == java.awt.event.KeyEvent.VK_LEFT + +internal val KeyEvent.isHome: Boolean + get() = key.nativeKeyCode == java.awt.event.KeyEvent.VK_HOME + +internal val KeyEvent.isMoveEnd: Boolean + get() = key.nativeKeyCode == java.awt.event.KeyEvent.VK_END + +internal val KeyEvent.isPgUp: Boolean + get() = key.nativeKeyCode == java.awt.event.KeyEvent.VK_PAGE_UP + +internal val KeyEvent.isPgDn: Boolean + get() = key.nativeKeyCode == java.awt.event.KeyEvent.VK_PAGE_DOWN + +internal fun lerp(start: Float, stop: Float, fraction: Float): Float = + (1 - fraction) * start + fraction * stop + +@Immutable +@JvmInline +public value class SliderState(public val state: ULong) : FocusableComponentState { + + override val isActive: Boolean + get() = state and Active != 0UL + + override val isEnabled: Boolean + get() = state and Enabled != 0UL + + override val isFocused: Boolean + get() = state and Focused != 0UL + + override val isHovered: Boolean + get() = state and Hovered != 0UL + + override val isPressed: Boolean + get() = state and Pressed != 0UL + + public fun copy( + enabled: Boolean = isEnabled, + focused: Boolean = isFocused, + pressed: Boolean = isPressed, + hovered: Boolean = isHovered, + active: Boolean = isActive, + ): SliderState = + of( + enabled = enabled, + focused = focused, + pressed = pressed, + hovered = hovered, + active = active, + ) + + override fun toString(): String = + "${javaClass.simpleName}(isEnabled=$isEnabled, isFocused=$isFocused, isHovered=$isHovered, " + + "isPressed=$isPressed, isActive=$isActive)" + + public companion object { + + public fun of( + enabled: Boolean = true, + focused: Boolean = false, + pressed: Boolean = false, + hovered: Boolean = false, + active: Boolean = true, + ): SliderState = + SliderState( + (if (enabled) Enabled else 0UL) or + (if (focused) Focused else 0UL) or + (if (hovered) Hovered else 0UL) or + (if (pressed) Pressed else 0UL) or + (if (active) Active else 0UL), + ) + } +} + +private class SliderDraggableState( + val onDelta: (Float) -> Unit, +) : DraggableState { + + var isDragging by mutableStateOf(false) + private set + + private val dragScope: DragScope = object : DragScope { + override fun dragBy(pixels: Float) { + onDelta(pixels) + } + } + + private val scrollMutex = MutatorMutex() + + override suspend fun drag( + dragPriority: MutatePriority, + block: suspend DragScope.() -> Unit, + ) { + coroutineScope { + isDragging = true + scrollMutex.mutateWith(dragScope, dragPriority, block) + isDragging = false + } + } + + override fun dispatchRawDelta(delta: Float) { + onDelta(delta) + } +} diff --git a/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/styling/SliderStyling.kt b/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/styling/SliderStyling.kt new file mode 100644 index 000000000..aeacf9091 --- /dev/null +++ b/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/styling/SliderStyling.kt @@ -0,0 +1,109 @@ +package org.jetbrains.jewel.ui.component.styling + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.ProvidableCompositionLocal +import androidx.compose.runtime.Stable +import androidx.compose.runtime.State +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpSize +import org.jetbrains.jewel.foundation.GenerateDataFunctions +import org.jetbrains.jewel.foundation.theme.JewelTheme +import org.jetbrains.jewel.ui.component.SliderState + +@Stable +@GenerateDataFunctions +public class SliderStyle( + public val colors: SliderColors, + public val metrics: SliderMetrics, + public val thumbShape: Shape, +) { + + public companion object +} + +@Immutable +@GenerateDataFunctions +public class SliderColors( + public val track: Color, + public val trackFilled: Color, + public val trackDisabled: Color, + public val trackFilledDisabled: Color, + public val stepMarker: Color, + public val thumbFill: Color, + public val thumbFillDisabled: Color, + public val thumbFillFocused: Color, + public val thumbFillPressed: Color, + public val thumbFillHovered: Color, + public val thumbBorder: Color, + public val thumbBorderFocused: Color, + public val thumbBorderDisabled: Color, + public val thumbBorderPressed: Color, + public val thumbBorderHovered: Color, +) { + + @Composable + public fun thumbFillFor(state: SliderState): State = + rememberUpdatedState( + state.chooseColor( + normal = thumbFill, + disabled = thumbFillDisabled, + focused = thumbFillFocused, + pressed = thumbFillPressed, + hovered = thumbFillHovered, + ), + ) + + @Composable + public fun thumbBorderFor(state: SliderState): State = + rememberUpdatedState( + state.chooseColor( + normal = thumbBorder, + disabled = thumbBorderDisabled, + focused = thumbBorderFocused, + pressed = thumbBorderPressed, + hovered = thumbBorderHovered, + ), + ) + + @Composable + private fun SliderState.chooseColor( + normal: Color, + disabled: Color, + pressed: Color, + hovered: Color, + focused: Color, + ) = + when { + !isEnabled -> disabled + isFocused -> focused + isPressed && !JewelTheme.isSwingCompatMode -> pressed + isHovered && !JewelTheme.isSwingCompatMode -> hovered + else -> normal + } + + public companion object +} + +@Immutable +@GenerateDataFunctions +public class SliderMetrics( + public val trackHeight: Dp, + public val thumbSize: DpSize, + public val thumbBorderWidth: Dp, + public val stepLineHeight: Dp, + public val stepLineWidth: Dp, + public val trackToStepSpacing: Dp, +) { + + public companion object +} + +public val LocalSliderStyle: ProvidableCompositionLocal = + staticCompositionLocalOf { + error("No default SliderStyle provided. Have you forgotten the theme?") + } diff --git a/ui/src/main/kotlin/org/jetbrains/jewel/ui/theme/JewelTheme.kt b/ui/src/main/kotlin/org/jetbrains/jewel/ui/theme/JewelTheme.kt index 55838e082..4b3f65997 100644 --- a/ui/src/main/kotlin/org/jetbrains/jewel/ui/theme/JewelTheme.kt +++ b/ui/src/main/kotlin/org/jetbrains/jewel/ui/theme/JewelTheme.kt @@ -40,12 +40,14 @@ import org.jetbrains.jewel.ui.component.styling.LocalMenuStyle import org.jetbrains.jewel.ui.component.styling.LocalOutlinedButtonStyle import org.jetbrains.jewel.ui.component.styling.LocalRadioButtonStyle import org.jetbrains.jewel.ui.component.styling.LocalScrollbarStyle +import org.jetbrains.jewel.ui.component.styling.LocalSliderStyle import org.jetbrains.jewel.ui.component.styling.LocalTextAreaStyle import org.jetbrains.jewel.ui.component.styling.LocalTextFieldStyle import org.jetbrains.jewel.ui.component.styling.LocalTooltipStyle import org.jetbrains.jewel.ui.component.styling.MenuStyle import org.jetbrains.jewel.ui.component.styling.RadioButtonStyle import org.jetbrains.jewel.ui.component.styling.ScrollbarStyle +import org.jetbrains.jewel.ui.component.styling.SliderStyle import org.jetbrains.jewel.ui.component.styling.TabStyle import org.jetbrains.jewel.ui.component.styling.TextAreaStyle import org.jetbrains.jewel.ui.component.styling.TextFieldStyle @@ -165,6 +167,11 @@ public val JewelTheme.Companion.iconButtonStyle: IconButtonStyle @ReadOnlyComposable get() = LocalIconButtonStyle.current +public val JewelTheme.Companion.sliderStyle: SliderStyle + @Composable + @ReadOnlyComposable + get() = LocalSliderStyle.current + @Composable public fun BaseJewelTheme( theme: ThemeDefinition,