All Downloads are FREE. Search and download functionalities are using the official Maven repository.

commonMain.io.github.lyxnx.compose.screenables.ScreenDefinition.kt Maven / Gradle / Ivy

There is a newer version: 1.1.3
Show newest version
@file:Suppress("MemberVisibilityCanBePrivate")

package io.github.lyxnx.compose.screenables

import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
import androidx.compose.material3.windowsizeclass.WindowHeightSizeClass
import androidx.compose.material3.windowsizeclass.WindowSizeClass
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.ProvidableCompositionLocal
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp

/**
 * Represents the app's window screen definition
 *
 * This includes data such as:
 * - The fold (if there is one), along with the regions created either side of it
 * (eg top/bottom for a horizontal fold, and left/right for a vertical fold);
 * - The orientation of the screen ([isPortrait]);
 * - and the [WindowSizeClass] of the window
 */
@Immutable
public data class ScreenDefinition(
    /**
     * The orientation of the fold, if this is a foldable device and the fold would be visible
     *
     * For example, a device with a left and a right screen and a fold down the middle:
     * ```
     * +---++---+
     * |   ||   |
     * | L || R |
     * |   ||   |
     * +---++---+
     * ```
     * Currently, the fold orientation is vertical
     *
     * Folding the device so the right screen is folded back:
     * ```
     * +---+
     * |   |
     * | L |
     * |   |
     * +---+
     * ```
     * Now the fold orientation would be null, as there is no fold to interrupt content, and as a result the following would be 0:
     * [foldWidth], [foldHeight], [leftContentSize], [rightContentSize], [topContentSize], and [bottomContentSize]
     */
    val foldOrientation: FoldOrientation?,
    /**
     * The actual width of the fold
     */
    val foldWidth: Dp,
    /**
     * The actual height of the fold
     */
    val foldHeight: Dp,
    /**
     * The size of the left content (if the fold is vertical)
     */
    val leftContentSize: DpSize,
    /**
     * The size of the right content (if the fold is vertical)
     */
    val rightContentSize: DpSize,
    /**
     * The size of the top content (if fold is horizontal)
     */
    val topContentSize: DpSize,
    /**
     * The size of the bottom content (if the fold is horizontal)
     */
    val bottomContentSize: DpSize,
    /**
     * Whether the device orientation is currently portrait
     */
    val isPortrait: Boolean,
    /**
     * The computed window size class
     */
    val windowSizeClass: WindowSizeClass,
    /**
     * The computed window size
     */
    val windowSize: DpSize
) {

    /**
     * Whether there is an disruptive fold present
     */
    val hasFold: Boolean = foldOrientation != null

    /**
     * Size of the actual fold, usually a hinge on the device
     */
    val foldSize: DpSize = DpSize(foldWidth, foldHeight)

    /**
     * Whether the fold is horizontal (if at all)
     */
    val isFoldedHorizontally: Boolean = foldOrientation == FoldOrientation.HORIZONTAL

    /**
     * Whether the fold is vertical (if at all)
     */
    val isFoldedVertically: Boolean = foldOrientation == FoldOrientation.VERTICAL

    /**
     * Shorthand to check if the width size class is [WindowWidthSizeClass.Expanded]
     */
    val isExpandedWidth: Boolean = windowSizeClass.widthSizeClass == WindowWidthSizeClass.Expanded

    /**
     * Shorthand to check if the width size class is [WindowWidthSizeClass.Medium]
     */
    val isMediumWidth: Boolean = windowSizeClass.widthSizeClass == WindowWidthSizeClass.Medium

    /**
     * Shorthand to check if the width size class is [WindowWidthSizeClass.Compact]
     */
    val isCompactWidth: Boolean = windowSizeClass.widthSizeClass == WindowWidthSizeClass.Compact

    /**
     * Shorthand to check if the height size class is [WindowHeightSizeClass.Expanded]
     */
    val isExpandedHeight: Boolean = windowSizeClass.heightSizeClass == WindowHeightSizeClass.Expanded

    /**
     * Shorthand to check if the height size class is [WindowHeightSizeClass.Medium]
     */
    val isMediumHeight: Boolean = windowSizeClass.heightSizeClass == WindowHeightSizeClass.Medium

    /**
     * Shorthand to check if the height size class is [WindowHeightSizeClass.Compact]
     */
    val isCompactHeight: Boolean = windowSizeClass.heightSizeClass == WindowHeightSizeClass.Compact

    /**
     * Shorthand to get the window width
     */
    val width: WindowWidthSizeClass = windowSizeClass.widthSizeClass

    /**
     * Shorthand to get the window height
     */
    val height: WindowHeightSizeClass = windowSizeClass.heightSizeClass

    /**
     * Whether this screen definition is likely to represent a tablet in portrait mode
     */
    val isTabletPortrait: Boolean = isPortrait && width >= WindowWidthSizeClass.Medium

    /**
     * Whether this screen definition is likely to represent a tablet in landscape mode
     */
    val isTabletLandscape: Boolean = !isPortrait && height >= WindowHeightSizeClass.Medium

    /**
     * Whether this screen definition is likely to represent a tablet in either landscape or portrait mode
     */
    val isTablet: Boolean = isTabletPortrait || isTabletLandscape

    /**
     * Whether this screen definition represents a foldable device, that is [hasFold] is true
     */
    val isFoldable: Boolean = hasFold

    @OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
    public companion object {
        /**
         * [ScreenDefinition] for a generic phone in portrait mode
         */
        public val PhonePortrait: ScreenDefinition = ScreenDefinition(
            foldOrientation = null,
            foldWidth = 0.dp,
            foldHeight = 0.dp,
            leftContentSize = DpSize.Zero,
            rightContentSize = DpSize.Zero,
            topContentSize = DpSize.Zero,
            bottomContentSize = DpSize.Zero,
            isPortrait = true,
            windowSizeClass = WindowSizeClass.calculateFromSize(
                size = Size(
                    Previews.PHONE_PREVIEW_MINOR.toFloat(),
                    Previews.PHONE_PREVIEW_MAJOR.toFloat()
                ),
                density = Density(1f)
            ),
            windowSize = DpSize(Previews.PHONE_PREVIEW_MINOR.dp, Previews.PHONE_PREVIEW_MAJOR.dp)
        )

        /**
         * [ScreenDefinition] for a generic phone in landscape mode
         */
        public val PhoneLandscape: ScreenDefinition = ScreenDefinition(
            foldOrientation = null,
            foldWidth = 0.dp,
            foldHeight = 0.dp,
            leftContentSize = DpSize.Zero,
            rightContentSize = DpSize.Zero,
            topContentSize = DpSize.Zero,
            bottomContentSize = DpSize.Zero,
            isPortrait = false,
            windowSizeClass = WindowSizeClass.calculateFromSize(
                size = Size(
                    Previews.PHONE_PREVIEW_MAJOR.toFloat(),
                    Previews.PHONE_PREVIEW_MINOR.toFloat()
                ),
                density = Density(1f)
            ),
            windowSize = DpSize(Previews.PHONE_PREVIEW_MAJOR.dp, Previews.PHONE_PREVIEW_MINOR.dp)
        )

        /**
         * [ScreenDefinition] for a generic tablet in portrait mode
         */
        public val TabletPortrait: ScreenDefinition = ScreenDefinition(
            foldOrientation = null,
            foldWidth = 0.dp,
            foldHeight = 0.dp,
            leftContentSize = DpSize.Zero,
            rightContentSize = DpSize.Zero,
            topContentSize = DpSize.Zero,
            bottomContentSize = DpSize.Zero,
            isPortrait = true,
            windowSizeClass = WindowSizeClass.calculateFromSize(
                size = Size(
                    Previews.TABLET_PREVIEW_MINOR.toFloat(),
                    Previews.TABLET_PREVIEW_MAJOR.toFloat()
                ),
                density = Density(1f)
            ),
            windowSize = DpSize(Previews.TABLET_PREVIEW_MINOR.dp, Previews.TABLET_PREVIEW_MAJOR.dp)
        )

        /**
         * [ScreenDefinition] for a generic tablet in landscape mode
         */
        public val TabletLandscape: ScreenDefinition = ScreenDefinition(
            foldOrientation = null,
            foldWidth = 0.dp,
            foldHeight = 0.dp,
            leftContentSize = DpSize.Zero,
            rightContentSize = DpSize.Zero,
            topContentSize = DpSize.Zero,
            bottomContentSize = DpSize.Zero,
            isPortrait = false,
            windowSizeClass = WindowSizeClass.calculateFromSize(
                size = Size(
                    Previews.TABLET_PREVIEW_MAJOR.toFloat(),
                    Previews.TABLET_PREVIEW_MINOR.toFloat()
                ),
                density = Density(1f)
            ),
            windowSize = DpSize(Previews.TABLET_PREVIEW_MAJOR.dp, Previews.TABLET_PREVIEW_MINOR.dp)
        )
    }
}

/**
 * CompositionLocal containing the current screen definition
 *
 * The [ScreenDefinition] contains values relating to the current app window,
 * including whether there is an disruptive fold and the different portions created by said fold.
 * For example, a vertical fold will split the screen to have a left and a right portion - not necessarily of equal sizes
 *
 * Defaults to a sensible portrait phone without a fold, so that it can be used in previews or anywhere else
 * that doesn't update the definition
 */
public val LocalScreenDefinition: ProvidableCompositionLocal =
    compositionLocalOf { ScreenDefinition.PhonePortrait }

/**
 * Provides the given [definition][ScreenDefinition] that can be referenced using [LocalScreenDefinition] within [content]
 */
@Composable
public fun ProvideLocalScreenDefinition(definition: ScreenDefinition, content: @Composable () -> Unit) {
    CompositionLocalProvider(LocalScreenDefinition provides definition) {
        content()
    }
}

/**
 * Creates a screen definition
 */
@Composable
public expect fun calculateScreenDefinition(): ScreenDefinition




© 2015 - 2024 Weber Informatics LLC | Privacy Policy