android/ui: restyle the search bar (#272)

Updates tailscale/corp#18202

restyles the search bar to look a little more iOS like.  switch to a normal text field which gives us much more flexibility over the look and all of the flexbiility of SearchBar.

Signed-off-by: Jonathan Nobels <jonathan@tailscale.com>
pull/274/head
Jonathan Nobels 2 months ago committed by GitHub
parent 6e503f29a9
commit e9465988dd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -11,6 +11,8 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ListItemColors import androidx.compose.material3.ListItemColors
import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextFieldDefaults
import androidx.compose.material3.TextFieldColors
import androidx.compose.material3.TopAppBarColors import androidx.compose.material3.TopAppBarColors
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.Typography import androidx.compose.material3.Typography
@ -205,3 +207,21 @@ val ColorScheme.secondaryButton: ButtonColors
val ColorScheme.disabled: Color val ColorScheme.disabled: Color
get() = Color(0xFFAFACAB) // gray-400 get() = Color(0xFFAFACAB) // gray-400
@OptIn(ExperimentalMaterial3Api::class)
val ColorScheme.searchBarColors: TextFieldColors
@Composable
get() {
val defaults = OutlinedTextFieldDefaults.colors()
return OutlinedTextFieldDefaults.colors(
focusedLeadingIconColor = MaterialTheme.colorScheme.onSurface,
unfocusedLeadingIconColor = MaterialTheme.colorScheme.onSurface,
focusedTextColor = MaterialTheme.colorScheme.onSurface,
unfocusedTextColor = MaterialTheme.colorScheme.onSurfaceVariant,
disabledTextColor = MaterialTheme.colorScheme.onSurfaceVariant,
focusedContainerColor = MaterialTheme.colorScheme.background,
unfocusedContainerColor = MaterialTheme.colorScheme.background,
disabledContainerColor = MaterialTheme.colorScheme.background,
focusedBorderColor = Color.Transparent,
unfocusedBorderColor = Color.Transparent)
}

@ -1,16 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package com.tailscale.ipn.ui.util
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
@Composable
fun settingsRowModifier(): Modifier {
return Modifier.clip(shape = RoundedCornerShape(8.dp)).fillMaxWidth()
}

@ -24,6 +24,8 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.ArrowDropDown import androidx.compose.material.icons.outlined.ArrowDropDown
import androidx.compose.material.icons.outlined.Clear
import androidx.compose.material.icons.outlined.Close
import androidx.compose.material.icons.outlined.Search import androidx.compose.material.icons.outlined.Search
import androidx.compose.material.icons.outlined.Settings import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material3.Button import androidx.compose.material3.Button
@ -33,16 +35,20 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.ListItem import androidx.compose.material3.ListItem
import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.SearchBar
import androidx.compose.material3.SearchBarDefaults
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.SpanStyle
@ -66,6 +72,7 @@ import com.tailscale.ipn.ui.model.Tailcfg
import com.tailscale.ipn.ui.theme.disabled import com.tailscale.ipn.ui.theme.disabled
import com.tailscale.ipn.ui.theme.listItem import com.tailscale.ipn.ui.theme.listItem
import com.tailscale.ipn.ui.theme.primaryListItem import com.tailscale.ipn.ui.theme.primaryListItem
import com.tailscale.ipn.ui.theme.searchBarColors
import com.tailscale.ipn.ui.theme.secondaryButton import com.tailscale.ipn.ui.theme.secondaryButton
import com.tailscale.ipn.ui.theme.surfaceContainerListItem import com.tailscale.ipn.ui.theme.surfaceContainerListItem
import com.tailscale.ipn.ui.util.Lists import com.tailscale.ipn.ui.util.Lists
@ -318,75 +325,94 @@ fun PeerList(
val searchTermStr by viewModel.searchTerm.collectAsState(initial = "") val searchTermStr by viewModel.searchTerm.collectAsState(initial = "")
val netmap = viewModel.netmap.collectAsState() val netmap = viewModel.netmap.collectAsState()
SearchBar( val focusManager = LocalFocusManager.current
query = searchTermStr, var isFocussed by remember { mutableStateOf(false) }
onQueryChange = onSearch,
onSearch = onSearch,
active = true,
onActiveChange = {},
shape = RoundedCornerShape(10.dp),
leadingIcon = { Icon(Icons.Outlined.Search, null) },
trailingIcon = {
if (searchTermStr.isNotEmpty()) ClearButton({ onSearch("") }) else CloseButton()
},
tonalElevation = 0.dp,
shadowElevation = 0.dp,
colors =
SearchBarDefaults.colors(
containerColor = MaterialTheme.colorScheme.surface,
dividerColor = MaterialTheme.colorScheme.outline),
modifier = Modifier.fillMaxWidth()) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
) {
var first = true
peerList.value.forEach { peerSet ->
if (!first) {
item(key = "spacer_${peerSet.user?.DisplayName}") {
Lists.ItemDivider()
Spacer(Modifier.height(24.dp))
}
}
first = false
stickyHeader {
ListItem(
modifier = Modifier.heightIn(max = 48.dp),
headlineContent = {
Text(
text =
peerSet.user?.DisplayName ?: stringResource(id = R.string.unknown_user),
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.SemiBold)
})
}
itemsWithDividers(peerSet.peers, key = { it.StableID }) { peer -> Box(modifier = Modifier.fillMaxWidth().background(color = MaterialTheme.colorScheme.surface)) {
ListItem( OutlinedTextField(
modifier = Modifier.clickable { onNavigateToPeerDetails(peer) }, modifier =
colors = MaterialTheme.colorScheme.listItem, Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp).onFocusChanged {
headlineContent = { isFocussed = it.isFocused
Row(verticalAlignment = Alignment.CenterVertically) { },
Box( singleLine = true,
modifier = shape = MaterialTheme.shapes.large,
Modifier.padding(top = 2.dp) colors = MaterialTheme.colorScheme.searchBarColors,
.size(10.dp) leadingIcon = { Icon(imageVector = Icons.Outlined.Search, contentDescription = "search") },
.background( trailingIcon = {
color = peer.connectedColor(netmap.value), if (isFocussed) {
shape = RoundedCornerShape(percent = 50))) {} IconButton(
Spacer(modifier = Modifier.size(8.dp)) onClick = {
Text(text = peer.ComputedName, style = MaterialTheme.typography.titleMedium) focusManager.clearFocus()
} onSearch("")
}, }) {
supportingContent = { Icon(
Text( imageVector =
text = peer.Addresses?.first()?.split("/")?.first() ?: "", if (searchTermStr.isEmpty()) Icons.Outlined.Close
style = MaterialTheme.typography.bodyMedium) else Icons.Outlined.Clear,
}) contentDescription = "clear search",
} tint = MaterialTheme.colorScheme.onSurfaceVariant)
}
} }
},
placeholder = {
Text(
text = stringResource(id = R.string.search),
style = MaterialTheme.typography.bodyLarge,
maxLines = 1)
},
value = searchTermStr,
onValueChange = { onSearch(it) })
}
LazyColumn(
modifier = Modifier.fillMaxSize(),
) {
var first = true
peerList.value.forEach { peerSet ->
if (!first) {
item(key = "spacer_${peerSet.user?.DisplayName}") {
Lists.ItemDivider()
Spacer(Modifier.height(24.dp))
} }
} }
first = false
stickyHeader {
ListItem(
modifier = Modifier.heightIn(max = 48.dp),
headlineContent = {
Text(
text = peerSet.user?.DisplayName ?: stringResource(id = R.string.unknown_user),
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.SemiBold)
})
}
itemsWithDividers(peerSet.peers, key = { it.StableID }) { peer ->
ListItem(
modifier = Modifier.clickable { onNavigateToPeerDetails(peer) },
colors = MaterialTheme.colorScheme.listItem,
headlineContent = {
Row(verticalAlignment = Alignment.CenterVertically) {
Box(
modifier =
Modifier.padding(top = 2.dp)
.size(10.dp)
.background(
color = peer.connectedColor(netmap.value),
shape = RoundedCornerShape(percent = 50))) {}
Spacer(modifier = Modifier.size(8.dp))
Text(text = peer.ComputedName, style = MaterialTheme.typography.titleMedium)
}
},
supportingContent = {
Text(
text = peer.Addresses?.first()?.split("/")?.first() ?: "",
style = MaterialTheme.typography.bodyMedium)
})
}
}
}
} }
@OptIn(ExperimentalPermissionsApi::class) @OptIn(ExperimentalPermissionsApi::class)

@ -16,6 +16,7 @@
<string name="offline">Offline</string> <string name="offline">Offline</string>
<string name="ok">OK</string> <string name="ok">OK</string>
<string name="warning">Warning</string> <string name="warning">Warning</string>
<string name="search">Search\n</string>
<!-- Strings for the about screen --> <!-- Strings for the about screen -->
<string name="app_name">Tailscale</string> <string name="app_name">Tailscale</string>

Loading…
Cancel
Save