slint-mapping 0.1.0

Map framework for Slint β€” interactive map component with tile rendering, panning, zooming, and marker overlays.
Documentation

slint-mapping

Live demo License Slint Rust Platforms Pages

A slippy-map widget for Slint. Pan, scroll-zoom, OSM tiles, markers, route polylines β€” written so a Slint app can drop it in the way you'd drop in a ListView.

🌍 Try the live demo β†’ It renders real OSM tiles in the browser, supports pan and cursor-anchored scroll-zoom, and shows the marker and polyline overlays working. The whole thing compiles to wasm.

Table of contents

πŸ—ΊοΈ About

The library covers the map side of an app: tiles on the screen, the right tiles for where the camera is pointed, gestures that move it around, and overlays drawn on top. Slint handles everything else β€” your search bar, your bottom sheet, your turn-by-turn list. The two fit together because both use Slint's normal for loops and property bindings; the map isn't a black box that breaks the rest of your layout.

The architecture is straightforward:

  • A TileSource trait says "give me the PNG for (x, y, z)". Implementations exist for an offline directory, the live OSM HTTP CDN, and a browser version using gloo-net.
  • A TileCache trait sits in front, layered memory-then-disk so warm tiles paint instantly.
  • A MapController owns the camera and wires user gestures to pan/zoom math.
  • A Router trait with one implementation against OSRM-compatible servers, for turn-by-turn directions.

Each trait is small enough to implement in an afternoon if you want to plug in a different backend (Mapbox, Stadia, a self-hosted Valhalla).

✨ What works

Pan and scroll-zoom, with the zoom anchored on the cursor: the point under the mouse stays put when you zoom in. Fractional zoom levels (10.5, 11.25, etc.) render at the right scale instead of snapping to the nearest integer.

OSM tiles over HTTP with a layered cache β€” in memory in front of a plain-files disk cache. Two fetch workers pull in parallel and a third thread handles PNG decode off the network path. Tiles that 404 or fail to decode get remembered for the session so a single bad key doesn't loop forever.

Marker overlays (icons or filled circles) and polyline overlays (routes, GPS traces, drawn paths). Both live inside Layers that you can show or hide as a unit, the way you would in a GIS app.

A Router trait with one implementation, OsrmRouter, that talks to any OSRM-compatible server. The OSRM project's public demo server works for prototyping; self-host for real traffic.

A small CLI, slint-mapping-prefetch, that warms the on-disk cache for a bounding box across a zoom range so the map works offline once you've taken the app somewhere with no signal.

🚧 What's missing

Pinch-zoom on wasm and Android works through a small platform-specific shim that calls into the same zoom_by callback the mouse-wheel path uses β€” see docs/android-pinch.md for the Android setup and wasm-demo/web/index.html for the wasm one. Native trackpad pinch on macOS / iOS Just Works via ScaleRotateGestureHandler; same handler lights up on Android the day winit-android grows WindowEvent::PinchGesture support.

Polygon fills need upstream improvements to Slint's Path; only stroked polylines work today. Vector tiles (MVT) would be another TileSource impl and nobody has asked for them yet. On-device routing graphs are out β€” OsrmRouter talks HTTP, so bring your own server.

[!NOTE] If any of these are blockers for what you're building, open an issue. The order I tackle them is heavily influenced by whether someone actually wants them.

πŸš€ Quick start

The sample tile bundle shipped with the crate covers the whole world at zoom 0–3, which is enough to confirm the pan/zoom math works without touching the network:

use slint::ComponentHandle;
use slint_mapping::{sources::FileTileSource, MapController, MapView, SAMPLE_TILES_DIR};

fn main() -> Result<(), slint::PlatformError> {
    let map = MapView::new()?;
    let _ctl = MapController::new(&map, FileTileSource::new(SAMPLE_TILES_DIR));
    map.show()?;
    slint::run_event_loop()
}

Swap FileTileSource for OsmTileSource (behind the http feature) when you want live tiles, and set a real User-Agent β€” OSM's tile CDN quietly rejects anything that doesn't identify itself:

use slint_mapping::{
    cache::{FileTileCache, LayeredTileCache},
    sources::OsmTileSource,
};
use std::sync::Arc;

let cache: Arc<dyn slint_mapping::TileCache> = Arc::new(LayeredTileCache::new(
    Box::new(FileTileCache::new("~/.cache/my-app/tiles")),
    vec![],
));
let source = OsmTileSource::new(cache)
    .with_user_agent("my-app/0.1 (+https://example.com)");

