refactor(#619): kill DRAWER_GESTURE_SCREENS — screen-owned drawer gestures via HermesScaffold (#624)

Replaces the fragile DRAWER_GESTURE_SCREENS global set + defensive
LaunchedEffect(snapTo(Closed)) + closeDrawer callback workaround with a
single source of truth: each screen declares its drawer-gesture preference
via HermesScaffold's new drawerGesturesEnabled parameter (default true).

Architecture:
- DrawerGestureController holds the active gesture state. HermesScaffold
  reconciles each screen's preference into it via SideEffect; when a screen
  opts out while the drawer is open, the controller closes the drawer itself.
- LocalDrawerGestureController CompositionLocal flows the controller from
  MainNavigation down to ModalNavigationDrawer, which reads .enabled and
  passes it to Material's gesturesEnabled parameter.
- DisableDrawerGestures() helper for entry screens that don't use
  HermesScaffold (Landing, AuthLogin, PairingCodeEntry).
- All 6 Settings drill-down sub-pages pass drawerGesturesEnabled = false.
- NavigationController.closeDrawer removed; navigateTo no longer needs a
  synchronous drawer-dismiss hook.

Removes: DRAWER_GESTURE_SCREENS set, both LaunchedEffect(gesturesEnabled)
and LaunchedEffect(drawerGesturesEnabled) blocks, drawerGesturesEnabled
local state, closeDrawer callback on NavigationController.

Fixes the recurring drawer-scrim stuck-open bug class (PRs #445, #454, #455).

Verified: ktlintCheck, assembleDebug, testDebugUnitTest all green locally.

Co-authored-by: Hy4ri <hy4ri@users.noreply.github.com>
This commit is contained in:
M57andHy4ri authored and GitHub committed 2026-07-18 14:25:23 +03:00
1 parent 0c2b9679b7
commit 0a0b612d3a
5 files changed
+297 -204

No files matched your search

@@ -30,13 +30,12 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.rememberDrawerState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -54,6 +53,9 @@ import com.m57.hermescontrol.data.ws.ConnectionStatus
import com.m57.hermescontrol.data.ws.HermesWsClient
import com.m57.hermescontrol.theme.BottomNavDisplayMode
import com.m57.hermescontrol.theme.LocalHermesStatusColors
import com.m57.hermescontrol.ui.common.DisableDrawerGestures
import com.m57.hermescontrol.ui.common.DrawerGestureController
import com.m57.hermescontrol.ui.common.LocalDrawerGestureController
import com.m57.hermescontrol.ui.settings.SettingsAboutPage
import com.m57.hermescontrol.ui.settings.SettingsAppearancePage
import com.m57.hermescontrol.ui.settings.SettingsBehaviorPage
@@ -69,8 +71,6 @@ import com.m57.hermescontrol.ui.pairing.PairingCodeEntryScreen as PairingCodeEnt
private fun resolveBottomNavItems(names: List<String>): List<ScreenDefinition> =
names.mapNotNull { name -> ScreenRegistry.ALL_SCREENS.firstOrNull { it.key::class.simpleName == name } }
private val DRAWER_GESTURE_SCREENS: Set<NavKey> = ScreenRegistry.ALL_SCREENS.mapTo(mutableSetOf()) { it.key }
private fun appEntryProvider(
sessionId: String?,
openDrawer: () -> Unit,
@@ -85,6 +85,8 @@ private fun appEntryProvider(
NavigationController.navigateTo(PairingCodeEntryScreen)
},
)
// Landing doesn't use HermesScaffold — opt out of drawer gestures explicitly (issue #619).
DisableDrawerGestures()
}
entry<AuthLoginScreen> {
@@ -96,6 +98,7 @@ private fun appEntryProvider(
NavigationController.goBack()
},
)
DisableDrawerGestures()
}
entry<PairingCodeEntryScreen> {
@@ -107,6 +110,7 @@ private fun appEntryProvider(
NavigationController.goBack()
},
)
DisableDrawerGestures()
}
ScreenRegistry.ALL_SCREENS.forEach { screen ->
@@ -116,6 +120,10 @@ private fun appEntryProvider(
}
// ── Settings drill-down sub-pages ───────────────────────────────────
// Each passes drawerGesturesEnabled = false to HermesScaffold — this is the
// single source of truth that prevents the drawer-scrim stuck-open bug
// (issue #619). No global gesture set, no closeDrawer callback, no
// LaunchedEffect(snapTo(Closed)) — the scaffold reconciles automatically.
entry<SettingsConnection> {
SettingsConnectionPage(
onBack = { NavigationController.goBack() },
@@ -169,9 +177,16 @@ fun MainNavigation(sessionId: String? = null) {
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
val scope = rememberCoroutineScope()
// Synchronous drawer dismiss hook used by NavigationController.navigateTo
// when drilling into a non-gesture sub-page (see NavigationController.closeDrawer).
NavigationController.closeDrawer = { scope.launch { drawerState.close() } }
// Single source of truth for drawer gestures (issue #619).
// HermesScaffold / DisableDrawerGestures reconcile each screen's preference
// into this controller via SideEffect; ModalNavigationDrawer reads .enabled
// below. No DRAWER_GESTURE_SCREENS set, no LaunchedEffect(snapTo(Closed)),
// no closeDrawer callback — the controller closes the drawer itself when a
// screen opts out.
val gestureController =
remember(drawerState, scope) {
DrawerGestureController(drawerState, scope)
}
val bottomNavItemsState by AuthManager.bottomNavItemsFlow.collectAsState()
val bottomNavDisplayMode by AuthManager.bottomNavDisplayModeFlow.collectAsState()
@@ -186,194 +201,175 @@ fun MainNavigation(sessionId: String? = null) {
currentScreen != LandingScreen &&
currentScreen != AuthLoginScreen &&
currentScreen != PairingCodeEntryScreen
val gesturesEnabled = currentScreen in DRAWER_GESTURE_SCREENS
var drawerGesturesEnabled by remember { mutableStateOf(false) }
LaunchedEffect(gesturesEnabled) {
if (gesturesEnabled) {
// Delay enabling gestures slightly to prevent residual swipe-to-open from back button taps
kotlinx.coroutines.delay(200)
drawerGesturesEnabled = true
} else {
drawerGesturesEnabled = false
}
}
val openDrawer: () -> Unit = { scope.launch { drawerState.open() } }
// B7 (Jun 30 2026, kanban t_424): close drawer if gestures are disabled to dismiss scrim.
// Also force-close on transition into a non-gesture screen so the drawer's scrim
// surface cannot intercept the first back-button tap (e.g. settings sub-pages drilled
// down from the gesture-enabled Settings root). snapTo(Closed) is a safe no-op when
// already closed.
LaunchedEffect(drawerGesturesEnabled) {
if (!drawerGesturesEnabled) {
drawerState.snapTo(DrawerValue.Closed)
}
}
ModalNavigationDrawer(
drawerState = drawerState,
gesturesEnabled = drawerGesturesEnabled,
drawerContent = {
ModalDrawerSheet(
modifier = Modifier.verticalScroll(rememberScrollState()),
) {
val connectionStatus by HermesWsClient.connectionStatus.collectAsState()
val statusColor =
when (connectionStatus) {
ConnectionStatus.CONNECTED -> LocalHermesStatusColors.current.success
ConnectionStatus.CONNECTING,
ConnectionStatus.RECONNECTING,
-> LocalHermesStatusColors.current.warning
ConnectionStatus.DISCONNECTED,
ConnectionStatus.NO_NETWORK,
ConnectionStatus.AUTH_EXPIRED,
-> LocalHermesStatusColors.current.error
}
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(start = 16.dp, top = 12.dp, end = 16.dp, bottom = 2.dp),
CompositionLocalProvider(LocalDrawerGestureController provides gestureController) {
ModalNavigationDrawer(
drawerState = drawerState,
gesturesEnabled = gestureController.enabled,
drawerContent = {
ModalDrawerSheet(
modifier = Modifier.verticalScroll(rememberScrollState()),
) {
Text(
text = stringResource(R.string.nav_drawer_title),
style =
MaterialTheme.typography.headlineSmall.copy(
fontWeight = FontWeight.Bold,
),
color = MaterialTheme.colorScheme.primary,
)
Spacer(modifier = Modifier.width(8.dp))
Box(
modifier =
Modifier
.size(10.dp)
.background(color = statusColor, shape = CircleShape),
)
}
Text(
text = stringResource(R.string.nav_drawer_subtitle),
modifier = Modifier.padding(start = 16.dp, bottom = 8.dp, end = 16.dp),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant)
val connectionStatus by HermesWsClient.connectionStatus.collectAsState()
val statusColor =
when (connectionStatus) {
ConnectionStatus.CONNECTED -> LocalHermesStatusColors.current.success
for (section in DrawerSection.entries) {
Text(
text = stringResource(section.titleRes).uppercase(),
modifier =
Modifier.padding(
start = 16.dp,
top = 8.dp,
bottom = 4.dp,
end = 16.dp,
),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
fontWeight = FontWeight.SemiBold,
)
ScreenRegistry.ALL_SCREENS
.filter { it.drawerSection == section }
.forEach { entry ->
NavigationDrawerItem(
icon = { Icon(entry.icon, contentDescription = null) },
label = { Text(stringResource(entry.labelRes)) },
selected = currentScreen == entry.key,
onClick = {
scope.launch { drawerState.close() }
NavigationController.navigateTo(entry.key)
},
colors =
NavigationDrawerItemDefaults.colors(
selectedContainerColor = MaterialTheme.colorScheme.primaryContainer,
selectedTextColor = MaterialTheme.colorScheme.onPrimaryContainer,
selectedIconColor = MaterialTheme.colorScheme.onPrimaryContainer,
),
)
}
}
ConnectionStatus.CONNECTING,
ConnectionStatus.RECONNECTING,
-> LocalHermesStatusColors.current.warning
Spacer(modifier = Modifier.height(8.dp))
HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant)
Spacer(modifier = Modifier.height(12.dp))
}
},
) {
Scaffold(
contentWindowInsets = WindowInsets.navigationBars,
bottomBar = {
if (showBottomBar) {
val barHeight =
when (bottomNavDisplayMode) {
BottomNavDisplayMode.ICON_ONLY -> 56.dp
BottomNavDisplayMode.TEXT_ONLY -> 44.dp
BottomNavDisplayMode.ICON_AND_TEXT -> 80.dp
ConnectionStatus.DISCONNECTED,
ConnectionStatus.NO_NETWORK,
ConnectionStatus.AUTH_EXPIRED,
-> LocalHermesStatusColors.current.error
}
NavigationBar(
modifier = Modifier.height(barHeight),
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(start = 16.dp, top = 12.dp, end = 16.dp, bottom = 2.dp),
) {
bottomNavItems.forEach { item ->
val showIcon =
bottomNavDisplayMode == BottomNavDisplayMode.ICON_AND_TEXT ||
bottomNavDisplayMode == BottomNavDisplayMode.ICON_ONLY
val showLabel =
bottomNavDisplayMode == BottomNavDisplayMode.ICON_AND_TEXT ||
bottomNavDisplayMode == BottomNavDisplayMode.TEXT_ONLY
val isSelected = currentScreen == item.key
NavigationBarItem(
selected = isSelected,
onClick = { NavigationController.navigateTo(item.key) },
colors =
if (bottomNavDisplayMode == BottomNavDisplayMode.TEXT_ONLY) {
NavigationBarItemDefaults.colors(
indicatorColor = Color.Transparent,
)
} else {
NavigationBarItemDefaults.colors()
},
icon = {
if (showIcon) {
Icon(item.icon, contentDescription = stringResource(item.labelRes))
} else {
Text(
text = stringResource(item.labelRes),
style = MaterialTheme.typography.labelMedium,
fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal,
)
}
},
label =
if (showLabel && showIcon) {
{ Text(stringResource(item.labelRes)) }
} else {
null
},
modifier =
Modifier.testTag(
"nav_${item.key::class.simpleName?.lowercase()?.removeSuffix("screen") ?: ""}",
),
)
}
Text(
text = stringResource(R.string.nav_drawer_title),
style =
MaterialTheme.typography.headlineSmall.copy(
fontWeight = FontWeight.Bold,
),
color = MaterialTheme.colorScheme.primary,
)
Spacer(modifier = Modifier.width(8.dp))
Box(
modifier =
Modifier
.size(10.dp)
.background(color = statusColor, shape = CircleShape),
)
}
Text(
text = stringResource(R.string.nav_drawer_subtitle),
modifier = Modifier.padding(start = 16.dp, bottom = 8.dp, end = 16.dp),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant)
for (section in DrawerSection.entries) {
Text(
text = stringResource(section.titleRes).uppercase(),
modifier =
Modifier.padding(
start = 16.dp,
top = 8.dp,
bottom = 4.dp,
end = 16.dp,
),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
fontWeight = FontWeight.SemiBold,
)
ScreenRegistry.ALL_SCREENS
.filter { it.drawerSection == section }
.forEach { entry ->
NavigationDrawerItem(
icon = { Icon(entry.icon, contentDescription = null) },
label = { Text(stringResource(entry.labelRes)) },
selected = currentScreen == entry.key,
onClick = {
scope.launch { drawerState.close() }
NavigationController.navigateTo(entry.key)
},
colors =
NavigationDrawerItemDefaults.colors(
selectedContainerColor = MaterialTheme.colorScheme.primaryContainer,
selectedTextColor = MaterialTheme.colorScheme.onPrimaryContainer,
selectedIconColor = MaterialTheme.colorScheme.onPrimaryContainer,
),
)
}
}
Spacer(modifier = Modifier.height(8.dp))
HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant)
Spacer(modifier = Modifier.height(12.dp))
}
},
) { paddingValues ->
NavDisplay(
backStack = backStack,
onBack = { NavigationController.goBack() },
entryProvider =
appEntryProvider(sessionId, openDrawer),
modifier =
Modifier
.padding(paddingValues)
.consumeWindowInsets(paddingValues)
.fillMaxSize(),
)
) {
Scaffold(
contentWindowInsets = WindowInsets.navigationBars,
bottomBar = {
if (showBottomBar) {
val barHeight =
when (bottomNavDisplayMode) {
BottomNavDisplayMode.ICON_ONLY -> 56.dp
BottomNavDisplayMode.TEXT_ONLY -> 44.dp
BottomNavDisplayMode.ICON_AND_TEXT -> 80.dp
}
NavigationBar(
modifier = Modifier.height(barHeight),
) {
bottomNavItems.forEach { item ->
val showIcon =
bottomNavDisplayMode == BottomNavDisplayMode.ICON_AND_TEXT ||
bottomNavDisplayMode == BottomNavDisplayMode.ICON_ONLY
val showLabel =
bottomNavDisplayMode == BottomNavDisplayMode.ICON_AND_TEXT ||
bottomNavDisplayMode == BottomNavDisplayMode.TEXT_ONLY
val isSelected = currentScreen == item.key
NavigationBarItem(
selected = isSelected,
onClick = { NavigationController.navigateTo(item.key) },
colors =
if (bottomNavDisplayMode == BottomNavDisplayMode.TEXT_ONLY) {
NavigationBarItemDefaults.colors(
indicatorColor = Color.Transparent,
)
} else {
NavigationBarItemDefaults.colors()
},
icon = {
if (showIcon) {
Icon(item.icon, contentDescription = stringResource(item.labelRes))
} else {
Text(
text = stringResource(item.labelRes),
style = MaterialTheme.typography.labelMedium,
fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal,
)
}
},
label =
if (showLabel && showIcon) {
{ Text(stringResource(item.labelRes)) }
} else {
null
},
modifier =
Modifier.testTag(
"nav_${item.key::class.simpleName?.lowercase()?.removeSuffix(
"screen",
) ?: ""}",
),
)
}
}
}
},
) { paddingValues ->
NavDisplay(
backStack = backStack,
onBack = { NavigationController.goBack() },
entryProvider =
appEntryProvider(sessionId, openDrawer),
modifier =
Modifier
.padding(paddingValues)
.consumeWindowInsets(paddingValues)
.fillMaxSize(),
)
}
}
}
}
@@ -18,11 +18,6 @@ object NavigationController {
var backStack: NavBackStack<NavKey>? = null
var pendingSessionId: String? = null
// Set by MainNavigation so navigation can synchronously dismiss the drawer
// (e.g. when drilling into a non-gesture sub-page) before a recomposition,
// preventing the drawer's gesture surface from intercepting the first tap.
var closeDrawer: (() -> Unit)? = null
// Bottom-nav primary screens — dynamic, updated by Navigation.kt via
// updatePrimaryScreens() when the user customises the bottom nav bar.
// Default matches the default 5 bottom-nav items.
@@ -51,11 +46,11 @@ object NavigationController {
if (isPrimaryScreen(key)) {
stack.clear()
} else {
// Drilling into a sub-page (non-primary): dismiss the drawer
// synchronously so its gesture surface can't eat the next tap.
closeDrawer?.invoke()
}
// Drawer dismissal is handled by DrawerGestureController (issue #619):
// when a non-gesture sub-page composes, its HermesScaffold reconciles
// drawerGesturesEnabled=false and the controller closes the drawer
// itself via SideEffect. No synchronous closeDrawer callback here.
stack.add(key)
}
@@ -11,6 +11,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material3.DrawerState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
@@ -21,6 +22,11 @@ import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.layout.layout
@@ -29,6 +35,8 @@ import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Dp
import com.m57.hermescontrol.R
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlin.math.roundToInt
/** Sealed interface for navigation icon types — eliminates boolean-flag anti-pattern. */
@@ -44,6 +52,66 @@ sealed interface NavIcon {
) : NavIcon
}
/**
* Single source of truth for whether the modal navigation drawer's swipe gestures
* should be enabled for the currently-composed screen.
*
* Each [HermesScaffold] (or [DisableDrawerGestures]) reconciles its preference into
* the singleton instance via a [SideEffect]; `ModalNavigationDrawer` in `Navigation.kt`
* reads [enabled] and passes it straight to Material's `gesturesEnabled` parameter.
*
* This replaces the old `DRAWER_GESTURE_SCREENS` set + `LaunchedEffect(snapTo(Closed))`
* workaround documented in issue #619: gesture state now flows from the rendered
* screen automatically, and the drawer controller closes the drawer itself when a
* screen opts out — no global allow-list, no defensive close callbacks.
*/
class DrawerGestureController(
private val drawerState: DrawerState,
private val scope: CoroutineScope,
) {
var enabled by mutableStateOf(false)
private set
/**
* Reconcile a screen's desired gesture state.
*
* Called from a [SideEffect] on every screen composition. If a screen opts out
* of gestures while the drawer is open, we close it synchronously so the scrim
* cannot intercept the next tap (the bug class from PRs #445 / #454 / #455).
*/
fun reconcile(desired: Boolean) {
if (desired == enabled) return
enabled = desired
if (!desired && drawerState.isOpen) {
scope.launch { drawerState.close() }
}
}
}
/**
* CompositionLocal that exposes the active [DrawerGestureController].
*
* Provided by `MainNavigation` and read both by [HermesScaffold] (to reconcile a
* screen's preference) and by `ModalNavigationDrawer` (to gate gestures).
*/
val LocalDrawerGestureController =
compositionLocalOf<DrawerGestureController?> {
null
}
/**
* Opt out of drawer gestures for a screen that does NOT use [HermesScaffold].
*
* Used by full-bleed entry screens (Landing, AuthLogin, PairingCodeEntry) that
* render their own layout. Scaffold-based screens should instead pass
* `drawerGesturesEnabled = false` to [HermesScaffold].
*/
@Composable
fun DisableDrawerGestures() {
val controller = LocalDrawerGestureController.current ?: return
SideEffect { controller.reconcile(false) }
}
/**
* Shared Scaffold wrapper that standardizes the TopAppBar.
*
@@ -52,6 +120,7 @@ sealed interface NavIcon {
* - Uses Spacing tokens internally
* - Simplified API: [navigationIcon] replaces the old showBack/onBack/onOpenDrawer trio
* - Safe-area aware via Scaffold insets
* - Drawer-gesture aware via [drawerGesturesEnabled] (issue #619)
*
* Screens that still need a Back arrow instead of a Menu icon can set
* `navigationIcon = NavIcon.Back { … }` instead.
@@ -62,6 +131,16 @@ fun HermesScaffold(
modifier: Modifier = Modifier,
title: @Composable () -> Unit,
navigationIcon: NavIcon? = null,
/**
* Whether the modal navigation drawer's swipe-open/swipe-close gestures should
* be active while this screen is visible. Defaults to `true` (primary screens).
*
* Pass `false` for drill-down sub-pages (e.g. Settings sub-pages) where the
* drawer should be closed and locked. The [DrawerGestureController] reconciles
* this preference and closes the drawer automatically if a screen opts out
* while the drawer is open — see issue #619.
*/
drawerGesturesEnabled: Boolean = true,
onRefresh: (() -> Unit)? = null,
isRefreshing: Boolean = false,
pinTopBar: Boolean = false,
@@ -69,6 +148,9 @@ fun HermesScaffold(
actions: @Composable () -> Unit = {},
content: @Composable (PaddingValues) -> Unit,
) {
val gestureController = LocalDrawerGestureController.current
SideEffect { gestureController?.reconcile(drawerGesturesEnabled) }
val scrollBehavior =
if (pinTopBar) {
TopAppBarDefaults.pinnedScrollBehavior()
@@ -39,6 +39,10 @@ import com.m57.hermescontrol.ui.settings.components.TestResultCard
* Drill-down sub-pages for Settings. Each is its own NavKey destination
* (see Navigation.kt) so the native back stack handles navigation — no
* manual routing. The SettingsViewModel stays the single source of truth.
*
* All sub-pages pass `drawerGesturesEnabled = false` to [HermesScaffold] so the
* modal drawer's swipe gestures are disabled and the drawer auto-closes when a
* sub-page is entered — the single source of truth introduced in issue #619.
*/
@Composable
@@ -53,6 +57,9 @@ internal fun SettingsConnectionPage(
HermesScaffold(
title = { Text(stringResource(R.string.settings_sec_connection)) },
navigationIcon = NavIcon.Back(onBack),
// Non-primary drill-down: opt out of drawer gestures so the scrim can't
// get stuck open (issue #619). DrawerGestureController handles the close.
drawerGesturesEnabled = false,
) {
Column(
modifier =
@@ -104,6 +111,7 @@ internal fun SettingsAppearancePage(
HermesScaffold(
title = { Text(stringResource(R.string.settings_sec_appearance)) },
navigationIcon = NavIcon.Back(onBack),
drawerGesturesEnabled = false,
) {
Column(
modifier =
@@ -137,6 +145,7 @@ internal fun SettingsChatPage(
HermesScaffold(
title = { Text(stringResource(R.string.settings_sec_chat)) },
navigationIcon = NavIcon.Back(onBack),
drawerGesturesEnabled = false,
) {
Column(
modifier =
@@ -166,6 +175,7 @@ internal fun SettingsNavBarPage(
HermesScaffold(
title = { Text(stringResource(R.string.settings_sec_nav_bar)) },
navigationIcon = NavIcon.Back(onBack),
drawerGesturesEnabled = false,
) {
Column(
modifier =
@@ -198,6 +208,7 @@ internal fun SettingsBehaviorPage(
HermesScaffold(
title = { Text(stringResource(R.string.settings_sec_behavior)) },
navigationIcon = NavIcon.Back(onBack),
drawerGesturesEnabled = false,
) {
Column(
modifier =
@@ -224,6 +235,7 @@ internal fun SettingsAboutPage(
HermesScaffold(
title = { Text(stringResource(R.string.settings_sec_about)) },
navigationIcon = NavIcon.Back(onBack),
drawerGesturesEnabled = false,
) {
Column(
modifier =