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}