1 /*
2  * Copyright 2024 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.compose.material3.adaptive.layout
18 
19 import androidx.compose.foundation.clickable
20 import androidx.compose.foundation.interaction.MutableInteractionSource
21 import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
22 import androidx.compose.runtime.Composable
23 import androidx.compose.runtime.rememberCoroutineScope
24 import androidx.compose.ui.Modifier
25 import androidx.compose.ui.node.ModifierNodeElement
26 import androidx.compose.ui.node.ParentDataModifierNode
27 import androidx.compose.ui.platform.InspectorInfo
28 import androidx.compose.ui.platform.debugInspectorInfo
29 import androidx.compose.ui.semantics.SemanticsActions
30 import androidx.compose.ui.semantics.SemanticsConfiguration
31 import androidx.compose.ui.semantics.SemanticsPropertyReceiver
32 import androidx.compose.ui.semantics.contentDescription
33 import androidx.compose.ui.semantics.getOrNull
34 import androidx.compose.ui.semantics.onClick
35 import androidx.compose.ui.semantics.semantics
36 import androidx.compose.ui.semantics.stateDescription
37 import androidx.compose.ui.unit.Density
38 import androidx.compose.ui.unit.Dp
39 import kotlinx.coroutines.launch
40 
41 /**
42  * This function sets up the default semantics of pane expansion drag handles with the given
43  * [PaneExpansionState]. It will provide suitable [contentDescription] as well as [onClick] function
44  * to move the pane expansion among anchors that can be operated via accessibility services.
45  *
46  * It's supposed to be used with a [PaneScaffoldScope.paneExpansionDraggable] modifier, or a plain
47  * [semantics] modifier associated with a drag handle composable.
48  */
49 @ExperimentalMaterial3AdaptiveApi
50 @Composable
PaneExpansionStatenull51 fun PaneExpansionState.defaultDragHandleSemantics(): SemanticsPropertyReceiver.() -> Unit {
52     val coroutineScope = rememberCoroutineScope()
53     val contentDesc = getString(Strings.defaultPaneExpansionDragHandleContentDescription)
54     val currentAnchor = currentAnchor
55     val stateDesc =
56         if (currentAnchor != null) {
57             getString(
58                 Strings.defaultPaneExpansionDragHandleStateDescription,
59                 currentAnchor.description
60             )
61         } else {
62             null
63         }
64     val nextAnchor = nextAnchor
65     val actionLabel =
66         if (nextAnchor != null) {
67             getString(
68                 Strings.defaultPaneExpansionDragHandleActionDescription,
69                 nextAnchor.description
70             )
71         } else {
72             null
73         }
74     return semantics@{
75         contentDescription = contentDesc
76         if (stateDesc != null) {
77             stateDescription = stateDesc
78         }
79         if (nextAnchor == null) {
80             // TODO(conrachen): handle this case
81             return@semantics
82         }
83         onClick(label = actionLabel) {
84             coroutineScope.launch { animateTo(nextAnchor) }
85             return@onClick true
86         }
87     }
88 }
89 
systemGestureExclusionnull90 internal expect fun Modifier.systemGestureExclusion(): Modifier
91 
92 internal data class MinTouchTargetSizeElement(val size: Dp) :
93     ModifierNodeElement<MinTouchTargetSizeNode>() {
94     private val inspectorInfo = debugInspectorInfo {
95         name = "minTouchTargetSize"
96         properties["size"] = size
97     }
98 
99     override fun create(): MinTouchTargetSizeNode {
100         return MinTouchTargetSizeNode(size)
101     }
102 
103     override fun update(node: MinTouchTargetSizeNode) {
104         node.size = size
105     }
106 
107     override fun InspectorInfo.inspectableProperties() {
108         inspectorInfo()
109     }
110 }
111 
112 internal class MinTouchTargetSizeNode(var size: Dp) : ParentDataModifierNode, Modifier.Node() {
modifyParentDatanull113     override fun Density.modifyParentData(parentData: Any?) =
114         ((parentData as? PaneScaffoldParentDataImpl) ?: PaneScaffoldParentDataImpl()).also {
115             it.minTouchTargetSize = size
116         }
117 }
118 
semanticsActionnull119 internal fun Modifier.semanticsAction(
120     semanticsProperties: (SemanticsPropertyReceiver.() -> Unit),
121     interactionSource: MutableInteractionSource
122 ): Modifier {
123     val semanticsConfiguration = SemanticsConfiguration().also { it.semanticsProperties() }
124     return this.then(
125         semanticsConfiguration.getOrNull(SemanticsActions.OnClick)?.action?.let {
126             Modifier.clickable(interactionSource = interactionSource, indication = null) { it() }
127         } ?: Modifier
128     )
129 }
130