diff --git a/androidApp/src/google/kotlin/org/meshtastic/app/map/component/MarkerBitmapRenderer.kt b/androidApp/src/google/kotlin/org/meshtastic/app/map/component/MarkerBitmapRenderer.kt
deleted file mode 100644
index 9b0c161eb8..0000000000
--- a/androidApp/src/google/kotlin/org/meshtastic/app/map/component/MarkerBitmapRenderer.kt
+++ /dev/null
@@ -1,144 +0,0 @@
-/*
- * Copyright (c) 2026 Meshtastic LLC
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-package org.meshtastic.app.map.component
-
-import android.content.Context
-import android.graphics.Canvas
-import android.graphics.Paint
-import android.graphics.RectF
-import android.graphics.Typeface
-import android.text.TextPaint
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.platform.LocalDensity
-import androidx.core.graphics.createBitmap
-import com.google.android.gms.maps.MapsInitializer
-import com.google.android.gms.maps.model.BitmapDescriptor
-import com.google.android.gms.maps.model.BitmapDescriptorFactory
-import org.meshtastic.core.model.Node
-
-private const val CHIP_CORNER_RADIUS_DP = 4f
-private const val CHIP_PADDING_HORIZONTAL_DP = 8f
-private const val CHIP_MIN_WIDTH_DP = 64f
-private const val CHIP_MIN_HEIGHT_DP = 28f
-private const val CHIP_TEXT_SIZE_SP = 14f
-private const val EMOJI_TEXT_SIZE_SP = 32f
-private const val EMOJI_PADDING_DP = 2f
-
-/**
- * Renders a node chip marker as a [BitmapDescriptor] using Canvas — avoids the off-screen ComposeView pipeline in
- * maps-compose's `MarkerComposable`/`rememberComposeBitmapDescriptor` which can crash with "The ComposeView was
- * measured to have a width or height of zero" during subcomposition races (googlemaps/android-maps-compose#875).
- */
-@Composable
-fun rememberNodeChipDescriptor(node: Node): BitmapDescriptor {
- val context = LocalContext.current
- val density = LocalDensity.current.density
- val fontScale = LocalDensity.current.fontScale
- return remember(node.num, node.user.short_name, node.colors, node.isIgnored) {
- ensureMapsInitialized(context)
- renderNodeChipBitmap(node, density, fontScale)
- }
-}
-
-/** Renders an emoji waypoint marker as a [BitmapDescriptor] using Canvas. */
-@Composable
-fun rememberEmojiMarkerDescriptor(codePoint: Int): BitmapDescriptor {
- val context = LocalContext.current
- val density = LocalDensity.current.density
- val fontScale = LocalDensity.current.fontScale
- return remember(codePoint) {
- ensureMapsInitialized(context)
- renderEmojiBitmap(codePoint, density, fontScale)
- }
-}
-
-/**
- * [BitmapDescriptorFactory] only works after the Maps SDK has been initialized, which normally happens when a
- * GoogleMap/MapView is created. These descriptors are built during composition, and on the node-detail inline map the
- * icon is computed before that screen's GoogleMap has loaded the SDK — so [BitmapDescriptorFactory.fromBitmap] crashes
- * with "IBitmapDescriptorFactory is not initialized". Initialize explicitly first; [MapsInitializer.initialize] is
- * synchronous and idempotent, so it is a no-op once the SDK is already up.
- */
-@Suppress("DEPRECATION")
-private fun ensureMapsInitialized(context: Context) {
- MapsInitializer.initialize(context)
-}
-
-private fun renderNodeChipBitmap(node: Node, density: Float, fontScale: Float): BitmapDescriptor {
- val (textColorInt, nodeColorInt) = node.colors
- val scaledDensity = density * fontScale
-
- val textPaint =
- TextPaint(Paint.ANTI_ALIAS_FLAG).apply {
- textSize = CHIP_TEXT_SIZE_SP * scaledDensity
- typeface = Typeface.DEFAULT_BOLD
- color = textColorInt
- textAlign = Paint.Align.CENTER
- isStrikeThruText = node.isIgnored
- }
- val label = node.user.short_name.ifEmpty { "???" }
-
- val textWidth = textPaint.measureText(label)
- val paddingH = CHIP_PADDING_HORIZONTAL_DP * density
- val minWidth = CHIP_MIN_WIDTH_DP * density
- val minHeight = CHIP_MIN_HEIGHT_DP * density
-
- val width = maxOf(minWidth, textWidth + paddingH * 2).toInt()
- val height = minHeight.toInt()
-
- val bitmap = createBitmap(width, height)
- val canvas = Canvas(bitmap)
-
- val bgPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = nodeColorInt }
- val cornerRadius = CHIP_CORNER_RADIUS_DP * density
- canvas.drawRoundRect(RectF(0f, 0f, width.toFloat(), height.toFloat()), cornerRadius, cornerRadius, bgPaint)
-
- val textX = width / 2f
- val textY = (height / 2f) - ((textPaint.descent() + textPaint.ascent()) / 2f)
- canvas.drawText(label, textX, textY, textPaint)
-
- return BitmapDescriptorFactory.fromBitmap(bitmap)
-}
-
-private fun renderEmojiBitmap(codePoint: Int, density: Float, fontScale: Float): BitmapDescriptor {
- val scaledDensity = density * fontScale
- val textPaint =
- TextPaint(Paint.ANTI_ALIAS_FLAG).apply {
- textSize = EMOJI_TEXT_SIZE_SP * scaledDensity
- textAlign = Paint.Align.CENTER
- }
- val emoji = String(Character.toChars(codePoint))
- val padding = EMOJI_PADDING_DP * density
-
- val textWidth = textPaint.measureText(emoji)
- val metrics = textPaint.fontMetrics
- val textHeight = metrics.descent - metrics.ascent
-
- val width = (textWidth + padding * 2).toInt().coerceAtLeast(1)
- val height = (textHeight + padding * 2).toInt().coerceAtLeast(1)
-
- val bitmap = createBitmap(width, height)
- val canvas = Canvas(bitmap)
-
- val textX = width / 2f
- val textY = padding - metrics.ascent
- canvas.drawText(emoji, textX, textY, textPaint)
-
- return BitmapDescriptorFactory.fromBitmap(bitmap)
-}
diff --git a/desktopApp/build.gradle.kts b/desktopApp/build.gradle.kts
index d684fafaef..c54a322059 100644
--- a/desktopApp/build.gradle.kts
+++ b/desktopApp/build.gradle.kts
@@ -233,7 +233,42 @@ compose.desktop {
}
}
+/**
+ * Selects the MapLibre Native desktop backend capability matching the build host.
+ *
+ * MapLibre Compose ships the native renderer as mutually-exclusive per-OS/arch capability variants of
+ * `maplibre-native-bindings-jni`. Gradle can't auto-pick among them, so without an explicit selection no native library
+ * is linked and the map canvas renders black. See https://maplibre.org/maplibre-compose/getting-started/.
+ *
+ * macOS is Apple-Silicon only (Metal); Linux/Windows use OpenGL here for broadest driver/Flatpak/CI compatibility
+ * (Vulkan variants — `*-amd64-vulkan` — are also published if preferred).
+ */
+fun maplibreNativeTarget(): String {
+ val os = System.getProperty("os.name").lowercase()
+ val arch = System.getProperty("os.arch").lowercase()
+ return when {
+ os.contains("mac") || os.contains("darwin") -> {
+ require(arch == "aarch64" || arch == "arm64") {
+ "MapLibre Native desktop ships only a macOS arm64 (Apple Silicon) backend; host arch '$arch' is unsupported."
+ }
+ "macos-aarch64-metal"
+ }
+
+ os.contains("win") -> "windows-amd64-opengl"
+
+ else -> "linux-amd64-opengl"
+ }
+}
+
dependencies {
+ // MapLibre Native renderer for the current desktop host (runtime-only). Selects exactly one OS/arch
+ // capability — required, or the map renders black. See maplibreNativeTarget() above.
+ runtimeOnly("org.maplibre.compose:maplibre-native-bindings-jni:${libs.versions.maplibre.compose.get()}") {
+ capabilities {
+ requireCapability("org.maplibre.compose:maplibre-native-bindings-jni-${maplibreNativeTarget()}")
+ }
+ }
+
implementation(libs.aboutlibraries.core)
implementation(libs.aboutlibraries.compose.m3)
diff --git a/feature/map/src/androidMain/kotlin/org/meshtastic/feature/map/MapOverlaysSupport.kt b/feature/map/src/androidMain/kotlin/org/meshtastic/feature/map/MapOverlaysSupport.kt
new file mode 100644
index 0000000000..c1f6cc6a02
--- /dev/null
+++ b/feature/map/src/androidMain/kotlin/org/meshtastic/feature/map/MapOverlaysSupport.kt
@@ -0,0 +1,20 @@
+/*
+ * Copyright (c) 2026 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.meshtastic.feature.map
+
+/** Android implements the full MapLibre Compose sources/layers API. */
+actual val mapOverlaysSupported: Boolean = true
diff --git a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/MapOverlaysSupport.kt b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/MapOverlaysSupport.kt
new file mode 100644
index 0000000000..dc57f4959d
--- /dev/null
+++ b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/MapOverlaysSupport.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 2026 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.meshtastic.feature.map
+
+/**
+ * Whether the current platform's MapLibre Compose target implements programmatic map sources and layers
+ * (`rememberGeoJsonSource`, `CircleLayer`, `SymbolLayer`, `LineLayer`, `HillshadeLayer`, …).
+ *
+ * As of maplibre-compose 0.13.0 the desktop (JVM) target stubs the **entire** sources/layers API with `TODO()`, so
+ * composing any overlay throws `NotImplementedError` ("An operation is not implemented") and tears down the window. The
+ * base map style still renders natively from its style URI. Every source/layer composition must therefore be guarded
+ * behind this flag; when `false`, only the base map is shown.
+ *
+ * Re-evaluate on each maplibre-compose upgrade — once the desktop target implements layers/sources, set this `true` for
+ * JVM and the guards become no-ops.
+ */
+expect val mapOverlaysSupported: Boolean
diff --git a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/MapScreen.kt b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/MapScreen.kt
index 42d0f6c2e6..dd104fe584 100644
--- a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/MapScreen.kt
+++ b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/MapScreen.kt
@@ -133,7 +133,7 @@ fun MapScreen(
// Location tracking state: 3-mode cycling (Off → Track → TrackBearing → Off)
var isLocationTrackingEnabled by remember { mutableStateOf(false) }
- var bearingUpdate by remember { mutableStateOf(BearingUpdate.TRACK_LOCATION) }
+ var bearingUpdate by remember { mutableStateOf(BearingUpdate.TRACK_AUTOMATIC) }
val locationProvider = rememberLocationProviderOrNull()
val locationState = rememberUserLocationState(locationProvider ?: rememberNullLocationProvider())
val locationAvailable = locationProvider != null
@@ -144,8 +144,13 @@ fun MapScreen(
if (isLocationTrackingEnabled) {
when (bearingUpdate) {
BearingUpdate.IGNORE -> GestureOptions.PositionLocked
+
BearingUpdate.ALWAYS_NORTH -> GestureOptions.ZoomOnly
- BearingUpdate.TRACK_LOCATION -> GestureOptions.ZoomOnly
+
+ BearingUpdate.TRACK_AUTOMATIC,
+ BearingUpdate.TRACK_COURSE,
+ BearingUpdate.TRACK_ORIENTATION,
+ -> GestureOptions.ZoomOnly
}
} else {
GestureOptions.Standard
@@ -220,7 +225,7 @@ fun MapScreen(
LocationTrackingEffect(
locationState = locationState,
enabled = isLocationTrackingEnabled,
- trackBearing = bearingUpdate == BearingUpdate.TRACK_LOCATION,
+ trackBearing = bearingUpdate == BearingUpdate.TRACK_AUTOMATIC,
) {
cameraState.updateFromLocation(updateBearing = bearingUpdate)
}
@@ -239,7 +244,7 @@ fun MapScreen(
modifier = Modifier.align(Alignment.TopEnd).padding(paddingValues),
bearing = cameraState.position.bearing.toFloat(),
onCompassClick = { scope.launch { cameraState.animateTo(cameraState.position.copy(bearing = 0.0)) } },
- followPhoneBearing = isLocationTrackingEnabled && bearingUpdate == BearingUpdate.TRACK_LOCATION,
+ followPhoneBearing = isLocationTrackingEnabled && bearingUpdate == BearingUpdate.TRACK_AUTOMATIC,
activeFilterCount = activeFilterCount,
filterDropdownContent = {
MapFilterDropdown(
@@ -267,27 +272,23 @@ fun MapScreen(
},
layersContent = { OfflineMapContent(styleUri = selectedMapStyle.styleUri, cameraState = cameraState) },
isLocationTrackingEnabled = isLocationTrackingEnabled,
- isTrackingBearing = bearingUpdate == BearingUpdate.TRACK_LOCATION,
+ isTrackingBearing = bearingUpdate == BearingUpdate.TRACK_AUTOMATIC,
onToggleLocationTracking = {
if (!locationAvailable) {
scope.launch { snackbarHostState.showSnackbar(locationUnavailableMsg) }
} else if (!isLocationTrackingEnabled) {
// Off → Track with bearing
- bearingUpdate = BearingUpdate.TRACK_LOCATION
+ bearingUpdate = BearingUpdate.TRACK_AUTOMATIC
isLocationTrackingEnabled = true
} else {
when (bearingUpdate) {
- BearingUpdate.TRACK_LOCATION -> {
+ BearingUpdate.TRACK_AUTOMATIC -> {
// TrackBearing → TrackNorth
bearingUpdate = BearingUpdate.ALWAYS_NORTH
}
- BearingUpdate.ALWAYS_NORTH -> {
- // TrackNorth → Off
- isLocationTrackingEnabled = false
- }
-
- BearingUpdate.IGNORE -> {
+ else -> {
+ // TrackNorth (or any other) → Off
isLocationTrackingEnabled = false
}
}
diff --git a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/InlineMap.kt b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/InlineMap.kt
index 5e4683823e..541bd6ec78 100644
--- a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/InlineMap.kt
+++ b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/InlineMap.kt
@@ -41,6 +41,7 @@ import org.maplibre.spatialk.geojson.Feature
import org.maplibre.spatialk.geojson.FeatureCollection
import org.maplibre.spatialk.geojson.Point
import org.meshtastic.core.model.Node
+import org.meshtastic.feature.map.mapOverlaysSupported
import org.meshtastic.feature.map.model.MapStyle
import org.meshtastic.feature.map.util.MARKER_STROKE_WIDTH
import org.meshtastic.feature.map.util.NODE_MARKER_RADIUS
@@ -84,6 +85,10 @@ fun InlineMap(node: Node, modifier: Modifier = Modifier) {
options =
MapOptions(gestureOptions = GestureOptions.AllDisabled, ornamentOptions = OrnamentOptions.AllDisabled),
) {
+ // Desktop (maplibre-compose 0.13.0) stubs all layers/sources; render base map only. See
+ // [mapOverlaysSupported].
+ if (!mapOverlaysSupported) return@MaplibreMap
+
val source = rememberGeoJsonSource(data = GeoJsonData.Features(nodeFeature))
// Node marker dot
diff --git a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/MaplibreMapContent.kt b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/MaplibreMapContent.kt
index fadaa46f51..887e3003dd 100644
--- a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/MaplibreMapContent.kt
+++ b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/MaplibreMapContent.kt
@@ -79,6 +79,7 @@ import org.maplibre.compose.util.ClickResult
import org.maplibre.spatialk.geojson.Point
import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.model.Node
+import org.meshtastic.feature.map.mapOverlaysSupported
import org.meshtastic.feature.map.util.MARKER_STROKE_WIDTH
import org.meshtastic.feature.map.util.NODE_MARKER_RADIUS
import org.meshtastic.feature.map.util.PRECISION_CIRCLE_STROKE_ALPHA
@@ -157,6 +158,10 @@ fun MaplibreMapContent(
onMapLoadFinished = onMapLoad,
onMapLoadFailed = onMapLoadFail,
) {
+ // MapLibre Compose layers/sources are stubbed on desktop (maplibre-compose 0.13.0); gate overlays off
+ // there so the base map still renders without throwing NotImplementedError. See [mapOverlaysSupported].
+ if (!mapOverlaysSupported) return@MaplibreMap
+
// --- Terrain hillshade overlay ---
if (showHillshade) {
val demSource = rememberRasterDemSource(tiles = TERRAIN_TILES, encoding = RasterDemEncoding.Terrarium)
@@ -181,7 +186,7 @@ fun MaplibreMapContent(
if (locationState != null) {
LocationPuck(
idPrefix = "user-location",
- locationState = locationState,
+ location = locationState.location,
cameraState = cameraState,
colors = LocationPuckDefaults.colors(),
)
diff --git a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/NodeTrackMap.kt b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/NodeTrackMap.kt
index 619245a9f3..0d2a42f237 100644
--- a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/NodeTrackMap.kt
+++ b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/NodeTrackMap.kt
@@ -28,6 +28,7 @@ import org.maplibre.compose.map.GestureOptions
import org.maplibre.compose.map.MapOptions
import org.maplibre.compose.map.MaplibreMap
import org.maplibre.compose.map.OrnamentOptions
+import org.meshtastic.feature.map.mapOverlaysSupported
import org.meshtastic.feature.map.model.MapStyle
import org.meshtastic.feature.map.util.computeBoundingBox
import org.meshtastic.feature.map.util.toGeoPositionOrNull
@@ -79,6 +80,9 @@ fun NodeTrackMap(
options =
MapOptions(gestureOptions = GestureOptions.RotationLocked, ornamentOptions = OrnamentOptions.AllEnabled),
) {
+ // Desktop (maplibre-compose 0.13.0) stubs all layers/sources; render base map only. See [mapOverlaysSupported].
+ if (!mapOverlaysSupported) return@MaplibreMap
+
NodeTrackLayers(
positions = positions,
selectedPositionTime = selectedPositionTime,
diff --git a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/TracerouteMap.kt b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/TracerouteMap.kt
index f1523e757b..064bc33293 100644
--- a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/TracerouteMap.kt
+++ b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/TracerouteMap.kt
@@ -30,6 +30,7 @@ import org.maplibre.compose.map.MaplibreMap
import org.maplibre.compose.map.OrnamentOptions
import org.meshtastic.core.model.Node
import org.meshtastic.core.model.TracerouteOverlay
+import org.meshtastic.feature.map.mapOverlaysSupported
import org.meshtastic.feature.map.model.MapStyle
import org.meshtastic.feature.map.util.computeBoundingBox
import org.meshtastic.feature.map.util.toGeoPositionOrNull
@@ -87,6 +88,9 @@ fun TracerouteMap(
options =
MapOptions(gestureOptions = GestureOptions.RotationLocked, ornamentOptions = OrnamentOptions.AllEnabled),
) {
+ // Desktop (maplibre-compose 0.13.0) stubs all layers/sources; render base map only. See [mapOverlaysSupported].
+ if (!mapOverlaysSupported) return@MaplibreMap
+
TracerouteLayers(
overlay = tracerouteOverlay,
nodePositions = tracerouteNodePositions,
diff --git a/feature/map/src/iosMain/kotlin/org/meshtastic/feature/map/MapOverlaysSupport.kt b/feature/map/src/iosMain/kotlin/org/meshtastic/feature/map/MapOverlaysSupport.kt
new file mode 100644
index 0000000000..2dfb87c2ea
--- /dev/null
+++ b/feature/map/src/iosMain/kotlin/org/meshtastic/feature/map/MapOverlaysSupport.kt
@@ -0,0 +1,20 @@
+/*
+ * Copyright (c) 2026 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.meshtastic.feature.map
+
+/** iOS implements the full MapLibre Compose sources/layers API. */
+actual val mapOverlaysSupported: Boolean = true
diff --git a/feature/map/src/jvmMain/kotlin/org/meshtastic/feature/map/MapOverlaysSupport.kt b/feature/map/src/jvmMain/kotlin/org/meshtastic/feature/map/MapOverlaysSupport.kt
new file mode 100644
index 0000000000..4e1df79af6
--- /dev/null
+++ b/feature/map/src/jvmMain/kotlin/org/meshtastic/feature/map/MapOverlaysSupport.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (c) 2026 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.meshtastic.feature.map
+
+/**
+ * Desktop (JVM) does NOT implement the MapLibre Compose sources/layers API in maplibre-compose 0.13.0 — every
+ * `Layer`/`Source` is stubbed with `TODO()`. Overlays are gated off so the base map renders without crashing.
+ */
+actual val mapOverlaysSupported: Boolean = false
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 55c4eb56ef..b5d2d11e6f 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -47,14 +47,15 @@ compose-multiplatform-material3 = "1.11.0-alpha07"
# AndroidCompose.kt's resolutionStrategy force-aligns these groups to *this* version
# at resolution time, so it is the source of truth for the Android target.
androidx-compose-bom-aligned = "1.11.2"
-# `androidx-compose-material` (M2) is independent of CMP. Pinned because
-# maps-compose-widgets requests `androidx.compose.material:material` without
-# a version (relying on a BOM that we exclude). M2 is frozen at 1.7.8.
+# `androidx-compose-material` (M2) is independent of CMP. Pinned because some
+# transitive consumers request `androidx.compose.material:material` without a
+# version (relying on a BOM that we exclude). M2 is frozen at 1.7.8.
+# Consumed by build-logic AndroidCompose.kt to force-align the M2 material group.
androidx-compose-material = "1.7.8"
jetbrains-adaptive = "1.3.0-beta01"
# MapLibre
-maplibre-compose = "0.12.1"
+maplibre-compose = "0.13.0"
# ML Kit
mlkit-barcode-scanning = "17.3.0"