mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-06-16 09:49:20 -04:00
fix(map): align with Meshtastic design standards and M3 best practices
- Replace hardcoded hex colors with MaterialTheme.colorScheme tokens (primary, onPrimary, onSurfaceVariant, surface) so map layers respect light/dark mode transitions - Add TooltipBox with PlainTooltip to MapButton for desktop hover accessibility (design standard §4: tooltips for icon-only buttons) - Set explicit containerColor and scrimColor on NodeInfoSheet's ModalBottomSheet for M3 compliance - Import MaterialTheme in MaplibreMapContent, TracerouteLayers, and InlineMap to read semantic color tokens in composable scope Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -16,6 +16,7 @@
|
||||
*/
|
||||
package org.meshtastic.feature.map.component
|
||||
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.key
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -62,6 +63,8 @@ fun InlineMap(node: Node, modifier: Modifier = Modifier) {
|
||||
val position = node.validPosition ?: return
|
||||
val geoPos = toGeoPositionOrNull(position.latitude_i, position.longitude_i) ?: return
|
||||
|
||||
val labelColor = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
|
||||
// Adaptive zoom: zoom out for imprecise positions so the precision circle is visible
|
||||
val precisionMeters = precisionBitsToMeters(position.precision_bits)
|
||||
val zoom = if (precisionMeters > PRECISION_THRESHOLD_METERS) LOW_PRECISION_ZOOM else DEFAULT_ZOOM
|
||||
@@ -103,7 +106,7 @@ fun InlineMap(node: Node, modifier: Modifier = Modifier) {
|
||||
textSize = const(0.9f.em),
|
||||
textOffset = offset(0f.em, LABEL_OFFSET.em),
|
||||
textAnchor = const(SymbolAnchor.Bottom),
|
||||
textColor = const(Color.DarkGray),
|
||||
textColor = const(labelColor),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -16,15 +16,23 @@
|
||||
*/
|
||||
package org.meshtastic.feature.map.component
|
||||
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FilledIconButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButtonDefaults
|
||||
import androidx.compose.material3.PlainTooltip
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TooltipAnchorPosition
|
||||
import androidx.compose.material3.TooltipBox
|
||||
import androidx.compose.material3.TooltipDefaults
|
||||
import androidx.compose.material3.rememberTooltipState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
|
||||
/** A compact icon button used in map control overlays. Uses [FilledIconButton] for a consistent, compact appearance. */
|
||||
/** A compact icon button used in map control overlays. Uses [FilledIconButton] with a hover tooltip for desktop. */
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
internal fun MapButton(
|
||||
icon: ImageVector,
|
||||
@@ -33,11 +41,17 @@ internal fun MapButton(
|
||||
modifier: Modifier = Modifier,
|
||||
iconTint: Color? = null,
|
||||
) {
|
||||
FilledIconButton(onClick = onClick, modifier = modifier) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = contentDescription,
|
||||
tint = iconTint ?: IconButtonDefaults.filledIconButtonColors().contentColor,
|
||||
)
|
||||
TooltipBox(
|
||||
positionProvider = TooltipDefaults.rememberTooltipPositionProvider(TooltipAnchorPosition.Above),
|
||||
tooltip = { PlainTooltip { Text(contentDescription) } },
|
||||
state = rememberTooltipState(),
|
||||
) {
|
||||
FilledIconButton(onClick = onClick, modifier = modifier) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = contentDescription,
|
||||
tint = iconTint ?: IconButtonDefaults.filledIconButtonColors().contentColor,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
*/
|
||||
package org.meshtastic.feature.map.component
|
||||
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -74,7 +75,6 @@ import org.meshtastic.feature.map.util.nodesToFeatureCollection
|
||||
import org.meshtastic.feature.map.util.waypointsToFeatureCollection
|
||||
import org.maplibre.spatialk.geojson.Position as GeoPosition
|
||||
|
||||
private val NodeMarkerColor = Color(0xFF6750A4)
|
||||
private val OnlineStrokeColor = Color(0xFF4CAF50) // Green — node heard within online threshold
|
||||
private val OfflineStrokeColor = Color(0xFF9E9E9E) // Gray — node not heard recently
|
||||
private const val CLUSTER_RADIUS = 50
|
||||
@@ -194,6 +194,11 @@ private fun NodeMarkerLayers(
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val featureCollection = remember(nodes, myNodeNum) { nodesToFeatureCollection(nodes, myNodeNum) }
|
||||
|
||||
// Read M3 semantic colors for map layers (recomposes on theme change)
|
||||
val clusterColor = MaterialTheme.colorScheme.primary
|
||||
val labelColor = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
val clusterLabelColor = MaterialTheme.colorScheme.onPrimary
|
||||
|
||||
val nodesSource =
|
||||
rememberGeoJsonSource(
|
||||
data = GeoJsonData.Features(featureCollection),
|
||||
@@ -207,10 +212,10 @@ private fun NodeMarkerLayers(
|
||||
source = nodesSource,
|
||||
filter = feature.has("cluster"),
|
||||
radius = const(20.dp),
|
||||
color = const(NodeMarkerColor), // Material primary
|
||||
color = const(clusterColor),
|
||||
opacity = const(CLUSTER_OPACITY),
|
||||
strokeWidth = const(MARKER_STROKE_WIDTH),
|
||||
strokeColor = const(Color.White),
|
||||
strokeColor = const(clusterLabelColor),
|
||||
onClick = { features ->
|
||||
val cluster = features.firstOrNull() ?: return@CircleLayer ClickResult.Pass
|
||||
val target = (cluster.geometry as? Point)?.coordinates ?: return@CircleLayer ClickResult.Pass
|
||||
@@ -232,7 +237,7 @@ private fun NodeMarkerLayers(
|
||||
source = nodesSource,
|
||||
filter = feature.has("cluster"),
|
||||
textField = feature["point_count"].asString(),
|
||||
textColor = const(Color.White),
|
||||
textColor = const(clusterLabelColor),
|
||||
textSize = const(1.2f.em),
|
||||
)
|
||||
|
||||
@@ -242,7 +247,7 @@ private fun NodeMarkerLayers(
|
||||
source = nodesSource,
|
||||
filter = !feature.has("cluster"),
|
||||
radius = const(NODE_MARKER_RADIUS),
|
||||
color = feature["background_color"].convertToColor(const(NodeMarkerColor)),
|
||||
color = feature["background_color"].convertToColor(const(clusterColor)),
|
||||
strokeWidth = const(MARKER_STROKE_WIDTH),
|
||||
strokeColor =
|
||||
switch(
|
||||
@@ -279,7 +284,7 @@ private fun NodeMarkerLayers(
|
||||
),
|
||||
textSize = const(0.9f.em),
|
||||
textOffset = offset(0f.em, LABEL_OFFSET_EM.em),
|
||||
textColor = const(Color.DarkGray),
|
||||
textColor = const(labelColor),
|
||||
textAllowOverlap = const(true),
|
||||
iconAllowOverlap = const(true),
|
||||
)
|
||||
@@ -301,13 +306,13 @@ private fun NodeMarkerLayers(
|
||||
radius = (feature["precision_meters"].convertToNumber(const(0f)) * metersToPixels).dp,
|
||||
color =
|
||||
feature["background_color"].convertToColor(
|
||||
const(NodeMarkerColor.copy(alpha = PRECISION_CIRCLE_FILL_ALPHA)),
|
||||
const(clusterColor.copy(alpha = PRECISION_CIRCLE_FILL_ALPHA)),
|
||||
),
|
||||
opacity = const(PRECISION_CIRCLE_FILL_ALPHA),
|
||||
strokeWidth = const(1.dp),
|
||||
strokeColor =
|
||||
feature["background_color"].convertToColor(
|
||||
const(NodeMarkerColor.copy(alpha = PRECISION_CIRCLE_STROKE_ALPHA)),
|
||||
const(clusterColor.copy(alpha = PRECISION_CIRCLE_STROKE_ALPHA)),
|
||||
),
|
||||
strokeOpacity = const(PRECISION_CIRCLE_STROKE_ALPHA),
|
||||
)
|
||||
|
||||
@@ -50,7 +50,12 @@ import org.meshtastic.core.ui.component.SignalInfo
|
||||
internal fun NodeInfoSheet(node: Node, onDismiss: () -> Unit, onViewDetails: (Int) -> Unit) {
|
||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||
|
||||
ModalBottomSheet(onDismissRequest = onDismiss, sheetState = sheetState) {
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = onDismiss,
|
||||
sheetState = sheetState,
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
scrimColor = MaterialTheme.colorScheme.scrim.copy(alpha = 0.32f),
|
||||
) {
|
||||
Column(modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp, vertical = 16.dp)) {
|
||||
// Node name
|
||||
Text(
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
*/
|
||||
package org.meshtastic.feature.map.component
|
||||
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -52,9 +53,9 @@ import org.meshtastic.feature.map.util.toGeoPositionOrNull
|
||||
import org.meshtastic.feature.map.util.typedFeatureCollection
|
||||
import org.maplibre.spatialk.geojson.Position as GeoPosition
|
||||
|
||||
private val ForwardRouteColor = Color(0xFF4CAF50)
|
||||
private val ReturnRouteColor = Color(0xFFF44336)
|
||||
private val HopMarkerColor = Color(0xFF9C27B0)
|
||||
private val ForwardRouteColor = Color(0xFF4CAF50) // Success green — forward path
|
||||
private val ReturnRouteColor = Color(0xFFF44336) // Error red — return path
|
||||
private val HopMarkerColor = Color(0xFF9C27B0) // Tertiary purple — hop points
|
||||
private const val HEX_RADIX = 16
|
||||
private const val ROUTE_OPACITY = 0.8f
|
||||
|
||||
@@ -72,6 +73,8 @@ internal fun TracerouteLayers(
|
||||
if (overlay == null) return
|
||||
|
||||
val unknownNodeName = stringResource(Res.string.unknown)
|
||||
val labelColor = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
val markerStrokeColor = MaterialTheme.colorScheme.surface
|
||||
|
||||
// Build route line features
|
||||
val routeData =
|
||||
@@ -123,7 +126,7 @@ internal fun TracerouteLayers(
|
||||
radius = const(NODE_MARKER_RADIUS),
|
||||
color = const(HopMarkerColor), // Purple
|
||||
strokeWidth = const(MARKER_STROKE_WIDTH),
|
||||
strokeColor = const(Color.White),
|
||||
strokeColor = const(markerStrokeColor),
|
||||
)
|
||||
SymbolLayer(
|
||||
id = "traceroute-hop-labels",
|
||||
@@ -131,7 +134,7 @@ internal fun TracerouteLayers(
|
||||
textField = feature["short_name"].asString(),
|
||||
textSize = const(1.em),
|
||||
textOffset = offset(0f.em, -2f.em),
|
||||
textColor = const(Color.DarkGray),
|
||||
textColor = const(labelColor),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user