Skip to main content

slint_mapping/
controller.rs

1//! [`MapController`] — wires a Slint `MapView` instance to a
2//! [`TileSource`].
3//!
4//! The controller:
5//! 1. Owns the active tile source (boxed, so it's swappable at
6//!    runtime).
7//! 2. Holds the canonical camera state (`centre_lon`, `centre_lat`,
8//!    `zoom`) — the Slint properties of the same name are mirrors,
9//!    updated by [`MapController::refresh`].
10//! 3. Translates the `pan` and `zoom-by` callbacks the Slint
11//!    `MapView` fires (during user gestures) back into camera moves.
12//! 4. Pushes the visible-tile model into the `MapView` after every
13//!    refresh.
14
15use crate::camera::{pan as camera_pan, zoom_anchored as camera_zoom_anchored};
16use crate::source::TileSource;
17use crate::viewport::visible_tiles;
18use crate::MapView;
19use crate::Tile as SlintTile;
20use slint::{ComponentHandle, ModelRc, VecModel};
21use std::cell::RefCell;
22use std::rc::Rc;
23
24/// Camera state. Authoritative on the Rust side; the Slint mirror
25/// properties are written by [`MapController::refresh`].
26#[derive(Debug, Clone, Copy)]
27struct Camera {
28    longitude: f64,
29    latitude: f64,
30    zoom: f64,
31}
32
33/// Owns the source + camera; refreshes the `MapView`'s tile model.
34pub struct MapController {
35    inner: Rc<RefCell<Inner>>,
36}
37
38struct Inner {
39    map: slint::Weak<MapView>,
40    source: Box<dyn TileSource>,
41    camera: Camera,
42    tiles_model: Rc<VecModel<SlintTile>>,
43}
44
45impl MapController {
46    /// Wire a `MapView` instance to a tile source. The controller
47    /// immediately computes the visible tiles for the current camera
48    /// and pushes them into the view; subsequent
49    /// [`refresh`](Self::refresh) calls do the same.
50    ///
51    /// Callbacks (`pan`, `zoom-by`) on the `MapView` are bound to
52    /// camera-mutating handlers — user gestures Just Work.
53    pub fn new<S: TileSource + 'static>(map: &MapView, source: S) -> Self {
54        let tiles_model = Rc::new(VecModel::<SlintTile>::from(Vec::new()));
55        map.set_tiles(ModelRc::from(tiles_model.clone()));
56
57        let inner = Rc::new(RefCell::new(Inner {
58            map: map.as_weak(),
59            source: Box::new(source),
60            camera: Camera {
61                longitude: map.get_longitude() as f64,
62                latitude: map.get_latitude() as f64,
63                zoom: map.get_zoom() as f64,
64            },
65            tiles_model,
66        }));
67
68        // Wire the pan callback: pixel delta → camera shift in lat/lon.
69        // All math lives in `crate::camera::pan` so it can be unit-tested.
70        {
71            let inner_cb = Rc::clone(&inner);
72            map.on_pan(move |dx, dy| {
73                {
74                    let mut i = inner_cb.borrow_mut();
75                    let tile_size = i.source.tile_size();
76                    let (lon, lat) = camera_pan(
77                        i.camera.longitude,
78                        i.camera.latitude,
79                        i.camera.zoom,
80                        dx as f64,
81                        dy as f64,
82                        tile_size,
83                    );
84                    i.camera.longitude = lon;
85                    i.camera.latitude = lat;
86                }
87                refresh_inner(&inner_cb);
88            });
89        }
90
91        // Wire the zoom-by callback: scroll delta + anchor → camera
92        // zoom + re-centre so the anchor stays under the cursor.
93        {
94            let inner_cb = Rc::clone(&inner);
95            map.on_zoom_by(move |delta, anchor_x, anchor_y| {
96                {
97                    let mut i = inner_cb.borrow_mut();
98                    let Some(map) = i.map.upgrade() else { return };
99                    let (vw, vh) = logical_size(&map);
100                    let (lon, lat, z) = camera_zoom_anchored(
101                        i.camera.longitude,
102                        i.camera.latitude,
103                        i.camera.zoom,
104                        delta as f64,
105                        anchor_x as f64,
106                        anchor_y as f64,
107                        vw,
108                        vh,
109                        i.source.tile_size(),
110                        i.source.min_zoom(),
111                        i.source.max_zoom(),
112                    );
113                    i.camera.longitude = lon;
114                    i.camera.latitude = lat;
115                    i.camera.zoom = z;
116                }
117                refresh_inner(&inner_cb);
118            });
119        }
120
121        let c = MapController { inner };
122        c.refresh();
123        c
124    }
125
126    /// Recompute the visible tiles for the current camera and push
127    /// them into the view. Called automatically after every gesture;
128    /// call manually after [`set_centre`](Self::set_centre) /
129    /// [`set_zoom`](Self::set_zoom) / a viewport resize.
130    pub fn refresh(&self) {
131        refresh_inner(&self.inner);
132    }
133
134    pub fn set_centre(&self, longitude: f64, latitude: f64) {
135        {
136            let mut i = self.inner.borrow_mut();
137            i.camera.longitude = longitude;
138            i.camera.latitude = latitude;
139        }
140        self.refresh();
141    }
142
143    pub fn set_zoom(&self, zoom: f64) {
144        {
145            let mut i = self.inner.borrow_mut();
146            let min_z = i.source.min_zoom() as f64;
147            let max_z = i.source.max_zoom() as f64;
148            i.camera.zoom = zoom.clamp(min_z, max_z);
149        }
150        self.refresh();
151    }
152
153    /// Swap the underlying tile source at runtime — useful for layer
154    /// toggling. Triggers an immediate refresh.
155    pub fn set_source<S: TileSource + 'static>(&self, source: S) {
156        {
157            let mut i = self.inner.borrow_mut();
158            i.source = Box::new(source);
159        }
160        self.refresh();
161    }
162}
163
164// ---- internal helpers ----
165
166/// Recompute visible tiles + push to model. Free function so closures
167/// can call it without borrowing through `self`.
168fn refresh_inner(inner: &Rc<RefCell<Inner>>) {
169    let i = inner.borrow();
170    let Some(map) = i.map.upgrade() else { return };
171
172    // Mirror camera state out to the Slint properties.
173    map.set_longitude(i.camera.longitude as f32);
174    map.set_latitude(i.camera.latitude as f32);
175    map.set_zoom(i.camera.zoom as f32);
176
177    let (vw, vh) = logical_size(&map);
178    if vw <= 0.0 || vh <= 0.0 {
179        return; // Window not laid out yet.
180    }
181
182    let placed = visible_tiles(
183        i.camera.longitude,
184        i.camera.latitude,
185        i.camera.zoom,
186        vw,
187        vh,
188        i.source.tile_size(),
189    );
190
191    // Replace the entire model. For 30-50 tiles this is cheap; if it
192    // becomes a hot path we can diff against the existing model.
193    let mut new_rows: Vec<SlintTile> = Vec::with_capacity(placed.len());
194    for p in placed {
195        if let Some(image) = i.source.tile(p.key) {
196            new_rows.push(SlintTile {
197                x: p.x,
198                y: p.y,
199                size: p.size,
200                image,
201            });
202        }
203        // If `tile()` returned None, just skip — the source will have
204        // started any background fetch internally; next refresh picks
205        // it up.
206    }
207    // `set_vec` swaps the whole backing storage and emits one reset
208    // event — cheaper than per-row diffing for a full repaint.
209    i.tiles_model.set_vec(new_rows);
210}
211
212/// Read the MapView Window's current logical (DPI-independent) size.
213/// We can't use `get_width`/`get_height` accessors because slint
214/// doesn't generate them for inherited `Window` properties; route
215/// through the underlying `slint::Window` instead.
216fn logical_size(map: &MapView) -> (f64, f64) {
217    let w = map.window();
218    let phys = w.size();
219    let scale = w.scale_factor() as f64;
220    let scale = if scale == 0.0 { 1.0 } else { scale };
221    (phys.width as f64 / scale, phys.height as f64 / scale)
222}