Maps
Introduction
MapWidget is a slippy-map widget: a Web-Mercator tile grid you can pan and
zoom, with hooks for tap-to-pin and viewport changes, and projection helpers to
place your own markers. It is the AzulMaps (P3) goal app. Like the other
widgets it owns its state in a RefAny dataset that survives relayout via a
merge callback - you just create it, configure it, and call .dom().
Creating a map
use azul::widgets::{MapWidget, MapViewport, MapTileLayer};
let map = MapWidget::create(MapTileLayer::default())
.with_viewport(MapViewport {
centre_lat_deg: 37.7749, // San Francisco
centre_lon_deg: -122.4194,
zoom: 11.0,
bearing_deg: 0.0,
pitch_deg: 0.0,
})
.dom();
MapTileLayer configures the tile source (and attribution). The widget computes
the visible XYZ tile grid from the viewport and renders one GPU-translated tile
div per tile. Pan (drag) and pinch-zoom are handled by the widget itself -
no wiring needed.
Hooks: observing taps and the viewport
Two backreference-DI hooks (see architecture) let your app react without globals - the callback receives the data and writes back to your state:
with_on_pin_tap(data, cb)- fires when the user taps the map (a press + release with no drag), with the tapped [MapLatLon].with_on_viewport_changed(data, cb)- fires on pan/zoom, with the new [MapViewport] (persist it, sync a minimap, etc.).
use azul::dom::MapPinTapCallback;
use azul::option::OptionRefAny;
let map = MapWidget::create(layer)
.with_viewport(viewport)
.with_on_pin_tap(
state.clone(),
MapPinTapCallback { cb: on_pin_tap, callable: OptionRefAny::None },
)
.dom();
extern "C" fn on_pin_tap(mut data: RefAny, _info: CallbackInfo, coord: MapLatLon) -> Update {
if let Some(mut s) = data.downcast_mut::<MyState>() {
s.pins.push((coord.lat_deg, coord.lon_deg)); // drop a pin
}
Update::RefreshDom
}
The widget does the tap-vs-drag detection and the projection for you, so you just store the lat/lon.
Projection: placing your own markers
To draw markers/overlays on top of the map, convert between lat/lon and screen pixels with the two static helpers (small-angle Web Mercator):
// lat/lon -> screen position (place a pin div here):
let p = MapWidget::px_at_latlon(viewport, MapLatLon { lat_deg, lon_deg }, container_size);
// screen position -> lat/lon (inverse):
let coord = MapWidget::latlon_at_px(viewport, px, container_size);
container_size is the map node's pixel size (e.g. from
CallbackInfo::get_hit_node_rect().size). Re-projecting each layout makes your
pins track the map as it pans and zooms. examples/azul-maps uses exactly this
to render dropped pins + a coordinate readout.
Sharing connections and threads
By default each tile opens its own connection and downloads on its own thread.
That is fine for a map shown now and then. An app that keeps a map open, or
shows several, can give the maps a shared HttpClient (open connections to the
tile server are reused instead of a TLS handshake per tile) and a shared
ThreadPool (a fixed number of download threads).
That is configuration, so it does not belong in layout(). Create the pools
once with your app state, and hand them to the map in its on_mount hook. The
hook runs when the map enters the tree, receives the map's current MapSetup
and returns the one to use:
use azul::dom::MapMountCallback;
use azul::http::{HttpClient, HttpClientConfig};
use azul::task::ThreadPool;
use azul::widgets::MapSetup;
struct MyState {
viewport: MapViewport,
tiles: HttpClient, // HttpClient::create(HttpClientConfig::create())
workers: ThreadPool, // ThreadPool::create(4)
}
// in layout(): UI only
let map = MapWidget::create(layer)
.with_viewport(viewport)
.with_on_mount(
state.clone(),
MapMountCallback { cb: on_map_mount, callable: OptionRefAny::None },
)
.dom();
extern "C" fn on_map_mount(mut data: RefAny, _info: CallbackInfo, setup: MapSetup) -> MapSetup {
let Some(s) = data.downcast_ref::<MyState>() else { return setup };
setup
.with_http_client(s.tiles.clone())
.with_thread_pool(s.workers.clone())
.with_max_in_flight(8) // at most 8 tiles downloading at once
}
The map keeps its setup through every relayout (the merge callback carries it)
and drops its clones when it leaves the tree. If the map is mounted again, the
hook runs again with the setup it returned last time. examples/c/async.c
shows the same flow in C.
Geolocation
To show „you are here“, drop a Dom::create_geolocation_probe(...) into the map
subtree and read the fix with CallbackInfo::get_location_fix - see
Device Input.
Notes
- Tile fetch + MVT decode + MapCSS-to-SVG are behind the
map-tilesCargo feature; with it off, the grid + viewport bookkeeping still work (the demo shows the projection math without live tiles). - The tile cache lives in the widget's dataset
RefAnyand is carried across relayout by aDatasetMergeCallback, so panning doesn't refetch everything.
See also
- widgets - the widget model.
- callbacks - the hook +
RefAnymechanism. - Device Input - geolocation for the „you are here“ dot.