Kaynağa Gözat

custom card

milan jurkulák 2 yıl önce
ebeveyn
işleme
325819ce7d

+ 48 - 0
src/main/kotlin/eu/mjdev/desktop/components/card/Card.kt

@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) Milan Jurkulák 2024.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package eu.mjdev.desktop.components.card
+
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import eu.mjdev.desktop.components.card.base.*
+import eu.mjdev.desktop.components.card.base.CardDefaults.toClickableSurfaceBorder
+import eu.mjdev.desktop.components.card.base.CardDefaults.toClickableSurfaceColors
+import eu.mjdev.desktop.components.card.base.CardDefaults.toClickableSurfaceGlow
+import eu.mjdev.desktop.components.card.base.CardDefaults.toClickableSurfaceScale
+import eu.mjdev.desktop.components.card.base.CardDefaults.toClickableSurfaceShape
+import eu.mjdev.desktop.components.surface.Surface
+
+@Composable
+fun Card(
+    onClick: () -> Unit,
+    modifier: Modifier = Modifier,
+    onLongClick: (() -> Unit)? = null,
+    shape: CardShape = CardDefaults.shape(),
+    colors: CardColors = CardDefaults.colors(),
+    scale: CardScale = CardDefaults.scale(),
+    border: CardBorder = CardDefaults.border(),
+    glow: CardGlow = CardDefaults.glow(),
+    interactionSource: MutableInteractionSource? = null,
+    content: @Composable ColumnScope.() -> Unit
+) = Surface(
+    onClick = onClick,
+    onLongClick = onLongClick,
+    modifier = modifier,
+    shape = shape.toClickableSurfaceShape(),
+    colors = colors.toClickableSurfaceColors(),
+    scale = scale.toClickableSurfaceScale(),
+    border = border.toClickableSurfaceBorder(),
+    glow = glow.toClickableSurfaceGlow(),
+    interactionSource = interactionSource,
+) {
+    Column(content = content)
+}

+ 49 - 0
src/main/kotlin/eu/mjdev/desktop/components/card/ClassicCard.kt

@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) Milan Jurkulák 2024.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package eu.mjdev.desktop.components.card
+
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.*
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import eu.mjdev.desktop.components.card.base.*
+
+@Suppress("unused")
+@Composable
+fun ClassicCard(
+    onClick: () -> Unit,
+    image: @Composable BoxScope.() -> Unit,
+    title: @Composable () -> Unit,
+    modifier: Modifier = Modifier,
+    onLongClick: (() -> Unit)? = null,
+    subtitle: @Composable () -> Unit = {},
+    description: @Composable () -> Unit = {},
+    shape: CardShape = CardDefaults.shape(),
+    colors: CardColors = CardDefaults.colors(),
+    scale: CardScale = CardDefaults.scale(),
+    border: CardBorder = CardDefaults.border(),
+    glow: CardGlow = CardDefaults.glow(),
+    contentPadding: PaddingValues = PaddingValues(),
+    interactionSource: MutableInteractionSource? = null
+) = Card(
+    onClick = onClick,
+    onLongClick = onLongClick,
+    modifier = modifier,
+    interactionSource = interactionSource,
+    shape = shape,
+    colors = colors,
+    scale = scale,
+    border = border,
+    glow = glow
+) {
+    Column(modifier = Modifier.padding(contentPadding)) {
+        Box(contentAlignment = CardDefaults.ContentImageAlignment, content = image)
+        Column { CardContent(title = title, subtitle = subtitle, description = description) }
+    }
+}

+ 85 - 0
src/main/kotlin/eu/mjdev/desktop/components/card/CompactCard.kt

