diff --git a/sheaf/app/build.gradle.kts b/sheaf/app/build.gradle.kts index 3e23b4b..d0df11a 100644 --- a/sheaf/app/build.gradle.kts +++ b/sheaf/app/build.gradle.kts @@ -141,6 +141,8 @@ dependencies { implementation(libs.androidx.ui.graphics) implementation(libs.androidx.ui.tooling.preview) implementation(libs.androidx.material3) + // Adaptive top-level navigation: bottom bar on phones, rail on wide screens. + implementation(libs.androidx.material3.adaptive.navigation.suite) implementation(libs.androidx.material.icons.extended) implementation(libs.androidx.navigation.compose) diff --git a/sheaf/app/src/main/AndroidManifest.xml b/sheaf/app/src/main/AndroidManifest.xml index 75ed739..3ecd471 100644 --- a/sheaf/app/src/main/AndroidManifest.xml +++ b/sheaf/app/src/main/AndroidManifest.xml @@ -112,8 +112,7 @@ android:name=".MainActivity" android:exported="true" android:launchMode="singleTop" - android:windowSoftInputMode="adjustResize" - android:screenOrientation="portrait"> + android:windowSoftInputMode="adjustResize"> diff --git a/sheaf/app/src/main/java/systems/lupine/sheaf/ui/SheafApp.kt b/sheaf/app/src/main/java/systems/lupine/sheaf/ui/SheafApp.kt index 2396216..2dc0ae2 100644 --- a/sheaf/app/src/main/java/systems/lupine/sheaf/ui/SheafApp.kt +++ b/sheaf/app/src/main/java/systems/lupine/sheaf/ui/SheafApp.kt @@ -2,14 +2,23 @@ package systems.lupine.sheaf.ui import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.widthIn +import androidx.compose.ui.Alignment +import androidx.compose.ui.unit.dp import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.MenuBook import androidx.compose.material.icons.automirrored.outlined.MenuBook import androidx.compose.material.icons.filled.* import androidx.compose.material.icons.outlined.* import androidx.compose.material3.* +import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffold +import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteType import androidx.compose.runtime.* +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import android.net.Uri @@ -139,6 +148,15 @@ data class TopLevelDest( val unselectedIcon: ImageVector, ) +// Widest a single content pane grows to; beyond this, centre it with gutters +// rather than stretching forms and lists across a whole tablet. A no-op on +// anything narrower (phones, split-screen). +private val MAX_CONTENT_WIDTH = 840.dp + +// Destinations that should use the full window width rather than the capped +// content pane (a pan/zoom canvas wants all the room it can get). +private val FULL_BLEED_ROUTES = setOf(Routes.RELATIONSHIP_GRAPH) + val topLevelDestinations = listOf( TopLevelDest(Routes.HOME, "Home", Icons.Filled.Home, Icons.Outlined.Home), TopLevelDest(Routes.PEOPLE, "Members", Icons.Filled.People, Icons.Outlined.People), @@ -204,41 +222,56 @@ fun SheafApp( LocalFileCdnBase provides fileCdnBase, LocalDisplayTimeZone provides displayZone, ) { - Scaffold( - bottomBar = { - if (showBottomBar) { - NavigationBar { - val currentDest = navBackStack?.destination - topLevelDestinations.forEach { dest -> - val selected = currentDest?.hierarchy?.any { it.route == dest.route } == true - NavigationBarItem( - selected = selected, - onClick = { - navController.navigate(dest.route) { - popUpTo(navController.graph.findStartDestination().id) { - saveState = true - } - launchSingleTop = true - restoreState = true - } - }, - icon = { - Icon( - if (selected) dest.selectedIcon else dest.unselectedIcon, - contentDescription = dest.label, - ) - }, - label = { Text(dest.label) }, + // Adaptive top-level chrome: a bottom bar on compact widths (phones), a + // navigation rail on wider ones (tablets, landscape, foldables, large + // windows), and nothing at all on non-top-level (full-screen) destinations. + // 600dp is the standard compact/medium width breakpoint. + val currentDest = navBackStack?.destination + val wide = LocalConfiguration.current.screenWidthDp >= 600 + val navSuiteType = when { + !showBottomBar -> NavigationSuiteType.None + wide -> NavigationSuiteType.NavigationRail + else -> NavigationSuiteType.NavigationBar + } + NavigationSuiteScaffold( + layoutType = navSuiteType, + navigationSuiteItems = { + topLevelDestinations.forEach { dest -> + val selected = currentDest?.hierarchy?.any { it.route == dest.route } == true + item( + selected = selected, + onClick = { + navController.navigate(dest.route) { + popUpTo(navController.graph.findStartDestination().id) { + saveState = true + } + launchSingleTop = true + restoreState = true + } + }, + icon = { + Icon( + if (selected) dest.selectedIcon else dest.unselectedIcon, + contentDescription = dest.label, ) - } - } + }, + label = { Text(dest.label) }, + ) } }, - ) { innerPadding -> + ) { + // Cap content width on wide windows so forms and lists don't stretch + // across a whole tablet; centre what remains. widthIn is a no-op below + // the cap, so phones are unaffected. Full-bleed screens (the pan/zoom + // relationship graph) opt out and use the whole area. + val fullBleed = currentRoute in FULL_BLEED_ROUTES + val contentModifier = if (fullBleed) Modifier.fillMaxSize() + else Modifier.fillMaxHeight().widthIn(max = MAX_CONTENT_WIDTH) + Box(Modifier.fillMaxSize(), contentAlignment = Alignment.TopCenter) { NavHost( navController = navController, startDestination = Routes.LOGIN, - modifier = Modifier.padding(innerPadding), + modifier = contentModifier, enterTransition = { fadeIn() }, exitTransition = { fadeOut() }, popEnterTransition = { fadeIn() }, @@ -675,6 +708,7 @@ fun SheafApp( ) } } + } } } } diff --git a/sheaf/gradle/libs.versions.toml b/sheaf/gradle/libs.versions.toml index 2a1e711..402b432 100644 --- a/sheaf/gradle/libs.versions.toml +++ b/sheaf/gradle/libs.versions.toml @@ -49,6 +49,7 @@ androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" } androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } androidx-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "material3" } +androidx-material3-adaptive-navigation-suite = { group = "androidx.compose.material3", name = "material3-adaptive-navigation-suite", version.ref = "material3" } androidx-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" } androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" } hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }