ui: keep a gap before the first library tab

Put an 8dp spacer at the start of the scrollable content and let the
reveal leave that gap when scrolling back towards the first tab, so the
selected pill no longer sits flush against the screen edge. The spacer
scrolls away, so a tab that is half off is still cut at the edge.
This commit is contained in:
austinried
2026-10-21 10:14:10 +00:00
parent b9a3ae5ed0
commit be08e50314
2 changed files with 44 additions and 5 deletions
@@ -830,6 +830,7 @@ private fun ListOptionsSheet(
private val BOTTOM_CLEARANCE = 80.dp
private val TAB_BAR_CONTENT_HEIGHT = 52.dp
private val TAB_STRIP_LEADING_GAP = 8.dp
private val TAB_ICON_SIZE = 24.dp
private val TAB_VERTICAL_PADDING = 6.dp
private val TOUCH_TARGET = 48.dp
@@ -903,14 +904,15 @@ internal fun LibraryTabs(
var viewportWidth by remember { mutableIntStateOf(0) }
var contentWidth by remember { mutableIntStateOf(0) }
var revealed by remember { mutableStateOf(false) }
val leadingGap = with(density) { TAB_STRIP_LEADING_GAP.toPx() }
LaunchedEffect(pagerState.settledPage, viewportWidth, contentWidth, layoutDirection, bounds[pagerState.settledPage]) {
if (viewportWidth == 0) return@LaunchedEffect
val settled = bounds[pagerState.settledPage] ?: return@LaunchedEffect
val start = if (layoutDirection == LayoutDirection.Rtl) contentWidth - settled.right else settled.left
val end = if (layoutDirection == LayoutDirection.Rtl) contentWidth - settled.left else settled.right
val start = if (layoutDirection == LayoutDirection.Rtl) contentWidth - settled.right else leadingGap + settled.left
val end = if (layoutDirection == LayoutDirection.Rtl) contentWidth - settled.left else leadingGap + settled.right
val target =
when {
start < scrollState.value -> start
start - leadingGap < scrollState.value -> start - leadingGap
end > scrollState.value + viewportWidth -> end - viewportWidth
else -> null
}
@@ -939,13 +941,14 @@ internal fun LibraryTabs(
Modifier
.weight(1f)
.onSizeChanged { viewportWidth = it.width }
.horizontalScroll(scrollState),
.horizontalScroll(scrollState)
.onSizeChanged { contentWidth = it.width },
verticalAlignment = Alignment.CenterVertically,
) {
Spacer(Modifier.width(TAB_STRIP_LEADING_GAP))
Box(
modifier =
Modifier
.onSizeChanged { contentWidth = it.width }
.drawBehind {
if (current != null &&
indicatorLeft != null &&
@@ -464,6 +464,41 @@ class LibraryScreenTest {
assertTabFullyVisible(LibraryTab.Playlists, labelWidth)
}
@Test
@Config(qualifiers = "de-rDE-w320dp-h640dp-xhdpi")
fun returningToTheFirstTabLeavesAGapAtTheEdge() {
val selectedTab = mutableStateOf(LibraryTab.Playlists)
composeRule.setContent {
SubtracksTheme {
LibraryScreen(
selectedTab = selectedTab.value,
onTabSelected = { selectedTab.value = it },
albums = remember { flowOf(PagingData.empty<Album>()) },
artists = remember { flowOf(PagingData.empty<Artist>()) },
playlists = remember { flowOf(PagingData.empty<Playlist>()) },
coverArt = { _, _ -> null },
onAlbumClick = {},
onArtistClick = {},
onPlaylistClick = {},
onSync = {},
onOpenSettings = {},
)
}
}
composeRule.waitForIdle()
composeRule.runOnIdle { selectedTab.value = LibraryTab.Home }
val expectedGap = with(composeRule.density) { TAB_EDGE_GAP.toPx() }
composeRule.waitUntil(5_000) { tabBounds(LibraryTab.Home).left <= expectedGap + 1f }
assertEquals(
"the first tab should keep a gap from the screen edge",
expectedGap,
tabBounds(LibraryTab.Home).left,
1f,
)
}
private fun assertTabFullyVisible(
tab: LibraryTab,
labelWidth: Float,
@@ -549,3 +584,4 @@ class LibraryScreenTest {
}
private val TAB_LABEL_CHROME = 56.dp
private val TAB_EDGE_GAP = 8.dp