3
3
4
4
package com.tailscale.ipn.ui.view
5
5
6
+ import androidx.compose.foundation.background
6
7
import androidx.compose.foundation.clickable
8
+ import androidx.compose.foundation.focusable
7
9
import androidx.compose.foundation.interaction.MutableInteractionSource
8
10
import androidx.compose.foundation.layout.Box
11
+ import androidx.compose.foundation.layout.padding
9
12
import androidx.compose.foundation.layout.size
10
13
import androidx.compose.foundation.shape.CircleShape
11
14
import androidx.compose.material.icons.Icons
12
15
import androidx.compose.material.icons.filled.Person
13
16
import androidx.compose.material3.Icon
17
+ import androidx.compose.material3.MaterialTheme
14
18
import androidx.compose.material3.ripple
15
19
import androidx.compose.runtime.Composable
20
+ import androidx.compose.runtime.mutableStateOf
16
21
import androidx.compose.runtime.remember
17
22
import androidx.compose.ui.Alignment
18
23
import androidx.compose.ui.Modifier
19
24
import androidx.compose.ui.draw.clip
25
+ import androidx.compose.ui.focus.onFocusChanged
26
+ import androidx.compose.ui.graphics.Color
27
+ import androidx.compose.ui.platform.LocalFocusManager
20
28
import androidx.compose.ui.res.stringResource
21
29
import androidx.compose.ui.unit.dp
22
30
import coil.annotation.ExperimentalCoilApi
@@ -27,22 +35,56 @@ import com.tailscale.ipn.ui.model.IpnLocal
27
35
@OptIn(ExperimentalCoilApi ::class )
28
36
@Composable
29
37
fun Avatar (profile : IpnLocal .LoginProfile ? , size : Int = 50, action : (() -> Unit )? = null) {
30
- Box (contentAlignment = Alignment .Center , modifier = Modifier .size(size.dp).clip(CircleShape )) {
31
- var modifier = Modifier .size((size * .8f ).dp)
32
- action?.let {
33
- modifier =
34
- modifier.clickable(
35
- interactionSource = remember { MutableInteractionSource () },
36
- indication = ripple(bounded = false ),
37
- onClick = action)
38
- }
39
- Icon (
40
- imageVector = Icons .Default .Person ,
41
- contentDescription = stringResource(R .string.settings_title),
42
- modifier = modifier)
38
+ var isFocused = remember { mutableStateOf(false ) }
39
+ val focusManager = LocalFocusManager .current
43
40
44
- profile?.UserProfile ?.ProfilePicURL ?.let { url ->
45
- AsyncImage (model = url, modifier = Modifier .size((size * 1.2f ).dp), contentDescription = null )
41
+ // Outer Box for the larger focusable and clickable area
42
+ Box (
43
+ contentAlignment = Alignment .Center ,
44
+ modifier = Modifier
45
+ .padding(4 .dp)
46
+ .size((size * 1.5f ).dp) // Focusable area is larger than the avatar
47
+ .clip(CircleShape ) // Ensure both the focus and click area are circular
48
+ .background(
49
+ if (isFocused.value) MaterialTheme .colorScheme.surface
50
+ else Color .Transparent ,
51
+ )
52
+ .onFocusChanged { focusState ->
53
+ isFocused.value = focusState.isFocused
54
+ }
55
+ .focusable() // Make this outer Box focusable (after onFocusChanged)
56
+ .clickable(
57
+ interactionSource = remember { MutableInteractionSource () },
58
+ indication = ripple(bounded = true ), // Apply ripple effect inside circular bounds
59
+ onClick = {
60
+ action?.invoke()
61
+ focusManager.clearFocus() // Clear focus after clicking the avatar
62
+ }
63
+ )
64
+ ) {
65
+ // Inner Box to hold the avatar content (Icon or AsyncImage)
66
+ Box (
67
+ contentAlignment = Alignment .Center ,
68
+ modifier = Modifier
69
+ .size(size.dp)
70
+ .clip(CircleShape )
71
+ ) {
72
+ if (profile?.UserProfile ?.ProfilePicURL != null ) {
73
+ AsyncImage (
74
+ model = profile.UserProfile .ProfilePicURL ,
75
+ modifier = Modifier .size(size.dp).clip(CircleShape ),
76
+ contentDescription = null
77
+ )
78
+ } else {
79
+ Icon (
80
+ imageVector = Icons .Default .Person ,
81
+ contentDescription = stringResource(R .string.settings_title),
82
+ modifier = Modifier
83
+ .size((size * 0.8f ).dp)
84
+ .clip(CircleShape ) // Icon size slightly smaller than the Box
85
+ )
86
+ }
87
+ }
46
88
}
47
- }
48
89
}
90
+
0 commit comments