If you want the map inside a richer Slint scene rather than as the whole window β€” a search bar pinned to the top, say, or a bottom sheet sliding up over the map β€” use MapEmbed instead of MapView. Same property surface, no Window of its own.

🧱 How it's built

Web Mercator is the only projection. Every common slippy-map provider uses it; the few outliers (national grids, polar projections) can ship as their own crate the day someone needs one.

There is no async runtime in the dependency tree. The library does its own threading on std::thread and posts results back to the UI through slint::invoke_from_event_loop. The two traits an app sees, TileSource and Router, are both synchronous and adapters own their own workers. The point is to drop into any Slint app without forcing tokio or async-std into someone else's build.

Geometry uses bespoke types: (f64, f64) for points, TileKey for tile addresses, Slint structs for overlay shapes. geo-types was on the table but it mostly adds a dep for what amounts to a tuple. If spatial joins or buffer ops start showing up, that calculation changes.

The disk cache writes plain PNGs in the standard {z}/{x}/{y}.png layout. You can seed it by rsyncing an existing OSM mirror, and other slippy-map tools (JOSM, QGIS, mb-util) read what we write. sled and redb were considered and neither earned the dep.

The wasm side is a separate story. ureq doesn't compile to wasm32-unknown-unknown (no sockets in the browser sandbox), and std::thread::spawn panics without cross-origin-isolation headers that GitHub Pages won't send. So there's a parallel WasmOsmTileSource that uses gloo-net and spawn_local. Same trait, different shape underneath.

πŸ“‚ Layout

slint-mapping/
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ projection.rs    Web Mercator
β”‚   β”œβ”€β”€ viewport.rs      visible tiles, lon/lat ↔ viewport px, anchor math
β”‚   β”œβ”€β”€ camera.rs        pan + zoom_anchored
β”‚   β”œβ”€β”€ controller.rs    wires MapView to a TileSource
β”‚   β”œβ”€β”€ source.rs        TileSource trait + TileKey
β”‚   β”œβ”€β”€ sources/         FileTileSource, OsmTileSource, WasmOsmTileSource
β”‚   β”œβ”€β”€ cache.rs         TileCache trait, FileTileCache, LayeredTileCache
β”‚   β”œβ”€β”€ routing.rs       Router trait + Route / Maneuver
β”‚   β”œβ”€β”€ routers/         OsrmRouter
β”‚   β”œβ”€β”€ prefetch.rs      bulk-fetch a bbox across a zoom range
β”‚   └── bin/prefetch.rs  the slint-mapping-prefetch CLI
β”œβ”€β”€ ui/
β”‚   └── map.slint        MapEmbed, MapView, MapPanel,
β”‚                        Tile / Marker / Polyline / Layer structs
β”œβ”€β”€ wasm-demo/           browser demo (workspace member)
└── .github/workflows/
    └── pages.yml        builds and publishes the demo on push to main

πŸ§ͺ Cargo features

Feature What it turns on Extra deps
(default) Offline tile sources, rendering none
http OsmTileSource, prefetch::region ureq, image
routing OsrmRouter, routing types ureq, serde, serde_json
wasm WasmOsmTileSource (wasm32 target only) gloo-net, wasm-bindgen, others
viewer the viewer and map_page examples Slint's desktop backend + Skia

🀝 Contributing

Issues and PRs welcome. If you've got an opinion on what should land next β€” pinch-zoom, vector tiles, polygon fills, an offline router, something I haven't thought of β€” say so on the issue tracker. There's no roadmap document; the next thing built is usually the next thing someone actually needs.

If you're poking at the internals, the tests/ directory is the honest documentation: every non-obvious bit of math has a test that explains why it's the way it is.

β˜• Support the project

If slint-mapping has been useful β€” saved you a weekend of writing your own tile fetcher, made a demo possible, anything β€” a coffee keeps weekend hacking time available for it.

Buy me a coffee GitHub Sponsors

One-offs are great. If you're a company shipping this in a product, a small recurring sponsorship via GitHub Sponsors is more useful β€” it gives me a rough sense of how many real users depend on it, which affects how much I'm willing to break in a refactor.

πŸ“ƒ License

Dual-licensed under either MIT or Apache-2.0 at your option.


↑ Back to top