Introduce StandardCardLayout & WideCardLayout in TV Material

Test: Instrumentation & screenshot tests added

Relnote: "Add TV opinionated material StandardCardLayout and
WideCardLayout"

Change-Id: I33faedd7fcf0792b0378e855803e19f33788628c
diff --git a/tv/tv-material/api/public_plus_experimental_current.txt b/tv/tv-material/api/public_plus_experimental_current.txt
index 44ffb2b..3f5fc27 100644
--- a/tv/tv-material/api/public_plus_experimental_current.txt
+++ b/tv/tv-material/api/public_plus_experimental_current.txt
@@ -92,6 +92,20 @@
     method @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void WideClassicCard(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> image, kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> subtitle, optional kotlin.jvm.functions.Function0<kotlin.Unit> description, optional androidx.tv.material3.CardShape shape, optional androidx.tv.material3.CardColors colors, optional androidx.tv.material3.CardScale scale, optional androidx.tv.material3.CardBorder border, optional androidx.tv.material3.CardGlow glow, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource);
   }
 
+  @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class CardLayoutColors {
+  }
+
+  @androidx.tv.material3.ExperimentalTvMaterial3Api public final class CardLayoutDefaults {
+    method @androidx.compose.runtime.Composable public void ImageCard(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.Modifier modifier, optional androidx.tv.material3.CardShape shape, optional androidx.tv.material3.CardColors colors, optional androidx.tv.material3.CardScale scale, optional androidx.tv.material3.CardBorder border, optional androidx.tv.material3.CardGlow glow, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.CardLayoutColors contentColor(optional long contentColor, optional long focusedContentColor, optional long pressedContentColor);
+    field public static final androidx.tv.material3.CardLayoutDefaults INSTANCE;
+  }
+
+  public final class CardLayoutKt {
+    method @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void StandardCardLayout(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Unit> imageCard, kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> subtitle, optional kotlin.jvm.functions.Function0<kotlin.Unit> description, optional androidx.tv.material3.CardLayoutColors contentColor, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource);
+    method @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void WideCardLayout(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Unit> imageCard, kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> subtitle, optional kotlin.jvm.functions.Function0<kotlin.Unit> description, optional androidx.tv.material3.CardLayoutColors contentColor, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource);
+  }
+
   @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class CardScale {
   }
 
diff --git a/tv/tv-material/src/androidTest/java/androidx/tv/material3/CardLayoutScreenshotTest.kt b/tv/tv-material/src/androidTest/java/androidx/tv/material3/CardLayoutScreenshotTest.kt
new file mode 100644
index 0000000..5c6a7ff
--- /dev/null
+++ b/tv/tv-material/src/androidTest/java/androidx/tv/material3/CardLayoutScreenshotTest.kt
@@ -0,0 +1,276 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.material3
+
+import android.os.Build
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.runtime.Composable
+import androidx.compose.testutils.assertAgainstGolden
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.SemanticsActions
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onChild
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performSemanticsAction
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.screenshot.AndroidXScreenshotTestRule
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalTvMaterial3Api::class)
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+class CardLayoutScreenshotTest {
+    @get:Rule
+    val rule = createComposeRule()
+
+    @get:Rule
+    val screenshotRule = AndroidXScreenshotTestRule(TV_GOLDEN_MATERIAL3)
+
+    private val boxSizeModifier = Modifier.size(220.dp, 180.dp)
+    private val standardCardLayoutSizeModifier = Modifier.size(150.dp, 120.dp)
+    private val wideCardLayoutSizeModifier = Modifier.size(180.dp, 100.dp)
+
+    @Test
+    fun standardCardLayout_lightTheme() {
+        rule.setContent {
+            LightMaterialTheme {
+                Box(
+                    modifier = boxSizeModifier.testTag(CardLayoutWrapperTag),
+                    contentAlignment = Alignment.Center
+                ) {
+                    StandardCardLayout(
+                        modifier = standardCardLayoutSizeModifier,
+                        imageCard = { interactionSource ->
+                            CardLayoutDefaults.ImageCard(
+                                onClick = { },
+                                interactionSource = interactionSource
+                            ) {
+                                SampleImage(
+                                    Modifier
+                                        .fillMaxWidth()
+                                        .height(80.dp)
+                                )
+                            }
+                        },
+                        title = { Text("Standard Card") }
+                    )
+                }
+            }
+        }
+
+        assertAgainstGolden("standardCardLayout_lightTheme")
+    }
+
+    @Test
+    fun standardCardLayout_darkTheme() {
+        rule.setContent {
+            DarkMaterialTheme {
+                Box(
+                    modifier = boxSizeModifier.testTag(CardLayoutWrapperTag),
+                    contentAlignment = Alignment.Center
+                ) {
+                    StandardCardLayout(
+                        modifier = standardCardLayoutSizeModifier,
+                        imageCard = { interactionSource ->
+                            CardLayoutDefaults.ImageCard(
+                                onClick = { },
+                                interactionSource = interactionSource
+                            ) {
+                                SampleImage(
+                                    Modifier
+                                        .fillMaxWidth()
+                                        .height(80.dp)
+                                )
+                            }
+                        },
+                        title = { Text("Standard Card") }
+                    )
+                }
+            }
+        }
+
+        assertAgainstGolden("standardCardLayout_darkTheme")
+    }
+
+    @Test
+    fun standardCardLayout_focused() {
+        rule.setContent {
+            Box(
+                modifier = boxSizeModifier
+                    .testTag(CardLayoutWrapperTag)
+                    .semantics(mergeDescendants = true) {},
+                contentAlignment = Alignment.Center
+            ) {
+                StandardCardLayout(
+                    modifier = standardCardLayoutSizeModifier,
+                    imageCard = { interactionSource ->
+                        CardLayoutDefaults.ImageCard(
+                            onClick = { },
+                            interactionSource = interactionSource
+                        ) {
+                            SampleImage(
+                                Modifier
+                                    .fillMaxWidth()
+                                    .height(80.dp)
+                            )
+                        }
+                    },
+                    title = { Text("Standard Card", Modifier.padding(top = 5.dp)) }
+                )
+            }
+        }
+
+        rule.onNodeWithTag(CardLayoutWrapperTag)
+            .onChild()
+            .performSemanticsAction(SemanticsActions.RequestFocus)
+        rule.waitForIdle()
+
+        assertAgainstGolden("standardCardLayout_focused")
+    }
+
+    @Test
+    fun wideCardLayout_lightTheme() {
+        rule.setContent {
+            LightMaterialTheme {
+                Box(
+                    modifier = boxSizeModifier.testTag(CardLayoutWrapperTag),
+                    contentAlignment = Alignment.Center
+                ) {
+                    WideCardLayout(
+                        modifier = wideCardLayoutSizeModifier,
+                        imageCard = { interactionSource ->
+                            CardLayoutDefaults.ImageCard(
+                                onClick = { },
+                                interactionSource = interactionSource
+                            ) {
+                                SampleImage(
+                                    Modifier
+                                        .fillMaxHeight()
+                                        .width(90.dp)
+                                )
+                            }
+                        },
+                        title = { Text("Wide Card", Modifier.padding(start = 8.dp)) },
+                    )
+                }
+            }
+        }
+
+        assertAgainstGolden("wideCardLayout_lightTheme")
+    }
+
+    @Test
+    fun wideCardLayout_darkTheme() {
+        rule.setContent {
+            DarkMaterialTheme {
+                Box(
+                    modifier = boxSizeModifier.testTag(CardLayoutWrapperTag),
+                    contentAlignment = Alignment.Center
+                ) {
+                    WideCardLayout(
+                        modifier = wideCardLayoutSizeModifier,
+                        imageCard = { interactionSource ->
+                            CardLayoutDefaults.ImageCard(
+                                onClick = { },
+                                interactionSource = interactionSource
+                            ) {
+                                SampleImage(
+                                    Modifier
+                                        .fillMaxHeight()
+                                        .width(90.dp)
+                                )
+                            }
+                        },
+                        title = { Text("Wide Card", Modifier.padding(start = 8.dp)) },
+                    )
+                }
+            }
+        }
+
+        assertAgainstGolden("wideCardLayout_darkTheme")
+    }
+
+    @Test
+    fun wideCardLayout_focused() {
+        rule.setContent {
+            Box(
+                modifier = boxSizeModifier
+                    .testTag(CardLayoutWrapperTag)
+                    .semantics(mergeDescendants = true) {},
+                contentAlignment = Alignment.Center
+            ) {
+                WideCardLayout(
+                    modifier = wideCardLayoutSizeModifier,
+                    imageCard = { interactionSource ->
+                        CardLayoutDefaults.ImageCard(
+                            onClick = { },
+                            interactionSource = interactionSource
+                        ) {
+                            SampleImage(
+                                Modifier
+                                    .fillMaxHeight()
+                                    .width(90.dp)
+                            )
+                        }
+                    },
+                    title = { Text("Wide Card", Modifier.padding(start = 8.dp)) },
+                )
+            }
+        }
+
+        rule.onNodeWithTag(CardLayoutWrapperTag)
+            .onChild()
+            .performSemanticsAction(SemanticsActions.RequestFocus)
+        rule.waitForIdle()
+
+        assertAgainstGolden("wideCardLayout_focused")
+    }
+
+    @Composable
+    fun SampleImage(modifier: Modifier = Modifier) {
+        Box(
+            modifier = modifier
+                .background(Color.Blue)
+        )
+    }
+
+    private fun assertAgainstGolden(goldenName: String) {
+        rule.onNodeWithTag(CardLayoutWrapperTag)
+            .captureToImage()
+            .assertAgainstGolden(screenshotRule, goldenName)
+    }
+}
+
+private const val CardLayoutWrapperTag = "card_layout_wrapper"
\ No newline at end of file
diff --git a/tv/tv-material/src/androidTest/java/androidx/tv/material3/CardLayoutTest.kt b/tv/tv-material/src/androidTest/java/androidx/tv/material3/CardLayoutTest.kt
new file mode 100644
index 0000000..f180509
--- /dev/null
+++ b/tv/tv-material/src/androidTest/java/androidx/tv/material3/CardLayoutTest.kt
@@ -0,0 +1,200 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.material3
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.key.Key
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.SemanticsActions
+import androidx.compose.ui.semantics.SemanticsProperties
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.test.SemanticsMatcher
+import androidx.compose.ui.test.assert
+import androidx.compose.ui.test.assertHasClickAction
+import androidx.compose.ui.test.assertIsEnabled
+import androidx.compose.ui.test.assertTextEquals
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onChild
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performKeyInput
+import androidx.compose.ui.test.performSemanticsAction
+import androidx.compose.ui.test.pressKey
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(
+    ExperimentalTestApi::class,
+    ExperimentalComposeUiApi::class,
+    ExperimentalTvMaterial3Api::class
+)
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class CardLayoutTest {
+    @get:Rule
+    val rule = createComposeRule()
+
+    @Test
+    fun standardCardLayout_semantics() {
+        val count = mutableStateOf(0)
+        rule.setContent {
+            StandardCardLayout(
+                modifier = Modifier
+                    .semantics(mergeDescendants = true) {}
+                    .testTag(StandardCardLayoutTag),
+                imageCard = { interactionSource ->
+                    CardLayoutDefaults.ImageCard(
+                        onClick = { count.value += 1 },
+                        interactionSource = interactionSource
+                    ) { SampleImage() }
+                },
+                title = { Text("${count.value}") }
+            )
+        }
+
+        rule.onNodeWithTag(StandardCardLayoutTag)
+            .onChild()
+            .assertHasClickAction()
+            .assert(SemanticsMatcher.keyNotDefined(SemanticsProperties.Role))
+            .performSemanticsAction(SemanticsActions.RequestFocus)
+            .assertIsEnabled()
+
+        rule.onNodeWithTag(StandardCardLayoutTag)
+            .assertTextEquals("0")
+            .performKeyInput { pressKey(Key.DirectionCenter) }
+            .assertTextEquals("1")
+    }
+
+    @Test
+    fun standardCardLayout_clickAction() {
+        val count = mutableStateOf(0f)
+        rule.setContent {
+            StandardCardLayout(
+                modifier = Modifier
+                    .semantics(mergeDescendants = true) {}
+                    .testTag(StandardCardLayoutTag),
+                imageCard = { interactionSource ->
+                    CardLayoutDefaults.ImageCard(
+                        onClick = { count.value += 1 },
+                        interactionSource = interactionSource
+                    ) { SampleImage() }
+                },
+                title = { Text("${count.value}") }
+            )
+        }
+
+        rule.onNodeWithTag(StandardCardLayoutTag)
+            .onChild()
+            .performSemanticsAction(SemanticsActions.RequestFocus)
+            .performKeyInput { pressKey(Key.DirectionCenter) }
+        Truth.assertThat(count.value).isEqualTo(1)
+
+        rule.onNodeWithTag(StandardCardLayoutTag)
+            .onChild()
+            .performSemanticsAction(SemanticsActions.RequestFocus)
+            .performKeyInput { pressKey(Key.DirectionCenter) }
+            .performKeyInput { pressKey(Key.DirectionCenter) }
+        Truth.assertThat(count.value).isEqualTo(3)
+    }
+
+    @Test
+    fun wideCardLayout_semantics() {
+        val count = mutableStateOf(0)
+        rule.setContent {
+            WideCardLayout(
+                modifier = Modifier
+                    .semantics(mergeDescendants = true) {}
+                    .testTag(WideCardLayoutTag),
+                imageCard = { interactionSource ->
+                    CardLayoutDefaults.ImageCard(
+                        onClick = { count.value += 1 },
+                        interactionSource = interactionSource
+                    ) { SampleImage() }
+                },
+                title = { Text("${count.value}") }
+            )
+        }
+
+        rule.onNodeWithTag(WideCardLayoutTag)
+            .onChild()
+            .assertHasClickAction()
+            .assert(SemanticsMatcher.keyNotDefined(SemanticsProperties.Role))
+            .performSemanticsAction(SemanticsActions.RequestFocus)
+            .assertIsEnabled()
+
+        rule.onNodeWithTag(WideCardLayoutTag)
+            .assertTextEquals("0")
+            .performKeyInput { pressKey(Key.DirectionCenter) }
+            .assertTextEquals("1")
+    }
+
+    @Test
+    fun wideCardLayout_clickAction() {
+        val count = mutableStateOf(0f)
+        rule.setContent {
+            WideCardLayout(
+                modifier = Modifier
+                    .semantics(mergeDescendants = true) {}
+                    .testTag(WideCardLayoutTag),
+                imageCard = { interactionSource ->
+                    CardLayoutDefaults.ImageCard(
+                        onClick = { count.value += 1 },
+                        interactionSource = interactionSource
+                    ) { SampleImage() }
+                },
+                title = { Text("${count.value}") }
+            )
+        }
+
+        rule.onNodeWithTag(WideCardLayoutTag)
+            .onChild()
+            .performSemanticsAction(SemanticsActions.RequestFocus)
+            .performKeyInput { pressKey(Key.DirectionCenter) }
+        Truth.assertThat(count.value).isEqualTo(1)
+
+        rule.onNodeWithTag(WideCardLayoutTag)
+            .onChild()
+            .performSemanticsAction(SemanticsActions.RequestFocus)
+            .performKeyInput { pressKey(Key.DirectionCenter) }
+            .performKeyInput { pressKey(Key.DirectionCenter) }
+        Truth.assertThat(count.value).isEqualTo(3)
+    }
+
+    @Composable
+    fun SampleImage() {
+        Box(
+            Modifier
+                .size(180.dp, 150.dp)
+                .testTag(SampleImageTag)
+        )
+    }
+}
+
+private const val StandardCardLayoutTag = "standard-card-layout"
+private const val WideCardLayoutTag = "wide-card-layout"
+
+private const val SampleImageTag = "sample-image"
\ No newline at end of file
diff --git a/tv/tv-material/src/androidTest/java/androidx/tv/material3/CardTest.kt b/tv/tv-material/src/androidTest/java/androidx/tv/material3/CardTest.kt
index 3ea23a8..68be7c4 100644
--- a/tv/tv-material/src/androidTest/java/androidx/tv/material3/CardTest.kt
+++ b/tv/tv-material/src/androidTest/java/androidx/tv/material3/CardTest.kt
@@ -471,10 +471,8 @@
 }
 
 private const val CardTag = "card"
