mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-13 03:51:10 +00:00
Drop the glasses settings like Steam's: Play-style button, Settings lip, tabs
This commit is contained in:
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,
|
||||
|
||||
Reference in new issue
Block a user