Stores: the Library dropdown opens under its tab, as wide as its entries

The dropdown used the menus' right-aligned placement and their 220 dp
minimum, so under a tab near the left it was pushed to the edge over
the rail. AnchoredMenu has a compact mode now - under its anchor, left
edges aligned, as wide as its entries between the tab's width and
260 dp - which the Library dropdown uses.
This commit is contained in:
The412Banner committed 2026-10-09 09:40:01 -04:00
1 parent 6626cf99b7
commit f5c343bde0
3 files changed
+33 -11

No files matched your search

@@ -233,6 +233,19 @@ class MenuHost {
@Composable
fun rememberMenuHost(): MenuHost = remember { MenuHost() }
/** Under the anchor with left edges aligned, kept on screen: a dropdown that belongs to a tab. */
private class BelowStartProvider(private val gap: Int) : PopupPositionProvider {
override fun calculatePosition(anchorBounds: IntRect, windowSize: IntSize, layoutDirection: LayoutDirection, popupContentSize: IntSize): IntOffset {
var x = anchorBounds.left
if (x + popupContentSize.width > windowSize.width - 8) x = windowSize.width - 8 - popupContentSize.width
if (x < 8) x = 8
var y = anchorBounds.bottom + gap
if (y + popupContentSize.height > windowSize.height - 8) y = anchorBounds.top - gap - popupContentSize.height
if (y < 8) y = 8
return IntOffset(x, y)
}
}
private class BelowEndProvider(private val gap: Int) : PopupPositionProvider {
override fun calculatePosition(anchorBounds: IntRect, windowSize: IntSize, layoutDirection: LayoutDirection, popupContentSize: IntSize): IntOffset {
var x = anchorBounds.right - popupContentSize.width
@@ -245,7 +258,12 @@ private class BelowEndProvider(private val gap: Int) : PopupPositionProvider {
}
@Composable
fun AnchoredMenu(open: Boolean, onDismiss: () -> Unit, title: String? = null, note: String? = null, content: @Composable ColumnScope.(FocusRequester) -> Unit) {
fun AnchoredMenu(
open: Boolean, onDismiss: () -> Unit, title: String? = null, note: String? = null,
/** A tab's dropdown: under its anchor, left edges aligned, as wide as its entries (within [minWidth]..260 dp). */
compact: Boolean = false, minWidth: androidx.compose.ui.unit.Dp = 120.dp,
content: @Composable ColumnScope.(FocusRequester) -> Unit,
) {
val state = remember { MutableTransitionState(false) }
state.targetState = open
if (!state.currentState && !state.targetState && state.isIdle) return
@@ -259,14 +277,14 @@ fun AnchoredMenu(open: Boolean, onDismiss: () -> Unit, title: String? = null, no
}
}
val gap = with(LocalDensity.current) { 6.dp.roundToPx() }
val provider = remember(gap) { BelowEndProvider(gap) }
val provider = remember(gap, compact) { if (compact) BelowStartProvider(gap) else BelowEndProvider(gap) }
val colors = MaterialTheme.colorScheme
val pal = LocalPalette.current
Popup(popupPositionProvider = provider, onDismissRequest = onDismiss, properties = PopupProperties(focusable = true)) {
AnimatedVisibility(
visibleState = state,
enter = fadeIn(Motion.tw(180)) + scaleIn(Motion.sp(0.7f), initialScale = 0.94f, transformOrigin = TransformOrigin(1f, 0f)),
exit = fadeOut(Motion.tw(140)) + scaleOut(Motion.tw(140), targetScale = 0.96f, transformOrigin = TransformOrigin(1f, 0f)),
enter = fadeIn(Motion.tw(180)) + scaleIn(Motion.sp(0.7f), initialScale = 0.94f, transformOrigin = TransformOrigin(if (compact) 0f else 1f, 0f)),
exit = fadeOut(Motion.tw(140)) + scaleOut(Motion.tw(140), targetScale = 0.96f, transformOrigin = TransformOrigin(if (compact) 0f else 1f, 0f)),
label = "menu",
) {
// A long list (a driver menu with its downloads) must not run off the screen: the menu is
@@ -274,7 +292,7 @@ fun AnchoredMenu(open: Boolean, onDismiss: () -> Unit, title: String? = null, no
val maxHeight = (androidx.compose.ui.platform.LocalConfiguration.current.screenHeightDp * 0.86f).dp
Column(
modifier = Modifier
.widthIn(min = 220.dp, max = 340.dp)
.then(if (compact) Modifier.widthIn(min = minWidth, max = 260.dp).width(androidx.compose.foundation.layout.IntrinsicSize.Max) else Modifier.widthIn(min = 220.dp, max = 340.dp))
.heightIn(max = maxHeight)
.shadow(24.dp, RowShape, ambientColor = Color.Black, spotColor = Color.Black)
.clip(RowShape)
@@ -1,5 +1,6 @@
package com.droiddeck.launcher.ui
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.graphics.Brush
@@ -333,8 +334,8 @@ private fun Storefront(
listOf("store" to stringResource(R.string.stores_tab_store), "library" to libraryLabel).filter { it.first in tabsFor(store) },
tab,
onPick = { key -> if (key == "library") { if (tab == "library") filterOpen = !filterOpen else onTab(key) } else onTab(key) },
menuFor = { key ->
if (key == "library") AnchoredMenu(filterOpen, onDismiss = { filterOpen = false }) { first ->
menuFor = { key, tabWidth ->
if (key == "library") AnchoredMenu(filterOpen, onDismiss = { filterOpen = false }, compact = true, minWidth = tabWidth) { first ->
MenuItem(stringResource(R.string.stores_tab_count, stringResource(R.string.stores_tab_all), library.size), checked = !installedOnly, focusRequester = first) { installedOnly = false; filterOpen = false; onTab("library") }
MenuItem(stringResource(R.string.stores_tab_count, stringResource(R.string.stores_tab_installed), installedItems.size), checked = installedOnly) { installedOnly = true; filterOpen = false; onTab("library") }
}
@@ -386,7 +387,7 @@ private fun Storefront(
@Composable
private fun SubTabs(
tabs: List<Pair<String, String>>, selected: String, onPick: (String) -> Unit,
menuFor: @Composable (String) -> Unit = {}, trailing: @Composable () -> Unit,
menuFor: @Composable (String, androidx.compose.ui.unit.Dp) -> Unit = { _, _ -> }, trailing: @Composable () -> Unit,
) {
val colors = MaterialTheme.colorScheme
val pal = LocalPalette.current
@@ -398,7 +399,9 @@ private fun SubTabs(
val src = remember { MutableInteractionSource() }
val hot = rememberHot(src)
val pick = { onPick(key) }
Box {
val density = androidx.compose.ui.platform.LocalDensity.current
var width by remember { mutableStateOf(0.dp) }
Box(Modifier.onSizeChanged { width = with(density) { it.width.toDp() } }) {
Column(
modifier = Modifier.paneItem("storetab:$key").clip(RoundedCornerShape(8.dp))
.background(if (hot) pal.signal.copy(alpha = 0.12f) else Color.Transparent)
@@ -411,7 +414,7 @@ private fun SubTabs(
Box(Modifier.padding(top = 4.dp).fillMaxWidth().height(2.dp).background(if (on) pal.signal else Color.Transparent))
}
// A tab's own dropdown (Library's filter), anchored under it.
menuFor(key)
menuFor(key, width)
}
}
}
+2 -1
View File
@@ -157,7 +157,8 @@ without it and Setup says "Stores engine not built").
GOG Library 33 / All 120): Library's dropdown filters All (every owned game, the default, the
tab's count) or Installed, and the tab reads the filter ("Library (33)", "Installed (1)"); the old
All view is gone. Amazon has Library alone. Search follows the filter; the shoulders cycle Store
and Library.
and Library. The dropdown opens under the Library tab, left edges aligned, as wide as its
entries (at least the tab, at most 260 dp) - `AnchoredMenu(compact = true)`.
- **The Stores chip row paints nothing** (maintainer: a black box behind the chips): it sits over the
page's own background; the content scrolling under it is clipped at its edge and faded there by
the content's alpha once scrolled, never by a colour.