diff --git a/sharedUI/src/commonMain/kotlin/org/shahondin1624/App.kt b/sharedUI/src/commonMain/kotlin/org/shahondin1624/App.kt index aed923b..9d5d815 100644 --- a/sharedUI/src/commonMain/kotlin/org/shahondin1624/App.kt +++ b/sharedUI/src/commonMain/kotlin/org/shahondin1624/App.kt @@ -9,6 +9,7 @@ import androidx.compose.material.icons.filled.Person import androidx.compose.material.icons.filled.Settings import androidx.compose.material3.* import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue import androidx.compose.runtime.setValue import androidx.compose.runtime.rememberCoroutineScope @@ -23,6 +24,8 @@ import org.shahondin1624.lib.components.charactermodel.attributespage.Attributes import org.shahondin1624.model.EXAMPLE_CHARACTER import org.shahondin1624.theme.AppTheme import org.shahondin1624.theme.LocalThemeIsDark +import org.shahondin1624.theme.LocalWindowSizeClass +import org.shahondin1624.theme.WindowSizeClass @OptIn(ExperimentalMaterial3Api::class) @Preview @@ -30,6 +33,20 @@ import org.shahondin1624.theme.LocalThemeIsDark fun App( onThemeChanged: @Composable (isDark: Boolean) -> Unit = {} ) = AppTheme(onThemeChanged) { + BoxWithConstraints(modifier = Modifier.fillMaxSize()) { + val windowSizeClass = WindowSizeClass.fromWidth(maxWidth) + + CompositionLocalProvider(LocalWindowSizeClass provides windowSizeClass) { + AppContent(onThemeChanged = {}) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun AppContent( + onThemeChanged: @Composable (isDark: Boolean) -> Unit = {} +) { val character = EXAMPLE_CHARACTER var isDark by LocalThemeIsDark.current val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) diff --git a/sharedUI/src/commonMain/kotlin/org/shahondin1624/theme/WindowSizeClass.kt b/sharedUI/src/commonMain/kotlin/org/shahondin1624/theme/WindowSizeClass.kt new file mode 100644 index 0000000..12fc339 --- /dev/null +++ b/sharedUI/src/commonMain/kotlin/org/shahondin1624/theme/WindowSizeClass.kt @@ -0,0 +1,36 @@ +package org.shahondin1624.theme + +import androidx.compose.runtime.compositionLocalOf +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +/** + * Classification of window widths following Material Design 3 breakpoints. + * + * - [Compact] : width < 600 dp (phones in portrait) + * - [Medium] : 600 dp <= width <= 840 dp (tablets, foldables) + * - [Expanded] : width > 840 dp (desktops, landscape tablets) + */ +enum class WindowSizeClass { + Compact, + Medium, + Expanded; + + companion object { + /** + * Returns the [WindowSizeClass] for the given [width]. + */ + fun fromWidth(width: Dp): WindowSizeClass = when { + width < 600.dp -> Compact + width <= 840.dp -> Medium + else -> Expanded + } + } +} + +/** + * CompositionLocal providing the current [WindowSizeClass]. + * Set at the app root via [BoxWithConstraints] so every composable + * in the tree can read it without parameter drilling. + */ +val LocalWindowSizeClass = compositionLocalOf { WindowSizeClass.Expanded } diff --git a/sharedUI/src/commonTest/kotlin/org/shahondin1624/WindowSizeClassTest.kt b/sharedUI/src/commonTest/kotlin/org/shahondin1624/WindowSizeClassTest.kt new file mode 100644 index 0000000..1b9d656 --- /dev/null +++ b/sharedUI/src/commonTest/kotlin/org/shahondin1624/WindowSizeClassTest.kt @@ -0,0 +1,42 @@ +package org.shahondin1624 + +import androidx.compose.ui.unit.dp +import org.shahondin1624.theme.WindowSizeClass +import kotlin.test.Test +import kotlin.test.assertEquals + +class WindowSizeClassTest { + + @Test + fun compactForWidthBelow600() { + assertEquals(WindowSizeClass.Compact, WindowSizeClass.fromWidth(0.dp)) + assertEquals(WindowSizeClass.Compact, WindowSizeClass.fromWidth(320.dp)) + assertEquals(WindowSizeClass.Compact, WindowSizeClass.fromWidth(599.dp)) + } + + @Test + fun mediumForWidth600To840() { + assertEquals(WindowSizeClass.Medium, WindowSizeClass.fromWidth(600.dp)) + assertEquals(WindowSizeClass.Medium, WindowSizeClass.fromWidth(720.dp)) + assertEquals(WindowSizeClass.Medium, WindowSizeClass.fromWidth(840.dp)) + } + + @Test + fun expandedForWidthAbove840() { + assertEquals(WindowSizeClass.Expanded, WindowSizeClass.fromWidth(841.dp)) + assertEquals(WindowSizeClass.Expanded, WindowSizeClass.fromWidth(1200.dp)) + assertEquals(WindowSizeClass.Expanded, WindowSizeClass.fromWidth(1920.dp)) + } + + @Test + fun boundaryAt600() { + assertEquals(WindowSizeClass.Compact, WindowSizeClass.fromWidth(599.9.dp)) + assertEquals(WindowSizeClass.Medium, WindowSizeClass.fromWidth(600.dp)) + } + + @Test + fun boundaryAt840() { + assertEquals(WindowSizeClass.Medium, WindowSizeClass.fromWidth(840.dp)) + assertEquals(WindowSizeClass.Expanded, WindowSizeClass.fromWidth(840.1.dp)) + } +}