Merge changes I69e7ef22,I6ca5a886 into main
* changes:
Enforce dark mode in QS
Create a border for focus.
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 20c8c6a3..2d32fd7 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.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.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 @@
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 @@
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 @@
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 917a4ff..ccd953d 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.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.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.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 @@
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 @@
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 @@
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 e761c73..58ce194 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 @@
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 @@
.selectable(
selected = selected,
interactionSource = interactionSource,
- indication =
- ShortcutHelperIndication(interactionSource, interactionsConfig),
+ indication = ShortcutHelperIndication(interactionsConfig),
enabled = enabled,
onClick = onClick,
)
@@ -181,8 +181,7 @@
)
.clickable(
interactionSource = interactionSource,
- indication =
- ShortcutHelperIndication(interactionSource, interactionsConfig),
+ indication = ShortcutHelperIndication(interactionsConfig),
enabled = enabled,
onClick = onClick,
),
@@ -507,10 +506,8 @@
}
}
-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 @@
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 ec6a17b..21c45c5 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.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.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.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 @@
@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 @@
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 2efe500..4e094cc 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 @@
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.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.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.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 @@
}
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 @@
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 177a5be..0e09ad2 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.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.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 @@
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 @@
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 fe59c4d3..cb57c67 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.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.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.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.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 @@
hapticsViewModel = hapticsViewModel,
modifier =
modifier
+ .borderOnFocus(color = MaterialTheme.colorScheme.secondary, tileShape.topEnd)
.fillMaxWidth()
.bounceable(
bounceable = currentBounceableInfo.bounceable,
@@ -381,7 +386,7 @@
}
@Composable
- fun animateIconShape(state: Int): Shape {
+ fun animateIconShape(state: Int): RoundedCornerShape {
return animateShape(
state = state,
activeCornerRadius = ActiveIconCornerRadius,
@@ -390,7 +395,7 @@
}
@Composable
- fun animateTileShape(state: Int): Shape {
+ fun animateTileShape(state: Int): RoundedCornerShape {
return animateShape(
state = state,
activeCornerRadius = ActiveTileCornerRadius,
@@ -399,7 +404,7 @@
}
@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 @@
},
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 0000000..e6caa0d
--- /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
+ }
+}