commonMain.io.github.lyxnx.compose.screenables.ScreenDefinition.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of screenables-desktop Show documentation
Show all versions of screenables-desktop Show documentation
Jetpack Compose screen definition utility
@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