diff --git a/docs/guide/utils.md b/docs/guide/utils.md index 93950e6b..75da6017 100644 --- a/docs/guide/utils.md +++ b/docs/guide/utils.md @@ -189,73 +189,3 @@ The `PressFeedbackType` enum defines different types of visual feedback that can | None | No visual feedback | | Sink | Applies a sink effect, where the component scales down slightly when pressed | | Tilt | Applies a tilt effect, where the component tilts slightly based on the touch position | - -## Smooth Rounded Corners (G2RoundedCornerShape) - -`G2RoundedCornerShape` provides visually smoother corners than the standard `RoundedCornerShape` by blending part of the circular arc with Bézier transitions. It supports: a single uniform corner size, per-corner sizes (Dp / px / percent), preset or custom smoothness via `CornerSmoothness`, and a ready-made `CapsuleShape()` helper. - -> Source: https://github.com/Kyant0/Capsule (Apache-2.0 License). - -```kotlin -G2RoundedCornerShape(size: Dp, cornerSmoothness: CornerSmoothness = CornerSmoothness.Default) -G2RoundedCornerShape( - topStart: Dp = 0.dp, - topEnd: Dp = 0.dp, - bottomEnd: Dp = 0.dp, - bottomStart: Dp = 0.dp, - cornerSmoothness: CornerSmoothness = CornerSmoothness.Default -) -G2RoundedCornerShape(percent: Int, cornerSmoothness: CornerSmoothness = CornerSmoothness.Default) -CapsuleShape(cornerSmoothness: CornerSmoothness = CornerSmoothness.Default) -``` - -`CornerSmoothness` parameters: -* `circleFraction`: 0f..1f portion of a quarter circle preserved (1f = normal rounded corner, no smoothing blend) -* `extendedFraction`: how much the control points extend horizontally/vertically to create a softer capsule-like shape - -Presets: -* `CornerSmoothness.Default` – balanced smoothness (softened corners) -* `CornerSmoothness.None` – equivalent to a regular rounded corner (no extra smoothing) - -### Basic Usage - -```kotlin -Surface(shape = G2RoundedCornerShape(16.dp)) { - /* 内容 */ -} -``` - -### Per-Corner Sizes - -```kotlin -Surface( - shape = G2RoundedCornerShape( - topStart = 16.dp, - topEnd = 16.dp, - bottomStart = 8.dp, - bottomEnd = 8.dp, - cornerSmoothness = CornerSmoothness.Default - ) -) { /* Content */ } -``` - -### Capsule Shape - -```kotlin -Surface(shape = CapsuleShape()) { /* Content */ } -``` - -### Custom Smoothness - -You can craft your own smoothness (smaller `circleFraction` & higher `extendedFraction` => softer / more elongated transition): - -```kotlin -val ExtraSmooth = CornerSmoothness( - circleFraction = 0.55f, // retain 55% of the arc; lower -> more smoothing - extendedFraction = 0.90f // push Bézier handles further for a pill-like feel -) - -Surface(shape = G2RoundedCornerShape(24.dp, cornerSmoothness = ExtraSmooth)) { - // Content -} -``` diff --git a/docs/zh_CN/guide/utils.md b/docs/zh_CN/guide/utils.md index f0e0e0a7..b673f8e6 100644 --- a/docs/zh_CN/guide/utils.md +++ b/docs/zh_CN/guide/utils.md @@ -189,73 +189,3 @@ Box( | None | 无视觉反馈 | | Sink | 应用下沉效果,组件在按下时轻微缩小 | | Tilt | 应用倾斜效果,组件根据触摸位置轻微倾斜 | - -## 平滑圆角 (G2RoundedCornerShape) - -`G2RoundedCornerShape` 通过在标准圆角圆弧与 Bézier 过渡之间混合,实现比 `RoundedCornerShape` 更柔和、视觉更自然的圆角。它支持:统一圆角值、分别设置四个角(支持 Dp / px / 百分比)、通过 `CornerSmoothness` 预设或自定义平滑度,以及快捷的 `CapsuleShape()` 胶囊形状。 - -> 来源: https://github.com/Kyant0/Capsule (Apache-2.0 License)。 - -```kotlin -G2RoundedCornerShape(size: Dp, cornerSmoothness: CornerSmoothness = CornerSmoothness.Default) -G2RoundedCornerShape( - topStart: Dp = 0.dp, - topEnd: Dp = 0.dp, - bottomEnd: Dp = 0.dp, - bottomStart: Dp = 0.dp, - cornerSmoothness: CornerSmoothness = CornerSmoothness.Default -) -G2RoundedCornerShape(percent: Int, cornerSmoothness: CornerSmoothness = CornerSmoothness.Default) -CapsuleShape(cornerSmoothness: CornerSmoothness = CornerSmoothness.Default) -``` - -`CornerSmoothness` 参数说明: -* `circleFraction`: 0f..1f,表示保留四分之一圆弧的比例(1f = 传统圆角,不做平滑混合) -* `extendedFraction`: 控制 Bézier 控制点向外延伸的程度,越大越接近椭圆/胶囊视觉 - -预设: -* `CornerSmoothness.Default` – 默认柔滑(推荐常规使用) -* `CornerSmoothness.None` – 与普通圆角等效(无额外平滑) - -### 基本使用 - -```kotlin -Surface(shape = G2RoundedCornerShape(16.dp)) { - /* 内容 */ -} -``` - -### 分别指定四个角 - -```kotlin -Surface( - shape = G2RoundedCornerShape( - topStart = 16.dp, - topEnd = 16.dp, - bottomStart = 8.dp, - bottomEnd = 8.dp, - cornerSmoothness = CornerSmoothness.Default - ) -) { /* 内容 */ } -``` - -### 胶囊形状 - -```kotlin -Surface(shape = CapsuleShape()) { /* 内容 */ } -``` - -### 自定义平滑度 - -(降低 `circleFraction` + 提高 `extendedFraction` => 更软、更延展) - -```kotlin -val ExtraSmooth = CornerSmoothness( - circleFraction = 0.55f, // 保留 55% 圆弧,越低越“圆润” - extendedFraction = 0.90f // 控制点更外扩,接近胶囊 -) - -Surface(shape = G2RoundedCornerShape(24.dp, cornerSmoothness = ExtraSmooth)) { - // 内容 -} -``` diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2d41486e..ff0b9db8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,6 +2,7 @@ android-gradle-plugin = "8.13.0" androidx-activity = "1.11.0" androidx-window = "1.5.0" +capsule = "2.1.1-patch2" jetbrains-compose = "1.9.1" jetbrains-compose-hotReload = "1.0.0-rc02" jetbrains-androidx-navigation = "2.9.1" @@ -16,6 +17,7 @@ androidx-window = { group = "androidx.window", name = "window", version.ref = "a jetbrains-androidx-navigation = { module = "org.jetbrains.androidx.navigation:navigation-compose", version.ref = "jetbrains-androidx-navigation" } jetbrains-compose-ui-backhandler = { module = "org.jetbrains.compose.ui:ui-backhandler", version.ref = "jetbrains-compose" } jetbrains-compose-window-size = { module = "org.jetbrains.compose.material3:material3-window-size-class", version.ref = "jetbrains-compose-window-size" } +gaze-capsule = { module = "com.mocharealm.gaze:capsule", version.ref = "capsule" } [plugins] android-application = { id = "com.android.application", version.ref = "android-gradle-plugin" } diff --git a/miuix/build.gradle.kts b/miuix/build.gradle.kts index f10063d6..4e6e50b9 100644 --- a/miuix/build.gradle.kts +++ b/miuix/build.gradle.kts @@ -66,6 +66,8 @@ kotlin { implementation(libs.jetbrains.compose.ui.backhandler) implementation(libs.jetbrains.compose.window.size) + + implementation(libs.gaze.capsule) // Capsule for Multiplatform } } } diff --git a/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/Button.kt b/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/Button.kt index db1bb443..1e6868f1 100644 --- a/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/Button.kt +++ b/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/Button.kt @@ -20,8 +20,8 @@ import androidx.compose.ui.semantics.role import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import com.mocharealm.gaze.capsule.ContinuousRoundedRectangle import top.yukonga.miuix.kmp.theme.MiuixTheme -import top.yukonga.miuix.kmp.utils.G2RoundedCornerShape /** * A [Button] component with Miuix style. @@ -48,7 +48,7 @@ fun Button( insideMargin: PaddingValues = ButtonDefaults.InsideMargin, content: @Composable RowScope.() -> Unit ) { - val shape = remember(cornerRadius) { G2RoundedCornerShape(cornerRadius) } + val shape = remember(cornerRadius) { ContinuousRoundedRectangle(cornerRadius) } val color = if (enabled) colors.color else colors.disabledColor Surface( onClick = onClick, @@ -93,7 +93,7 @@ fun TextButton( minHeight: Dp = ButtonDefaults.MinHeight, insideMargin: PaddingValues = ButtonDefaults.InsideMargin ) { - val shape = remember(cornerRadius) { G2RoundedCornerShape(cornerRadius) } + val shape = remember(cornerRadius) { ContinuousRoundedRectangle(cornerRadius) } val color = if (enabled) colors.color else colors.disabledColor val textColor = if (enabled) colors.textColor else colors.disabledTextColor Surface( diff --git a/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/Card.kt b/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/Card.kt index 0e5c4b74..ceeca18b 100644 --- a/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/Card.kt +++ b/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/Card.kt @@ -24,10 +24,10 @@ import androidx.compose.ui.semantics.isTraversalGroup import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import com.mocharealm.gaze.capsule.ContinuousRoundedRectangle +import com.mocharealm.gaze.capsule.continuities.G1Continuity import top.yukonga.miuix.kmp.theme.LocalContentColor import top.yukonga.miuix.kmp.theme.MiuixTheme -import top.yukonga.miuix.kmp.utils.CornerSmoothness -import top.yukonga.miuix.kmp.utils.G2RoundedCornerShape import top.yukonga.miuix.kmp.utils.PressFeedbackType import top.yukonga.miuix.kmp.utils.SinkFeedback import top.yukonga.miuix.kmp.utils.TiltFeedback @@ -141,8 +141,8 @@ private fun BasicCard( cornerRadius: Dp = CardDefaults.CornerRadius, content: @Composable () -> Unit, ) { - val shape = remember(cornerRadius) { G2RoundedCornerShape(cornerRadius) } - val clipShape = remember(cornerRadius) { G2RoundedCornerShape(cornerRadius, CornerSmoothness.None) } + val shape = remember(cornerRadius) { ContinuousRoundedRectangle(cornerRadius) } + val clipShape = remember(cornerRadius) { ContinuousRoundedRectangle(cornerRadius, G1Continuity) } CompositionLocalProvider( LocalContentColor provides colors.contentColor, @@ -152,7 +152,7 @@ private fun BasicCard( .semantics(mergeDescendants = false) { isTraversalGroup = true } - .clip(clipShape) // For touch feedback, there is a problem when using G2RoundedCornerShape. + .clip(clipShape) // For touch feedback, there is a problem when using G2Continuity. .background(color = colors.color, shape = shape), propagateMinConstraints = true, ) { diff --git a/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/Checkbox.kt b/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/Checkbox.kt index f25a093d..581b2d3d 100644 --- a/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/Checkbox.kt +++ b/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/Checkbox.kt @@ -35,9 +35,9 @@ import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.semantics.Role import androidx.compose.ui.unit.dp +import com.mocharealm.gaze.capsule.ContinuousCapsule import kotlinx.coroutines.launch import top.yukonga.miuix.kmp.theme.MiuixTheme -import top.yukonga.miuix.kmp.utils.CapsuleShape import top.yukonga.miuix.kmp.utils.pressable /** @@ -94,7 +94,7 @@ fun Checkbox( .wrapContentSize(Alignment.Center) .requiredSize(25.5.dp) .pressable(enabled = enabled, delay = null) - .clip(CapsuleShape) + .clip(ContinuousCapsule) .drawBehind { drawCircle(backgroundColor) } diff --git a/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/ColorPalette.kt b/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/ColorPalette.kt index 325ae97d..b92d5899 100644 --- a/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/ColorPalette.kt +++ b/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/ColorPalette.kt @@ -35,8 +35,8 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp import androidx.compose.ui.util.lerp -import top.yukonga.miuix.kmp.utils.CapsuleShape -import top.yukonga.miuix.kmp.utils.G2RoundedCornerShape +import com.mocharealm.gaze.capsule.ContinuousCapsule +import com.mocharealm.gaze.capsule.ContinuousRoundedRectangle import top.yukonga.miuix.kmp.utils.Hsv import top.yukonga.miuix.kmp.utils.toHsv import kotlin.math.abs @@ -120,7 +120,7 @@ fun ColorPalette( modifier = Modifier .fillMaxWidth() .height(26.dp) - .clip(CapsuleShape()) + .clip(ContinuousCapsule) .background(lastEmittedColor ?: initialColor) ) } @@ -159,7 +159,7 @@ fun ColorPalette( alpha = it val newColor = base.copy(alpha = it) lastAcceptedHSV = - base.toHsv().let { it -> Triple(it.h.toFloat(), (it.s / 100.0).toFloat(), (it.v / 100.0).toFloat()) } + base.toHsv().let { Triple(it.h.toFloat(), (it.s / 100.0).toFloat(), (it.v / 100.0).toFloat()) } lastEmittedColor = newColor onColorChangedState.value(newColor) } @@ -182,7 +182,7 @@ private fun PaletteCanvas( val totalColumns = hueColumns + if (includeGrayColumn) 1 else 0 val rowSV = remember(rows) { buildRowSV(rows) } val grayV = remember(rows) { buildGrayV(rows) } - val shape = G2RoundedCornerShape(cornerRadius) + val shape = ContinuousRoundedRectangle(cornerRadius) var sizePx by remember { mutableStateOf(IntSize.Zero) } @@ -253,9 +253,9 @@ private fun PaletteCanvas( y = with(density) { cyPx.toDp() - indicatorSize / 2 } ) .size(indicatorSize) - .clip(CapsuleShape()) - .border(6.dp, Color.White, CapsuleShape()) - .background(Color.Transparent, CapsuleShape()) + .clip(ContinuousCapsule) + .border(6.dp, Color.White, ContinuousCapsule) + .background(Color.Transparent, ContinuousCapsule) ) } } diff --git a/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/ColorPicker.kt b/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/ColorPicker.kt index ca5ccf16..13e06455 100644 --- a/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/ColorPicker.kt +++ b/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/ColorPicker.kt @@ -37,7 +37,7 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import top.yukonga.miuix.kmp.utils.CapsuleShape +import com.mocharealm.gaze.capsule.ContinuousCapsule import top.yukonga.miuix.kmp.utils.ColorUtils import top.yukonga.miuix.kmp.utils.Hsv import top.yukonga.miuix.kmp.utils.OkLab @@ -154,7 +154,7 @@ fun HsvColorPicker( modifier = Modifier .fillMaxWidth() .height(26.dp) - .clip(CapsuleShape()) + .clip(ContinuousCapsule) .background(selectedColor) ) } @@ -371,7 +371,7 @@ fun OkHsvColorPicker( modifier = Modifier .fillMaxWidth() .height(26.dp) - .clip(CapsuleShape()) + .clip(ContinuousCapsule) .background(selectedColor) ) } @@ -596,7 +596,7 @@ fun OkLabColorPicker( modifier = Modifier .fillMaxWidth() .height(26.dp) - .clip(CapsuleShape()) + .clip(ContinuousCapsule) .background(selectedColor) ) } @@ -840,7 +840,7 @@ private fun ColorSlider( Box( modifier = Modifier - .clip(CapsuleShape()) + .clip(ContinuousCapsule) .then(modifier) .height(sliderHeightDp) .onGloballyPositioned { coordinates -> @@ -903,8 +903,8 @@ private fun SliderIndicator( modifier = modifier .offset(x = indicatorOffsetXDp) .size(indicatorSize) - .border(6.dp, Color.White, CapsuleShape()) - .background(Color.Transparent, CapsuleShape()) + .border(6.dp, Color.White, ContinuousCapsule) + .background(Color.Transparent, ContinuousCapsule) ) } diff --git a/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/FloatingActionButton.kt b/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/FloatingActionButton.kt index df490e2e..42db3bca 100644 --- a/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/FloatingActionButton.kt +++ b/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/FloatingActionButton.kt @@ -23,8 +23,8 @@ import androidx.compose.ui.semantics.role import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import com.mocharealm.gaze.capsule.ContinuousCapsule import top.yukonga.miuix.kmp.theme.MiuixTheme -import top.yukonga.miuix.kmp.utils.CapsuleShape /** * A [FloatingActionButton] component with Miuix style. @@ -43,7 +43,7 @@ import top.yukonga.miuix.kmp.utils.CapsuleShape fun FloatingActionButton( onClick: () -> Unit, modifier: Modifier = Modifier, - shape: Shape = CapsuleShape(), + shape: Shape = ContinuousCapsule, containerColor: Color = MiuixTheme.colorScheme.primary, shadowElevation: Dp = 4.dp, minWidth: Dp = 60.dp, diff --git a/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/FloatingToolbar.kt b/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/FloatingToolbar.kt index 492d86b7..34937a7f 100644 --- a/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/FloatingToolbar.kt +++ b/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/FloatingToolbar.kt @@ -26,8 +26,8 @@ import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import com.mocharealm.gaze.capsule.ContinuousRoundedRectangle import top.yukonga.miuix.kmp.theme.MiuixTheme -import top.yukonga.miuix.kmp.utils.G2RoundedCornerShape /** * A [FloatingToolbar] that renders its content in a Card, arranged either horizontally or vertically. @@ -54,7 +54,7 @@ fun FloatingToolbar( content: @Composable () -> Unit ) { val density = LocalDensity.current - val roundedCornerShape = remember(cornerRadius) { G2RoundedCornerShape(cornerRadius) } + val roundedCornerShape = remember(cornerRadius) { ContinuousRoundedRectangle(cornerRadius) } val dividerColor = MiuixTheme.colorScheme.dividerLine Box( diff --git a/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/IconButton.kt b/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/IconButton.kt index 53b6a217..0e2d15a2 100644 --- a/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/IconButton.kt +++ b/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/IconButton.kt @@ -20,8 +20,8 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.semantics.Role import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import com.mocharealm.gaze.capsule.ContinuousRoundedRectangle import top.yukonga.miuix.kmp.interfaces.HoldDownInteraction -import top.yukonga.miuix.kmp.utils.G2RoundedCornerShape /** * A [IconButton] component with Miuix style. @@ -51,7 +51,7 @@ fun IconButton( minWidth: Dp = IconButtonDefaults.MinWidth, content: @Composable () -> Unit ) { - val shape = remember(cornerRadius) { G2RoundedCornerShape(cornerRadius) } + val shape = remember(cornerRadius) { ContinuousRoundedRectangle(cornerRadius) } val interactionSource = remember { MutableInteractionSource() } val holdDown = remember { mutableStateOf(null) } diff --git a/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/ListPopup.kt b/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/ListPopup.kt index 8f856c3a..b817e81b 100644 --- a/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/ListPopup.kt +++ b/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/ListPopup.kt @@ -39,9 +39,9 @@ import androidx.compose.ui.unit.IntRect import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp +import com.mocharealm.gaze.capsule.ContinuousRoundedRectangle import top.yukonga.miuix.kmp.theme.MiuixTheme import top.yukonga.miuix.kmp.utils.BackHandler -import top.yukonga.miuix.kmp.utils.G2RoundedCornerShape import top.yukonga.miuix.kmp.utils.MiuixPopupUtils.Companion.PopupLayout import top.yukonga.miuix.kmp.utils.getWindowSize import kotlin.math.min @@ -144,7 +144,7 @@ fun ListPopup( enableWindowDim = enableWindowDim, transformOrigin = { transformOrigin }, ) { - val shape = remember { G2RoundedCornerShape(16.dp) } + val shape = remember { ContinuousRoundedRectangle(16.dp) } val elevationPx = with(density) { shadowElevation.toPx() } Box( diff --git a/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/NavigationBar.kt b/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/NavigationBar.kt index c51d3b97..11756226 100644 --- a/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/NavigationBar.kt +++ b/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/NavigationBar.kt @@ -47,8 +47,8 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import com.mocharealm.gaze.capsule.ContinuousRoundedRectangle import top.yukonga.miuix.kmp.theme.MiuixTheme -import top.yukonga.miuix.kmp.utils.G2RoundedCornerShape import top.yukonga.miuix.kmp.utils.Platform import top.yukonga.miuix.kmp.utils.platform @@ -239,7 +239,7 @@ fun FloatingNavigationBar( Modifier .background( color = MiuixTheme.colorScheme.dividerLine, - shape = G2RoundedCornerShape(cornerRadius) + shape = ContinuousRoundedRectangle(cornerRadius) ) .padding(0.75.dp) } else Modifier @@ -248,11 +248,11 @@ fun FloatingNavigationBar( if (shadowElevation > 0.dp) { Modifier.graphicsLayer( shadowElevation = with(density) { shadowElevation.toPx() }, - shape = G2RoundedCornerShape(cornerRadius), + shape = ContinuousRoundedRectangle(cornerRadius), clip = cornerRadius > 0.dp ) } else if (cornerRadius > 0.dp) { - Modifier.clip(G2RoundedCornerShape(cornerRadius)) + Modifier.clip(ContinuousRoundedRectangle(cornerRadius)) } else { Modifier } diff --git a/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/SearchBar.kt b/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/SearchBar.kt index 052d0b39..2c4e2e0d 100644 --- a/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/SearchBar.kt +++ b/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/SearchBar.kt @@ -44,13 +44,13 @@ import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import com.mocharealm.gaze.capsule.ContinuousCapsule import kotlinx.coroutines.delay import top.yukonga.miuix.kmp.icon.MiuixIcons import top.yukonga.miuix.kmp.icon.icons.basic.Search import top.yukonga.miuix.kmp.icon.icons.basic.SearchCleanup import top.yukonga.miuix.kmp.theme.MiuixTheme import top.yukonga.miuix.kmp.utils.BackHandler -import top.yukonga.miuix.kmp.utils.CapsuleShape /** * A [SearchBar] component with Miuix style. @@ -170,7 +170,7 @@ fun InputField( ) { Icon( modifier = Modifier - .clip(CapsuleShape()) + .clip(ContinuousCapsule) .clickable { onQueryChange("") }, imageVector = MiuixIcons.Basic.SearchCleanup, tint = MiuixTheme.colorScheme.onSurfaceContainerHighest, @@ -211,7 +211,7 @@ fun InputField( modifier = Modifier .background( color = MiuixTheme.colorScheme.surfaceContainerHigh, - shape = CapsuleShape() + shape = ContinuousCapsule ), contentAlignment = Alignment.CenterStart ) { diff --git a/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/Slider.kt b/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/Slider.kt index 4356e03c..b20c0d08 100644 --- a/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/Slider.kt +++ b/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/Slider.kt @@ -41,8 +41,8 @@ import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.util.lerp +import com.mocharealm.gaze.capsule.ContinuousRoundedRectangle import top.yukonga.miuix.kmp.theme.MiuixTheme -import top.yukonga.miuix.kmp.utils.G2RoundedCornerShape import kotlin.math.abs /** @@ -96,7 +96,7 @@ fun Slider( var isDragging by remember { mutableStateOf(false) } val hapticState = remember { SliderHapticState() } val interactionSource = remember { MutableInteractionSource() } - val shape = remember(height) { G2RoundedCornerShape(height) } + val shape = remember(height) { ContinuousRoundedRectangle(height) } val coercedValue = value.coerceIn(valueRange.start, valueRange.endInclusive) val stepFractions = remember(steps) { stepsToTickFractions(steps) } @@ -141,7 +141,10 @@ fun Slider( val thumbRadius = size.height / 2f val availableWidth = (size.width - 2f * thumbRadius).coerceAtLeast(0f) val visualFraction = - if (availableWidth == 0f) 0f else ((offset.x - thumbRadius) / availableWidth).coerceIn(0f, 1f) + if (availableWidth == 0f) 0f else ((offset.x - thumbRadius) / availableWidth).coerceIn( + 0f, + 1f + ) val fractionForValue = if (reverseDirection) 1f - visualFraction else visualFraction val calculatedValue = fractionToValue(fractionForValue) onValueChange(calculatedValue) @@ -153,7 +156,10 @@ fun Slider( val thumbRadius = size.height / 2f val availableWidth = (size.width - 2f * thumbRadius).coerceAtLeast(0f) val visualFraction = - if (availableWidth == 0f) 0f else ((dragOffset - thumbRadius) / availableWidth).coerceIn(0f, 1f) + if (availableWidth == 0f) 0f else ((dragOffset - thumbRadius) / availableWidth).coerceIn( + 0f, + 1f + ) val fractionForValue = if (reverseDirection) 1f - visualFraction else visualFraction val calculatedValue = fractionToValue(fractionForValue) onValueChange(calculatedValue) @@ -242,7 +248,7 @@ fun VerticalSlider( var isDragging by remember { mutableStateOf(false) } val hapticState = remember { SliderHapticState() } val interactionSource = remember { MutableInteractionSource() } - val shape = remember(width) { G2RoundedCornerShape(width) } + val shape = remember(width) { ContinuousRoundedRectangle(width) } val coercedValue = value.coerceIn(valueRange.start, valueRange.endInclusive) val stepFractions = remember(steps) { stepsToTickFractions(steps) } @@ -285,7 +291,10 @@ fun VerticalSlider( val thumbRadius = size.width / 2f val availableHeight = (size.height - 2f * thumbRadius).coerceAtLeast(0f) val visualFraction = - if (availableHeight == 0f) 0f else ((offset.y - thumbRadius) / availableHeight).coerceIn(0f, 1f) + if (availableHeight == 0f) 0f else ((offset.y - thumbRadius) / availableHeight).coerceIn( + 0f, + 1f + ) val fractionForValue = if (reverseDirection) visualFraction else 1f - visualFraction val calculatedValue = fractionToValueVertical(fractionForValue) onValueChange(calculatedValue) @@ -296,7 +305,10 @@ fun VerticalSlider( val thumbRadius = size.width / 2f val availableHeight = (size.height - 2f * thumbRadius).coerceAtLeast(0f) val visualFraction = - if (availableHeight == 0f) 0f else ((dragOffset - thumbRadius) / availableHeight).coerceIn(0f, 1f) + if (availableHeight == 0f) 0f else ((dragOffset - thumbRadius) / availableHeight).coerceIn( + 0f, + 1f + ) val fractionForValue = if (reverseDirection) visualFraction else 1f - visualFraction val calculatedValue = fractionToValueVertical(fractionForValue) onValueChange(calculatedValue) @@ -389,7 +401,7 @@ fun RangeSlider( val isDragging = isDraggingStart || isDraggingEnd val hapticState = remember { RangeSliderHapticState() } val interactionSource = remember { MutableInteractionSource() } - val shape = remember(height) { G2RoundedCornerShape(height) } + val shape = remember(height) { ContinuousRoundedRectangle(height) } var currentStartValue by remember { mutableFloatStateOf(value.start) } var currentEndValue by remember { mutableFloatStateOf(value.endInclusive) } @@ -602,7 +614,7 @@ fun RangeSlider( @Composable private fun SliderTrack( modifier: Modifier, - shape: G2RoundedCornerShape, + shape: ContinuousRoundedRectangle, backgroundColor: Color, foregroundColor: Color, effect: Boolean, @@ -719,7 +731,7 @@ private fun SliderTrack( @Composable private fun RangeSliderTrack( modifier: Modifier, - shape: G2RoundedCornerShape, + shape: ContinuousRoundedRectangle, backgroundColor: Color, foregroundColor: Color, effect: Boolean, diff --git a/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/Switch.kt b/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/Switch.kt index 3f47a342..f1281529 100644 --- a/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/Switch.kt +++ b/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/Switch.kt @@ -44,8 +44,8 @@ import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.semantics.Role import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import com.mocharealm.gaze.capsule.ContinuousCapsule import top.yukonga.miuix.kmp.theme.MiuixTheme -import top.yukonga.miuix.kmp.utils.CapsuleShape import kotlin.math.absoluteValue /** @@ -128,7 +128,7 @@ fun Switch( .wrapContentSize(Alignment.Center) .size(50.dp, 28.5.dp) .requiredSize(50.dp, 28.5.dp) - .clip(CapsuleShape()) + .clip(ContinuousCapsule) .drawBehind { drawRect(backgroundColor) } diff --git a/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/TabRow.kt b/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/TabRow.kt index 85953971..70cf0dfa 100644 --- a/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/TabRow.kt +++ b/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/TabRow.kt @@ -35,8 +35,8 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.times +import com.mocharealm.gaze.capsule.ContinuousRoundedRectangle import top.yukonga.miuix.kmp.theme.MiuixTheme -import top.yukonga.miuix.kmp.utils.G2RoundedCornerShape import top.yukonga.miuix.kmp.utils.overScrollHorizontal /** @@ -139,7 +139,7 @@ fun TabRowWithContour( Box( modifier = Modifier .fillMaxSize() - .clip(G2RoundedCornerShape(cornerRadius + contourPadding)) + .clip(ContinuousRoundedRectangle(cornerRadius + contourPadding)) .background(color = colors.backgroundColor(false)) .padding(contourPadding) ) { @@ -148,7 +148,7 @@ fun TabRowWithContour( modifier = Modifier .fillMaxSize() .overScrollHorizontal() - .clip(G2RoundedCornerShape(cornerRadius)), + .clip(ContinuousRoundedRectangle(cornerRadius)), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(contourPadding), overscrollEffect = null @@ -176,7 +176,7 @@ private fun TabItem( onClick: () -> Unit, enabled: Boolean, colors: TabRowColors, - shape: G2RoundedCornerShape, + shape: ContinuousRoundedRectangle, width: Dp ) { Surface( @@ -211,7 +211,7 @@ private fun TabItemWithContour( onClick: () -> Unit, enabled: Boolean, colors: TabRowColors, - shape: G2RoundedCornerShape, + shape: ContinuousRoundedRectangle, width: Dp ) { Box( @@ -239,7 +239,7 @@ private fun TabItemWithContour( */ private data class TabRowConfig( val tabWidth: Dp, - val shape: G2RoundedCornerShape, + val shape: ContinuousRoundedRectangle, val listState: androidx.compose.foundation.lazy.LazyListState ) @@ -260,7 +260,7 @@ private fun rememberTabRowConfig( val tabWidth = remember(tabs.size, minWidth, maxWidth, lazyRowAvailableWidth, spacing) { calculateTabWidth(tabs.size, minWidth, maxWidth, spacing, lazyRowAvailableWidth) } - val shape = remember(cornerRadius) { G2RoundedCornerShape(cornerRadius) } + val shape = remember(cornerRadius) { ContinuousRoundedRectangle(cornerRadius) } return TabRowConfig(tabWidth, shape, listState) } diff --git a/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/extra/SuperBottomSheet.kt b/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/extra/SuperBottomSheet.kt index 59154e91..64ae8259 100644 --- a/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/extra/SuperBottomSheet.kt +++ b/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/extra/SuperBottomSheet.kt @@ -52,12 +52,12 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import androidx.compose.ui.util.lerp +import com.mocharealm.gaze.capsule.ContinuousRoundedRectangle import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.launch import top.yukonga.miuix.kmp.basic.Text import top.yukonga.miuix.kmp.theme.MiuixTheme -import top.yukonga.miuix.kmp.utils.G2RoundedCornerShape import top.yukonga.miuix.kmp.utils.MiuixPopupUtils.Companion.DialogLayout import top.yukonga.miuix.kmp.utils.PredictiveBackHandler import top.yukonga.miuix.kmp.utils.getWindowSize @@ -334,7 +334,7 @@ private fun SuperBottomSheetColumn( Modifier ) .padding(horizontal = outsideMargin.width) - .clip(G2RoundedCornerShape(topStart = cornerRadius, topEnd = cornerRadius)) + .clip(ContinuousRoundedRectangle(topStart = cornerRadius, topEnd = cornerRadius)) .background(backgroundColor) .padding(horizontal = insideMargin.width) .padding(bottom = insideMargin.height) @@ -509,7 +509,7 @@ private fun DragHandleArea( .graphicsLayer { scaleY = pressScale.value } - .clip(G2RoundedCornerShape(2.dp)) + .clip(ContinuousRoundedRectangle(2.dp)) .background(dragHandleColor.copy(alpha = handleAlpha)) ) } diff --git a/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/extra/SuperDialog.kt b/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/extra/SuperDialog.kt index af470868..62cb1ae3 100644 --- a/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/extra/SuperDialog.kt +++ b/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/extra/SuperDialog.kt @@ -35,10 +35,10 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp +import com.mocharealm.gaze.capsule.ContinuousRoundedRectangle import kotlinx.coroutines.launch import top.yukonga.miuix.kmp.basic.Text import top.yukonga.miuix.kmp.theme.MiuixTheme -import top.yukonga.miuix.kmp.utils.G2RoundedCornerShape import top.yukonga.miuix.kmp.utils.MiuixPopupUtils.Companion.DialogLayout import top.yukonga.miuix.kmp.utils.PredictiveBackHandler import top.yukonga.miuix.kmp.utils.getRoundedCorner @@ -213,7 +213,7 @@ private fun SuperDialogContent( .pointerInput(Unit) { detectTapGestures { /* Consume click to prevent dismissal */ } } - .clip(G2RoundedCornerShape(bottomCornerRadius)) + .clip(ContinuousRoundedRectangle(bottomCornerRadius)) .background(backgroundColor) .padding(horizontal = insideMargin.width, vertical = insideMargin.height) diff --git a/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/utils/G2RoundedCornerShape.kt b/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/utils/G2RoundedCornerShape.kt deleted file mode 100644 index a9329d4b..00000000 --- a/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/utils/G2RoundedCornerShape.kt +++ /dev/null @@ -1,608 +0,0 @@ -// Copyright 2025, compose-miuix-ui contributors -// SPDX-License-Identifier: Apache-2.0 - -package top.yukonga.miuix.kmp.utils - -// Based on https://github.com/Kyant0/Capsule - -import androidx.annotation.FloatRange -import androidx.annotation.IntRange -import androidx.compose.foundation.shape.CornerBasedShape -import androidx.compose.foundation.shape.CornerSize -import androidx.compose.runtime.Immutable -import androidx.compose.runtime.Stable -import androidx.compose.ui.geometry.CornerRadius -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.geometry.Rect -import androidx.compose.ui.geometry.RoundRect -import androidx.compose.ui.geometry.Size -import androidx.compose.ui.geometry.center -import androidx.compose.ui.geometry.toRect -import androidx.compose.ui.graphics.Outline -import androidx.compose.ui.graphics.Path -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.LayoutDirection -import androidx.compose.ui.unit.LayoutDirection.Ltr -import androidx.compose.ui.unit.dp -import androidx.compose.ui.util.fastCoerceAtMost -import androidx.compose.ui.util.fastCoerceIn -import kotlin.math.PI -import kotlin.math.cos -import kotlin.math.min -import kotlin.math.sin - -@Immutable -open class G2RoundedCornerShape( - topStart: CornerSize, - topEnd: CornerSize, - bottomEnd: CornerSize, - bottomStart: CornerSize, - val cornerSmoothness: CornerSmoothness = CornerSmoothness.Default -) : - CornerBasedShape( - topStart = topStart, - topEnd = topEnd, - bottomEnd = bottomEnd, - bottomStart = bottomStart - ) { - - override fun createOutline( - size: Size, - topStart: Float, - topEnd: Float, - bottomEnd: Float, - bottomStart: Float, - layoutDirection: LayoutDirection - ): Outline { - if (topStart + topEnd + bottomEnd + bottomStart == 0f) { - return Outline.Rectangle(size.toRect()) - } - - val (width, height) = size - val (centerX, centerY) = size.center - - val maxR = min(centerX, centerY) - val topLeft = (if (layoutDirection == Ltr) topStart else topEnd).fastCoerceIn(0f, maxR) - val topRight = (if (layoutDirection == Ltr) topEnd else topStart).fastCoerceIn(0f, maxR) - val bottomRight = (if (layoutDirection == Ltr) bottomEnd else bottomStart).fastCoerceIn(0f, maxR) - val bottomLeft = (if (layoutDirection == Ltr) bottomStart else bottomEnd).fastCoerceIn(0f, maxR) - - if (cornerSmoothness.circleFraction >= 1f || - (width == height && - topLeft == centerX && - topLeft == topRight && bottomLeft == bottomRight) - ) { - return Outline.Rounded( - RoundRect( - rect = size.toRect(), - topLeft = CornerRadius(topLeft), - topRight = CornerRadius(topRight), - bottomRight = CornerRadius(bottomRight), - bottomLeft = CornerRadius(bottomLeft) - ) - ) - } - - return Outline.Generic( - cornerSmoothness.createRoundedRectanglePath( - size = size, - topRight = topRight, - topLeft = topLeft, - bottomLeft = bottomLeft, - bottomRight = bottomRight - ) - ) - } - - override fun copy( - topStart: CornerSize, - topEnd: CornerSize, - bottomEnd: CornerSize, - bottomStart: CornerSize - ): G2RoundedCornerShape { - return G2RoundedCornerShape( - topStart = topStart, - topEnd = topEnd, - bottomEnd = bottomEnd, - bottomStart = bottomStart, - cornerSmoothness = cornerSmoothness - ) - } - - fun copy( - topStart: CornerSize = this.topStart, - topEnd: CornerSize = this.topEnd, - bottomEnd: CornerSize = this.bottomEnd, - bottomStart: CornerSize = this.bottomStart, - cornerSmoothness: CornerSmoothness = this.cornerSmoothness - ): G2RoundedCornerShape { - return G2RoundedCornerShape( - topStart = topStart, - topEnd = topEnd, - bottomEnd = bottomEnd, - bottomStart = bottomStart, - cornerSmoothness = cornerSmoothness - ) - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other !is G2RoundedCornerShape) return false - - if (topStart != other.topStart) return false - if (topEnd != other.topEnd) return false - if (bottomEnd != other.bottomEnd) return false - if (bottomStart != other.bottomStart) return false - if (cornerSmoothness != other.cornerSmoothness) return false - - return true - } - - override fun hashCode(): Int { - var result = topStart.hashCode() - result = 31 * result + topEnd.hashCode() - result = 31 * result + bottomEnd.hashCode() - result = 31 * result + bottomStart.hashCode() - result = 31 * result + cornerSmoothness.hashCode() - return result - } - - override fun toString(): String { - return "G2RoundedCornerShape(topStart=$topStart, topEnd=$topEnd, bottomEnd=$bottomEnd, " + - "bottomStart=$bottomStart, cornerSmoothing=$cornerSmoothness)" - } -} - -@Stable -val G2RectangleShape: G2RoundedCornerShape = G2RoundedCornerShape(0f) - -@Stable -val CapsuleShape: G2RoundedCornerShape = CapsuleShape() - -@Suppress("FunctionName") -@Stable -fun CapsuleShape( - cornerSmoothness: CornerSmoothness = CornerSmoothness.Default -): G2RoundedCornerShape = - G2RoundedCornerShape( - topStartPercent = 50, - topEndPercent = 50, - bottomEndPercent = 50, - bottomStartPercent = 50, - cornerSmoothness = cornerSmoothness - ) - -@Stable -fun G2RoundedCornerShape( - corner: CornerSize, - cornerSmoothness: CornerSmoothness = CornerSmoothness.Default -): G2RoundedCornerShape = - G2RoundedCornerShape( - topStart = corner, - topEnd = corner, - bottomEnd = corner, - bottomStart = corner, - cornerSmoothness = cornerSmoothness - ) - -@Stable -fun G2RoundedCornerShape( - size: Dp, - cornerSmoothness: CornerSmoothness = CornerSmoothness.Default -): G2RoundedCornerShape = - G2RoundedCornerShape( - corner = CornerSize(size), - cornerSmoothness = cornerSmoothness - ) - -@Stable -fun G2RoundedCornerShape( - @FloatRange(from = 0.0) size: Float, - cornerSmoothness: CornerSmoothness = CornerSmoothness.Default -): G2RoundedCornerShape = - G2RoundedCornerShape( - corner = CornerSize(size), - cornerSmoothness = cornerSmoothness - ) - -@Stable -fun G2RoundedCornerShape( - @IntRange(from = 0, to = 100) percent: Int, - cornerSmoothness: CornerSmoothness = CornerSmoothness.Default -): G2RoundedCornerShape = - G2RoundedCornerShape( - corner = CornerSize(percent), - cornerSmoothness = cornerSmoothness - ) - -@Stable -fun G2RoundedCornerShape( - topStart: Dp = 0.dp, - topEnd: Dp = 0.dp, - bottomEnd: Dp = 0.dp, - bottomStart: Dp = 0.dp, - cornerSmoothness: CornerSmoothness = CornerSmoothness.Default -): G2RoundedCornerShape = - G2RoundedCornerShape( - topStart = CornerSize(topStart), - topEnd = CornerSize(topEnd), - bottomEnd = CornerSize(bottomEnd), - bottomStart = CornerSize(bottomStart), - cornerSmoothness = cornerSmoothness - ) - -@Stable -fun G2RoundedCornerShape( - @FloatRange(from = 0.0) topStart: Float = 0f, - @FloatRange(from = 0.0) topEnd: Float = 0f, - @FloatRange(from = 0.0) bottomEnd: Float = 0f, - @FloatRange(from = 0.0) bottomStart: Float = 0f, - cornerSmoothness: CornerSmoothness = CornerSmoothness.Default -): G2RoundedCornerShape = - G2RoundedCornerShape( - topStart = CornerSize(topStart), - topEnd = CornerSize(topEnd), - bottomEnd = CornerSize(bottomEnd), - bottomStart = CornerSize(bottomStart), - cornerSmoothness = cornerSmoothness - ) - -@Stable -fun G2RoundedCornerShape( - @IntRange(from = 0, to = 100) topStartPercent: Int = 0, - @IntRange(from = 0, to = 100) topEndPercent: Int = 0, - @IntRange(from = 0, to = 100) bottomEndPercent: Int = 0, - @IntRange(from = 0, to = 100) bottomStartPercent: Int = 0, - cornerSmoothness: CornerSmoothness = CornerSmoothness.Default -): G2RoundedCornerShape = - G2RoundedCornerShape( - topStart = CornerSize(topStartPercent), - topEnd = CornerSize(topEndPercent), - bottomEnd = CornerSize(bottomEndPercent), - bottomStart = CornerSize(bottomStartPercent), - cornerSmoothness = cornerSmoothness - ) - -@Immutable -data class CornerSmoothness( - @param:FloatRange(from = 0.0, to = 1.0) val circleFraction: Float, - @param:FloatRange(from = 0.0) val extendedFraction: Float -) { - - private val circleRadians = HalfPI * circleFraction - private val bezierRadians = (HalfPI - circleRadians) / 2f - - private val sin = sin(bezierRadians) - private val cos = cos(bezierRadians) - private val a = 1f - sin / (1f + cos) - private val d = 1.5f * sin / (1f + cos) / (1f + cos) - private val ad = a + d - - private fun Path.topRightCorner0(size: Size, r: Float, dy: Float) { - val w = size.width - cubicTo( - w, - r * ad, - w, - r * a, - w - r * (1f - cos), - r * (1f - sin) - ) - } - - private fun Path.topRightCircle(size: Size, r: Float) { - if (circleRadians > 0f) { - arcToRad( - rect = Rect( - center = Offset(size.width - r, r), - radius = r - ), - startAngleRadians = -bezierRadians, - sweepAngleRadians = -circleRadians, - forceMoveTo = false - ) - } - } - - private fun Path.topRightCorner1(size: Size, r: Float, dx: Float) { - val w = size.width - cubicTo( - w - r * a, - 0f, - w - r * ad, - 0f, - w - r - dx, - 0f - ) - } - - private fun Path.topLeftCorner1(size: Size, r: Float, dx: Float) { - cubicTo( - r * ad, - 0f, - r * a, - 0f, - r * (1f - sin), - r * (1f - cos) - ) - } - - private fun Path.topLeftCircle(size: Size, r: Float) { - if (circleRadians > 0f) { - arcToRad( - rect = Rect( - center = Offset(r, r), - radius = r - ), - startAngleRadians = -(HalfPI + bezierRadians), - sweepAngleRadians = -circleRadians, - forceMoveTo = false - ) - } - } - - private fun Path.topLeftCorner0(size: Size, r: Float, dy: Float) { - cubicTo( - 0f, - r * a, - 0f, - r * ad, - 0f, - r + dy - ) - } - - private fun Path.bottomLeftCorner0(size: Size, r: Float, dy: Float) { - val h = size.height - cubicTo( - 0f, - h - r * ad, - 0f, - h - r * a, - r * (1f - cos), - h - r * (1f - sin) - ) - } - - private fun Path.bottomLeftCircle(size: Size, r: Float) { - if (circleRadians > 0f) { - arcToRad( - rect = Rect( - center = Offset(r, size.height - r), - radius = r - ), - startAngleRadians = -(HalfPI * 2f + bezierRadians), - sweepAngleRadians = -circleRadians, - forceMoveTo = false - ) - } - } - - private fun Path.bottomLeftCorner1(size: Size, r: Float, dx: Float) { - val h = size.height - cubicTo( - r * a, - h, - r * ad, - h, - r - dx, - h - ) - } - - private fun Path.bottomRightCorner1(size: Size, r: Float, dx: Float) { - val w = size.width - val h = size.height - cubicTo( - w - r * ad, - h, - w - r * a, - h, - w - r * (1f - sin), - h - r * (1f - cos) - ) - } - - private fun Path.bottomRightCircle(size: Size, r: Float) { - if (circleRadians > 0f) { - arcToRad( - rect = Rect( - center = Offset(size.width - r, size.height - r), - radius = r - ), - startAngleRadians = -(HalfPI * 3f + bezierRadians), - sweepAngleRadians = -circleRadians, - forceMoveTo = false - ) - } - } - - private fun Path.bottomRightCorner0(size: Size, r: Float, dy: Float) { - val w = size.width - val h = size.height - cubicTo( - w, - h - r * a, - w, - h - r * ad, - w, - h - r + dy - ) - } - - private fun Path.rightCircle(size: Size, r: Float) { - arcToRad( - rect = Rect( - center = Offset(size.width - r, r), - radius = r - ), - startAngleRadians = bezierRadians + circleRadians, - sweepAngleRadians = -(bezierRadians + circleRadians) * 2f, - forceMoveTo = false - ) - } - - private fun Path.leftCircle(size: Size, r: Float) { - arcToRad( - rect = Rect( - center = Offset(r, r), - radius = r - ), - startAngleRadians = -(HalfPI + bezierRadians), - sweepAngleRadians = -(bezierRadians + circleRadians) * 2f, - forceMoveTo = false - ) - } - - private fun Path.topCircle(size: Size, r: Float) { - arcToRad( - rect = Rect( - center = Offset(r, r), - radius = r - ), - startAngleRadians = -bezierRadians, - sweepAngleRadians = -(bezierRadians + circleRadians) * 2f, - forceMoveTo = false - ) - } - - private fun Path.bottomCircle(size: Size, r: Float) { - arcToRad( - rect = Rect( - center = Offset(r, size.height - r), - radius = r - ), - startAngleRadians = -(HalfPI * 2f + bezierRadians), - sweepAngleRadians = -(bezierRadians + circleRadians) * 2f, - forceMoveTo = false - ) - } - - internal fun createRoundedRectanglePath( - size: Size, - topRight: Float, - topLeft: Float, - bottomLeft: Float, - bottomRight: Float - ): Path { - val (width, height) = size - val (centerX, centerY) = size.center - val maxR = min(centerX, centerY) - - val topRightDy = (topRight * extendedFraction).fastCoerceAtMost(centerY - topRight) - val topRightDx = (topRight * extendedFraction).fastCoerceAtMost(centerX - topRight) - val topLeftDx = (topLeft * extendedFraction).fastCoerceAtMost(centerX - topLeft) - val topLeftDy = (topLeft * extendedFraction).fastCoerceAtMost(centerY - topLeft) - val bottomLeftDy = (bottomLeft * extendedFraction).fastCoerceAtMost(centerY - bottomLeft) - val bottomLeftDx = (bottomLeft * extendedFraction).fastCoerceAtMost(centerX - bottomLeft) - val bottomRightDx = (bottomRight * extendedFraction).fastCoerceAtMost(centerX - bottomRight) - val bottomRightDy = (bottomRight * extendedFraction).fastCoerceAtMost(centerY - bottomRight) - - return Path().apply { - when { - // capsule - topRight == maxR && topLeft == maxR && bottomLeft == maxR && bottomRight == maxR -> { - if (width > height) { - // right circle - rightCircle(size, maxR) - // top right corner - topRightCorner1(size, topRight, topRightDx) - // top line - lineTo(topLeft + topLeftDx, 0f) - // top left corner - topLeftCorner1(size, topLeft, topLeftDx) - // left circle - leftCircle(size, maxR) - // bottom left corner - bottomLeftCorner1(size, bottomLeft, -bottomLeftDx) - // bottom line - lineTo(width - bottomRight - bottomRightDx, height) - // bottom right corner - bottomRightCorner1(size, bottomRight, -bottomRightDx) - } else { - // right line - moveTo(width, height - bottomRight - bottomRightDy) - lineTo(width, topRight + topRightDy) - // top right corner - topRightCorner0(size, topRight, -topRightDy) - // top circle - topCircle(size, maxR) - // top left corner - topLeftCorner0(size, topLeft, topLeftDy) - // left line - lineTo(0f, height - bottomLeft - bottomLeftDy) - // bottom left corner - bottomLeftCorner0(size, bottomLeft, bottomLeftDy) - // bottom circle - bottomCircle(size, maxR) - // bottom right corner - bottomRightCorner0(size, bottomRight, -bottomRightDy) - } - } - - // rounded rectangle - else -> { - // right line - moveTo(width, height - bottomRight - bottomRightDy) - lineTo(width, topRight + topRightDy) - - // top right corner - if (topRight > 0f) { - topRightCorner0(size, topRight, -topRightDy) - topRightCircle(size, topRight) - topRightCorner1(size, topRight, topRightDx) - } - - // top line - lineTo(topLeft + topLeftDx, 0f) - - // top left corner - if (topLeft > 0f) { - topLeftCorner1(size, topLeft, topLeftDx) - topLeftCircle(size, topLeft) - topLeftCorner0(size, topLeft, topLeftDy) - } - - // left line - lineTo(0f, height - bottomLeft - bottomLeftDy) - - // bottom left corner - if (bottomLeft > 0f) { - bottomLeftCorner0(size, bottomLeft, bottomLeftDy) - bottomLeftCircle(size, bottomLeft) - bottomLeftCorner1(size, bottomLeft, -bottomLeftDx) - } - - // bottom line - lineTo(width - bottomRight - bottomRightDx, height) - - // bottom right corner - if (bottomRight > 0f) { - bottomRightCorner1(size, bottomRight, -bottomRightDx) - bottomRightCircle(size, bottomRight) - bottomRightCorner0(size, bottomRight, -bottomRightDy) - } - } - } - } - } - - companion object { - - @Stable - val Default: CornerSmoothness = - CornerSmoothness( - circleFraction = 0.2f, - extendedFraction = 1f - ) - - @Stable - val None: CornerSmoothness = - CornerSmoothness( - circleFraction = 1f, - extendedFraction = 0f - ) - } -} - -private const val HalfPI = (PI / 2f).toFloat() \ No newline at end of file