33
44package com.tailscale.ipn.ui.view
55
6+ import androidx.compose.foundation.background
67import androidx.compose.foundation.clickable
8+ import androidx.compose.foundation.focusable
79import androidx.compose.foundation.interaction.MutableInteractionSource
810import androidx.compose.foundation.layout.Box
11+ import androidx.compose.foundation.layout.padding
912import androidx.compose.foundation.layout.size
1013import androidx.compose.foundation.shape.CircleShape
1114import androidx.compose.material.icons.Icons
1215import androidx.compose.material.icons.filled.Person
1316import androidx.compose.material3.Icon
17+ import androidx.compose.material3.MaterialTheme
1418import androidx.compose.material3.ripple
1519import androidx.compose.runtime.Composable
20+ import androidx.compose.runtime.mutableStateOf
1621import androidx.compose.runtime.remember
1722import androidx.compose.ui.Alignment
1823import androidx.compose.ui.Modifier
1924import 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
2028import androidx.compose.ui.res.stringResource
2129import androidx.compose.ui.unit.dp
2230import coil.annotation.ExperimentalCoilApi
@@ -27,22 +35,56 @@ import com.tailscale.ipn.ui.model.IpnLocal
2735@OptIn(ExperimentalCoilApi ::class )
2836@Composable
2937fun 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
4340
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+ }
4688 }
47- }
4889}
90+
0 commit comments