From 81b81f9caff4e5615b7183e0b11cd4106ca47534 Mon Sep 17 00:00:00 2001 From: AmirHossein Abdolmotallebi Date: Thu, 17 Oct 2024 01:36:01 +0330 Subject: [PATCH] upgrade compose version to v1.7.0 --- .../desktop/ui/theme/ABDownloaderTheme.kt | 29 +- .../desktop/ui/theme/MaterialRipple.kt | 409 ++++++++++++++++++ .../desktop/ui/widget/menu/Option.kt | 2 +- gradle/libs.versions.toml | 2 +- .../ir/amirab/util/compose/IconSource.kt | 1 - 5 files changed, 412 insertions(+), 31 deletions(-) create mode 100644 desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/ui/theme/MaterialRipple.kt diff --git a/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/ui/theme/ABDownloaderTheme.kt b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/ui/theme/ABDownloaderTheme.kt index f19eb28..d85fb36 100644 --- a/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/ui/theme/ABDownloaderTheme.kt +++ b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/ui/theme/ABDownloaderTheme.kt @@ -10,12 +10,6 @@ import com.abdownloadmanager.desktop.utils.div import androidx.compose.animation.core.tween import androidx.compose.foundation.* import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.ripple.LocalRippleTheme -import androidx.compose.material.ripple.RippleAlpha -import androidx.compose.material.ripple.RippleTheme -import androidx.compose.material.ripple.RippleTheme.Companion.defaultRippleAlpha -import androidx.compose.material.ripple.RippleTheme.Companion.defaultRippleColor -import androidx.compose.material.ripple.rememberRipple import androidx.compose.runtime.* import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.TextUnit @@ -116,10 +110,7 @@ fun ABDownloaderTheme( CompositionLocalProvider( LocalContextMenuRepresentation provides myContextMenuRepresentation(), LocalScrollbarStyle provides myDefaultScrollBarStyle(), - // there is a modification in newer version of compose that change line height - // I want to remove material design for good but for now I override this - LocalRippleTheme provides remember { MyRippleTheme() }, - LocalIndication provides rememberRipple(), + LocalIndication provides ripple(), LocalContentColor provides myColors.onBackground, LocalContentAlpha provides 1f, LocalTextSizes provides textSizes, @@ -133,24 +124,6 @@ fun ABDownloaderTheme( } } -private class MyRippleTheme:RippleTheme{ - @Composable - override fun defaultColor():Color { - return defaultRippleColor( - contentColor = LocalContentColor.current, - lightTheme = myColors.isLight - ) - } - - @Composable - override fun rippleAlpha(): RippleAlpha { - return defaultRippleAlpha( - contentColor = LocalContentColor.current, - lightTheme = myColors.isLight - ) - } -} - private class MyContextMenuRepresentation : ContextMenuRepresentation { @Composable override fun Representation(state: ContextMenuState, items: () -> List) { diff --git a/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/ui/theme/MaterialRipple.kt b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/ui/theme/MaterialRipple.kt new file mode 100644 index 0000000..c0786ce --- /dev/null +++ b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/ui/theme/MaterialRipple.kt @@ -0,0 +1,409 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.abdownloadmanager.desktop.ui.theme + +import androidx.compose.foundation.Indication +import androidx.compose.foundation.IndicationNodeFactory +import androidx.compose.foundation.interaction.Interaction +import androidx.compose.foundation.interaction.InteractionSource +import androidx.compose.foundation.interaction.PressInteraction +import androidx.compose.material.ripple.RippleAlpha +import androidx.compose.material.ripple.createRippleModifierNode +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.ProvidableCompositionLocal +import androidx.compose.runtime.Stable +import androidx.compose.runtime.compositionLocalOf +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorProducer +import androidx.compose.ui.graphics.isSpecified +import androidx.compose.ui.graphics.luminance +import androidx.compose.ui.node.CompositionLocalConsumerModifierNode +import androidx.compose.ui.node.DelegatableNode +import androidx.compose.ui.node.DelegatingNode +import androidx.compose.ui.node.ObserverModifierNode +import androidx.compose.ui.node.currentValueOf +import androidx.compose.ui.node.observeReads +import androidx.compose.ui.unit.Dp +import com.abdownloadmanager.utils.compose.LocalContentColor + +/** + * Creates a Ripple using the provided values and values inferred from the theme. + * + * A Ripple is a Material implementation of [Indication] that expresses different [Interaction]s + * by drawing ripple animations and state layers. + * + * A Ripple responds to [PressInteraction.Press] by starting a new animation, and + * responds to other [Interaction]s by showing a fixed state layer with varying alpha values + * depending on the [Interaction]. + * + * [MaterialTheme] provides Ripples using [androidx.compose.foundation.LocalIndication], so a Ripple + * will be used as the default [Indication] inside components such as + * [androidx.compose.foundation.clickable] and [androidx.compose.foundation.indication], in + * addition to Material provided components that use a Ripple as well. + * + * You can also explicitly create a Ripple and provide it to custom components in order to change + * the parameters from the default, such as to create an unbounded ripple with a fixed size. + * + * To create a Ripple with a manually defined color that can change over time, see the other + * [ripple] overload with a [ColorProducer] parameter. This will avoid unnecessary recompositions + * when changing the color, and preserve existing ripple state when the color changes. + * + * @param bounded If true, ripples are clipped by the bounds of the target layout. Unbounded + * ripples always animate from the target layout center, bounded ripples animate from the touch + * position. + * @param radius the radius for the ripple. If [Dp.Unspecified] is provided then the size will be + * calculated based on the target layout size. + * @param color the color of the ripple. This color is usually the same color used by the text or + * iconography in the component. This color will then have [RippleDefaults.rippleAlpha] + * applied to calculate the final color used to draw the ripple. If [Color.Unspecified] is + * provided the color used will be [RippleDefaults.rippleColor] instead. + */ +@Stable +fun ripple( + bounded: Boolean = true, + radius: Dp = Dp.Unspecified, + color: Color = Color.Unspecified, +): IndicationNodeFactory { + return if (radius == Dp.Unspecified && color == Color.Unspecified) { + if (bounded) return DefaultBoundedRipple else DefaultUnboundedRipple + } else { + RippleNodeFactory(bounded, radius, color) + } +} + +/** + * Creates a Ripple using the provided values and values inferred from the theme. + * + * A Ripple is a Material implementation of [Indication] that expresses different [Interaction]s + * by drawing ripple animations and state layers. + * + * A Ripple responds to [PressInteraction.Press] by starting a new ripple animation, and + * responds to other [Interaction]s by showing a fixed state layer with varying alpha values + * depending on the [Interaction]. + * + * [MaterialTheme] provides Ripples using [androidx.compose.foundation.LocalIndication], so a Ripple + * will be used as the default [Indication] inside components such as + * [androidx.compose.foundation.clickable] and [androidx.compose.foundation.indication], in + * addition to Material provided components that use a Ripple as well. + * + * You can also explicitly create a Ripple and provide it to custom components in order to change + * the parameters from the default, such as to create an unbounded ripple with a fixed size. + * + * To create a Ripple with a static color, see the [ripple] overload with a [Color] parameter. This + * overload is optimized for Ripples that have dynamic colors that change over time, to reduce + * unnecessary recompositions. + * + * @param color the color of the ripple. This color is usually the same color used by the text or + * iconography in the component. This color will then have [RippleDefaults.rippleAlpha] + * applied to calculate the final color used to draw the ripple. If you are creating this + * [ColorProducer] outside of composition (where it will be automatically remembered), make sure + * that its instance is stable (such as by remembering the object that holds it), or remember the + * returned [ripple] object to make sure that ripple nodes are not being created each recomposition. + * @param bounded If true, ripples are clipped by the bounds of the target layout. Unbounded + * ripples always animate from the target layout center, bounded ripples animate from the touch + * position. + * @param radius the radius for the ripple. If [Dp.Unspecified] is provided then the size will be + * calculated based on the target layout size. + */ +@Stable +fun ripple( + color: ColorProducer, + bounded: Boolean = true, + radius: Dp = Dp.Unspecified, +): IndicationNodeFactory { + return RippleNodeFactory(bounded, radius, color) +} + +/** + * Default values used by [ripple]. + */ +object RippleDefaults { + /** + * Represents the default color that will be used for a ripple if a color has not been + * explicitly set on the ripple instance. + * + * @param contentColor the color of content (text or iconography) in the component that + * contains the ripple. + * @param lightTheme whether the theme is light or not + */ + fun rippleColor( + contentColor: Color, + lightTheme: Boolean, + ): Color { + val contentLuminance = contentColor.luminance() + // If we are on a colored surface (typically indicated by low luminance content), the + // ripple color should be white. + return if (!lightTheme && contentLuminance < 0.5) { + Color.White + // Otherwise use contentColor + } else { + contentColor + } + } + + /** + * Represents the default [RippleAlpha] that will be used for a ripple to indicate different + * states. + * + * @param contentColor the color of content (text or iconography) in the component that + * contains the ripple. + * @param lightTheme whether the theme is light or not + */ + fun rippleAlpha(contentColor: Color, lightTheme: Boolean): RippleAlpha { + return when { + lightTheme -> { + if (contentColor.luminance() > 0.5) { + LightThemeHighContrastRippleAlpha + } else { + LightThemeLowContrastRippleAlpha + } + } + + else -> { + DarkThemeRippleAlpha + } + } + } +} + +/** + * CompositionLocal used for providing [RippleConfiguration] down the tree. This acts as a + * tree-local 'override' for ripples used inside components that you cannot directly control, such + * as to change the color of a specific component's ripple, or disable it entirely by providing + * `null`. + * + * In most cases you should rely on the default theme behavior for consistency with other components + * - this exists as an escape hatch for individual components and is not intended to be used for + * full theme customization across an application. For this use case you should instead build your + * own custom ripple that queries your design system theme values directly using + * [createRippleModifierNode]. + */ +@Suppress("OPT_IN_MARKER_ON_WRONG_TARGET") +val LocalRippleConfiguration: ProvidableCompositionLocal = + compositionLocalOf { RippleConfiguration() } + +/** + * Configuration for [ripple] appearance, provided using [LocalRippleConfiguration]. In most cases + * the default values should be used, for custom design system use cases you should instead + * build your own custom ripple using [createRippleModifierNode]. To disable the ripple, provide + * `null` using [LocalRippleConfiguration]. + * + * @param color the color override for the ripple. If [Color.Unspecified], then the default color + * from the theme will be used instead. Note that if the ripple has a color explicitly set with + * the parameter on [ripple], that will always be used instead of this value. + * @param rippleAlpha the [RippleAlpha] override for this ripple. If null, then the default alpha + * will be used instead. + */ +@Immutable +class RippleConfiguration( + val color: Color = Color.Unspecified, + val rippleAlpha: RippleAlpha? = null, +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is RippleConfiguration) return false + + if (color != other.color) return false + if (rippleAlpha != other.rippleAlpha) return false + + return true + } + + override fun hashCode(): Int { + var result = color.hashCode() + result = 31 * result + (rippleAlpha?.hashCode() ?: 0) + return result + } + + override fun toString(): String { + return "RippleConfiguration(color=$color, rippleAlpha=$rippleAlpha)" + } +} + +@Stable +private class RippleNodeFactory private constructor( + private val bounded: Boolean, + private val radius: Dp, + private val colorProducer: ColorProducer?, + private val color: Color, +) : IndicationNodeFactory { + constructor( + bounded: Boolean, + radius: Dp, + colorProducer: ColorProducer, + ) : this(bounded, radius, colorProducer, Color.Unspecified) + + constructor( + bounded: Boolean, + radius: Dp, + color: Color, + ) : this(bounded, radius, null, color) + + override fun create(interactionSource: InteractionSource): DelegatableNode { + val colorProducer = colorProducer ?: ColorProducer { color } + return DelegatingThemeAwareRippleNode(interactionSource, bounded, radius, colorProducer) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is RippleNodeFactory) return false + + if (bounded != other.bounded) return false + if (radius != other.radius) return false + if (colorProducer != other.colorProducer) return false + return color == other.color + } + + override fun hashCode(): Int { + var result = bounded.hashCode() + result = 31 * result + radius.hashCode() + result = 31 * result + colorProducer.hashCode() + result = 31 * result + color.hashCode() + return result + } +} + +private class DelegatingThemeAwareRippleNode( + private val interactionSource: InteractionSource, + private val bounded: Boolean, + private val radius: Dp, + private val color: ColorProducer, +) : DelegatingNode(), CompositionLocalConsumerModifierNode, ObserverModifierNode { + private var rippleNode: DelegatableNode? = null + + override fun onAttach() { + updateConfiguration() + } + + override fun onObservedReadsChanged() { + updateConfiguration() + } + + /** + * Handles [LocalRippleConfiguration] changing between null / non-null. Changes to + * [RippleConfiguration.color] and [RippleConfiguration.rippleAlpha] are handled as part of + * the ripple definition. + */ + private fun updateConfiguration() { + observeReads { + val configuration = currentValueOf(LocalRippleConfiguration) + if (configuration == null) { + removeRipple() + } else { + if (rippleNode == null) attachNewRipple() + } + } + } + + private fun attachNewRipple() { + val calculateColor = ColorProducer { + val userDefinedColor = color() + if (userDefinedColor.isSpecified) { + userDefinedColor + } else { + // If this is null, the ripple will be removed, so this should always be non-null in + // normal use + val rippleConfiguration = currentValueOf(LocalRippleConfiguration) + if (rippleConfiguration?.color?.isSpecified == true) { + rippleConfiguration.color + } else { + RippleDefaults.rippleColor( + contentColor = currentValueOf(LocalContentColor), + lightTheme = currentValueOf(LocalMyColors).isLight + ) + } + } + } + val calculateRippleAlpha = { + // If this is null, the ripple will be removed, so this should always be non-null in + // normal use + val rippleConfiguration = currentValueOf(LocalRippleConfiguration) + rippleConfiguration?.rippleAlpha ?: RippleDefaults.rippleAlpha( + contentColor = currentValueOf(LocalContentColor), + lightTheme = currentValueOf(LocalMyColors).isLight + ) + } + + rippleNode = delegate( + createRippleModifierNode( + interactionSource, + bounded, + radius, + calculateColor, + calculateRippleAlpha + ) + ) + } + + private fun removeRipple() { + rippleNode?.let { undelegate(it) } + } +} + +private val DefaultBoundedRipple = RippleNodeFactory( + bounded = true, + radius = Dp.Unspecified, + color = Color.Unspecified +) + +private val DefaultUnboundedRipple = RippleNodeFactory( + bounded = false, + radius = Dp.Unspecified, + color = Color.Unspecified +) + +/** + * Alpha values for high luminance content in a light theme. + * + * This content will typically be placed on colored surfaces, so it is important that the + * contrast here is higher to meet accessibility standards, and increase legibility. + * + * These levels are typically used for text / iconography in primary colored tabs / + * bottom navigation / etc. + */ +private val LightThemeHighContrastRippleAlpha = RippleAlpha( + pressedAlpha = 0.24f, + focusedAlpha = 0.24f, + draggedAlpha = 0.16f, + hoveredAlpha = 0.08f +) + +/** + * Alpha levels for low luminance content in a light theme. + * + * This content will typically be placed on grayscale surfaces, so the contrast here can be lower + * without sacrificing accessibility and legibility. + * + * These levels are typically used for body text on the main surface (white in light theme, grey + * in dark theme) and text / iconography in surface colored tabs / bottom navigation / etc. + */ +private val LightThemeLowContrastRippleAlpha = RippleAlpha( + pressedAlpha = 0.12f, + focusedAlpha = 0.12f, + draggedAlpha = 0.08f, + hoveredAlpha = 0.04f +) + +/** + * Alpha levels for all content in a dark theme. + */ +private val DarkThemeRippleAlpha = RippleAlpha( + pressedAlpha = 0.10f, + focusedAlpha = 0.12f, + draggedAlpha = 0.08f, + hoveredAlpha = 0.04f +) diff --git a/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/ui/widget/menu/Option.kt b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/ui/widget/menu/Option.kt index debf568..ead61f1 100644 --- a/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/ui/widget/menu/Option.kt +++ b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/ui/widget/menu/Option.kt @@ -52,7 +52,7 @@ fun ShowOptions( .then(itemPadding) .basicMarquee( iterations = Int.MAX_VALUE, - delayMillis = 0 + initialDelayMillis = 0 ), fontSize = myTextSizes.base, maxLines = 1, diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 322c747..dbd3f6f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] kotlin = "2.0.20" ksp = "2.0.20-1.0.25" -compose = "1.6.11" +compose = "1.7.0" ktorVersion = "2.3.7" kotlin-serialization = "1.7.2" okhttp = "4.12.0" diff --git a/shared/compose-utils/src/main/kotlin/ir/amirab/util/compose/IconSource.kt b/shared/compose-utils/src/main/kotlin/ir/amirab/util/compose/IconSource.kt index 6ccbafa..8902a21 100644 --- a/shared/compose-utils/src/main/kotlin/ir/amirab/util/compose/IconSource.kt +++ b/shared/compose-utils/src/main/kotlin/ir/amirab/util/compose/IconSource.kt @@ -5,7 +5,6 @@ import androidx.compose.runtime.Immutable import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.rememberVectorPainter -import androidx.compose.ui.res.ClassLoaderResourceLoader import androidx.compose.ui.res.painterResource import okio.FileSystem import okio.Path.Companion.toPath