Drop the glasses settings like Steam's: Play-style button, Settings lip, tabs

This commit is contained in:
maxjivi05 committed 2026-10-13 11:24:01 -04:00
1 parent 1e66ff17c1
commit 9fe2a53711
4 files changed
+234 -81

No files matched your search

@@ -1,7 +1,14 @@
package com.droiddeck.launcher.ui
import android.view.KeyEvent
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.LocalIndication
import androidx.compose.foundation.background
@@ -10,14 +17,14 @@ import androidx.compose.foundation.focusGroup
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.hoverable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsHoveredAsState
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraintsScope
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
@@ -29,7 +36,6 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
@@ -38,14 +44,19 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.runtime.withFrameNanos
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusDirection
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusProperties
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
@@ -54,19 +65,24 @@ import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.lerp
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.input.InputMode
import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.boundsInParent
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalInputModeManager
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.util.lerp
import androidx.compose.ui.zIndex
import com.droiddeck.launcher.R
@@ -77,22 +93,29 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlin.math.roundToInt
private const val DROP_PIECES = 10
private const val DROP_PIECES = 12
internal class DropSheetState(private val scope: CoroutineScope) {
var open by mutableStateOf(false)
var shown by mutableStateOf(false)
var lipBack by mutableStateOf(false)
var tab by mutableStateOf(0)
var travel by mutableStateOf(1)
var settledByDrag = false
var engaged = false
private set
val pull = Animatable(0f)
val lift = Animatable(0f)
val ride = Animatable(0f)
val sink = Animatable(0f)
val hot = Animatable(1f)
val lipShow = Animatable(0f)
val lipGone = Animatable(0f)
val pieces = List(DROP_PIECES) { Animatable(0f) }
var travelPx = 1f
private var from = 0f
private var progress = 0f
var progress = 0f
private set
private var dragJob: Job? = null
fun cancelDrag() {
@@ -106,8 +129,9 @@ internal class DropSheetState(private val scope: CoroutineScope) {
settledByDrag = false
open = false
shown = false
lipBack = false
scope.launch {
listOf(lift, ride, sink).forEach { it.snapTo(0f) }
listOf(pull, lift, ride, sink, lipGone).forEach { it.snapTo(0f) }
pieces.forEach { it.snapTo(0f) }
hot.snapTo(1f)
}
@@ -122,11 +146,13 @@ internal class DropSheetState(private val scope: CoroutineScope) {
if (!open) shown = true
}
fun drag(dy: Float) {
progress = (progress + dy / travelPx).coerceIn(0f, 1.04f)
val p = progress
fun drag(dy: Float) = follow((progress + dy / travelPx).coerceIn(0f, 1.04f))
fun follow(p: Float) {
progress = p
scope.launch {
lift.snapTo(p); ride.snapTo(p); sink.snapTo(p.coerceIn(0f, 1f)); hot.snapTo(0f)
lipGone.snapTo(1f)
pieces.forEach { it.snapTo(p.coerceIn(0f, 1f)) }
}
}
@@ -149,7 +175,7 @@ internal class DropSheetState(private val scope: CoroutineScope) {
launch { sink.animateTo(to, Motion.tw(220)) }
pieces.forEachIndexed { i, piece -> launch { if (opens) delay(Motion.ms(i * 35).toLong()); piece.animateTo(to, Motion.tw(if (opens) 200 else 90)) } }
}
if (!opens) { shown = false; hot.snapTo(1f) }
if (!opens) { shown = false; lipGone.snapTo(0f); hot.snapTo(1f); lipBack = false }
}
if (open != opens) open = opens else settledByDrag = false
}
@@ -161,23 +187,30 @@ internal fun rememberDropSheetState(): DropSheetState {
return remember { DropSheetState(scope) }
}
@OptIn(androidx.compose.ui.ExperimentalComposeUiApi::class)
@OptIn(ExperimentalComposeUiApi::class, androidx.compose.animation.ExperimentalAnimationApi::class)
@Composable
internal fun BoxWithConstraintsScope.DropSheet(
state: DropSheetState,
title: String,
icon: ImageVector,
label: String,
icon: ImageVector,
tabs: List<Pair<String, String>>,
covered: () -> Float = { 0f },
rows: @Composable () -> Unit,
rows: @Composable (tab: String) -> Unit,
) {
val density = LocalDensity.current
val colors = MaterialTheme.colorScheme
val pal = LocalPalette.current
val inputMode = LocalInputModeManager.current
val firstRow = remember { FocusRequester() }
val narrow = LocalNarrowPane.current
val chipFocus = remember(tabs.size) { List(tabs.size) { FocusRequester() } }
val buttonFocus = remember { FocusRequester() }
val buttonSrc = remember { MutableInteractionSource() }
val buttonHovered by buttonSrc.collectIsHoveredAsState()
var buttonFocused by remember { mutableStateOf(false) }
val rightForButton = remember { booleanArrayOf(false) }
var rest by remember { mutableStateOf<Rect?>(null) }
val openState = rememberUpdatedState(state.open)
if (state.tab > tabs.lastIndex) state.tab = 0
fun focusButton() { if (inputMode.inputMode == InputMode.Keyboard) runCatching { buttonFocus.requestFocus() } }
fun close() {
if (!state.open) return
@@ -196,18 +229,22 @@ internal fun BoxWithConstraintsScope.DropSheet(
if (state.settledByDrag) {
state.settledByDrag = false
if (state.open) {
if (inputMode.inputMode == InputMode.Keyboard) { withFrameNanos { }; runCatching { firstRow.requestFocus() } }
if (inputMode.inputMode == InputMode.Keyboard) { withFrameNanos { }; runCatching { chipFocus[state.tab].requestFocus() } }
} else focusButton()
return@LaunchedEffect
}
if (state.engaged) return@LaunchedEffect
state.cancelDrag()
if (state.open) {
state.lipBack = false
state.shown = true
coroutineScope {
launch { state.pull.animateTo(24f, Motion.sp(0.7f, 380f)) }
launch { state.sink.animateTo(1f, Motion.tw(260)) }
launch {
delay(Motion.ms(120).toLong())
launch { state.lipGone.animateTo(1f, Motion.tw(140)) }
launch { state.pull.animateTo(0f, Motion.sp(1f, 500f)) }
launch { state.lift.animateTo(1f, Motion.sp(0.7f, 380f)) }
launch { state.ride.animateTo(1f, Motion.sp(0.7f, 380f)) }
launch { state.hot.snapTo(1f); delay(Motion.ms(100).toLong()); state.hot.animateTo(0f, Motion.tw(200)) }
@@ -216,7 +253,7 @@ internal fun BoxWithConstraintsScope.DropSheet(
if (inputMode.inputMode == InputMode.Keyboard) launch {
delay(Motion.ms(300).toLong())
withFrameNanos { }
runCatching { firstRow.requestFocus() }
runCatching { chipFocus[state.tab].requestFocus() }
}
}
} else if (state.shown) {
@@ -225,22 +262,61 @@ internal fun BoxWithConstraintsScope.DropSheet(
launch { delay(Motion.ms(60).toLong()); state.lift.animateTo(0f, Motion.sp(1f, 380f)) }
launch { delay(Motion.ms(100).toLong()); state.ride.animateTo(0f, Motion.sp(0.55f, 300f)) }
launch { state.sink.animateTo(0f, Motion.tw(300)) }
launch { delay(Motion.ms(420).toLong()); state.lipGone.snapTo(0f); state.lipBack = true }
}
state.shown = false
state.lipBack = false
state.lipGone.snapTo(0f)
state.hot.snapTo(1f)
} else {
listOf(state.sink, state.lift, state.ride).forEach { it.snapTo(0f) }
listOf(state.pull, state.sink, state.lift, state.ride, state.lipGone).forEach { it.snapTo(0f) }
state.pieces.forEach { it.snapTo(0f) }
state.hot.snapTo(1f)
}
}
BackHandler(enabled = state.open) { close() }
if (state.ride.isRunning || (state.shown && !state.open)) VeilRing()
val lipWanted = !state.open && (!state.shown || state.lipBack) && covered() < 0.5f &&
(buttonFocused || buttonHovered || inputMode.inputMode == InputMode.Touch)
LaunchedEffect(lipWanted) {
if (lipWanted) {
if (!state.lipBack) delay(Motion.ms(160).toLong())
state.lipShow.animateTo(1f, Motion.tw(220))
} else {
delay(Motion.ms(140).toLong())
state.lipShow.animateTo(0f, Motion.tw(220))
}
}
val flick = with(density) { 600.dp.toPx() }
val rowsPush = remember(state) {
object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
if (!state.engaged || available.y <= 0f) return Offset.Zero
val before = state.progress
state.follow((before + available.y / state.travelPx).coerceAtMost(1f))
return Offset(0f, (state.progress - before) * state.travelPx)
}
override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset {
if (available.y >= 0f || source != NestedScrollSource.Drag || !openState.value) return Offset.Zero
if (!state.engaged) state.beginDrag()
state.follow((state.progress + available.y / state.travelPx).coerceIn(0f, 1f))
return available
}
override suspend fun onPreFling(available: Velocity): Velocity {
if (!state.engaged) return Velocity.Zero
state.settle(available.y, flick)
return available
}
}
}
val w = constraints.maxWidth.toFloat()
val h = constraints.maxHeight.toFloat()
val inset = with(density) { 12.dp.toPx() }
val edge = with(density) { 3.dp.toPx() }
val buttonH = rest?.height ?: with(density) { 48.dp.toPx() }
val buttonH = rest?.height ?: with(density) { 56.dp.toPx() }
val openBottom = h - sheetTop(h, buttonH, with(density) { 16.dp.toPx() }, edge)
val lower = rest?.let { (openBottom - edge - buttonH / 2f) - it.top } ?: 0f
state.travelPx = openBottom.coerceAtLeast(1f)
@@ -254,69 +330,106 @@ internal fun BoxWithConstraintsScope.DropSheet(
if (state.shown) {
val bottom = lerp(0f, openBottom, state.lift.value)
val sheetLeft = inset
val sheetRight = w - inset
val corner = with(density) { 18.dp.toPx() }
val rim = with(density) { 1.4.dp.toPx() }
val path = remember { Path() }
val rimPath = remember { Path() }
Canvas(Modifier.fillMaxSize()) {
val l = inset
val r = w - inset
val t = -corner
rimPath.reset()
rimPath.moveTo(l, t)
rimPath.lineTo(l, bottom - corner)
rimPath.arcTo(Rect(l, bottom - 2 * corner, l + 2 * corner, bottom), 180f, -90f, false)
rimPath.lineTo(r - corner, bottom)
rimPath.arcTo(Rect(r - 2 * corner, bottom - 2 * corner, r, bottom), 90f, -90f, false)
rimPath.lineTo(r, t)
rimPath.moveTo(sheetLeft, t)
rimPath.lineTo(sheetLeft, bottom - corner)
rimPath.arcTo(Rect(sheetLeft, bottom - 2 * corner, sheetLeft + 2 * corner, bottom), 180f, -90f, false)
rimPath.lineTo(sheetRight - corner, bottom)
rimPath.arcTo(Rect(sheetRight - 2 * corner, bottom - 2 * corner, sheetRight, bottom), 90f, -90f, false)
rimPath.lineTo(sheetRight, t)
path.reset()
path.addPath(rimPath)
path.close()
drawPath(path, lerp(colors.surface, pal.signal, 0.22f * state.hot.value))
drawPath(rimPath, pal.signal.copy(alpha = lerp(0.55f, 0.9f, state.hot.value)), style = Stroke(rim))
}
val sheetW = w - 2 * inset
val rowsTop = with(density) { 62.dp.toPx() }
val rowsBottom = openBottom - edge - buttonH / 2f - with(density) { 8.dp.toPx() }
val pill = rest?.translate(0f, lower)
val sheetW = sheetRight - sheetLeft
val chipsH = with(density) { 44.dp.toPx() }
val closeW = with(density) { 64.dp.toPx() }
val chipsBottom = with(density) {
if (narrow && pill != null) pill.top - 10.dp.toPx() else openBottom - 14.dp.toPx()
}
val chipsTop = chipsBottom - chipsH
val chipsRight = with(density) {
if (narrow || pill == null) sheetW - 16.dp.toPx() else pill.left - sheetLeft - 20.dp.toPx()
}
val rowsBottom = chipsTop - with(density) { 8.dp.toPx() }
Box(
Modifier
.offset { IntOffset(inset.roundToInt(), (bottom - openBottom).roundToInt()) }
.offset { IntOffset(sheetLeft.roundToInt(), (bottom - openBottom).roundToInt()) }
.size(with(density) { sheetW.toDp() }, with(density) { openBottom.toDp() })
.pointerInput(Unit) { detectTapGestures { } }
.controllerBack { close() }
.focusProperties { exit = { if (state.open) FocusRequester.Cancel else FocusRequester.Default } }
.bumpers(
onPrevious = { if (state.tab > 0) { state.travel = -1; state.tab--; runCatching { chipFocus[state.tab].requestFocus() } } },
onNext = { if (state.tab < tabs.lastIndex) { state.travel = 1; state.tab++; runCatching { chipFocus[state.tab].requestFocus() } } },
)
.focusProperties {
exit = { dir ->
if (!openState.value) FocusRequester.Default
else if (dir == FocusDirection.Right) { rightForButton[0] = true; buttonFocus }
else FocusRequester.Cancel
}
}
.focusGroup(),
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.offset { IntOffset(0, 18.dp.roundToPx()) }
.fillMaxWidth()
.padding(start = 20.dp, end = 64.dp)
.graphicsLayer { val v = state.pieces[0].value; alpha = v; translationY = (1f - v) * -6.dp.toPx() },
) {
Icon(icon, null, tint = pal.signal, modifier = Modifier.size(22.dp))
Spacer(Modifier.width(10.dp))
Text(title, fontSize = 17.sp, fontWeight = FontWeight.SemiBold, color = colors.onBackground, maxLines = 1, overflow = TextOverflow.Ellipsis)
}
Column(
Box(
Modifier
.offset { IntOffset(0, rowsTop.roundToInt()) }
.nestedScroll(rowsPush)
.fillMaxWidth()
.height(with(density) { (rowsBottom - rowsTop).coerceAtLeast(0f).toDp() })
.focusRequester(firstRow)
.focusGroup()
.verticalScroll(rememberScrollState())
.padding(start = 16.dp, end = 16.dp, bottom = 12.dp),
.height(with(density) { rowsBottom.coerceAtLeast(0f).toDp() }),
) {
CompositionLocalProvider(LocalSheetBeat provides remember { SheetBeat(state.pieces) }) { rows() }
AnimatedContent(
targetState = state.tab.coerceIn(0, tabs.lastIndex.coerceAtLeast(0)),
transitionSpec = {
val d = state.travel
val shift = with(density) { 24.dp.roundToPx() }
(slideInHorizontally(Motion.tw(240)) { shift * d } + fadeIn(Motion.tw(240)))
.togetherWith(slideOutHorizontally(Motion.tw(240)) { -shift * d } + fadeOut(Motion.tw(240)))
},
label = "dropTab",
) { t ->
val leaving = transition.targetState == androidx.compose.animation.EnterExitState.PostExit
val leavingNow = rememberUpdatedState(leaving)
CompositionLocalProvider(LocalSheetBeat provides remember { SheetBeat(state.pieces) }) {
Column(
Modifier.fillMaxSize()
.focusProperties { enter = { if (leavingNow.value) FocusRequester.Cancel else FocusRequester.Default } }
.focusGroup()
.verticalScroll(rememberScrollState())
.padding(start = 16.dp, end = 16.dp, top = 8.dp, bottom = 8.dp),
) { tabs.getOrNull(t)?.let { rows(it.first) } }
}
}
}
Box(
Modifier
.offset { IntOffset(closeW.roundToInt(), chipsTop.roundToInt()) }
.zIndex(1f)
.width(with(density) { (chipsRight - closeW).coerceAtLeast(0f).toDp() })
.graphicsLayer { val v = state.pieces[0].value; alpha = v; translationY = (1f - v) * -6.dp.toPx() }
.onPreviewKeyEvent { e ->
val k = e.nativeKeyEvent
if (k.keyCode == KeyEvent.KEYCODE_DPAD_DOWN) { if (k.action == KeyEvent.ACTION_DOWN) close(); true } else false
},
) {
SheetChips(tabs.map { it.second }, state.tab, chipFocus) { i -> state.travel = if (i > state.tab) 1 else -1; state.tab = i }
}
val closeSource = remember { MutableInteractionSource() }
val closeHot = rememberHot(closeSource)
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.offset { IntOffset((sheetW - 52.dp.toPx()).roundToInt(), 8.dp.roundToPx()) }
.offset { IntOffset(8.dp.roundToPx(), chipsTop.roundToInt()) }
.zIndex(1f).size(44.dp).testTag("drop-close")
.clip(CircleShape).glideBorder(closeHot, CircleShape, pal.signal)
.hoverable(closeSource)
@@ -326,28 +439,55 @@ internal fun BoxWithConstraintsScope.DropSheet(
}
}
val src = remember { MutableInteractionSource() }
val hot = rememberHot(src)
val enabled = covered() < 0.5f
val toggle = { if (state.open) close() else state.open = true }
val sideInset = if (narrow) 20.dp else 44.dp
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
Modifier
.align(Alignment.TopEnd)
.padding(top = 16.dp, end = 20.dp)
.padding(top = sideInset, end = sideInset)
.onGloballyPositioned { rest = it.boundsInParent() }
.graphicsLayer { translationY = lower * state.ride.value; alpha = 1f - covered() }
.zIndex(2f)
.size(48.dp)
.testTag("glasses-open")
.focusRequester(buttonFocus)
.clip(CircleShape)
.background(lerp(colors.surface.copy(alpha = 0.82f), pal.signal, 0.22f * state.ride.value))
.glideBorder(hot, CircleShape, pal.signal, pal.line)
.hoverable(src)
.focusProperties { canFocus = covered() < 0.5f }
.clickable(interactionSource = src, indication = LocalIndication.current, enabled = covered() < 0.5f, role = Role.Button, onClick = toggle)
.controllerConfirm(enabled = covered() < 0.5f, onClick = toggle),
.focusProperties { canFocus = enabled }
.onFocusChanged {
if (it.hasFocus && rightForButton[0]) { rightForButton[0] = false; if (openState.value) close() }
buttonFocused = it.hasFocus
}
.hoverable(buttonSrc)
.onPreviewKeyEvent { e ->
val k = e.nativeKeyEvent
if (k.keyCode != KeyEvent.KEYCODE_DPAD_DOWN && !(state.open && k.keyCode == KeyEvent.KEYCODE_DPAD_LEFT)) false
else {
if (k.action == KeyEvent.ACTION_DOWN && k.repeatCount == 0) {
if (state.open) runCatching { chipFocus[state.tab].requestFocus() } else if (enabled) state.open = true
}
true
}
},
) {
Icon(icon, label, tint = if (hot || state.open) colors.onBackground else colors.onSurfaceVariant, modifier = Modifier.size(26.dp))
PrimaryButton(label, large = true, icon = icon, modifier = Modifier.testTag("glasses-open")) { if (enabled) toggle() }
}
val r0 = rest
if (r0 != null && (state.lipShow.value > 0.01f || state.pull.value > 0.5f)) {
var handleW by remember { mutableStateOf(0f) }
val lipH = 32.dp + state.pull.value.dp
val drop = { if (!state.open && enabled) state.open = true }
val settingsLabel = stringResource(R.string.drawer_page_settings)
Box(
Modifier
.offset { IntOffset((r0.center.x - handleW / 2f).roundToInt(), (r0.bottom + lower * state.ride.value + (state.lipShow.value - 1f) * 8.dp.toPx()).roundToInt()) }
.onSizeChanged { handleW = it.width.toFloat() }
.graphicsLayer { alpha = state.lipShow.value * (1f - state.lipGone.value) * (1f - covered()) }
.zIndex(2f)
.heightIn(min = 44.dp).testTag("glasses-sheet-open")
.semantics { contentDescription = settingsLabel }
.focusProperties { canFocus = false }
.clickable(interactionSource = remember { MutableInteractionSource() }, indication = null, role = Role.Button, onClick = drop),
) {
Handle(lipH, down = true)
}
}
}
@@ -62,6 +62,17 @@ internal fun rememberGlasses(): Pair<GlassesState, GlassesSettings> {
return g to s
}
internal object GlassesTab {
const val DISPLAY = "display"
const val HARDWARE = "hardware"
const val TRACKING = "tracking"
}
@Composable
internal fun glassesTabs(g: GlassesState): List<Pair<String, String>> =
listOf(GlassesTab.DISPLAY to stringResource(R.string.glasses_display)) +
(if (g.canControl) listOf(GlassesTab.HARDWARE to stringResource(R.string.glasses_hardware), GlassesTab.TRACKING to stringResource(R.string.glasses_tracking)) else emptyList())
@Composable
internal fun GlassesGroups(
host: MenuHost,
@@ -70,9 +81,11 @@ internal fun GlassesGroups(
track: (String) -> Modifier = { Modifier },
onRecenter: (() -> Unit)? = null,
inSession: Boolean = true,
tab: String? = null,
steamVr: @Composable () -> Unit = {},
) {
SettingsGroup(g.model) {
val display = tab == null || tab == GlassesTab.DISPLAY
if (display) SettingsGroup(g.model) {
val status = if (g.hasDisplay) stringResource(R.string.glasses_status, g.width, g.height, kotlin.math.round(g.hz).toInt(),
stringResource(if (g.sbs) R.string.glasses_mode_3d else R.string.glasses_mode_2d)) else null
when {
@@ -89,7 +102,7 @@ internal fun GlassesGroups(
ToggleRow(host, "glasses-game", stringResource(R.string.glasses_game_on), stringResource(R.string.glasses_game_on_hint),
s.gameOnGlasses, chipModifier = track("glasses-game")) { Glasses.setGameOnGlasses(it) }
}
if (g.canControl) SettingsGroup(stringResource(R.string.glasses_display)) {
if (display && g.canControl) SettingsGroup(stringResource(R.string.glasses_display)) {
ChoiceRow(host, "glasses-hz", stringResource(R.string.glasses_refresh), null,
listOf(60, 90, 120).map { it to "$it Hz" }, s.refreshHz, chipModifier = track("glasses-hz")) { Glasses.setRefreshHz(it) }
if (g.sbsSupport == GlassesState.SBS_FAILS && !g.sbs) {
@@ -116,7 +129,7 @@ internal fun GlassesGroups(
}
steamVr()
}
if (g.canControl) SettingsGroup(stringResource(R.string.glasses_hardware)) {
if ((tab == null || tab == GlassesTab.HARDWARE) && g.canControl) SettingsGroup(stringResource(R.string.glasses_hardware)) {
SliderRow(stringResource(R.string.glasses_brightness), null, g.brightness.coerceAtLeast(0), 0..g.brightnessMax, step = 1,
modifier = track("glasses-brightness")) { Glasses.setBrightness(it) }
SliderRow(stringResource(R.string.glasses_volume), null, g.volume.coerceAtLeast(0), 0..g.volumeMax, step = 1,
@@ -124,7 +137,7 @@ internal fun GlassesGroups(
ToggleRow(host, "glasses-sunblock", stringResource(R.string.glasses_sunblock), stringResource(R.string.glasses_sunblock_hint),
g.sunblock, chipModifier = track("glasses-sunblock")) { Glasses.setSunblock(it) }
}
if (g.canControl) SettingsGroup(stringResource(R.string.glasses_tracking)) {
if ((tab == null || tab == GlassesTab.TRACKING) && g.canControl) SettingsGroup(stringResource(R.string.glasses_tracking)) {
ToggleRow(host, "glasses-tracking", stringResource(R.string.glasses_head_tracking), stringResource(R.string.glasses_head_tracking_hint),
s.headTracking, chipModifier = track("glasses-tracking")) { Glasses.setHeadTracking(it) }
if (onRecenter != null && s.headTracking) ActionRow(stringResource(R.string.glasses_recenter), null,
@@ -43,6 +43,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.KeyboardArrowDown
import androidx.compose.material.icons.filled.KeyboardArrowUp
import androidx.compose.ui.platform.testTag
import kotlinx.coroutines.Job
@@ -724,7 +725,7 @@ internal fun LiftHero(
* saying there is a drawer to pull up. Still with animations off.
*/
@Composable
private fun Handle(height: Dp) {
internal fun Handle(height: Dp, down: Boolean = false) {
val colors = MaterialTheme.colorScheme
val pal = LocalPalette.current
val shape = RoundedCornerShape(bottomStart = 12.dp, bottomEnd = 12.dp)
@@ -752,8 +753,8 @@ private fun Handle(height: Dp) {
) {
Text(stringResource(R.string.drawer_page_settings), fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = colors.onSurfaceVariant, maxLines = 1)
Icon(
Icons.Filled.KeyboardArrowUp, null, tint = colors.onSurfaceVariant,
modifier = Modifier.size(16.dp).graphicsLayer { translationY = if (still) 0f else -3.dp.toPx() * lift },
if (down) Icons.Filled.KeyboardArrowDown else Icons.Filled.KeyboardArrowUp, null, tint = colors.onSurfaceVariant,
modifier = Modifier.size(16.dp).graphicsLayer { translationY = if (still) 0f else (if (down) 3.dp else -3.dp).toPx() * lift },
)
}
}
@@ -106,13 +106,12 @@ internal fun SteamHome(s: FrontEndState, a: FrontEndActions, modifier: Modifier)
val (glasses, glassesSettings) = rememberGlasses()
val drop = rememberDropSheetState()
LaunchedEffect(glasses.present) { if (!glasses.present && (drop.open || drop.shown)) drop.reset() }
val glassesLabel = stringResource(R.string.glasses_open)
LiftHero(
key = "steam", tabs = tabs,
drop = drop.takeIf { glasses.present },
overlay = { lifted ->
if (glasses.present) DropSheet(drop, glasses.model, GlassesIcon, glassesLabel, covered = lifted) {
GlassesGroups(rememberMenuHost(), glasses, glassesSettings, inSession = false)
if (glasses.present) DropSheet(drop, glasses.model, GlassesIcon, glassesTabs(glasses), covered = lifted) { tab ->
GlassesGroups(rememberMenuHost(), glasses, glassesSettings, inSession = false, tab = tab)
}
},
onOpen = a.onSteamSettings, onPrimary = a.onPlay,