From 44af9f96a2b077f8a6ff2b285c10f52ba91fc96e Mon Sep 17 00:00:00 2001 From: jisungbin Date: Mon, 3 Jul 2023 18:27:48 +0900 Subject: [PATCH 1/7] Cleanup --- bom/build.gradle.kts | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/bom/build.gradle.kts b/bom/build.gradle.kts index a3c816eee..0ebecdc1d 100644 --- a/bom/build.gradle.kts +++ b/bom/build.gradle.kts @@ -11,17 +11,12 @@ plugins { } dependencies { - val ignoreProjects = listOf( - projects.bom.dependencyProject, - projects.uiSample.dependencyProject, - projects.utilBackendTest.dependencyProject, - projects.catalog.dependencyProject, - projects.materialIconGenerator.dependencyProject, - ) - constraints { rootProject.subprojects.forEach { project -> - if (project !in ignoreProjects) { + if ( + project != projects.bom.dependencyProject && + File(project.projectDir, "version.txt").exists() + ) { api( ArtifactConfig.of(project).toString() .also { artifact -> From 0a43efcb98969cf28338b713379f3ca4aa2aa11c Mon Sep 17 00:00:00 2001 From: jisungbin Date: Mon, 3 Jul 2023 18:28:32 +0900 Subject: [PATCH 2/7] Add snapshot-test common util module --- util-compose-snapshot-test/build.gradle.kts | 27 +++++++++ .../src/main/AndroidManifest.xml | 8 +++ .../snapshot/test/SnapshotPathGenerator.kt | 43 ++++++++++++++ .../util/compose/snapshot/test/TestLayout.kt | 56 +++++++++++++++++++ .../util/compose/snapshot/test/UxTesting.kt | 28 ++++++++++ .../util/compose/snapshot/test/qualifier.kt | 12 ++++ 6 files changed, 174 insertions(+) create mode 100644 util-compose-snapshot-test/build.gradle.kts create mode 100644 util-compose-snapshot-test/src/main/AndroidManifest.xml create mode 100644 util-compose-snapshot-test/src/main/kotlin/team/duckie/quackquack/util/compose/snapshot/test/SnapshotPathGenerator.kt create mode 100644 util-compose-snapshot-test/src/main/kotlin/team/duckie/quackquack/util/compose/snapshot/test/TestLayout.kt create mode 100644 util-compose-snapshot-test/src/main/kotlin/team/duckie/quackquack/util/compose/snapshot/test/UxTesting.kt create mode 100644 util-compose-snapshot-test/src/main/kotlin/team/duckie/quackquack/util/compose/snapshot/test/qualifier.kt diff --git a/util-compose-snapshot-test/build.gradle.kts b/util-compose-snapshot-test/build.gradle.kts new file mode 100644 index 000000000..49e9974c5 --- /dev/null +++ b/util-compose-snapshot-test/build.gradle.kts @@ -0,0 +1,27 @@ +/* +* Designed and developed by Duckie Team 2023. +* +* Licensed under the MIT. +* Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE +*/ + +@file:Suppress("UnstableApiUsage") + +plugins { + quackquack("android-library") + quackquack("android-compose") + quackquack("kotlin-explicit-api") +} + +android { + namespace = "team.duckie.quackquack.util.compose.snapshot.test" +} + +dependencies { + implementations( + libs.compose.runtime, + libs.compose.ui.core, + libs.compose.foundation, + libs.test.junit.core + ) +} diff --git a/util-compose-snapshot-test/src/main/AndroidManifest.xml b/util-compose-snapshot-test/src/main/AndroidManifest.xml new file mode 100644 index 000000000..b1e11303c --- /dev/null +++ b/util-compose-snapshot-test/src/main/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + diff --git a/util-compose-snapshot-test/src/main/kotlin/team/duckie/quackquack/util/compose/snapshot/test/SnapshotPathGenerator.kt b/util-compose-snapshot-test/src/main/kotlin/team/duckie/quackquack/util/compose/snapshot/test/SnapshotPathGenerator.kt new file mode 100644 index 000000000..b167b6f4c --- /dev/null +++ b/util-compose-snapshot-test/src/main/kotlin/team/duckie/quackquack/util/compose/snapshot/test/SnapshotPathGenerator.kt @@ -0,0 +1,43 @@ +/* + * Designed and developed by Duckie Team 2023. + * + * Licensed under the MIT. + * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE + */ + +@file:Suppress("NOTHING_TO_INLINE") + +package team.duckie.quackquack.util.compose.snapshot.test + +import java.io.File +import org.junit.rules.TestWatcher +import org.junit.runner.Description + +public const val BaseSnapshotPath: String = "src/test/snapshots" + +public inline fun snapshotPath( + domain: String, + snapshotName: String = getCurrentMethodName(), + isGif: Boolean = false, +): File = + File("$BaseSnapshotPath/$domain/$snapshotName.${if (isGif) "gif" else "png"}") + +// https://stackoverflow.com/a/32329165/14299073 +public inline fun getCurrentMethodName(): String = Thread.currentThread().stackTrace[1].methodName + +public class SnapshotPathGeneratorRule(private val domain: String) : TestWatcher() { + init { + File("$BaseSnapshotPath/$domain").mkdirs() + } + + private var realtimeTestMethodName: String? = null + + override fun starting(description: Description) { + realtimeTestMethodName = description.methodName + } + + public operator fun invoke(isGif: Boolean = false): File = + File("$BaseSnapshotPath/$domain/$realtimeTestMethodName.${if (isGif) "gif" else "png"}") +} + +public annotation class SnapshotName(public val name: String) diff --git a/util-compose-snapshot-test/src/main/kotlin/team/duckie/quackquack/util/compose/snapshot/test/TestLayout.kt b/util-compose-snapshot-test/src/main/kotlin/team/duckie/quackquack/util/compose/snapshot/test/TestLayout.kt new file mode 100644 index 000000000..3ebe3a5b5 --- /dev/null +++ b/util-compose-snapshot-test/src/main/kotlin/team/duckie/quackquack/util/compose/snapshot/test/TestLayout.kt @@ -0,0 +1,56 @@ +/* + * Designed and developed by Duckie Team 2023. + * + * Licensed under the MIT. + * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE + */ + +package team.duckie.quackquack.util.compose.snapshot.test + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.text.BasicText +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +@Composable +public fun TestColumn(contentGap: Dp = 15.dp, content: @Composable () -> Unit) { + Column( + modifier = Modifier.wrapContentSize(), + verticalArrangement = Arrangement.spacedBy(contentGap), + ) { + content() + } +} + +@Composable +public fun WithLabel( + label: String, + isTitle: Boolean = false, + labelGap: Dp = 4.dp, + contentGap: Dp = if (isTitle) 15.dp else 4.dp, + content: @Composable ColumnScope.() -> Unit, +) { + Column( + modifier = Modifier.wrapContentSize(), + verticalArrangement = Arrangement.spacedBy(labelGap), + ) { + BasicText( + label, + style = TextStyle.Default.let { style -> + if (isTitle) style.copy(fontWeight = FontWeight.Bold) else style + }, + ) + Column( + modifier = Modifier.wrapContentSize(), + verticalArrangement = Arrangement.spacedBy(contentGap), + content = content, + ) + } +} diff --git a/util-compose-snapshot-test/src/main/kotlin/team/duckie/quackquack/util/compose/snapshot/test/UxTesting.kt b/util-compose-snapshot-test/src/main/kotlin/team/duckie/quackquack/util/compose/snapshot/test/UxTesting.kt new file mode 100644 index 000000000..5c86cea1e --- /dev/null +++ b/util-compose-snapshot-test/src/main/kotlin/team/duckie/quackquack/util/compose/snapshot/test/UxTesting.kt @@ -0,0 +1,28 @@ +/* + * Designed and developed by Duckie Team 2023. + * + * Licensed under the MIT. + * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE + */ + +package team.duckie.quackquack.util.compose.snapshot.test + +import android.annotation.SuppressLint +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.platform.LocalConfiguration + +public const val LargestFontScale: Float = 2f // Maximum font scale based on Galaxy A31 + +@SuppressLint("ComposableNaming") +@Composable +public fun withIncreaseFontScale(fontScale: Float, content: @Composable () -> Unit) { + val testConfiguration = LocalConfiguration.current.apply { + this.fontScale = fontScale + } + + CompositionLocalProvider( + LocalConfiguration provides testConfiguration, + content = content, + ) +} diff --git a/util-compose-snapshot-test/src/main/kotlin/team/duckie/quackquack/util/compose/snapshot/test/qualifier.kt b/util-compose-snapshot-test/src/main/kotlin/team/duckie/quackquack/util/compose/snapshot/test/qualifier.kt new file mode 100644 index 000000000..f254fc34f --- /dev/null +++ b/util-compose-snapshot-test/src/main/kotlin/team/duckie/quackquack/util/compose/snapshot/test/qualifier.kt @@ -0,0 +1,12 @@ +/* + * Designed and developed by Duckie Team 2023. + * + * Licensed under the MIT. + * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE + */ + +package team.duckie.quackquack.util.compose.snapshot.test + +// Copied form Pixel7 qualifier +public const val MultilinesSnapshotQualifier: String = + "w400dp-h8000dp-normal-long-notround-any-420dpi-keyshidden-nonav" From 7a7d6c1c5eec55b169a2b366d4f206076f454113 Mon Sep 17 00:00:00 2001 From: jisungbin Date: Mon, 3 Jul 2023 18:48:30 +0900 Subject: [PATCH 3/7] Cleanup - CoilImageLoader.builder renamed to CoilImageLoader.quackBuild --- .../plugin/image/gif/QuackImageGifPlugin.kt | 15 ++++---- ...Test.kt => QuackImageGifPluginSnapshot.kt} | 2 +- .../ui/plugin/image/QuackImagePlugin.kt | 3 +- .../ui/plugin/image/QuackImagePluginTest.kt | 2 +- .../quackquack/ui/plugin/LocalQuackPlugins.kt | 7 ++-- .../quackquack/ui/plugin/QuackPlugin.kt | 3 ++ .../kotlin/team/duckie/quackquack/ui/image.kt | 34 +++++++++++-------- 7 files changed, 38 insertions(+), 28 deletions(-) rename ui-plugin/image/gif/src/test/kotlin/team/duckie/quackquack/ui/plugin/image/gif/{QuackImageGifPluginTest.kt => QuackImageGifPluginSnapshot.kt} (98%) diff --git a/ui-plugin/image/gif/src/main/kotlin/team/duckie/quackquack/ui/plugin/image/gif/QuackImageGifPlugin.kt b/ui-plugin/image/gif/src/main/kotlin/team/duckie/quackquack/ui/plugin/image/gif/QuackImageGifPlugin.kt index a318023a8..a9760804f 100644 --- a/ui-plugin/image/gif/src/main/kotlin/team/duckie/quackquack/ui/plugin/image/gif/QuackImageGifPlugin.kt +++ b/ui-plugin/image/gif/src/main/kotlin/team/duckie/quackquack/ui/plugin/image/gif/QuackImageGifPlugin.kt @@ -18,11 +18,12 @@ import team.duckie.quackquack.ui.plugin.image.QuackImagePlugin * 내부적으로 [QuackImagePlugin.CoilImageLoader]를 사용합니다. */ @Stable -public val QuackImageGifPlugin: QuackImagePlugin.CoilImageLoader = QuackImagePlugin.CoilImageLoader { _, _, _, _ -> - components { - add( - if (Build.VERSION.SDK_INT >= 28) ImageDecoderDecoder.Factory() - else GifDecoder.Factory(), - ) +public val QuackImageGifPlugin: QuackImagePlugin.CoilImageLoader = + QuackImagePlugin.CoilImageLoader { _, _, _, _ -> + components { + add( + if (Build.VERSION.SDK_INT >= 28) ImageDecoderDecoder.Factory() + else GifDecoder.Factory(), + ) + } } -} diff --git a/ui-plugin/image/gif/src/test/kotlin/team/duckie/quackquack/ui/plugin/image/gif/QuackImageGifPluginTest.kt b/ui-plugin/image/gif/src/test/kotlin/team/duckie/quackquack/ui/plugin/image/gif/QuackImageGifPluginSnapshot.kt similarity index 98% rename from ui-plugin/image/gif/src/test/kotlin/team/duckie/quackquack/ui/plugin/image/gif/QuackImageGifPluginTest.kt rename to ui-plugin/image/gif/src/test/kotlin/team/duckie/quackquack/ui/plugin/image/gif/QuackImageGifPluginSnapshot.kt index cf4d004ba..a82f809d9 100644 --- a/ui-plugin/image/gif/src/test/kotlin/team/duckie/quackquack/ui/plugin/image/gif/QuackImageGifPluginTest.kt +++ b/ui-plugin/image/gif/src/test/kotlin/team/duckie/quackquack/ui/plugin/image/gif/QuackImageGifPluginSnapshot.kt @@ -24,7 +24,7 @@ import team.duckie.quackquack.ui.plugin.rememberQuackPlugins @Ignore("GIF 녹화 안됨") @RunWith(AndroidJUnit4::class) -class QuackImageGifPluginTest { +class QuackImageGifPluginSnapshot { @get:Rule val compose = createAndroidComposeRule() diff --git a/ui-plugin/image/src/main/kotlin/team/duckie/quackquack/ui/plugin/image/QuackImagePlugin.kt b/ui-plugin/image/src/main/kotlin/team/duckie/quackquack/ui/plugin/image/QuackImagePlugin.kt index 1602e26e9..2798a1b52 100644 --- a/ui-plugin/image/src/main/kotlin/team/duckie/quackquack/ui/plugin/image/QuackImagePlugin.kt +++ b/ui-plugin/image/src/main/kotlin/team/duckie/quackquack/ui/plugin/image/QuackImagePlugin.kt @@ -31,8 +31,9 @@ public sealed interface QuackImagePlugin : QuackPlugin { * @param contentDescription `QuackImage`의 `contentDescription` 인자로 제공된 값 * @param quackPluginLocal `Modifier.quackPluginLocal`로 제공된 값 */ + // compose-ui 의존성 없어서 Modifier.quackPluginLocal에 링크를 적용하지 않음 @Stable - public fun ImageLoader.Builder.builder( + public fun ImageLoader.Builder.quackBuild( context: Context, src: Any?, contentDescription: String?, diff --git a/ui-plugin/image/src/test/kotlin/team/duckie/quackquack/ui/plugin/image/QuackImagePluginTest.kt b/ui-plugin/image/src/test/kotlin/team/duckie/quackquack/ui/plugin/image/QuackImagePluginTest.kt index 862a68853..0a8b8ecf6 100644 --- a/ui-plugin/image/src/test/kotlin/team/duckie/quackquack/ui/plugin/image/QuackImagePluginTest.kt +++ b/ui-plugin/image/src/test/kotlin/team/duckie/quackquack/ui/plugin/image/QuackImagePluginTest.kt @@ -97,7 +97,7 @@ private fun imageResultOf(drawable: Drawable, request: ImageRequest) = private class QuackImageCoilBuilderIntercepter( private val map: MutableMap = mutableMapOf(), ) : QuackImagePlugin.CoilImageLoader { - override fun ImageLoader.Builder.builder( + override fun ImageLoader.Builder.quackBuild( context: Context, src: Any?, contentDescription: String?, diff --git a/ui-plugin/src/main/kotlin/team/duckie/quackquack/ui/plugin/LocalQuackPlugins.kt b/ui-plugin/src/main/kotlin/team/duckie/quackquack/ui/plugin/LocalQuackPlugins.kt index 6810abacc..56550a5a3 100644 --- a/ui-plugin/src/main/kotlin/team/duckie/quackquack/ui/plugin/LocalQuackPlugins.kt +++ b/ui-plugin/src/main/kotlin/team/duckie/quackquack/ui/plugin/LocalQuackPlugins.kt @@ -15,6 +15,7 @@ import androidx.compose.runtime.staticCompositionLocalOf * 만약 등록된 플러그인이 없다면 [EmptyQuackPlugins]를 반환합니다. * 플러그인을 등록하려면 [rememberQuackPlugins]를 사용하세요. */ -public val LocalQuackPlugins: ProvidableCompositionLocal = staticCompositionLocalOf { - EmptyQuackPlugins -} +public val LocalQuackPlugins: ProvidableCompositionLocal = + staticCompositionLocalOf { + EmptyQuackPlugins + } diff --git a/ui-plugin/src/main/kotlin/team/duckie/quackquack/ui/plugin/QuackPlugin.kt b/ui-plugin/src/main/kotlin/team/duckie/quackquack/ui/plugin/QuackPlugin.kt index 34ae684bc..0a063878d 100644 --- a/ui-plugin/src/main/kotlin/team/duckie/quackquack/ui/plugin/QuackPlugin.kt +++ b/ui-plugin/src/main/kotlin/team/duckie/quackquack/ui/plugin/QuackPlugin.kt @@ -25,6 +25,7 @@ public interface QuackPlugin @Immutable public interface QuackPlugins { /** 등록된 전체 플러그인 */ + @Stable public val plugins: MutableVector /** @@ -36,6 +37,7 @@ public interface QuackPlugins { * } * ``` */ + @Stable public operator fun QuackPlugin.unaryPlus() } @@ -54,6 +56,7 @@ public object EmptyQuackPlugins : QuackPlugins { } /** capacity가 16인 기본 플러그인 저장소 인스턴스를 제공합니다. */ +@Immutable private class QuackPluginsScope : QuackPlugins { override val plugins = mutableVectorOf() override fun QuackPlugin.unaryPlus() { diff --git a/ui/src/main/kotlin/team/duckie/quackquack/ui/image.kt b/ui/src/main/kotlin/team/duckie/quackquack/ui/image.kt index df4260991..7d4287ee3 100644 --- a/ui/src/main/kotlin/team/duckie/quackquack/ui/image.kt +++ b/ui/src/main/kotlin/team/duckie/quackquack/ui/image.kt @@ -126,25 +126,29 @@ public fun QuackImage( contentScale: ContentScale = ContentScale.Fit, contentDescription: String? = null, ) { + val context = LocalContext.current + val localQuackPlugins = LocalQuackPlugins.current val currentColorFilter = remember(tint) { tint.toColorFilterOrNull() } val imageLoader = - LocalQuackPlugins.current.takeIf { it != EmptyQuackPlugins }?.let { plugins -> - val context = LocalContext.current - val quackPluginLocal = modifier.getElementByTypeOrNull() - val imageLoaderPlugins = plugins.lastByTypeOrNull() + remember(localQuackPlugins, src, modifier, contentDescription) { + localQuackPlugins.takeIf { it != EmptyQuackPlugins }?.let { plugins -> + val imageLoaderPlugins = plugins.lastByTypeOrNull() + ?: return@let null + val quackPluginLocal = modifier.getElementByTypeOrNull() - var builder = ImageLoader.Builder(context) - if (imageLoaderPlugins != null) { - with(imageLoaderPlugins) { - builder = builder.builder( - context = context, - src = src, - contentDescription = contentDescription, - quackPluginLocal = quackPluginLocal, - ) - } + val builder = + with(imageLoaderPlugins) { + ImageLoader + .Builder(context) + .quackBuild( + context = context, + src = src, + contentDescription = contentDescription, + quackPluginLocal = quackPluginLocal, + ) + } + builder.build() } - builder.build() } ?: @Suppress("DEPRECATION") LocalImageLoader.current AsyncImage( From cd769e6eeb80e06fba6b071e523509ddefaac07b Mon Sep 17 00:00:00 2001 From: jisungbin Date: Mon, 3 Jul 2023 18:50:25 +0900 Subject: [PATCH 4/7] Add QuackInterceptorPlugin that a plugin that allows to intercept and change certain conditions before a QuackQuack component is drawn. --- ui-plugin/interceptor/build.gradle.kts | 55 ++++++++ .../interceptor/src/main/AndroidManifest.xml | 8 ++ .../interceptor/QuackInterceptorPlugin.kt | 40 ++++++ .../ui/plugin/interceptor/extensions.kt | 64 +++++++++ .../interceptor/QuackInterceptorPluginTest.kt | 129 ++++++++++++++++++ .../src/test/resources/robolectric.properties | 8 ++ .../QuackTagRadiusStyleIntercepted.png | Bin 0 -> 2280 bytes ui-plugin/interceptor/version.txt | 1 + 8 files changed, 305 insertions(+) create mode 100644 ui-plugin/interceptor/build.gradle.kts create mode 100644 ui-plugin/interceptor/src/main/AndroidManifest.xml create mode 100644 ui-plugin/interceptor/src/main/kotlin/team/duckie/quackquack/ui/plugin/interceptor/QuackInterceptorPlugin.kt create mode 100644 ui-plugin/interceptor/src/main/kotlin/team/duckie/quackquack/ui/plugin/interceptor/extensions.kt create mode 100644 ui-plugin/interceptor/src/test/kotlin/team/duckie/quackquack/ui/plugin/interceptor/QuackInterceptorPluginTest.kt create mode 100644 ui-plugin/interceptor/src/test/resources/robolectric.properties create mode 100644 ui-plugin/interceptor/src/test/snapshots/QuackInterceptorPlugin/QuackTagRadiusStyleIntercepted.png create mode 100644 ui-plugin/interceptor/version.txt diff --git a/ui-plugin/interceptor/build.gradle.kts b/ui-plugin/interceptor/build.gradle.kts new file mode 100644 index 000000000..5118130d8 --- /dev/null +++ b/ui-plugin/interceptor/build.gradle.kts @@ -0,0 +1,55 @@ +/* + * Designed and developed by Duckie Team 2023. + * + * Licensed under the MIT. + * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE + */ + +@file:Suppress("UnstableApiUsage") + +import org.jetbrains.dokka.gradle.DokkaMultiModuleTask + +plugins { + quackquack("android-library") + quackquack("android-compose") + quackquack("kotlin-explicit-api") + quackquack("quack-publishing") + alias(libs.plugins.test.roborazzi) +} + +tasks.withType { + dependsOn(":ui-plugin:dokkaHtmlMultiModule") +} + +android { + namespace = "team.duckie.quackquack.ui.plugin.interceptor" + + testOptions { + unitTests { + isIncludeAndroidResources = true + isReturnDefaultValues = true + all { test -> + test.systemProperty("robolectric.graphicsMode", "NATIVE") + } + } + } +} + +dependencies { + api(projects.uiPlugin.orArtifact()) + implementations( + libs.compose.runtime, + libs.compose.ui.core, + projects.material, + projects.utilModifier, + ) + testImplementations( + libs.test.robolectric, + libs.test.junit.compose, + libs.test.kotest.assertion.core, + libs.test.kotlin.coroutines, // needed for compose-ui-test + libs.bundles.test.roborazzi, + projects.ui, + projects.utilComposeSnapshotTest, + ) +} diff --git a/ui-plugin/interceptor/src/main/AndroidManifest.xml b/ui-plugin/interceptor/src/main/AndroidManifest.xml new file mode 100644 index 000000000..b1e11303c --- /dev/null +++ b/ui-plugin/interceptor/src/main/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + diff --git a/ui-plugin/interceptor/src/main/kotlin/team/duckie/quackquack/ui/plugin/interceptor/QuackInterceptorPlugin.kt b/ui-plugin/interceptor/src/main/kotlin/team/duckie/quackquack/ui/plugin/interceptor/QuackInterceptorPlugin.kt new file mode 100644 index 000000000..69dc82b72 --- /dev/null +++ b/ui-plugin/interceptor/src/main/kotlin/team/duckie/quackquack/ui/plugin/interceptor/QuackInterceptorPlugin.kt @@ -0,0 +1,40 @@ +/* + * Designed and developed by Duckie Team 2023. + * + * Licensed under the MIT. + * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE + */ + +package team.duckie.quackquack.ui.plugin.interceptor + +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.Stable +import androidx.compose.ui.Modifier +import team.duckie.quackquack.ui.plugin.QuackPlugin +import team.duckie.quackquack.ui.plugin.QuackPluginLocal +import team.duckie.quackquack.ui.plugin.quackPluginLocal + +/** 꽥꽥 컴포넌트가 그려지기 전에 특정 조건을 가로채서 변경할 수 있는 플러그인입니다. */ +@Immutable +public sealed interface QuackInterceptorPlugin : QuackPlugin { + /** 컴포넌트의 디자인 토큰을 가로채서 변경합니다. */ + @Immutable + public fun interface DesignToken : QuackInterceptorPlugin { + /** + * 컴포넌트의 디자인 토큰을 가로채서 변경한 새로운 디자인 토큰을 반환합니다. + * + * @param componentName 플러그인이 적용될 컴포넌트의 이름 + * @param componentDesignToken 플러그인이 적용되고 있는 컴포넌트에 적용된 디자인 토큰. + * 이 값은 이 플러그인이 적용되기 전에 미리 갖고 있던 디자인 토큰을 나타냅니다. + * @param componentModifier 플러그인이 적용되고 있는 컴포넌트가 사용하고 있는 [Modifier] + * @param quackPluginLocal [Modifier.quackPluginLocal]로 제공된 값 + */ + @Stable + public fun intercept( + componentName: String, + componentDesignToken: Any, + componentModifier: Modifier, + quackPluginLocal: QuackPluginLocal?, + ): Any + } +} diff --git a/ui-plugin/interceptor/src/main/kotlin/team/duckie/quackquack/ui/plugin/interceptor/extensions.kt b/ui-plugin/interceptor/src/main/kotlin/team/duckie/quackquack/ui/plugin/interceptor/extensions.kt new file mode 100644 index 000000000..e865b0603 --- /dev/null +++ b/ui-plugin/interceptor/src/main/kotlin/team/duckie/quackquack/ui/plugin/interceptor/extensions.kt @@ -0,0 +1,64 @@ +/* + * Designed and developed by Duckie Team 2023. + * + * Licensed under the MIT. + * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE + */ + +package team.duckie.quackquack.ui.plugin.interceptor + +import androidx.annotation.VisibleForTesting +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import team.duckie.quackquack.ui.plugin.EmptyQuackPlugins +import team.duckie.quackquack.ui.plugin.LocalQuackPlugins +import team.duckie.quackquack.ui.plugin.QuackPluginLocal +import team.duckie.quackquack.ui.plugin.lastByTypeOrNull +import team.duckie.quackquack.util.modifier.getElementByTypeOrNull + +/** 현재 실행 중인 call-site의 메서드 이름을 반환합니다. */ +@PublishedApi +internal val currentMethodName: String + // https://stackoverflow.com/a/32329165/14299073 + inline get() = Thread.currentThread().stackTrace[1].methodName + +/** intercept된 스타일이 기존에 제공된 스타일과 다른 타입일 때 throw할 에러 메시지 */ +@PublishedApi +@VisibleForTesting +internal const val InterceptedStyleTypeExceptionMessage: String = + "The intercepted style is of a different type than the original style." + +/** + * 주어진 [디자인 토큰][style]에 [QuackInterceptorPlugin.DesignToken] 플러그인을 적용한 후, + * 새로운 [디자인 토큰][Style]을 반환합니다. + * + * 만약 [기존 디자인 토큰][style]과 새로운 디자인 토큰의 타입이 다르다면 [IllegalStateException]이 + * 발생합니다. 새로운 디자인 토큰의 타입은 [Style]과 일치할 것이라 가정합니다. + * + * @param Style intercept의 결과로 생성될 디자인 토큰의 타입 + * @param style 기존에 적용된 디자인 토큰 + * @param modifier 현재 컴포넌트에 적용된 [Modifier] + */ +@Composable +public inline fun rememberInterceptedStyleSafely(style: Any, modifier: Modifier): Style { + val localQuackPlugins = LocalQuackPlugins.current + + return remember(localQuackPlugins, style, modifier) { + localQuackPlugins.takeIf { it != EmptyQuackPlugins }?.let { plugins -> + val interceptorPlugin = plugins.lastByTypeOrNull() + ?: return@let null + val quackPluginLocal = modifier.getElementByTypeOrNull() + + interceptorPlugin + .intercept( + componentName = currentMethodName, + componentDesignToken = style, + componentModifier = modifier, + quackPluginLocal = quackPluginLocal, + ) + } ?: style + }.also { interceptedStyle -> + check(interceptedStyle is Style, lazyMessage = ::InterceptedStyleTypeExceptionMessage) + } as Style +} diff --git a/ui-plugin/interceptor/src/test/kotlin/team/duckie/quackquack/ui/plugin/interceptor/QuackInterceptorPluginTest.kt b/ui-plugin/interceptor/src/test/kotlin/team/duckie/quackquack/ui/plugin/interceptor/QuackInterceptorPluginTest.kt new file mode 100644 index 000000000..b728416d8 --- /dev/null +++ b/ui-plugin/interceptor/src/test/kotlin/team/duckie/quackquack/ui/plugin/interceptor/QuackInterceptorPluginTest.kt @@ -0,0 +1,129 @@ +/* + * Designed and developed by Duckie Team 2023. + * + * Licensed under the MIT. + * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE + */ + +@file:OptIn(ExperimentalQuackQuackApi::class) + +package team.duckie.quackquack.ui.plugin.interceptor + +import com.github.takahirom.roborazzi.RoborazziRule.Ignore as NoSnapshot +import androidx.activity.ComponentActivity +import androidx.compose.ui.Modifier +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onRoot +import androidx.compose.ui.unit.dp +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.github.takahirom.roborazzi.RoborazziRule +import io.kotest.assertions.throwables.shouldThrowWithMessage +import io.kotest.matchers.maps.shouldMatchExactly +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import team.duckie.quackquack.material.QuackColor +import team.duckie.quackquack.material.theme.QuackTheme +import team.duckie.quackquack.ui.QuackTag +import team.duckie.quackquack.ui.QuackTagStyle +import team.duckie.quackquack.ui.TagStyleMarker +import team.duckie.quackquack.ui.plugin.rememberQuackPlugins +import team.duckie.quackquack.ui.util.ExperimentalQuackQuackApi +import team.duckie.quackquack.util.compose.snapshot.test.SnapshotName +import team.duckie.quackquack.util.compose.snapshot.test.snapshotPath + +@RunWith(AndroidJUnit4::class) +class QuackInterceptorPluginTest { + @get:Rule + val compose = createAndroidComposeRule() + + @get:Rule + val roborazzi = RoborazziRule( + composeRule = compose, + captureRoot = compose.onRoot(), + options = RoborazziRule.Options( + outputFileProvider = { description, _, fileExtension -> + val snapshotName = description.getAnnotation(SnapshotName::class.java)?.name ?: description.methodName + snapshotPath( + domain = "QuackInterceptorPlugin", + snapshotName = snapshotName, + isGif = fileExtension == "gif", + ) + }, + ), + ) + + @SnapshotName("QuackTagRadiusStyleIntercepted") + @Test + fun `style intercept works fine`() { + val map = mutableMapOf() + + var interceptedStyle: QuackTagStyle? = null + val interceptedRadius = Int.MAX_VALUE.dp + + compose.setContent { + QuackTheme( + plugins = rememberQuackPlugins { + +QuackInterceptorPlugin.DesignToken { componentName, componentDesignToken, componentModifier, _ -> + map["componentName"] = componentName + map["componentDesignToken"] = componentDesignToken + map["componentModifier"] = componentModifier + + (if (componentName == "QuackTag") { + @Suppress("UNCHECKED_CAST") + componentDesignToken as QuackTagStyle + object : QuackTagStyle by componentDesignToken { + override val radius = interceptedRadius + override val colors = + componentDesignToken.colors.copy( + backgroundColor = QuackColor.Gray3, + contentColor = QuackColor.Black, + ) + } + } else { + componentDesignToken + }).also { intercepttedResult -> + @Suppress("UNCHECKED_CAST") + interceptedStyle = intercepttedResult as QuackTagStyle + } + } + }, + ) { + QuackTag( + text = "Intercepted Tag", + style = QuackTagStyle.Filled, + onClick = {}, + ) + } + } + + map.shouldMatchExactly( + "componentName" to { it shouldBe "QuackTag" }, + "componentDesignToken" to { it.toString() shouldBe QuackTagStyle.Filled.toString() }, + "componentModifier" to { it shouldBe Modifier }, + ) + interceptedStyle.shouldNotBeNull().radius shouldBe interceptedRadius + } + + @NoSnapshot + @Test + fun InterceptedStyleTypeExceptionMessage() { + shouldThrowWithMessage(InterceptedStyleTypeExceptionMessage) { + compose.setContent { + QuackTheme( + plugins = rememberQuackPlugins { + +QuackInterceptorPlugin.DesignToken { _, _, _, _ -> Unit } + }, + ) { + QuackTag( + text = "", + style = QuackTagStyle.Filled, + onClick = {}, + ) + } + } + } + } +} diff --git a/ui-plugin/interceptor/src/test/resources/robolectric.properties b/ui-plugin/interceptor/src/test/resources/robolectric.properties new file mode 100644 index 000000000..2531a1168 --- /dev/null +++ b/ui-plugin/interceptor/src/test/resources/robolectric.properties @@ -0,0 +1,8 @@ +# +# Designed and developed by Duckie Team 2023. +# +# Licensed under the MIT. +# Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE +# + +sdk=33 diff --git a/ui-plugin/interceptor/src/test/snapshots/QuackInterceptorPlugin/QuackTagRadiusStyleIntercepted.png b/ui-plugin/interceptor/src/test/snapshots/QuackInterceptorPlugin/QuackTagRadiusStyleIntercepted.png new file mode 100644 index 0000000000000000000000000000000000000000..d7838469def6dd09f2a9ecd1833a70ade1da5010 GIT binary patch literal 2280 zcmVP)000QDNkl1fn79J_9MEf(^m7-*`n=BylV^_w;IF65CJ062!Z46!?EQcKd0>q67mjXx}v50Mq zZxCK2PJ@sT$Y2nWz{hd~FsIEeFgN(Z>x{`NX5yFro>jdwRnv0-L1x@PKIpD`_3BmC z*YCZquCXOBFp#{(;K76Gt7I!xW~ESLQ7m;71(H4I4@9diCVDBbK3FHL7uM~ofqjDEawh&meUsJ;0CwYRsM=&jJ!2kZ1>LnU?o zut{6L;k=HuxhdMZMks6T3T{af78XXQ^3&*-{J)Uxe7S0XiMJNEx*hb(`j55sP3Lu% z*WN3VBn*$CpG*Eu&;PSd>A-~l;CXGXw$6W}#P}~24!9-Br1S)`7xts})~AY66Ml&1 zM`SPfgr@v!l)?puJa(vS zk_;I#g#K3&V1}5!4lVTjR2oDfAt5SBeke#YlT;Io=QY_>y)NCEBvBEc^J}gwY@+YN zzWz@%RO5wClJD0hoBJRpoM5ZlPDj@Mvxg+uqV4|nT}8-Sw6(QSZEY=Gx^zjYGtqhm*|TR^muU0&@nhC|{rYtdao4V0<8rugw7+oSg5u7NbfG=I_ihMGiT0FOG}F(NgM%tjJ(Iqn>V>XT*GF_!UkCeeWKh*##h)H4$#d4 zD_J^=1A4yZx)AaPGD$GOxL>$%Atfdz^7ViL18CK%Rl3o*c66)2i4!MyB2rUR^)kf6 zii!$ugMp)L&6+h7A0JQCrcI+`$Bt2SbhIuX5fQ;20P^m137e-(nL-&E8M@5q(W99V zM)rBq3p6w|P;zpzUZ0SVKvSnqauR#?`>*u5--sk2#*Q63==}Nfv~1ZjF59zb56fU8@r0r*FE5W{KU`}{ zN(x0qMe&3|X2F65G;rWRUN}J#V9}yQ9M1y+0w^XXhIZ}R#gmH(g`39YxYH$U23D?I z$rBB7g@=bTQP7uc--8DaJnDrf13|DcdGO#tPUe>{U#|D%3rWydT3Q+f2M03=F^@}^ zF4c9RUf6?;uS16pDgC`FNx%#6!}22cgWrTC^XARdW#9@?wq(f?y-bezaNW2sE-vN? z2db;9sek|ev}MZ{og@<`Okj_+apOk57f-k&Jn0fP4;wbjX(Bgo-pu6)6S95Y=tVo! z!#2?d`r-Ld?h8o}0&%}#!vMN)|FODq#{(2Oaj`9>>U6Jwr<_ZvNB14)2B~UQc{vGk83Qru)|1~uzALe88ma|OkD={-MDeX z&^}Lk0kn5~W12pFI?bIsm#=*x34qsqM_rRAPiB3%76d@PH)+x&U1rv-S^Ah>l_XtT ztu#vG2Y((u$JvNl&O(RUE*bfn9SXk(QysV?K zu~F||hLJ8zq98Wxl5G%hWuK8=M_~M@$2IJO?MAY|lP6DD2H{JtLwlhM;}m-)0vHc$ z6mtM#!K=!8d_r3b*}r@L{~!2ja#P86Zl@~fr;thVHUnW6ujELU5#ErWU~KvM`HHMB zu+?p&@5Ki`{1g6p2%nsm1^pHNM3S1C8j6gJ)WZ#cJjgL2e;L^GKBBPDo`XmFp`(h` zWBBB>EbNa!h(wZy%`my{Z3M!Y_|j|?c74I~>b10IMUr|l4ZqH7FAC=4YjPtKeF#lj z&Q~15d{9UNd=VZ>FADyuHZ;*^U|;`diqv@E$P48s6&Wcb`5b}V*o6NBue=jI{iH$w zzrC;*d}@ZCz5*@8z=Mn%6R&W=kR*ULI+A`W4dj#4ic%AP10<=Pr9l)M6{&E*BS`=x zFXi7PYApMh560r$quhy!w-9YD)LFMpd(Hijb}vtIN>W|HElC6-!ak#_+*Imb^A6`L zIM#+!OE{GBD#r{=ya8yT1YdLUv99xEDd!#C-~NuOa#Hnu5Kr)mBm(#VmN5o@3?-Es zi>=gA6lDGpn_dok&L63xI7nNsI9|8RWUJ4A#O7BhQ0Qu4xZh_00000 Date: Mon, 3 Jul 2023 18:53:29 +0900 Subject: [PATCH 5/7] Add QuackTextFieldFontFamilyRemovalPlugin plugin to temporarily resolve #761 --- .../interceptor/textfield/build.gradle.kts | 55 +++++++++++++++ .../textfield/src/main/AndroidManifest.xml | 8 +++ .../QuackTextFieldFontFamilyRemovalPlugin.kt | 55 +++++++++++++++ ...extFieldFontFamilyRemovalPluginSnapshot.kt | 64 ++++++++++++++++++ .../src/test/resources/robolectric.properties | 8 +++ .../DefaultTextField.png | Bin 0 -> 2628 bytes .../FilledTextField.png | Bin 0 -> 2749 bytes ui-plugin/interceptor/textfield/version.txt | 1 + 8 files changed, 191 insertions(+) create mode 100644 ui-plugin/interceptor/textfield/build.gradle.kts create mode 100644 ui-plugin/interceptor/textfield/src/main/AndroidManifest.xml create mode 100644 ui-plugin/interceptor/textfield/src/main/kotlin/team/duckie/quackquack/ui/plugin/interceptor/textfield/QuackTextFieldFontFamilyRemovalPlugin.kt create mode 100644 ui-plugin/interceptor/textfield/src/test/kotlin/team/duckie/quackquack/ui/plugin/interceptor/textfield/QuackTextFieldFontFamilyRemovalPluginSnapshot.kt create mode 100644 ui-plugin/interceptor/textfield/src/test/resources/robolectric.properties create mode 100644 ui-plugin/interceptor/textfield/src/test/snapshots/QuackTextFieldFontFamilyRemovalPlugin/DefaultTextField.png create mode 100644 ui-plugin/interceptor/textfield/src/test/snapshots/QuackTextFieldFontFamilyRemovalPlugin/FilledTextField.png create mode 100644 ui-plugin/interceptor/textfield/version.txt diff --git a/ui-plugin/interceptor/textfield/build.gradle.kts b/ui-plugin/interceptor/textfield/build.gradle.kts new file mode 100644 index 000000000..ff2dc563a --- /dev/null +++ b/ui-plugin/interceptor/textfield/build.gradle.kts @@ -0,0 +1,55 @@ +/* + * Designed and developed by Duckie Team 2023. + * + * Licensed under the MIT. + * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE + */ + +@file:Suppress("UnstableApiUsage") + +import org.jetbrains.dokka.gradle.DokkaMultiModuleTask + +plugins { + quackquack("android-library") + quackquack("android-compose") + quackquack("kotlin-explicit-api") + quackquack("test-junit") + quackquack("quack-publishing") + alias(libs.plugins.test.roborazzi) +} + +tasks.withType { + dependsOn(":ui-plugin:dokkaHtmlMultiModule") +} + +android { + namespace = "team.duckie.quackquack.ui.plugin.interceptor.textfield" + + testOptions { + unitTests { + isIncludeAndroidResources = true + isReturnDefaultValues = true + all { test -> + test.systemProperty("robolectric.graphicsMode", "NATIVE") + } + } + } +} + +dependencies { + api(projects.uiPlugin.interceptor.orArtifact()) + implementations( + libs.compose.runtime, + libs.compose.ui.text, + projects.ui, + ) + testImplementations( + libs.compose.foundation, + libs.test.robolectric, + libs.test.junit.compose, + libs.test.kotest.assertion.core, + libs.test.kotlin.coroutines, // needed for compose-ui-test + libs.bundles.test.roborazzi, + projects.utilComposeSnapshotTest, + ) +} diff --git a/ui-plugin/interceptor/textfield/src/main/AndroidManifest.xml b/ui-plugin/interceptor/textfield/src/main/AndroidManifest.xml new file mode 100644 index 000000000..b1e11303c --- /dev/null +++ b/ui-plugin/interceptor/textfield/src/main/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + diff --git a/ui-plugin/interceptor/textfield/src/main/kotlin/team/duckie/quackquack/ui/plugin/interceptor/textfield/QuackTextFieldFontFamilyRemovalPlugin.kt b/ui-plugin/interceptor/textfield/src/main/kotlin/team/duckie/quackquack/ui/plugin/interceptor/textfield/QuackTextFieldFontFamilyRemovalPlugin.kt new file mode 100644 index 000000000..305942a0d --- /dev/null +++ b/ui-plugin/interceptor/textfield/src/main/kotlin/team/duckie/quackquack/ui/plugin/interceptor/textfield/QuackTextFieldFontFamilyRemovalPlugin.kt @@ -0,0 +1,55 @@ +/* + * Designed and developed by Duckie Team 2023. + * + * Licensed under the MIT. + * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE + */ + +@file:OptIn(ExperimentalDesignToken::class) + +package team.duckie.quackquack.ui.plugin.interceptor.textfield + +import androidx.compose.runtime.Stable +import androidx.compose.ui.text.font.FontFamily +import team.duckie.quackquack.material.QuackTypography +import team.duckie.quackquack.ui.QuackTextFieldStyle +import team.duckie.quackquack.ui.TextFieldColorMarker +import team.duckie.quackquack.ui.TextFieldStyleMarker +import team.duckie.quackquack.ui.optin.ExperimentalDesignToken +import team.duckie.quackquack.ui.plugin.interceptor.QuackInterceptorPlugin + +/** + * 꽥꽥에서 제공되는 TextField 컴포넌트의 이름 모음. + * 여기에 포함된 컴포넌트에만 [QuackTextFieldFontFamilyRemovalPlugin]이 적용됩니다. + */ +private val quackTextFieldComponentNames = + listOf("QuackDefaultTextField", "QuackFilledTextField", "QuackOutlinedTextField") + +/** + * `QuackTextField`에 적용될 디자인 토큰에서 `fontFamily`의 값을 [FontFamily.Default]로 변경합니다. + * 이 플러그인은 [#761](https://github.com/duckie-team/quack-quack-android/issues/761)의 임시 해결책으로 제공됩니다. + */ +@Stable +public val QuackTextFieldFontFamilyRemovalPlugin: QuackInterceptorPlugin.DesignToken = + QuackInterceptorPlugin.DesignToken { componentName, componentDesignToken, _, _ -> + if (quackTextFieldComponentNames.contains(componentName)) { + @Suppress("UNCHECKED_CAST") + componentDesignToken as QuackTextFieldStyle + object : QuackTextFieldStyle by componentDesignToken { + override val typography = + with(componentDesignToken.typography) { + QuackTypography( + color = color, + size = size, + weight = weight, + letterSpacing = letterSpacing, + lineHeight = lineHeight, + textAlign = textAlign, + fontFamily = FontFamily.Default, + ) + } + } + } else { + componentDesignToken + } + } diff --git a/ui-plugin/interceptor/textfield/src/test/kotlin/team/duckie/quackquack/ui/plugin/interceptor/textfield/QuackTextFieldFontFamilyRemovalPluginSnapshot.kt b/ui-plugin/interceptor/textfield/src/test/kotlin/team/duckie/quackquack/ui/plugin/interceptor/textfield/QuackTextFieldFontFamilyRemovalPluginSnapshot.kt new file mode 100644 index 000000000..6ff3556a2 --- /dev/null +++ b/ui-plugin/interceptor/textfield/src/test/kotlin/team/duckie/quackquack/ui/plugin/interceptor/textfield/QuackTextFieldFontFamilyRemovalPluginSnapshot.kt @@ -0,0 +1,64 @@ +/* + * Designed and developed by Duckie Team 2023. + * + * Licensed under the MIT. + * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE + */ + +@file:OptIn(ExperimentalDesignToken::class, ExperimentalQuackQuackApi::class) + +package team.duckie.quackquack.ui.plugin.interceptor.textfield + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.github.takahirom.roborazzi.captureRoboImage +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import team.duckie.quackquack.material.theme.QuackTheme +import team.duckie.quackquack.ui.QuackDefaultTextField +import team.duckie.quackquack.ui.QuackFilledTextField +import team.duckie.quackquack.ui.QuackTextFieldStyle +import team.duckie.quackquack.ui.optin.ExperimentalDesignToken +import team.duckie.quackquack.ui.plugin.rememberQuackPlugins +import team.duckie.quackquack.ui.util.ExperimentalQuackQuackApi +import team.duckie.quackquack.util.compose.snapshot.test.SnapshotPathGeneratorRule + +@RunWith(AndroidJUnit4::class) +class QuackTextFieldFontFamilyRemovalPluginSnapshot { + @get:Rule + val snapshotPath = SnapshotPathGeneratorRule("QuackTextFieldFontFamilyRemovalPlugin") + + @Test + fun DefaultTextField() { + captureRoboImage(snapshotPath()) { + QuackTheme( + plugins = rememberQuackPlugins { + +QuackTextFieldFontFamilyRemovalPlugin + }, + ) { + QuackDefaultTextField( + value = "퍠꿻땗뷃휉퉶뷟퍯 <- 잘 보이니?", + onValueChange = {}, + style = QuackTextFieldStyle.Default, + ) + } + } + } + + @Test + fun FilledTextField() { + captureRoboImage(snapshotPath()) { + QuackTheme( + plugins = rememberQuackPlugins { + +QuackTextFieldFontFamilyRemovalPlugin + }, + ) { + QuackFilledTextField( + value = "퍠꿻땗뷃휉퉶뷟퍯 <- 잘 보이니?", + onValueChange = {}, + style = QuackTextFieldStyle.FilledLarge, + ) + } + } + } +} diff --git a/ui-plugin/interceptor/textfield/src/test/resources/robolectric.properties b/ui-plugin/interceptor/textfield/src/test/resources/robolectric.properties new file mode 100644 index 000000000..2531a1168 --- /dev/null +++ b/ui-plugin/interceptor/textfield/src/test/resources/robolectric.properties @@ -0,0 +1,8 @@ +# +# Designed and developed by Duckie Team 2023. +# +# Licensed under the MIT. +# Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE +# + +sdk=33 diff --git a/ui-plugin/interceptor/textfield/src/test/snapshots/QuackTextFieldFontFamilyRemovalPlugin/DefaultTextField.png b/ui-plugin/interceptor/textfield/src/test/snapshots/QuackTextFieldFontFamilyRemovalPlugin/DefaultTextField.png new file mode 100644 index 0000000000000000000000000000000000000000..aa1ec4703ac56ebd95f719be16f7ab77b89aa543 GIT binary patch literal 2628 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 anzk)B$%V1gY|eCe!qU|4TAi_1!hZoQn(yrZ literal 0 HcmV?d00001 diff --git a/ui-plugin/interceptor/textfield/src/test/snapshots/QuackTextFieldFontFamilyRemovalPlugin/FilledTextField.png b/ui-plugin/interceptor/textfield/src/test/snapshots/QuackTextFieldFontFamilyRemovalPlugin/FilledTextField.png new file mode 100644 index 0000000000000000000000000000000000000000..795f6bf811bdc30cd94e73e5e4341bdfb87f7189 GIT binary patch literal 2749 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 N8%qa^@+ Date: Mon, 3 Jul 2023 18:53:59 +0900 Subject: [PATCH 6/7] Register a new module in settings --- settings.gradle.kts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/settings.gradle.kts b/settings.gradle.kts index 22e287b80..b391ddb42 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -37,6 +37,7 @@ include( ":util-backend-kotlinc", ":util-backend-test", ":util-compose-runtime-test", + ":util-compose-snapshot-test", ":runtime", ":material", ":material-icon", @@ -46,6 +47,8 @@ include( ":ui-plugin", ":ui-plugin:image", ":ui-plugin:image:gif", + ":ui-plugin:interceptor", + ":ui-plugin:interceptor:textfield", ":ui-sample", ":sugar-material", ":sugar-processor", From e7aaca8c7ec3f707f83752f23d2a0efb14b1c6d6 Mon Sep 17 00:00:00 2001 From: jisungbin Date: Mon, 3 Jul 2023 18:54:37 +0900 Subject: [PATCH 7/7] Add support for QuackInterceptPlugin to design components --- ui/build.gradle.kts | 1 + .../kotlin/team/duckie/quackquack/ui/button.kt | 4 ++++ .../main/kotlin/team/duckie/quackquack/ui/tag.kt | 4 ++++ .../kotlin/team/duckie/quackquack/ui/textfield.kt | 14 +++++++++++--- 4 files changed, 20 insertions(+), 3 deletions(-) diff --git a/ui/build.gradle.kts b/ui/build.gradle.kts index ebcc61a2f..63fda769a 100644 --- a/ui/build.gradle.kts +++ b/ui/build.gradle.kts @@ -91,6 +91,7 @@ dependencies { projects.material.orArtifact(), projects.materialIcon.orArtifact(), projects.uiPlugin.image.orArtifact(), + projects.uiPlugin.interceptor.orArtifact(), projects.util.orArtifact(), projects.utilModifier.orArtifact(), projects.casaAnnotation.orArtifact(), diff --git a/ui/src/main/kotlin/team/duckie/quackquack/ui/button.kt b/ui/src/main/kotlin/team/duckie/quackquack/ui/button.kt index 846eaba46..ecab93987 100644 --- a/ui/src/main/kotlin/team/duckie/quackquack/ui/button.kt +++ b/ui/src/main/kotlin/team/duckie/quackquack/ui/button.kt @@ -52,6 +52,7 @@ import team.duckie.quackquack.runtime.QuackDataModifierModel import team.duckie.quackquack.runtime.quackMaterializeOf import team.duckie.quackquack.sugar.material.NoSugar import team.duckie.quackquack.sugar.material.SugarToken +import team.duckie.quackquack.ui.plugin.interceptor.rememberInterceptedStyleSafely import team.duckie.quackquack.ui.util.ExperimentalQuackQuackApi import team.duckie.quackquack.ui.util.QuackDsl import team.duckie.quackquack.ui.util.asLoose @@ -768,6 +769,9 @@ public fun QuackButton( rippleEnabled: Boolean = true, @CasaValue("{}") onClick: () -> Unit, ) { + @Suppress("NAME_SHADOWING") + val style: QuackButtonStyle = rememberInterceptedStyleSafely(style = style, modifier = modifier) + val isSmallButton = style is QuackSmallButtonStyle // TODO: 다른 경우로 사이즈를 지정하는 방법이 있을까? // TODO(3): LayoutModifierNode 지원 diff --git a/ui/src/main/kotlin/team/duckie/quackquack/ui/tag.kt b/ui/src/main/kotlin/team/duckie/quackquack/ui/tag.kt index a390c6b7a..62debea83 100644 --- a/ui/src/main/kotlin/team/duckie/quackquack/ui/tag.kt +++ b/ui/src/main/kotlin/team/duckie/quackquack/ui/tag.kt @@ -54,6 +54,7 @@ import team.duckie.quackquack.runtime.QuackDataModifierModel import team.duckie.quackquack.runtime.quackMaterializeOf import team.duckie.quackquack.sugar.material.NoSugar import team.duckie.quackquack.sugar.material.SugarToken +import team.duckie.quackquack.ui.plugin.interceptor.rememberInterceptedStyleSafely import team.duckie.quackquack.ui.util.ExperimentalQuackQuackApi import team.duckie.quackquack.ui.util.QuackDsl import team.duckie.quackquack.ui.util.asLoose @@ -552,6 +553,9 @@ public fun QuackTag( rippleEnabled: Boolean = true, @CasaValue("{}") onClick: () -> Unit, ) { + @Suppress("NAME_SHADOWING") + val style: QuackTagStyle = rememberInterceptedStyleSafely(style = style, modifier = modifier) + val isGrayscaleFlat = style is QuackGrayscaleFlatTagDefaults if (isGrayscaleFlat) { check(selected) { QuackTagErrors.GrayscaleFlatStyleUnselectedState } diff --git a/ui/src/main/kotlin/team/duckie/quackquack/ui/textfield.kt b/ui/src/main/kotlin/team/duckie/quackquack/ui/textfield.kt index 73b3b9e7d..7314f1bb5 100644 --- a/ui/src/main/kotlin/team/duckie/quackquack/ui/textfield.kt +++ b/ui/src/main/kotlin/team/duckie/quackquack/ui/textfield.kt @@ -87,6 +87,7 @@ import team.duckie.quackquack.runtime.quackMaterializeOf import team.duckie.quackquack.sugar.material.NoSugar import team.duckie.quackquack.sugar.material.SugarToken import team.duckie.quackquack.ui.optin.ExperimentalDesignToken +import team.duckie.quackquack.ui.plugin.interceptor.rememberInterceptedStyleSafely import team.duckie.quackquack.ui.token.HorizontalDirection import team.duckie.quackquack.ui.token.VerticalDirection import team.duckie.quackquack.ui.util.ExperimentalQuackQuackApi @@ -1048,6 +1049,11 @@ public fun