<a name="top"></a>
# slint-mapping
[](https://gregoryraymond.github.io/slint-mapping/)
[](#-license)
[](https://slint.dev)
[](https://www.rust-lang.org)
[](#)
[](https://github.com/gregoryraymond/slint-mapping/actions/workflows/pages.yml)
A slippy-map widget for [Slint](https://slint.dev). 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 β](https://gregoryraymond.github.io/slint-mapping/)**
> 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](#%EF%B8%8F-about)
- [β¨ What works](#-what-works)
- [π§ What's missing](#-whats-missing)
- [π Quick start](#-quick-start)
- [π§± How it's built](#-how-its-built)
- [π Layout](#-layout)
- [π§ͺ Cargo features](#-cargo-features)
- [π€ Contributing](#-contributing)
- [β Support the project](#-support-the-project)
- [π License](#-license)
## πΊοΈ 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 `Layer`s 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`](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:
```rust
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:
```rust
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
| (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.
[](https://buymeacoffee.com/gregoryraymond)
[](https://github.com/sponsors/gregoryraymond)
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](LICENSE-MIT) or
[Apache-2.0](LICENSE-APACHE) at your option.
---
<sub>[β Back to top](#top)</sub>