diff --git a/app/src/main/java/com/droiddeck/launcher/ui/DropSheet.kt b/app/src/main/java/com/droiddeck/launcher/ui/DropSheet.kt index 8df726607..01a74c543 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/DropSheet.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/DropSheet.kt @@ -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>, 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(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) + } } } diff --git a/app/src/main/java/com/droiddeck/launcher/ui/GlassesPanel.kt b/app/src/main/java/com/droiddeck/launcher/ui/GlassesPanel.kt index c81b682fb..99c8f6017 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/GlassesPanel.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/GlassesPanel.kt @@ -62,6 +62,17 @@ internal fun rememberGlasses(): Pair { 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> = + 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, diff --git a/app/src/main/java/com/droiddeck/launcher/ui/LiftSheet.kt b/app/src/main/java/com/droiddeck/launcher/ui/LiftSheet.kt index 07ac02176..302d8b0c7 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/LiftSheet.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/LiftSheet.kt @@ -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 }, ) } } diff --git a/app/src/main/java/com/droiddeck/launcher/ui/SteamHome.kt b/app/src/main/java/com/droiddeck/launcher/ui/SteamHome.kt index b1adb095f..1b9ef14de 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/SteamHome.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/SteamHome.kt @@ -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,