From 27273aaeb4002678310ac67825e62e1a44dcf24d Mon Sep 17 00:00:00 2001 From: Sebastiano Poggi Date: Mon, 11 Dec 2023 18:21:24 -0800 Subject: [PATCH] Create Slider component There are no specs for this component, and no Swing implementation. However, this has been derived from the general IntUI style with Chris' help to choose behaviour, appearance and colours. It is needed for a demo, hence why it was implemented already; if the IntUI specs will eventually include this component, we'll update the implementation as needed. --- 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 | 110 ++- .../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, 1252 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..0a179f8c9 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,15 +44,18 @@ 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; public final fun getUndecoratedDropdownStyle ()Lorg/jetbrains/jewel/ui/component/styling/DropdownStyle; public fun hashCode ()I + public synthetic fun provide (Lkotlin/jvm/functions/Function0;)Lorg/jetbrains/jewel/ui/ComponentStyling; public fun provide (Lkotlin/jvm/functions/Function2;)Lorg/jetbrains/jewel/ui/ComponentStyling; public fun provide ([Landroidx/compose/runtime/ProvidedValue;)Lorg/jetbrains/jewel/ui/ComponentStyling; public fun styles (Landroidx/compose/runtime/Composer;I)[Landroidx/compose/runtime/ProvidedValue; public fun toString ()Ljava/lang/String; + public synthetic fun with (Lkotlin/jvm/functions/Function0;)Lorg/jetbrains/jewel/ui/ComponentStyling; public fun with (Lkotlin/jvm/functions/Function2;)Lorg/jetbrains/jewel/ui/ComponentStyling; public fun with (Lorg/jetbrains/jewel/ui/ComponentStyling;)Lorg/jetbrains/jewel/ui/ComponentStyling; } @@ -551,6 +554,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 +1678,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 +2304,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,