diff --git a/adapt/src/androidMain/kotlin/design/adapt/previews/AdaptCircularIndicator.kt b/adapt/src/androidMain/kotlin/design/adapt/previews/AdaptCircularIndicator.kt new file mode 100644 index 0000000..406d038 --- /dev/null +++ b/adapt/src/androidMain/kotlin/design/adapt/previews/AdaptCircularIndicator.kt @@ -0,0 +1,92 @@ +/* + * Copyright 2023 Shubham Singh + * + * 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 design.adapt.previews + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import design.adapt.AdaptCircularIndicator +import design.adapt.Platform + +@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) +@Composable +private fun AndroidLightPreview() { + IndicatorPreview(platform = Platform.Android) +} + +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun AndroidDarkPreview() { + IndicatorPreview(platform = Platform.Android) +} + +@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) +@Composable +private fun IOSLightPreview() { + IndicatorPreview(platform = Platform.IOS) +} + +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun IOSDarkPreview() { + IndicatorPreview(platform = Platform.IOS) +} + +@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) +@Composable +private fun MacOSLightPreview() { + IndicatorPreview(platform = Platform.MacOS) +} + +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun MacOSDarkPreview() { + IndicatorPreview(platform = Platform.MacOS) +} + +@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) +@Composable +private fun WindowsLightPreview() { + IndicatorPreview(platform = Platform.Windows) +} + +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun WindowsDarkPreview() { + IndicatorPreview(platform = Platform.Windows) +} + +@Composable +private fun IndicatorPreview(platform: Platform) { + AdaptPreviewsTheme(platform = platform) { + Column( + modifier = Modifier + .background(Color.White.copy(alpha = 0.6f)) + .padding(24.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + AdaptCircularIndicator() + } + } +} diff --git a/adapt/src/androidMain/kotlin/design/adapt/previews/windows/WindowsProgressRing.kt b/adapt/src/androidMain/kotlin/design/adapt/previews/windows/WindowsProgressRing.kt index 67a7058..81bdef6 100644 --- a/adapt/src/androidMain/kotlin/design/adapt/previews/windows/WindowsProgressRing.kt +++ b/adapt/src/androidMain/kotlin/design/adapt/previews/windows/WindowsProgressRing.kt @@ -38,7 +38,7 @@ private fun ProgressRingPreview( verticalArrangement = Arrangement.spacedBy(16.dp) ) { WindowsProgressRing(trackColor = WindowsProgressRingDefaults.TrackColor) - WindowsProgressRing(progress = 0.6f, trackColor = WindowsProgressRingDefaults.TrackColor) + WindowsProgressRing(progress = { 0.6f }, trackColor = WindowsProgressRingDefaults.TrackColor) } } } diff --git a/adapt/src/commonMain/kotlin/design/adapt/AdaptCircularIndicator.kt b/adapt/src/commonMain/kotlin/design/adapt/AdaptCircularIndicator.kt new file mode 100644 index 0000000..7c27061 --- /dev/null +++ b/adapt/src/commonMain/kotlin/design/adapt/AdaptCircularIndicator.kt @@ -0,0 +1,146 @@ +/* + * Copyright 2023 Shubham Singh + * + * 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 design.adapt + +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ProgressIndicatorDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.unit.Dp +import design.adapt.cupertino.CupertinoSpinner +import design.adapt.cupertino.CupertinoSpinnerDefaults +import design.adapt.windows.WindowsProgressRing +import design.adapt.windows.WindowsProgressRingDefaults + +@Composable +fun AdaptCircularIndicator( + modifier: AdaptModifier = AdaptModifier(), + color: Color = AdaptCircularIndicatorDefaults.Color, + configuration: AdaptCircularIndicatorConfiguration = AdaptCircularIndicatorDefaults.configuration(), +) { + when (LocalPlatform.current) { + // TODO: Separate Web from Android here + Platform.Android, Platform.Web -> configuration.android.progress?.let { progress -> + CircularProgressIndicator( + modifier = modifier.android, + color = color, + strokeWidth = configuration.android.strokeWidth, + trackColor = configuration.android.trackColor, + strokeCap = configuration.android.strokeCap, + progress = progress, + ) + } ?: run { + CircularProgressIndicator( + modifier = modifier.android, + color = color, + strokeWidth = configuration.android.strokeWidth, + trackColor = configuration.android.trackColor, + strokeCap = configuration.android.strokeCap, + ) + } + + Platform.IOS -> CupertinoSpinner( + modifier = modifier.iOS, + color = color, + text = configuration.iOS.text, + ) + + Platform.MacOS -> CupertinoSpinner( + modifier = modifier.macOS, + color = color, + text = configuration.macOS.text, + ) + + Platform.Windows -> WindowsProgressRing( + modifier = modifier.windows, + progress = configuration.windows.progress, + color = color, + trackColor = configuration.windows.trackColor, + strokeWidth = configuration.windows.strokeWidth, + ) + } +} + +object AdaptCircularIndicatorDefaults { + @Composable + fun configuration( + android: AndroidCircularIndicatorConfiguration = AndroidCircularIndicatorConfiguration( + strokeWidth = ProgressIndicatorDefaults.CircularStrokeWidth, + trackColor = ProgressIndicatorDefaults.circularTrackColor, + strokeCap = ProgressIndicatorDefaults.CircularIndeterminateStrokeCap, + progress = null, + ), + iOS: IOSCircularIndicatorConfiguration = IOSCircularIndicatorConfiguration( + text = null, + ), + macOS: MacOSCircularIndicatorConfiguration = MacOSCircularIndicatorConfiguration( + text = null, + ), + windows: WindowsCircularIndicatorConfiguration = WindowsCircularIndicatorConfiguration( + progress = null, + trackColor = androidx.compose.ui.graphics.Color.Transparent, + strokeWidth = WindowsProgressRingDefaults.StrokeWidthMedium, + ), + ) = AdaptCircularIndicatorConfiguration( + android = android, + iOS = iOS, + macOS = macOS, + windows = windows, + ) + + val Color @Composable get() = when(LocalPlatform.current) { + Platform.Android, Platform.Web -> ProgressIndicatorDefaults.circularColor + Platform.IOS -> CupertinoSpinnerDefaults.Color + Platform.MacOS -> CupertinoSpinnerDefaults.Color + Platform.Windows -> WindowsProgressRingDefaults.Color + } +} + +@Immutable +class AdaptCircularIndicatorConfiguration( + val android: AndroidCircularIndicatorConfiguration, + val iOS: IOSCircularIndicatorConfiguration, + val macOS: MacOSCircularIndicatorConfiguration, + val windows: WindowsCircularIndicatorConfiguration, +) + +@Immutable +class AndroidCircularIndicatorConfiguration( + val strokeWidth: Dp, + val trackColor: Color, + val strokeCap: StrokeCap, + val progress: (() -> Float)?, +) + +@Immutable +class IOSCircularIndicatorConfiguration( + val text: (@Composable () -> Unit)?, +) + +@Immutable +class MacOSCircularIndicatorConfiguration( + val text: (@Composable () -> Unit)?, +) + +@Immutable +class WindowsCircularIndicatorConfiguration( + val progress: (() -> Float)?, + val trackColor: Color, + val strokeWidth: Dp, +) diff --git a/adapt/src/commonMain/kotlin/design/adapt/windows/WindowsButton.kt b/adapt/src/commonMain/kotlin/design/adapt/windows/WindowsButton.kt index cbe3c72..27562d9 100644 --- a/adapt/src/commonMain/kotlin/design/adapt/windows/WindowsButton.kt +++ b/adapt/src/commonMain/kotlin/design/adapt/windows/WindowsButton.kt @@ -16,6 +16,7 @@ package design.adapt.windows +import androidx.compose.animation.animateColorAsState import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable @@ -26,6 +27,7 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable @@ -39,6 +41,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import design.adapt.LocalContentColor @@ -77,23 +80,23 @@ fun WindowsButton( } else PaddingValues() } - val containerColor = remember(colors, enabled, isHovered, isPressed) { + val containerColor by animateColorAsState( when { - enabled && isHovered -> colors.hoveredContainerColor enabled && isPressed -> colors.pressedContainerColor + enabled && isHovered -> colors.hoveredContainerColor enabled -> colors.containerColor else -> colors.disabledContainerColor } - } + ) - val contentColor = remember(colors, enabled, isHovered, isPressed) { + val contentColor by animateColorAsState( when { - enabled && isHovered -> colors.hoveredContentColor enabled && isPressed -> colors.pressedContentColor + enabled && isHovered -> colors.hoveredContentColor enabled -> colors.contentColor else -> colors.disabledContentColor } - } + ) val processedIcon = remember(icon, isIconOnlyButton) { icon?.let { safeIcon -> @@ -110,8 +113,8 @@ fun WindowsButton( val borderColors: List? = remember(colors, enabled, isPressed) { when { - !enabled -> colors.disabledBorderColor?.let(::listOf) enabled && isPressed -> colors.pressedBorderColor?.let(::listOf) + !enabled -> colors.disabledBorderColor?.let(::listOf) else -> colors.borderColors } } @@ -120,6 +123,10 @@ fun WindowsButton( Row( modifier = modifier .padding(buttonMargin) + .defaultMinSize( + minWidth = WindowsButtonDefaults.defaultWidth(style = style, size = size), + minHeight = WindowsButtonDefaults.defaultHeight(style = style, size = size), + ) .focusBorder(interactionSource = interactionSource, shape = focusBorderShape) .clickable( enabled = enabled, @@ -146,7 +153,10 @@ fun WindowsButton( } ) .padding(contentPadding), - horizontalArrangement = Arrangement.spacedBy(8.dp), + horizontalArrangement = Arrangement.spacedBy( + space = 8.dp, + alignment = Alignment.CenterHorizontally + ), verticalAlignment = Alignment.CenterVertically, ) { if (iconSide == WindowsButtonIconSide.Start) processedIcon?.invoke() @@ -278,24 +288,24 @@ object WindowsButtonDefaults { return when { !compact && accentStyle && textOnly -> PaddingValues( - start = 47.5.dp, + start = 12.dp, top = 5.dp, - end = 47.5.dp, + end = 12.dp, bottom = 7.dp ) !compact && !accentStyle && textOnly -> PaddingValues( - start = 46.5.dp, + start = 11.dp, top = 4.dp, - end = 46.5.dp, + end = 11.dp, bottom = 6.dp ) !compact && hasAllContent -> PaddingValues( - start = 34.5.dp, + start = 11.dp, top = 4.dp, - end = 34.5.dp, - bottom = 6.dp, + end = 11.dp, + bottom = 4.dp, ) !compact && iconOnly -> PaddingValues(7.dp) @@ -309,6 +319,26 @@ object WindowsButtonDefaults { else -> PaddingValues() } } + + internal fun defaultWidth(style: WindowsButtonStyle, size: WindowsButtonSize): Dp { + val isCompact = size == WindowsButtonSize.Compact + return when { + !isCompact && (style == WindowsButtonStyle.Standard || style == WindowsButtonStyle.Subtle) -> 120.dp + !isCompact && style == WindowsButtonStyle.Accent -> 118.dp + isCompact -> 22.dp + else -> 0.dp + } + } + + internal fun defaultHeight(style: WindowsButtonStyle, size: WindowsButtonSize): Dp { + val isCompact = size == WindowsButtonSize.Compact + return when { + !isCompact && (style == WindowsButtonStyle.Standard || style == WindowsButtonStyle.Subtle) -> 32.dp + !isCompact && style == WindowsButtonStyle.Accent -> 30.dp + isCompact -> 22.dp + else -> 0.dp + } + } } enum class WindowsButtonStyle { Standard, Accent, Subtle } diff --git a/adapt/src/commonMain/kotlin/design/adapt/windows/WindowsProgressRing.kt b/adapt/src/commonMain/kotlin/design/adapt/windows/WindowsProgressRing.kt index 0562b72..ffb011c 100644 --- a/adapt/src/commonMain/kotlin/design/adapt/windows/WindowsProgressRing.kt +++ b/adapt/src/commonMain/kotlin/design/adapt/windows/WindowsProgressRing.kt @@ -22,7 +22,6 @@ import androidx.compose.animation.core.animateFloat import androidx.compose.animation.core.infiniteRepeatable import androidx.compose.animation.core.rememberInfiniteTransition import androidx.compose.animation.core.tween -import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.Canvas import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -31,15 +30,17 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.util.lerp @Composable fun WindowsProgressRing( modifier: Modifier = Modifier, - progress: Float? = null, - borderStroke: BorderStroke = WindowsProgressRingDefaults.BorderMedium, + progress: (() -> Float)? = null, + color: Color = WindowsProgressRingDefaults.Color, trackColor: Color = Color.Transparent, + strokeWidth: Dp = WindowsProgressRingDefaults.StrokeWidthMedium, ) { val startAngle = if (progress != null) -90f else rememberInfiniteTransition().animateFloat( @@ -54,9 +55,9 @@ fun WindowsProgressRing( ), ).value - val sweepAngle = if (progress != null) lerp(start = 0f, stop = 360f, fraction = progress) + val sweepAngle = if (progress != null) lerp(start = 0f, stop = 360f, fraction = progress()) else rememberInfiniteTransition().animateFloat( - initialValue = 0f, + initialValue = 10f, targetValue = 180f, animationSpec = infiniteRepeatable( animation = tween( @@ -71,7 +72,8 @@ fun WindowsProgressRing( modifier = modifier, startAngle = startAngle, sweepAngle = sweepAngle, - borderStroke = borderStroke, + color = color, + strokeWidth = strokeWidth, trackColor = trackColor, ) } @@ -81,7 +83,8 @@ private fun BasicProgressRing( modifier: Modifier = Modifier, startAngle: Float, sweepAngle: Float, - borderStroke: BorderStroke, + color: Color, + strokeWidth: Dp, trackColor: Color, ) { Canvas( @@ -90,19 +93,19 @@ private fun BasicProgressRing( width = WindowsProgressRingDefaults.SizeMedium, height = WindowsProgressRingDefaults.SizeMedium, ) - .padding(borderStroke.width / 2), + .padding(strokeWidth / 2), ) { drawCircle( color = trackColor, - style = Stroke(width = borderStroke.width.toPx()) + style = Stroke(width = strokeWidth.toPx()) ) drawArc( - brush = borderStroke.brush, + color = color, startAngle = startAngle, sweepAngle = sweepAngle, useCenter = false, style = Stroke( - width = borderStroke.width.toPx(), + width = strokeWidth.toPx(), cap = StrokeCap.Round, ), ) @@ -114,26 +117,13 @@ object WindowsProgressRingDefaults { val SizeMedium = 32.dp val SizeLarge = 64.dp - val BorderSmall - @Composable get() = BorderStroke( - width = 2.dp, - color = WindowsTheme.colorScheme.fillAccentDefault, - ) - - val BorderMedium - @Composable get() = BorderStroke( - width = 4.dp, - color = WindowsTheme.colorScheme.fillAccentDefault, - ) - - val BorderLarge - @Composable get() = BorderStroke( - width = 8.dp, - color = WindowsTheme.colorScheme.fillAccentDefault, - ) - + val Color @Composable get() = WindowsTheme.colorScheme.fillAccentDefault val TrackColor @Composable get() = WindowsTheme.colorScheme.strokeControlStrongDefault + val StrokeWidthSmall = 2.dp + val StrokeWidthMedium = 4.dp + val StrokeWidthLarge = 8.dp + internal const val START_ANGLE_ANIMATION_MILLIS = 700 internal const val SWEEP_ANGLE_ANIMATION_MILLIS = START_ANGLE_ANIMATION_MILLIS * 2 } diff --git a/sample/composeApp/src/commonMain/kotlin/design/adapt/App.kt b/sample/composeApp/src/commonMain/kotlin/design/adapt/App.kt index b10f9bd..100b47a 100644 --- a/sample/composeApp/src/commonMain/kotlin/design/adapt/App.kt +++ b/sample/composeApp/src/commonMain/kotlin/design/adapt/App.kt @@ -23,6 +23,7 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height @@ -153,23 +154,28 @@ private fun ColumnScaffold( } } +@Composable +fun SpacedRow(content: @Composable RowScope.() -> Unit) { + Row(horizontalArrangement = Arrangement.spacedBy(24.dp), content = content) +} + @Composable private fun ButtonsDemoPage() { ColumnScaffold(title = "Buttons") { - AdaptText(text = "This 👇🏼 button should look native-like on all platforms") + AdaptText(text = "This button should look native-like on all platforms") AdaptButton( onClick = {}, text = { AdaptText(text = "Adapt Button") } ) AdaptText( - text = "This 👇🏼 button should use the Material design system on all platforms" + text = "This button should use the Material design system on all platforms" ) Button( onClick = {}, content = { Text(text = "Android Button") } ) AdaptText( - text = "This 👇🏼 button should use iOS' variant of the Cupertino design " + + text = "This button should use iOS' variant of the Cupertino design " + "system on all platforms", ) IOSButton( @@ -177,7 +183,7 @@ private fun ButtonsDemoPage() { text = { AdaptText(text = "iOS Button") } ) AdaptText( - text = "This 👇🏼 button should use macOS' variant of the Cupertino design " + + text = "This button should use macOS' variant of the Cupertino design " + "system on all platforms", ) MacOSButton( @@ -185,7 +191,7 @@ private fun ButtonsDemoPage() { text = { AdaptText(text = "macOS Button") } ) AdaptText( - text = "This 👇🏼 button should use the WinUI design system on all platforms", + text = "This button should use the WinUI design system on all platforms", ) WindowsButton( onClick = {}, @@ -197,19 +203,30 @@ private fun ButtonsDemoPage() { @Composable fun IndicatorsDemoPage() { ColumnScaffold(title = "Indicators") { - // TODO(shubham): Add AdaptCircularIndicator as well + AdaptText(text = "This indicator should look native-like on all platforms") + AdaptCircularIndicator() AdaptText( - text = "This 👇🏼 indicator should use the Material design system on all platforms" + text = "This indicator should use the Material design system on all platforms" ) - CircularProgressIndicator() + SpacedRow { + CircularProgressIndicator() + CircularProgressIndicator(progress = { 0.43f }) + } AdaptText( - text = "This 👇🏼 indicator should use iOS' variant of the Cupertino design " + - "system on all platforms", + text = "This indicator should use the Cupertino design system on all platforms", ) - CupertinoSpinner() + SpacedRow { + CupertinoSpinner() + CupertinoSpinner( + text = { AdaptText(text = "Status...") } + ) + } AdaptText( - text = "This 👇🏼 indicator should use the WinUI design system on all platforms", + text = "This indicator should use the WinUI design system on all platforms", ) - WindowsProgressRing() + SpacedRow { + WindowsProgressRing() + WindowsProgressRing(progress = { 0.43f }) + } } }