-private const val StandardCardTag = "standard-card"
 private const val CompactCardTag = "compact-card"
 private const val ClassicCardTag = "classic-card"
-private const val WideCardTag = "wide-card"
 private const val WideClassicCardTag = "wide-classic-card"
 
 private const val SampleImageTag = "sample-image"
\ No newline at end of file
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/Card.kt b/tv/tv-material/src/main/java/androidx/tv/material3/Card.kt
index 53add8d..1c1fc72 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/Card.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/Card.kt
@@ -30,7 +30,6 @@
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.ReadOnlyComposable
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Alignment
@@ -325,19 +324,7 @@
 }
 
 @Composable
-private fun CardContent(
-    title: @Composable () -> Unit,
-    subtitle: @Composable () -> Unit = {},
-    description: @Composable () -> Unit = {},
-    contentColor: Color
-) {
-    CompositionLocalProvider(LocalContentColor provides contentColor) {
-        CardContent(title, subtitle, description)
-    }
-}
-
-@Composable
-private fun CardContent(
+internal fun CardContent(
     title: @Composable () -> Unit,
     subtitle: @Composable () -> Unit = {},
     description: @Composable () -> Unit = {}
@@ -399,22 +386,6 @@
     )
 
     /**
-     * Returns the content color [Color] from the colors [CardColors] for different
-     * interaction states.
-     */
-    internal fun contentColor(
-        focused: Boolean,
-        pressed: Boolean,
-        colors: CardColors
-    ): Color {
-        return when {
-            focused -> colors.focusedContentColor
-            pressed -> colors.pressedContentColor
-            else -> colors.contentColor
-        }
-    }
-
-    /**
      * Creates a [CardShape] that represents the default container shapes used in a Card.
      *
      * @param shape the default shape used when the Card has no other [Interaction]s.
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/CardLayout.kt b/tv/tv-material/src/main/java/androidx/tv/material3/CardLayout.kt
new file mode 100644
index 0000000..92aa581
--- /dev/null
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/CardLayout.kt
@@ -0,0 +1,288 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.material3
+
+import androidx.compose.foundation.interaction.Interaction
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.interaction.collectIsFocusedAsState
+import androidx.compose.foundation.interaction.collectIsPressedAsState
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.ReadOnlyComposable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+
+/**
+ * [StandardCardLayout] is an opinionated TV Material Card layout with an image and text content
+ * to show information about a subject.
+ *
+ * It provides a vertical layout with an image card slot at the top. And below that, there are
+ * slots for the title, subtitle and description.
+ *
+ * @param imageCard defines the [Composable] to be used for the image card. See
+ * [CardLayoutDefaults.ImageCard] to create an image card. The `interactionSource` param provided
+ * in the lambda function should be forwarded and used with the image card composable.
+ * @param title defines the [Composable] title placed below the image card in the CardLayout.
+ * @param modifier the [Modifier] to be applied to this CardLayout.
+ * @param subtitle defines the [Composable] supporting text placed below the title in CardLayout.
+ * @param description defines the [Composable] description placed below the subtitle in CardLayout.
+ * @param contentColor [CardLayoutColors] defines the content color used in the CardLayout
+ * for different interaction states. See [CardLayoutDefaults.contentColor].
+ * @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s
+ * for this CardLayout. You can create and pass in your own `remember`ed instance to observe
+ * [Interaction]s and customize the appearance / behavior of this card layout in different states.
+ * This interaction source param would also be forwarded to be used with the `imageCard` composable.
+ */
+@ExperimentalTvMaterial3Api
+@Composable
+fun StandardCardLayout(
+    imageCard: @Composable (interactionSource: MutableInteractionSource) -> Unit,
+    title: @Composable () -> Unit,
+    modifier: Modifier = Modifier,
+    subtitle: @Composable () -> Unit = {},
+    description: @Composable () -> Unit = {},
+    contentColor: CardLayoutColors = CardLayoutDefaults.contentColor(),
+    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }
+) {
+    val focused by interactionSource.collectIsFocusedAsState()
+    val pressed by interactionSource.collectIsPressedAsState()
+
+    Column(
+        modifier = modifier
+    ) {
+        Box(
+            contentAlignment = CardDefaults.ContentImageAlignment,
+        ) {
+            imageCard(interactionSource)
+        }
+        Column(
+            modifier = Modifier
+                .align(Alignment.CenterHorizontally),
+            horizontalAlignment = Alignment.CenterHorizontally
+        ) {
+            CardLayoutContent(
+                title = title,
+                subtitle = subtitle,
+                description = description,
+                contentColor = contentColor.color(
+                    focused = focused,
+                    pressed = pressed
+                )
+            )
+        }
+    }
+}
+
+/**
+ * [WideCardLayout] is an opinionated TV Material Card layout with an image and text content
+ * to show information about a subject.
+ *
+ * It provides a horizontal layout with an image card slot at the start, followed by the title,
+ * subtitle and description at the end.
+ *
+ * @param imageCard defines the [Composable] to be used for the image card. See
+ * [CardLayoutDefaults.ImageCard] to create an image card. The `interactionSource` param provided
+ * in the lambda function should to be forwarded and used with the image card composable.
+ * @param title defines the [Composable] title placed below the image card in the CardLayout.
+ * @param modifier the [Modifier] to be applied to this CardLayout.
+ * @param subtitle defines the [Composable] supporting text placed below the title in CardLayout.
+ * @param description defines the [Composable] description placed below the subtitle in CardLayout.
+ * @param contentColor [CardLayoutColors] defines the content color used in the CardLayout
+ * for different interaction states. See [CardLayoutDefaults.contentColor].
+ * @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s
+ * for this CardLayout. You can create and pass in your own `remember`ed instance to observe
+ * [Interaction]s and customize the appearance / behavior of this card layout in different states.
+ * This interaction source param would also be forwarded to be used with the `imageCard` composable.
+ */
+@ExperimentalTvMaterial3Api
+@Composable
+fun WideCardLayout(
+    imageCard: @Composable (interactionSource: MutableInteractionSource) -> Unit,
+    title: @Composable () -> Unit,
+    modifier: Modifier = Modifier,
+    subtitle: @Composable () -> Unit = {},
+    description: @Composable () -> Unit = {},
+    contentColor: CardLayoutColors = CardLayoutDefaults.contentColor(),
+    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }
+) {
+    val focused by interactionSource.collectIsFocusedAsState()
+    val pressed by interactionSource.collectIsPressedAsState()
+
+    Row(
+        modifier = modifier
+    ) {
+        Box(
+            contentAlignment = CardDefaults.ContentImageAlignment
+        ) {
+            imageCard(interactionSource)
+        }
+        Column {
+            CardLayoutContent(
+                title = title,
+                subtitle = subtitle,
+                description = description,
+                contentColor = contentColor.color(
+                    focused = focused,
+                    pressed = pressed
+                )
+            )
+        }
+    }
+}
+
+@Composable
+internal fun CardLayoutContent(
+    title: @Composable () -> Unit,
+    subtitle: @Composable () -> Unit = {},
+    description: @Composable () -> Unit = {},
+    contentColor: Color
+) {
+    CompositionLocalProvider(LocalContentColor provides contentColor) {
+        CardContent(title, subtitle, description)
+    }
+}
+
+@ExperimentalTvMaterial3Api
+object CardLayoutDefaults {
+    /**
+     * Creates [CardLayoutColors] that represents the default content colors used in a
+     * CardLayout.
+     *
+     * @param contentColor the default content color of this CardLayout.
+     * @param focusedContentColor the content color of this CardLayout when focused.
+     * @param pressedContentColor the content color of this CardLayout when pressed.
+     */
+    @ReadOnlyComposable
+    @Composable
+    fun contentColor(
+        contentColor: Color = MaterialTheme.colorScheme.onSurface,
+        focusedContentColor: Color = contentColor,
+        pressedContentColor: Color = focusedContentColor
+    ) = CardLayoutColors(
+        contentColor = contentColor,
+        focusedContentColor = focusedContentColor,
+        pressedContentColor = pressedContentColor
+    )
+
+    /**
+     * [ImageCard] is basically a [Card] composable with an image as the content. It is recommended
+     * to be used with the different CardLayout(s).
+     *
+     * This Card handles click events, calling its [onClick] lambda.
+     *
+     * @param onClick called when this card is clicked
+     * @param interactionSource the [MutableInteractionSource] representing the stream of
+     * [Interaction]s for this card. When using with the CardLayout(s), it is recommended to
+     * pass in the interaction state obtained from the parent lambda.
+     * @param modifier the [Modifier] to be applied to this card
+     * @param shape [CardShape] defines the shape of this card's container in different interaction
+     * states. See [CardDefaults.shape].
+     * @param colors [CardColors] defines the background & content colors used in this card for
+     * different interaction states. See [CardDefaults.colors].
+     * @param scale [CardScale] defines size of the card relative to its original size for different
+     * interaction states. See [CardDefaults.scale].
+     * @param border [CardBorder] defines a border around the card for different interaction states.
+     * See [CardDefaults.border].
+     * @param glow [CardGlow] defines a shadow to be shown behind the card for different interaction
+     * states. See [CardDefaults.glow].
+     * @param content defines the image content [Composable] to be displayed inside the Card.
+     */
+    @Composable
+    fun ImageCard(
+        onClick: () -> Unit,
+        interactionSource: MutableInteractionSource,
+        modifier: Modifier = Modifier,
+        shape: CardShape = CardDefaults.shape(),
+        colors: CardColors = CardDefaults.colors(),
+        scale: CardScale = CardDefaults.scale(),
+        border: CardBorder = CardDefaults.border(),
+        glow: CardGlow = CardDefaults.glow(),
+        content: @Composable () -> Unit
+    ) {
+        Card(
+            onClick = onClick,
+            modifier = modifier,
+            shape = shape,
+            colors = colors,
+            scale = scale,
+            border = border,
+            glow = glow,
+            interactionSource = interactionSource
+        ) {
+            content()
+        }
+    }
+}
+
+/**
+ * Represents the [Color] of content in a CardLayout for different interaction states.
+ */
+@ExperimentalTvMaterial3Api
+@Immutable
+class CardLayoutColors internal constructor(
+    internal val contentColor: Color,
+    internal val focusedContentColor: Color,
+    internal val pressedContentColor: Color,
+) {
+    /**
+     * Returns the content color [Color] for different interaction states.
+     */
+    internal fun color(
+        focused: Boolean,
+        pressed: Boolean
+    ): Color {
+        return when {
+            focused -> focusedContentColor
+            pressed -> pressedContentColor
+            else -> contentColor
+        }
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other == null || this::class != other::class) return false
+
+        other as CardLayoutColors
+
+        if (contentColor != other.contentColor) return false
+        if (focusedContentColor != other.focusedContentColor) return false
+        if (pressedContentColor != other.pressedContentColor) return false
+
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = contentColor.hashCode()
+        result = 31 * result + focusedContentColor.hashCode()
+        result = 31 * result + pressedContentColor.hashCode()
+        return result
+    }
+
+    override fun toString(): String {
+        return "CardLayoutContentColor(" +
+            "contentColor=$contentColor, " +
+            "focusedContentColor=$focusedContentColor, " +
+            "pressedContentColor=$pressedContentColor)"
+    }
+}