@@ -0,0 +1,85 @@
+/*
+ * Copyright (c) Milan Jurkulák 2024.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package eu.mjdev.desktop.components.card
+
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxScope
+import androidx.compose.foundation.layout.Column
+import androidx.compose.material.ProvideTextStyle
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.drawWithCache
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.graphicsLayer
+import eu.mjdev.desktop.components.card.base.*
+import eu.mjdev.desktop.components.card.base.CardDefaults.DescriptionAlpha
+import eu.mjdev.desktop.components.card.base.CardDefaults.SubtitleAlpha
+
+@Composable
+fun CompactCard(
+    onClick: () -> Unit,
+    image: @Composable BoxScope.() -> Unit,
+    title: @Composable () -> Unit,
+    modifier: Modifier = Modifier,
+    onLongClick: (() -> Unit)? = null,
+    subtitle: @Composable () -> Unit = {},
+    description: @Composable () -> Unit = {},
+    shape: CardShape = CardDefaults.shape(),
+    colors: CardColors = CardDefaults.compactCardColors(),
+    scale: CardScale = CardDefaults.scale(),
+    border: CardBorder = CardDefaults.border(),
+    glow: CardGlow = CardDefaults.glow(),
+    scrimBrush: Brush = CardDefaults.ScrimBrush,
+    interactionSource: MutableInteractionSource? = null
+) {
+    Card(
+        onClick = onClick,
+        onLongClick = onLongClick,
+        modifier = modifier,
+        interactionSource = interactionSource,
+        shape = shape,
+        colors = colors,
+        scale = scale,
+        border = border,
+        glow = glow
+    ) {
+        Box(contentAlignment = Alignment.BottomStart) {
+            Box(
+                modifier =
+                    Modifier.drawWithCache {
+                        onDrawWithContent {
+                            drawContent()
+                            drawRect(brush = scrimBrush)
+                        }
+                    },
+                contentAlignment = CardDefaults.ContentImageAlignment,
+                content = image
+            )
+            Column { CardContent(title = title, subtitle = subtitle, description = description) }
+        }
+    }
+}
+
+@Composable
+fun CardContent(
+    title: @Composable () -> Unit,
+    subtitle: @Composable () -> Unit = {},
+    description: @Composable () -> Unit = {}
+) {
+    ProvideTextStyle(MaterialTheme.typography.titleMedium) { title.invoke() }
+    ProvideTextStyle(MaterialTheme.typography.bodySmall) {
+        Box(Modifier.graphicsLayer { alpha = SubtitleAlpha }) { subtitle.invoke() }
+    }
+    ProvideTextStyle(MaterialTheme.typography.bodySmall) {
+        Box(Modifier.graphicsLayer { alpha = DescriptionAlpha }) { description.invoke() }
+    }
+}

+ 159 - 0
src/main/kotlin/eu/mjdev/desktop/components/card/FocusableCard.kt

@@ -0,0 +1,159 @@
+/*
+ * Copyright (c) Milan Jurkulák 2024.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package eu.mjdev.desktop.components.card
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.*
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.BrokenImage
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.MutableState
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusState
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import eu.mjdev.desktop.components.card.base.*
+import eu.mjdev.desktop.components.card.base.CardDefaults.colorFocusBorder
+import eu.mjdev.desktop.components.card.base.CardDefaults.colorFocusGlow
+import eu.mjdev.desktop.components.image.ImageAny
+import eu.mjdev.desktop.extensions.Compose.rememberFocusState
+import eu.mjdev.desktop.extensions.Compose.rememberFocusRequester
+import eu.mjdev.desktop.helpers.compose.FocusHelper
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.onFocusChanged
+import androidx.compose.ui.graphics.RectangleShape
+import eu.mjdev.desktop.components.text.AutoHideEmptyText
+import eu.mjdev.desktop.components.text.TextAny
+import eu.mjdev.desktop.extensions.Compose.focusState
+import eu.mjdev.desktop.extensions.Compose.isFocused
+import eu.mjdev.desktop.extensions.Compose.requestFocusOnTouch
+
+@Composable
+fun FocusableCard(
+    item: Any? = null,
+    modifier: Modifier = Modifier,
+    contentScale: ContentScale = ContentScale.Crop,
+    scale: CardScale = CardDefaults.scale(),
+    shape: CardShape = CardDefaults.shape(),
+    textColor: Color = Color.White,
+    textBackgroundUnselected: Color = Color.DarkGray.copy(alpha = 0.5f),
+    textBackgroundSelected: Color = Color.Green.copy(alpha = 0.5f),
+    colors: CardColors = CardDefaults.colors(),
+    border: CardBorder = CardDefaults.colorFocusBorder(Color.Green),
+    glow: CardGlow = CardDefaults.colorFocusGlow(Color.Green),
+    showTitle: Boolean = true,
+    placeholder: @Composable () -> Unit = {
+        Image(
+            imageVector = Icons.Filled.BrokenImage,
+            contentDescription = "",
+            contentScale = contentScale
+        )
+    },
+    imageRenderer: @Composable () -> Unit = {
+        ImageAny(
+            modifier = Modifier.fillMaxSize(),
+            src = item,
+            contentDescription = item?.toString(),
+            contentScale = contentScale,
+            placeholder = placeholder
+        )
+    },
+    focused: Boolean = false,
+    focusRequester: FocusRequester = rememberFocusRequester(item),
+    focusState: MutableState<FocusState> = rememberFocusState(
+        item,
+        FocusHelper(focused)
+    ),
+    onFocus: ((item: Any?, fromUser: Boolean) -> Unit)? = null,
+    onFocusChange: (state: FocusState) -> Unit = { state ->
+        if (state.isFocused || state.hasFocus) {
+            // todo focused from user ?
+            onFocus?.invoke(item, false)
+        }
+    },
+    titlePadding: PaddingValues = PaddingValues(8.dp),
+    cardWidth: Dp = computeCardWidth(),
+    aspectRatio: Float = 16f / 9f,
+    onClick: ((item: Any?) -> Unit)? = null,
+) = CompactCard(
+    scale = scale,
+    shape = shape,
+    colors = colors,
+    border = border,
+    glow = glow,
+    modifier = modifier
+        .size(
+            width = cardWidth,
+            height = cardWidth / aspectRatio
+        )
+        .focusState(focusState)
+        .onFocusChanged { state ->
+            onFocusChange(state)
+        }
+        .requestFocusOnTouch(focusRequester) {
+            if (focusState.isFocused) onClick?.invoke(item)
+        },
+    image = {
+        imageRenderer()
+    },
+    title = {
+        if (showTitle) {
+            Box(
+                modifier = Modifier
+                    .fillMaxWidth()
+                    .background(
+                        if (focusState.isFocused) textBackgroundSelected
+                        else textBackgroundUnselected,
+                        RectangleShape
+                    )
+            ) {
+                TextAny(
+                    modifier = Modifier.padding(titlePadding),
+                    maxLines = 1,
+                    color = textColor,
+                    style = MaterialTheme.typography.bodyMedium,
+                    text = item?.toString().orEmpty()
+                )
+            }
+        }
+    },
+    subtitle = {
+        if (showTitle) {
+            Box(
+                modifier = Modifier
+                    .fillMaxWidth()
+                    .background(
+                        if (focusState.isFocused)
+                            textBackgroundSelected
+                        else
+                            textBackgroundUnselected,
+                        RectangleShape
+                    )
+            ) {
+                AutoHideEmptyText(
+                    modifier = Modifier.padding(titlePadding),
+                    maxLines = 1,
+                    color = textColor,
+                    style = MaterialTheme.typography.bodySmall,
+                    text = "" // todo
+                )
+            }
+        }
+    },
+    description = {
+        // todo
+    },
+    onClick = {
+        onClick?.invoke(item)
+    },
+)

+ 93 - 0
src/main/kotlin/eu/mjdev/desktop/components/card/ItemCard.kt

@@ -0,0 +1,93 @@
+/*
+ * Copyright (c) Milan Jurkulák 2024.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package eu.mjdev.desktop.components.card
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.BrokenImage
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.Dp
+import eu.mjdev.desktop.components.card.base.*
+import eu.mjdev.desktop.components.card.base.CardDefaults.colorFocusBorder
+import eu.mjdev.desktop.components.card.base.CardDefaults.colorFocusGlow
+import eu.mjdev.desktop.helpers.compose.FocusHelper
+import eu.mjdev.desktop.components.image.ImageAny
+import eu.mjdev.desktop.extensions.Compose.rememberFocusRequester
+import eu.mjdev.desktop.extensions.Compose.rememberFocusState
+import androidx.compose.runtime.MutableState
+import androidx.compose.ui.focus.FocusState
+
+@Composable
+fun ItemCard(
+    item: Any? = null,
+    modifier: Modifier = Modifier,
+    contentScale: ContentScale = ContentScale.Crop,
+    scale: CardScale = CardDefaults.scale(),
+    shape: CardShape = CardDefaults.shape(),
+    textColor: Color = Color.White,
+    colors: CardColors = CardDefaults.colors(),
+    border: CardBorder = CardDefaults.colorFocusBorder(Color.Green),
+    glow: CardGlow = CardDefaults.colorFocusGlow(Color.Green),
+    aspectRatio: Float = 16f / 9f,
+    placeholder : @Composable () -> Unit = {
+        Image(
+            imageVector = Icons.Filled.BrokenImage,
+            "",
+            modifier
+        )
+    },
+    imageRenderer: @Composable () -> Unit = {
+        ImageAny(
+            modifier = modifier.fillMaxSize(),
+            src = item,
+            contentDescription = item?.toString(),
+            contentScale = contentScale,
+//            placeholder = placeholder
+        )
+    },
+    cardWidth: Dp = computeCardWidth(),
+    focused: Boolean = false,
+    focusState: MutableState<FocusState> = rememberFocusState(
+        item,
+        FocusHelper(focused)
+    ),
+    focusRequester: FocusRequester = rememberFocusRequester(item),
+    titlePadding: PaddingValues = PaddingValues(8.dp),
+    showTitle:Boolean = true,
+    onFocus: ((item: Any?, fromUser:Boolean) -> Unit)? = null,
+    onClick: ((item: Any?) -> Unit)? = null,
+) = FocusableCard(
+    modifier = modifier,
+    item = item,
+    focusState = focusState,
+    focusRequester = focusRequester,
+    focused = focused,
+    contentScale = contentScale,
+    aspectRatio = aspectRatio,
+    textColor = textColor,
+    scale = scale,
+    shape = shape,
+    colors = colors,
+    border = border,
+    glow = glow,
+    cardWidth = cardWidth,
+    imageRenderer = imageRenderer,
+    titlePadding = titlePadding,
+    onFocus = onFocus,
+    onClick = onClick,
+    showTitle = showTitle,
+    placeholder = placeholder
+)

+ 97 - 0
src/main/kotlin/eu/mjdev/desktop/components/card/PhotoCard.kt

@@ -0,0 +1,97 @@
+package eu.mjdev.desktop.components.card
+
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.ui.Modifier
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.MutableState
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.FocusState
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import eu.mjdev.desktop.components.card.base.CardDefaults
+import eu.mjdev.desktop.components.card.base.CardScale
+import eu.mjdev.desktop.extensions.Compose.rememberFocusState
+import eu.mjdev.desktop.extensions.Compose.rememberFocusRequester
+import eu.mjdev.desktop.helpers.compose.FocusHelper
+import eu.mjdev.desktop.components.image.ImageAny
+import eu.mjdev.desktop.components.image.PhotoImage
+import eu.mjdev.desktop.extensions.Compose.isFocused
+import eu.mjdev.desktop.provider.DesktopProvider
+import eu.mjdev.desktop.provider.DesktopProvider.Companion.LocalDesktop
+
+@Composable
+fun PhotoCard(
+    item: Any? = null,
+    modifier: Modifier = Modifier,
+    contentScale: ContentScale = ContentScale.Fit,
+    textColor: Color = Color.White,
+    focused: Boolean = false,
+    focusState: MutableState<FocusState> = rememberFocusState(
+        item,
+        FocusHelper(focused)
+    ),
+    focusRequester: FocusRequester = rememberFocusRequester(item),
+    contrast: Float = 5f,
+    brightness: Float = -255f,
+    placeholder: @Composable () -> Unit = {
+        ImageAny(
+            modifier = Modifier.fillMaxSize().padding(64.dp),
+            src = null,
+            contentScale = contentScale
+        )
+    },
+    imageRenderer: @Composable () -> Unit = {
+        PhotoImage(
+            modifier = Modifier.fillMaxSize(),
+            src = item,
+            contentScale = contentScale,
+            contrast = contrast,
+            brightness = brightness,
+            placeholder = placeholder,
+            borderColor = if (focusState.isFocused) Color.Green else Color.Black,
+            contentDescription = item?.toString(), // todo
+        )
+    },
+    showTitle: Boolean = true,
+    cardWidth: Dp = computeCardWidth(),
+    aspectRatio: Float = 16f / 9f,
+    scale: CardScale = CardDefaults.scale(),
+    titlePadding: PaddingValues = PaddingValues(8.dp),
+    onFocus: ((item: Any?, fromUser: Boolean) -> Unit)? = null,
+    onClick: ((item: Any?) -> Unit)? = null,
+) {
+    ItemCard(
+        item = item,
+        modifier = modifier,
+        contentScale = contentScale,
+        textColor = textColor,
+        focusState = focusState,
+        focusRequester = focusRequester,
+        focused = focused,
+        aspectRatio = aspectRatio,
+        cardWidth = cardWidth,
+        scale = scale,
+        imageRenderer = imageRenderer,
+        titlePadding = titlePadding,
+        onFocus = onFocus,
+        onClick = onClick,
+        showTitle = showTitle
+    )
+}
+
+@Composable
+fun computeCardWidth(
+    api: DesktopProvider = LocalDesktop.current,
+    ratio: Float = 2.5f
+): Dp =
+//    LocalConfiguration.current.let { config ->
+//    if (config.orientation == Configuration.ORIENTATION_PORTRAIT)
+    api.containerSize.width / ratio
+//    else
+//        config.screenHeightDp / ratio
+//}.dp
+

+ 51 - 0
src/main/kotlin/eu/mjdev/desktop/components/card/WideClassicCard.kt

@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) Milan Jurkulák 2024.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package eu.mjdev.desktop.components.card
+
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.*
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import eu.mjdev.desktop.components.card.base.*
+
+@Suppress("unused")
+@Composable
+fun WideClassicCard(
+    onClick: () -> Unit,
+    image: @Composable BoxScope.() -> Unit,
+    title: @Composable () -> Unit,
+    modifier: Modifier = Modifier,
+    onLongClick: (() -> Unit)? = null,
+    subtitle: @Composable () -> Unit = {},
+    description: @Composable () -> Unit = {},
+    shape: CardShape = CardDefaults.shape(),
+    colors: CardColors = CardDefaults.colors(),
+    scale: CardScale = CardDefaults.scale(),
+    border: CardBorder = CardDefaults.border(),
+    glow: CardGlow = CardDefaults.glow(),
+    contentPadding: PaddingValues = PaddingValues(),
+    interactionSource: MutableInteractionSource? = null
+) {
+    Card(
+        onClick = onClick,
+        onLongClick = onLongClick,
+        modifier = modifier,
+        interactionSource = interactionSource,
+        shape = shape,
+        colors = colors,
+        scale = scale,
+        border = border,
+        glow = glow
+    ) {
+        Row(modifier = Modifier.padding(contentPadding)) {
+            Box(contentAlignment = CardDefaults.ContentImageAlignment, content = image)
+            Column { CardContent(title = title, subtitle = subtitle, description = description) }
+        }
+    }
+}

+ 42 - 0
src/main/kotlin/eu/mjdev/desktop/components/card/base/CardBorder.kt

@@ -0,0 +1,42 @@
+/*
+ * Copyright (c) Milan Jurkulák 2024.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package eu.mjdev.desktop.components.card.base
+
+import androidx.compose.runtime.Immutable
+import eu.mjdev.desktop.components.surface.base.Border
+
+@Immutable
+class CardBorder
+internal constructor(
+    internal val border: Border,
+    internal val focusedBorder: Border,
+    internal val pressedBorder: Border
+) {
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other == null || this::class != other::class) return false
+        other as CardBorder
+        if (border != other.border) return false
+        if (focusedBorder != other.focusedBorder) return false
+        if (pressedBorder != other.pressedBorder) return false
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = border.hashCode()
+        result = 31 * result + focusedBorder.hashCode()
+        result = 31 * result + pressedBorder.hashCode()
+        return result
+    }
+
+    override fun toString(): String {
+        return "CardBorder(border=$border, focusedBorder=$focusedBorder, " +
+            "pressedBorder=$pressedBorder)"
+    }
+}

+ 56 - 0
src/main/kotlin/eu/mjdev/desktop/components/card/base/CardColors.kt

@@ -0,0 +1,56 @@
+/*
+ * Copyright (c) Milan Jurkulák 2024.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package eu.mjdev.desktop.components.card.base
+
+import androidx.compose.runtime.Immutable
+import androidx.compose.ui.graphics.Color
+
+@Immutable
+class CardColors
+internal constructor(
+    internal val containerColor: Color,
+    internal val contentColor: Color,
+    internal val focusedContainerColor: Color,
+    internal val focusedContentColor: Color,
+    internal val pressedContainerColor: Color,
+    internal val pressedContentColor: Color
+) {
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other == null || this::class != other::class) return false
+        other as CardColors
+        if (containerColor != other.containerColor) return false
+        if (contentColor != other.contentColor) return false
+        if (focusedContainerColor != other.focusedContainerColor) return false
+        if (focusedContentColor != other.focusedContentColor) return false
+        if (pressedContainerColor != other.pressedContainerColor) return false
+        if (pressedContentColor != other.pressedContentColor) return false
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = containerColor.hashCode()
+        result = 31 * result + contentColor.hashCode()
+        result = 31 * result + focusedContainerColor.hashCode()
+        result = 31 * result + focusedContentColor.hashCode()
+        result = 31 * result + pressedContainerColor.hashCode()
+        result = 31 * result + pressedContentColor.hashCode()
+        return result
+    }
+
+    override fun toString(): String {
+        return "CardColors(" +
+                "containerColor=$containerColor, " +
+                "contentColor=$contentColor, " +
+                "focusedContainerColor=$focusedContainerColor, " +
+                "focusedContentColor=$focusedContentColor, " +
+                "pressedContainerColor=$pressedContainerColor, " +
+                "pressedContentColor=$pressedContentColor)"
+    }
+}

+ 208 - 0
src/main/kotlin/eu/mjdev/desktop/components/card/base/CardDefaults.kt

@@ -0,0 +1,208 @@
+/*
+ * Copyright (c) Milan Jurkulák 2024.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package eu.mjdev.desktop.components.card.base
+
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.contentColorFor
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.ReadOnlyComposable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import eu.mjdev.desktop.components.surface.base.*
+
+@Suppress("ConstPropertyName", "unused", "MemberVisibilityCanBePrivate")
+object CardDefaults {
+    val ContentImageAlignment = Alignment.Center
+    val ContainerShape = RoundedCornerShape(8.dp)
+
+    const val SquareImageAspectRatio = 1f
+    const val VerticalImageAspectRatio = 2f / 3
+    const val HorizontalImageAspectRatio = 16f / 9
+
+    const val SubtitleAlpha = 0.6f
+    const val DescriptionAlpha = 0.8f
+
+    val ScrimBrush = Brush.verticalGradient(
+        listOf(
+            Color(red = 28, green = 27, blue = 31, alpha = 0),
+            Color(red = 28, green = 27, blue = 31, alpha = 204)
+        )
+    )
+
+    fun shape(
+        shape: Shape = ContainerShape,
+        focusedShape: Shape = shape,
+        pressedShape: Shape = shape
+    ) = CardShape(shape = shape, focusedShape = focusedShape, pressedShape = pressedShape)
+
+    @ReadOnlyComposable
+    @Composable
+    fun colors(
+        containerColor: Color = MaterialTheme.colorScheme.surfaceVariant,
+        contentColor: Color = contentColorFor(containerColor),
+        focusedContainerColor: Color = containerColor,
+        focusedContentColor: Color = contentColorFor(focusedContainerColor),
+        pressedContainerColor: Color = focusedContainerColor,
+        pressedContentColor: Color = contentColorFor(pressedContainerColor)
+    ) = CardColors(
+        containerColor = containerColor,
+        contentColor = contentColor,
+        focusedContainerColor = focusedContainerColor,
+        focusedContentColor = focusedContentColor,
+        pressedContainerColor = pressedContainerColor,
+        pressedContentColor = pressedContentColor
+    )
+
+    @ReadOnlyComposable
+    @Composable
+    fun compactCardColors(
+        containerColor: Color = MaterialTheme.colorScheme.surfaceVariant,
+        contentColor: Color = Color.White,
+        focusedContainerColor: Color = containerColor,
+        focusedContentColor: Color = contentColor,
+        pressedContainerColor: Color = focusedContainerColor,
+        pressedContentColor: Color = focusedContentColor
+    ) = CardColors(
+        containerColor = containerColor,
+        contentColor = contentColor,
+        focusedContainerColor = focusedContainerColor,
+        focusedContentColor = focusedContentColor,
+        pressedContainerColor = pressedContainerColor,
+        pressedContentColor = pressedContentColor
+    )
+
+    @Composable
+    fun CardDefaults.colorFocusBorder(
+        colorFocused: Color = Color.Green,
+        colorUnFocused: Color = Color.Black,
+        colorPressed: Color = Color.Yellow,
+        width: Dp = 1.dp,
+        roundCornerSize: Dp = 8.dp
+    ) = border(
+        border = Border(
+            border = BorderStroke(
+                width = width,
+                color = colorUnFocused
+            ),
+            shape = RoundedCornerShape(roundCornerSize)
+        ),
+        focusedBorder = Border(
+            border = BorderStroke(
+                width = width,
+                color = colorFocused
+            ),
+            shape = RoundedCornerShape(roundCornerSize)
+        ),
+    pressedBorder = Border(
+        border = BorderStroke(
+            width = width,
+            color = colorPressed
+        ),
+        shape = RoundedCornerShape(roundCornerSize)
+    )
+    )
+
+    @Composable
+    fun CardDefaults.colorFocusGlow(
+        focusColor: Color = Color.Green,
+        onUnFocusColor: Color = Color.Transparent,
+        elevation: Dp = 10.dp
+    ): CardGlow = glow(
+        glow = Glow(
+            elevationColor = onUnFocusColor,
+            elevation = elevation
+        ),
+        focusedGlow = Glow(
+            elevationColor = focusColor,
+            elevation = elevation
+        ),
+        pressedGlow = Glow(
+            elevationColor = onUnFocusColor,
+            elevation = elevation
+        )
+    )
+
+    fun scale(
+        scale: Float = 1f,
+        focusedScale: Float = 1.1f,
+        pressedScale: Float = scale
+    ) = CardScale(scale = scale, focusedScale = focusedScale, pressedScale = pressedScale)
+
+    @ReadOnlyComposable
+    @Composable
+    fun border(
+        border: Border = Border.None,
+        focusedBorder: Border = Border(
+            border = BorderStroke(width = 3.dp, color = Color.White), // todo theme
+            shape = ContainerShape
+        ),
+        pressedBorder: Border = focusedBorder
+    ) = CardBorder(
+        border = border,
+        focusedBorder = focusedBorder,
+        pressedBorder = pressedBorder
+    )
+
+    fun glow(
+        glow: Glow = Glow.None,
+        focusedGlow: Glow = glow,
+        pressedGlow: Glow = glow
+    ) = CardGlow(
+        glow = glow,
+        focusedGlow = focusedGlow,
+        pressedGlow = pressedGlow
+    )
+
+    fun CardColors.toClickableSurfaceColors() = ClickableSurfaceColors(
+        containerColor = containerColor,
+        contentColor = contentColor,
+        focusedContainerColor = focusedContainerColor,
+        focusedContentColor = focusedContentColor,
+        pressedContainerColor = pressedContainerColor,
+        pressedContentColor = pressedContentColor,
+        disabledContainerColor = containerColor,
+        disabledContentColor = contentColor
+    )
+
+    fun CardShape.toClickableSurfaceShape() =
+        ClickableSurfaceShape(
+            shape = shape,
+            focusedShape = focusedShape,
+            pressedShape = pressedShape,
+            disabledShape = shape,
+            focusedDisabledShape = shape
+        )
+
+    fun CardScale.toClickableSurfaceScale() =
+        ClickableSurfaceScale(
+            scale = scale,
+            focusedScale = focusedScale,
+            pressedScale = pressedScale,
+            disabledScale = scale,
+            focusedDisabledScale = scale
+        )
+
+    fun CardBorder.toClickableSurfaceBorder() =
+        ClickableSurfaceBorder(
+            border = border,
+            focusedBorder = focusedBorder,
+            pressedBorder = pressedBorder,
+            disabledBorder = border,
+            focusedDisabledBorder = border
+        )
+
+    fun CardGlow.toClickableSurfaceGlow() =
+        ClickableSurfaceGlow(glow = glow, focusedGlow = focusedGlow, pressedGlow = pressedGlow)
+}

+ 41 - 0
src/main/kotlin/eu/mjdev/desktop/components/card/base/CardGlow.kt

@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) Milan Jurkulák 2024.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package eu.mjdev.desktop.components.card.base
+
+import androidx.compose.runtime.Immutable
+import eu.mjdev.desktop.components.surface.base.Glow
+
+@Immutable
+class CardGlow
+internal constructor(
+    internal val glow: Glow,
+    internal val focusedGlow: Glow,
+    internal val pressedGlow: Glow
+) {
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other == null || this::class != other::class) return false
+        other as CardGlow
+        if (glow != other.glow) return false
+        if (focusedGlow != other.focusedGlow) return false
+        if (pressedGlow != other.pressedGlow) return false
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = glow.hashCode()
+        result = 31 * result + focusedGlow.hashCode()
+        result = 31 * result + pressedGlow.hashCode()
+        return result
+    }
+
+    override fun toString(): String {
+        return "CardGlow(glow=$glow, focusedGlow=$focusedGlow, pressedGlow=$pressedGlow)"
+    }
+}

+ 45 - 0
src/main/kotlin/eu/mjdev/desktop/components/card/base/CardScale.kt

@@ -0,0 +1,45 @@
+/*
+ * Copyright (c) Milan Jurkulák 2024.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package eu.mjdev.desktop.components.card.base
+
+import androidx.compose.runtime.Immutable
+
+@Suppress("unused")
+@Immutable
+class CardScale
+internal constructor(
+    internal val scale: Float,
+    internal val focusedScale: Float,
+    internal val pressedScale: Float
+) {
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other == null || this::class != other::class) return false
+        other as CardScale
+        if (scale != other.scale) return false
+        if (focusedScale != other.focusedScale) return false
+        if (pressedScale != other.pressedScale) return false
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = scale.hashCode()
+        result = 31 * result + focusedScale.hashCode()
+        result = 31 * result + pressedScale.hashCode()
+        return result
+    }
+
+    override fun toString(): String {
+        return "CardScale(scale=$scale, focusedScale=$focusedScale, pressedScale=$pressedScale)"
+    }
+
+    companion object {
+        val None = CardScale(scale = 1f, focusedScale = 1f, pressedScale = 1f)
+    }
+}

+ 42 - 0
src/main/kotlin/eu/mjdev/desktop/components/card/base/CardShape.kt

@@ -0,0 +1,42 @@
+/*
+ * Copyright (c) Milan Jurkulák 2024.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package eu.mjdev.desktop.components.card.base
+
+import androidx.compose.runtime.Immutable
+import androidx.compose.ui.graphics.Shape
+
+@Immutable
+class CardShape
+internal constructor(
+    internal val shape: Shape,
+    internal val focusedShape: Shape,
+    internal val pressedShape: Shape
+) {
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other == null || this::class != other::class) return false
+        other as CardShape
+        if (shape != other.shape) return false
+        if (focusedShape != other.focusedShape) return false
+        if (pressedShape != other.pressedShape) return false
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = shape.hashCode()
+        result = 31 * result + focusedShape.hashCode()
+        result = 31 * result + pressedShape.hashCode()
+        return result
+    }
+
+    override fun toString(): String {
+        return "CardShape(shape=$shape, focusedShape=$focusedShape, pressedShape=$pressedShape)"
+    }
+}
+

+ 50 - 0
src/main/kotlin/eu/mjdev/desktop/helpers/compose/ZIndexModifier.kt

@@ -0,0 +1,50 @@
+/*
+ * Copyright (c) Milan Jurkulák 2024.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package eu.mjdev.desktop.helpers.compose
+
+import androidx.compose.ui.Modifier
+
+import androidx.compose.runtime.Stable
+import androidx.compose.ui.layout.Measurable
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.layout.MeasureScope
+import androidx.compose.ui.node.LayoutModifierNode
+import androidx.compose.ui.node.ModifierNodeElement
+import androidx.compose.ui.platform.InspectorInfo
+import androidx.compose.ui.unit.Constraints
+
+@Stable
+fun Modifier.zIndex(
+    zIndex: Float
+): Modifier = this then ZIndexElement(zIndex = zIndex)
+
+data class ZIndexElement(val zIndex: Float) : ModifierNodeElement<ZIndexNode>() {
+    override fun create() = ZIndexNode(zIndex)
+    override fun update(node: ZIndexNode) {
+        node.zIndex = zIndex
+    }
+    override fun InspectorInfo.inspectableProperties() {
+        name = "zIndex"
+        properties["zIndex"] = zIndex
+    }
+}
+
+class ZIndexNode(var zIndex: Float) : LayoutModifierNode, Modifier.Node() {
+    override fun MeasureScope.measure(
+        measurable: Measurable,
+        constraints: Constraints
+    ): MeasureResult {
+        val placeable = measurable.measure(constraints)
+        return layout(placeable.width, placeable.height) {
+            placeable.place(0, 0, zIndex = zIndex)
+        }
+    }
+
+    override fun toString(): String = "ZIndexModifier(zIndex=$zIndex)"
+}