diff options
8 files changed, 260 insertions, 72 deletions
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/FooterActions.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/FooterActions.kt index 20c8c6a3f4bf..2d32fd768eaa 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/FooterActions.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/FooterActions.kt @@ -36,6 +36,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.CornerSize import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon import androidx.compose.material3.LocalContentColor @@ -84,6 +85,7 @@ import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsSecurityButtonVi import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel import com.android.systemui.qs.ui.composable.QuickSettings import com.android.systemui.qs.ui.composable.QuickSettingsTheme +import com.android.systemui.qs.ui.compose.borderOnFocus import com.android.systemui.res.R import kotlinx.coroutines.launch @@ -264,7 +266,11 @@ private fun IconButton(model: FooterActionsButtonViewModel, modifier: Modifier = color = colorAttr(model.backgroundColor), shape = CircleShape, onClick = model.onClick, - modifier = modifier, + modifier = + modifier.borderOnFocus( + color = MaterialTheme.colorScheme.secondary, + CornerSize(percent = 50), + ), ) { val tint = model.iconTint?.let { Color(it) } ?: Color.Unspecified Icon(model.icon, tint = tint, modifier = Modifier.size(20.dp)) @@ -291,7 +297,11 @@ private fun NumberButton( shape = CircleShape, onClick = onClick, interactionSource = interactionSource, - modifier = modifier, + modifier = + modifier.borderOnFocus( + color = MaterialTheme.colorScheme.secondary, + CornerSize(percent = 50), + ), ) { Box(Modifier.size(40.dp)) { Box( @@ -342,7 +352,10 @@ private fun TextButton( color = colorAttr(R.attr.underSurface), contentColor = MaterialTheme.colorScheme.onSurfaceVariant, borderStroke = BorderStroke(1.dp, colorAttr(R.attr.shadeInactive)), - modifier = modifier.padding(horizontal = 4.dp), + modifier = + modifier + .padding(horizontal = 4.dp) + .borderOnFocus(color = MaterialTheme.colorScheme.secondary, CornerSize(50)), onClick = onClick, ) { Row( diff --git a/packages/SystemUI/src/com/android/systemui/brightness/ui/compose/BrightnessSlider.kt b/packages/SystemUI/src/com/android/systemui/brightness/ui/compose/BrightnessSlider.kt index 917a4ff9036b..ccd953de7d03 100644 --- a/packages/SystemUI/src/com/android/systemui/brightness/ui/compose/BrightnessSlider.kt +++ b/packages/SystemUI/src/com/android/systemui/brightness/ui/compose/BrightnessSlider.kt @@ -24,6 +24,7 @@ import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CornerSize import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -31,7 +32,6 @@ import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue @@ -61,6 +61,7 @@ import com.android.systemui.haptics.slider.SeekableSliderTrackerConfig import com.android.systemui.haptics.slider.SliderHapticFeedbackConfig import com.android.systemui.haptics.slider.compose.ui.SliderHapticsViewModel import com.android.systemui.lifecycle.rememberViewModel +import com.android.systemui.qs.ui.compose.borderOnFocus import com.android.systemui.res.R import com.android.systemui.utils.PolicyRestriction @@ -102,11 +103,12 @@ private fun BrightnessSlider( null } - val overriddenByAppState by if (Flags.showToastWhenAppControlBrightness()) { - viewModel.brightnessOverriddenByWindow.collectAsStateWithLifecycle() - } else { - mutableStateOf(false) - } + val overriddenByAppState = + if (Flags.showToastWhenAppControlBrightness()) { + viewModel.brightnessOverriddenByWindow.collectAsStateWithLifecycle().value + } else { + false + } PlatformSlider( value = animatedValue, @@ -160,7 +162,7 @@ private fun BrightnessSlider( if (interaction is DragInteraction.Start && overriddenByAppState) { viewModel.showToast( context, - R.string.quick_settings_brightness_unable_adjust_msg + R.string.quick_settings_brightness_unable_adjust_msg, ) } } @@ -213,7 +215,11 @@ fun BrightnessSliderContainer( coroutineScope.launch { viewModel.onDrag(Drag.Stopped(GammaBrightness(it))) } }, modifier = - Modifier.then(if (viewModel.showMirror) Modifier.drawInOverlay() else Modifier) + Modifier.borderOnFocus( + color = MaterialTheme.colorScheme.secondary, + cornerSize = CornerSize(32.dp), + ) + .then(if (viewModel.showMirror) Modifier.drawInOverlay() else Modifier) .sliderBackground(containerColor) .fillMaxWidth(), formatter = viewModel::formatValue, diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/Surfaces.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/Surfaces.kt index e761c7313ff3..58ce194694df 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/Surfaces.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/Surfaces.kt @@ -18,6 +18,7 @@ package com.android.systemui.keyboard.shortcut.ui.composable import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.IndicationNodeFactory +import androidx.compose.foundation.LocalIndication import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable @@ -126,8 +127,7 @@ fun SelectableShortcutSurface( .selectable( selected = selected, interactionSource = interactionSource, - indication = - ShortcutHelperIndication(interactionSource, interactionsConfig), + indication = ShortcutHelperIndication(interactionsConfig), enabled = enabled, onClick = onClick, ) @@ -181,8 +181,7 @@ fun ClickableShortcutSurface( ) .clickable( interactionSource = interactionSource, - indication = - ShortcutHelperIndication(interactionSource, interactionsConfig), + indication = ShortcutHelperIndication(interactionsConfig), enabled = enabled, onClick = onClick, ), @@ -507,10 +506,8 @@ private class ShortcutHelperInteractionsNode( } } -data class ShortcutHelperIndication( - private val interactionSource: InteractionSource, - private val interactionsConfig: InteractionsConfig, -) : IndicationNodeFactory { +data class ShortcutHelperIndication(private val interactionsConfig: InteractionsConfig) : + IndicationNodeFactory { override fun create(interactionSource: InteractionSource): DelegatableNode { return ShortcutHelperInteractionsNode(interactionSource, interactionsConfig) } @@ -529,3 +526,15 @@ data class InteractionsConfig( val hoverPadding: Dp = 0.dp, val pressedPadding: Dp = hoverPadding, ) + +@Composable +fun ProvideShortcutHelperIndication( + interactionsConfig: InteractionsConfig, + content: @Composable () -> Unit, +) { + CompositionLocalProvider( + LocalIndication provides ShortcutHelperIndication(interactionsConfig) + ) { + content() + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt index ec6a17b24989..21c45c550e08 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt @@ -51,6 +51,7 @@ import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect @@ -78,6 +79,7 @@ import androidx.compose.ui.semantics.CustomAccessibilityAction import androidx.compose.ui.semantics.customActions import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.round import androidx.compose.ui.util.fastRoundToInt import androidx.compose.ui.viewinterop.AndroidView @@ -102,6 +104,8 @@ import com.android.systemui.Dumpable import com.android.systemui.brightness.ui.compose.BrightnessSliderContainer import com.android.systemui.compose.modifiers.sysuiResTag import com.android.systemui.dump.DumpManager +import com.android.systemui.keyboard.shortcut.ui.composable.InteractionsConfig +import com.android.systemui.keyboard.shortcut.ui.composable.ProvideShortcutHelperIndication import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.lifecycle.setSnapshotBinding import com.android.systemui.media.controls.ui.view.MediaHost @@ -240,51 +244,54 @@ constructor( @Composable private fun Content() { - PlatformTheme { - AnimatedVisibility( - visible = viewModel.isQsVisible, - modifier = - Modifier.graphicsLayer { alpha = viewModel.viewAlpha } - // Clipping before translation to match QSContainerImpl.onDraw - .offset { - IntOffset(x = 0, y = viewModel.viewTranslationY.fastRoundToInt()) - } - .thenIf(notificationScrimClippingParams.isEnabled) { - Modifier.notificationScrimClip { - notificationScrimClippingParams.params + PlatformTheme(isDarkTheme = true) { + ProvideShortcutHelperIndication(interactionsConfig = interactionsConfig()) { + AnimatedVisibility( + visible = viewModel.isQsVisible, + modifier = + Modifier.graphicsLayer { alpha = viewModel.viewAlpha } + // Clipping before translation to match QSContainerImpl.onDraw + .offset { + IntOffset(x = 0, y = viewModel.viewTranslationY.fastRoundToInt()) + } + .thenIf(notificationScrimClippingParams.isEnabled) { + Modifier.notificationScrimClip { + notificationScrimClippingParams.params + } } + // Disable touches in the whole composable while the mirror is showing. + // While the mirror is showing, an ancestor of the ComposeView is made + // alpha 0, but touches are still being captured by the composables. + .gesturesDisabled(viewModel.showingMirror), + ) { + val isEditing by + viewModel.containerViewModel.editModeViewModel.isEditing + .collectAsStateWithLifecycle() + val animationSpecEditMode = tween<Float>(EDIT_MODE_TIME_MILLIS) + AnimatedContent( + targetState = isEditing, + transitionSpec = { + fadeIn(animationSpecEditMode) togetherWith + fadeOut(animationSpecEditMode) + }, + label = "EditModeAnimatedContent", + ) { editing -> + if (editing) { + val qqsPadding = viewModel.qqsHeaderHeight + EditMode( + viewModel = viewModel.containerViewModel.editModeViewModel, + modifier = + Modifier.fillMaxWidth() + .padding(top = { qqsPadding }) + .padding( + horizontal = { + QuickSettingsShade.Dimensions.Padding.roundToPx() + } + ), + ) + } else { + CollapsableQuickSettingsSTL() } - // Disable touches in the whole composable while the mirror is showing. - // While the mirror is showing, an ancestor of the ComposeView is made - // alpha 0, but touches are still being captured by the composables. - .gesturesDisabled(viewModel.showingMirror), - ) { - val isEditing by - viewModel.containerViewModel.editModeViewModel.isEditing - .collectAsStateWithLifecycle() - val animationSpecEditMode = tween<Float>(EDIT_MODE_TIME_MILLIS) - AnimatedContent( - targetState = isEditing, - transitionSpec = { - fadeIn(animationSpecEditMode) togetherWith fadeOut(animationSpecEditMode) - }, - label = "EditModeAnimatedContent", - ) { editing -> - if (editing) { - val qqsPadding = viewModel.qqsHeaderHeight - EditMode( - viewModel = viewModel.containerViewModel.editModeViewModel, - modifier = - Modifier.fillMaxWidth() - .padding(top = { qqsPadding }) - .padding( - horizontal = { - QuickSettingsShade.Dimensions.Padding.roundToPx() - } - ), - ) - } else { - CollapsableQuickSettingsSTL() } } } @@ -1090,3 +1097,14 @@ private object ResIdTags { const val qsScroll = "expanded_qs_scroll_view" const val qsFooterActions = "qs_footer_actions" } + +@Composable +private fun interactionsConfig() = + InteractionsConfig( + hoverOverlayColor = MaterialTheme.colorScheme.onSurface, + hoverOverlayAlpha = 0.11f, + pressedOverlayColor = MaterialTheme.colorScheme.onSurface, + pressedOverlayAlpha = 0.15f, + // we are OK using this as our content is clipped and all corner radius are larger than this + surfaceCornerRadius = 28.dp, + ) diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PaginatedGridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PaginatedGridLayout.kt index 2efe500912cd..4e094cc77eae 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PaginatedGridLayout.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PaginatedGridLayout.kt @@ -18,10 +18,13 @@ package com.android.systemui.qs.panels.ui.compose import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.requiredHeight import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.shape.CornerSize +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Edit import androidx.compose.material3.Icon @@ -32,7 +35,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment @@ -41,6 +43,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.android.compose.animation.scene.SceneScope +import com.android.compose.modifiers.padding import com.android.systemui.compose.modifiers.sysuiResTag import com.android.systemui.lifecycle.rememberViewModel import com.android.systemui.qs.panels.dagger.PaginatedBaseLayoutType @@ -48,6 +51,7 @@ import com.android.systemui.qs.panels.ui.compose.PaginatedGridLayout.Dimensions. import com.android.systemui.qs.panels.ui.compose.PaginatedGridLayout.Dimensions.InterPageSpacing import com.android.systemui.qs.panels.ui.viewmodel.PaginatedGridViewModel import com.android.systemui.qs.panels.ui.viewmodel.TileViewModel +import com.android.systemui.qs.ui.compose.borderOnFocus import com.android.systemui.res.R import javax.inject.Inject @@ -89,9 +93,24 @@ constructor( } Column { + val contentPaddingValue = + if (pages.size > 1) { + InterPageSpacing + } else { + 0.dp + } + val contentPadding = PaddingValues(horizontal = contentPaddingValue) + + /* Use negative padding equal with value equal to content padding. That way, each page + * layout extends to the sides, but the content is as if there was no padding. That + * way, the clipping bounds of the HorizontalPager extend beyond the tiles in each page. + */ HorizontalPager( state = pagerState, - modifier = Modifier.sysuiResTag("qs_pager"), + modifier = + Modifier.sysuiResTag("qs_pager") + .padding(horizontal = { -contentPaddingValue.roundToPx() }), + contentPadding = contentPadding, pageSpacing = if (pages.size > 1) InterPageSpacing else 0.dp, beyondViewportPageCount = 1, verticalAlignment = Alignment.Top, @@ -114,7 +133,13 @@ constructor( CompositionLocalProvider(value = LocalContentColor provides Color.White) { IconButton( onClick = editModeStart, - modifier = Modifier.align(Alignment.CenterEnd), + shape = RoundedCornerShape(CornerSize(28.dp)), + modifier = + Modifier.align(Alignment.CenterEnd) + .borderOnFocus( + color = MaterialTheme.colorScheme.secondary, + cornerSize = CornerSize(FooterHeight / 2), + ), ) { Icon( imageVector = Icons.Default.Edit, diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt index 177a5be35592..0e09ad29f4fd 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt @@ -50,7 +50,6 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.graphics.Shape import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role @@ -75,6 +74,7 @@ import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults.SideIconWidth import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults.longPressLabel import com.android.systemui.qs.panels.ui.viewmodel.AccessibilityUiState +import com.android.systemui.qs.ui.compose.borderOnFocus import com.android.systemui.res.R private const val TEST_TAG_TOGGLE = "qs_tile_toggle_target" @@ -88,7 +88,7 @@ fun LargeTileContent( colors: TileColors, squishiness: () -> Float, accessibilityUiState: AccessibilityUiState? = null, - iconShape: Shape = RoundedCornerShape(CommonTileDefaults.InactiveCornerRadius), + iconShape: RoundedCornerShape = RoundedCornerShape(CommonTileDefaults.InactiveCornerRadius), toggleClick: (() -> Unit)? = null, onLongClick: (() -> Unit)? = null, ) { @@ -100,10 +100,12 @@ fun LargeTileContent( val longPressLabel = longPressLabel().takeIf { onLongClick != null } val animatedBackgroundColor by animateColorAsState(colors.iconBackground, label = "QSTileDualTargetBackgroundColor") + val focusBorderColor = MaterialTheme.colorScheme.secondary Box( modifier = Modifier.size(CommonTileDefaults.ToggleTargetSize).thenIf(toggleClick != null) { - Modifier.clip(iconShape) + Modifier.borderOnFocus(color = focusBorderColor, iconShape.topEnd) + .clip(iconShape) .verticalSquish(squishiness) .drawBehind { drawRect(animatedBackgroundColor) } .combinedClickable( diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt index fe59c4d36edc..cb57c6710553 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt @@ -39,6 +39,7 @@ import androidx.compose.foundation.lazy.grid.LazyGridScope import androidx.compose.foundation.lazy.grid.LazyGridState import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.rememberLazyGridState +import androidx.compose.foundation.shape.CornerSize import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable @@ -49,6 +50,7 @@ import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape import androidx.compose.ui.platform.LocalConfiguration @@ -59,6 +61,7 @@ import androidx.compose.ui.semantics.role import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.stateDescription import androidx.compose.ui.semantics.toggleableState +import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -82,6 +85,7 @@ import com.android.systemui.qs.panels.ui.viewmodel.TileUiState import com.android.systemui.qs.panels.ui.viewmodel.TileViewModel import com.android.systemui.qs.panels.ui.viewmodel.toUiState import com.android.systemui.qs.tileimpl.QSTileImpl +import com.android.systemui.qs.ui.compose.borderOnFocus import com.android.systemui.res.R import java.util.function.Supplier import kotlinx.coroutines.CoroutineScope @@ -139,6 +143,7 @@ fun Tile( hapticsViewModel = hapticsViewModel, modifier = modifier + .borderOnFocus(color = MaterialTheme.colorScheme.secondary, tileShape.topEnd) .fillMaxWidth() .bounceable( bounceable = currentBounceableInfo.bounceable, @@ -381,7 +386,7 @@ private object TileDefaults { } @Composable - fun animateIconShape(state: Int): Shape { + fun animateIconShape(state: Int): RoundedCornerShape { return animateShape( state = state, activeCornerRadius = ActiveIconCornerRadius, @@ -390,7 +395,7 @@ private object TileDefaults { } @Composable - fun animateTileShape(state: Int): Shape { + fun animateTileShape(state: Int): RoundedCornerShape { return animateShape( state = state, activeCornerRadius = ActiveTileCornerRadius, @@ -399,7 +404,7 @@ private object TileDefaults { } @Composable - fun animateShape(state: Int, activeCornerRadius: Dp, label: String): Shape { + fun animateShape(state: Int, activeCornerRadius: Dp, label: String): RoundedCornerShape { val animatedCornerRadius by animateDpAsState( targetValue = @@ -410,7 +415,15 @@ private object TileDefaults { }, label = label, ) - return RoundedCornerShape(animatedCornerRadius) + + val corner = remember { + object : CornerSize { + override fun toPx(shapeSize: Size, density: Density): Float { + return with(density) { animatedCornerRadius.toPx() } + } + } + } + return RoundedCornerShape(corner) } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/compose/BorderOnFocus.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/compose/BorderOnFocus.kt new file mode 100644 index 000000000000..e6caa0d7520d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/ui/compose/BorderOnFocus.kt @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.ui.compose + +import androidx.compose.foundation.shape.CornerSize +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusEventModifierNode +import androidx.compose.ui.focus.FocusState +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.drawscope.ContentDrawScope +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.node.DrawModifierNode +import androidx.compose.ui.node.ModifierNodeElement +import androidx.compose.ui.platform.InspectorInfo +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +/** + * Provides a rounded rect border when the element is focused. + * + * This should be used for elements that are themselves rounded rects. + */ +fun Modifier.borderOnFocus( + color: Color, + cornerSize: CornerSize, + strokeWidth: Dp = 3.dp, + padding: Dp = 2.dp, +) = this then BorderOnFocusElement(color, cornerSize, strokeWidth, padding) + +private class BorderOnFocusNode( + var color: Color, + var cornerSize: CornerSize, + var strokeWidth: Dp, + var padding: Dp, +) : FocusEventModifierNode, DrawModifierNode, Modifier.Node() { + + private var focused by mutableStateOf(false) + + override fun onFocusEvent(focusState: FocusState) { + focused = focusState.isFocused + } + + override fun ContentDrawScope.draw() { + drawContent() + val focusOutline = Rect(Offset.Zero, size).inflate(padding.toPx()) + if (focused) { + drawRoundRect( + color = color, + topLeft = focusOutline.topLeft, + size = focusOutline.size, + cornerRadius = CornerRadius(cornerSize.toPx(focusOutline.size, this)), + style = Stroke(strokeWidth.toPx()), + ) + } + } +} + +private data class BorderOnFocusElement( + val color: Color, + val cornerSize: CornerSize, + val strokeWidth: Dp, + val padding: Dp, +) : ModifierNodeElement<BorderOnFocusNode>() { + override fun create(): BorderOnFocusNode { + return BorderOnFocusNode(color, cornerSize, strokeWidth, padding) + } + + override fun update(node: BorderOnFocusNode) { + node.color = color + node.cornerSize = cornerSize + node.strokeWidth = strokeWidth + node.padding = padding + } + + override fun InspectorInfo.inspectableProperties() { + name = "borderOnFocus" + properties["color"] = color + properties["cornerSize"] = cornerSize + properties["strokeWidth"] = strokeWidth + properties["padding"] = padding + } +} |