diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/Avatar.kt b/android/src/main/java/com/tailscale/ipn/ui/view/Avatar.kt index 2e5662a28a..baae957ad3 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/Avatar.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/Avatar.kt @@ -3,20 +3,28 @@ package com.tailscale.ipn.ui.view +import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.focusable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Person import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ripple import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import coil.annotation.ExperimentalCoilApi @@ -27,22 +35,56 @@ import com.tailscale.ipn.ui.model.IpnLocal @OptIn(ExperimentalCoilApi::class) @Composable fun Avatar(profile: IpnLocal.LoginProfile?, size: Int = 50, action: (() -> Unit)? = null) { - Box(contentAlignment = Alignment.Center, modifier = Modifier.size(size.dp).clip(CircleShape)) { - var modifier = Modifier.size((size * .8f).dp) - action?.let { - modifier = - modifier.clickable( - interactionSource = remember { MutableInteractionSource() }, - indication = ripple(bounded = false), - onClick = action) - } - Icon( - imageVector = Icons.Default.Person, - contentDescription = stringResource(R.string.settings_title), - modifier = modifier) + var isFocused = remember { mutableStateOf(false) } + val focusManager = LocalFocusManager.current - profile?.UserProfile?.ProfilePicURL?.let { url -> - AsyncImage(model = url, modifier = Modifier.size((size * 1.2f).dp), contentDescription = null) + // Outer Box for the larger focusable and clickable area + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .padding(4.dp) + .size((size * 1.5f).dp) // Focusable area is larger than the avatar + .clip(CircleShape) // Ensure both the focus and click area are circular + .background( + if (isFocused.value) MaterialTheme.colorScheme.surface + else Color.Transparent, + ) + .onFocusChanged { focusState -> + isFocused.value = focusState.isFocused + } + .focusable() // Make this outer Box focusable (after onFocusChanged) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = ripple(bounded = true), // Apply ripple effect inside circular bounds + onClick = { + action?.invoke() + focusManager.clearFocus() // Clear focus after clicking the avatar + } + ) + ) { + // Inner Box to hold the avatar content (Icon or AsyncImage) + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .size(size.dp) + .clip(CircleShape) + ) { + if (profile?.UserProfile?.ProfilePicURL != null) { + AsyncImage( + model = profile.UserProfile.ProfilePicURL, + modifier = Modifier.size(size.dp).clip(CircleShape), + contentDescription = null + ) + } else { + Icon( + imageVector = Icons.Default.Person, + contentDescription = stringResource(R.string.settings_title), + modifier = Modifier + .size((size * 0.8f).dp) + .clip(CircleShape) // Icon size slightly smaller than the Box + ) + } + } } - } } + diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/MainView.kt b/android/src/main/java/com/tailscale/ipn/ui/view/MainView.kt index 9f7e5e055b..6966383ed5 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/MainView.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/MainView.kt @@ -22,7 +22,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.ArrowDropDown @@ -187,28 +186,14 @@ fun MainView( } }, trailingContent = { - Box( - modifier = - Modifier.weight(1f) - .focusable() - .clickable { navigation.onNavigateToSettings() } - .padding(8.dp), - contentAlignment = Alignment.CenterEnd) { - when (user) { - null -> SettingsButton { navigation.onNavigateToSettings() } - else -> - Box( - contentAlignment = Alignment.Center, - modifier = - Modifier.size(42.dp).clip(CircleShape).focusable().clickable { - navigation.onNavigateToSettings() - }) { - Avatar(profile = user, size = 36) { - navigation.onNavigateToSettings() - } - } - } + Box(modifier = Modifier.padding(8.dp), contentAlignment = Alignment.CenterEnd) { + when (user) { + null -> SettingsButton { navigation.onNavigateToSettings() } + else -> { + Avatar(profile = user, size = 36) { navigation.onNavigateToSettings() } } + } + } }) when (state) {