1
//! Geolocation manager — cross-platform state for the GPS/location surface
2
//! (SUPER_PLAN_2 §1.5 + research/04 §3 + research/08 §6).
3
//!
4
//! Three callers drive it:
5
//!
6
//! - The **layout pass** scans the styled DOM for `GeolocationProbe`
7
//!   NodeTypes. When the first probe appears the framework fires
8
//!   `PermissionDiffEvent::Subscribe(Capability::Geolocation)` and the
9
//!   platform backend starts a native `CLLocationManager` /
10
//!   `LocationManager` / `geoclue` subscription. The reverse on the
11
//!   last probe leaving.
12
//!
13
//! - The **platform backend** (`dll/src/desktop/extra/geolocation/<plat>.rs`)
14
//!   calls `set_latest_fix(...)` whenever the native subscription
15
//!   delivers an update. The manager debounces and records the most
16
//!   recent value; callbacks read it via `CallbackInfo::get_geolocation_fix`.
17
//!
18
//! - **Callbacks** read `latest_fix()` synchronously to render the map
19
//!   centre, decide whether to show "acquiring signal…", etc.
20
//!
21
//! No platform deps; `no_std`-friendly via `alloc::collections::BTreeMap`.
22

            
23
use alloc::collections::btree_map::BTreeMap;
24
use alloc::vec::Vec;
25

            
26
// `LocationFix` + `GeolocationProbeConfig` live in `azul-core` so
27
// `NodeType::GeolocationProbe(GeolocationProbeConfig)` can reference
28
// the config struct without a cyclic dep on `azul-layout`. We re-export
29
// them here for the existing `azul_layout::managers::geolocation::*`
30
// import paths.
31
pub use azul_core::geolocation::{GeolocationProbeConfig, LocationFix};
32

            
33
/// Diff event the layout pass emits when a probe appears or disappears.
34
/// Symmetric to `PermissionDiffEvent` — drives the platform backend's
35
/// native subscribe / release calls.
36
#[derive(Debug, Clone, Copy, PartialEq)]
37
#[repr(C, u8)]
38
pub enum GeolocationDiffEvent {
39
    /// First probe of this config landed in the layout — start a
40
    /// native subscription with these options.
41
    Subscribe { config: GeolocationProbeConfig },
42
    /// Last probe left — stop the native subscription.
43
    Release,
44
    /// Probe config changed without subscriber churn — reconfigure
45
    /// the running subscription in place (e.g. high_accuracy false →
46
    /// true).
47
    Reconfigure { config: GeolocationProbeConfig },
48
}
49

            
50
/// Cross-platform geolocation state. One per `App` (the OS gives us
51
/// a single per-process subscription, not per-window).
52
#[derive(Debug, Clone, PartialEq, Default)]
53
pub struct GeolocationManager {
54
    /// Most recent fix from the platform backend, or `None` until the
55
    /// first native sample arrives (or `None` again after a Release).
56
    pub latest_fix: Option<LocationFix>,
57
    /// Active probe config — set on each Subscribe / Reconfigure,
58
    /// cleared on Release.
59
    pub active_config: Option<GeolocationProbeConfig>,
60
    /// Diff queue drained once per frame by the platform backend.
61
    pending_events: Vec<GeolocationDiffEvent>,
62
    /// Refcount of `GeolocationProbe` nodes currently in the layout.
63
    refcount: u32,
64
}
65

            
66
impl GeolocationManager {
67
2327
    pub fn new() -> Self {
68
2327
        Self::default()
69
2327
    }
70

            
71
2
    pub fn latest_fix(&self) -> Option<LocationFix> {
72
2
        self.latest_fix
73
2
    }
74

            
75
2
    pub fn refcount(&self) -> u32 {
76
2
        self.refcount
77
2
    }
78

            
79
    /// Platform backend writes the freshly-received fix. Returns true
80
    /// if the fix actually advanced (different from the previous one)
81
    /// so the caller can mark the window dirty for relayout.
82
    ///
83
    /// Compared via bit-pattern equality so missing fields (encoded as
84
    /// `f32::NAN`) compare equal — `PartialEq` returns `false` on
85
    /// NaN-vs-NaN, which would make every fix look "changed" even
86
    /// when nothing actually moved.
87
6
    pub fn set_latest_fix(&mut self, fix: LocationFix) -> bool {
88
6
        let changed = match self.latest_fix {
89
3
            Some(prev) => !Self::location_fix_bitwise_eq(&prev, &fix),
90
3
            None => true,
91
        };
92
6
        self.latest_fix = Some(fix);
93
6
        changed
94
6
    }
95

            
96
3
    fn location_fix_bitwise_eq(a: &LocationFix, b: &LocationFix) -> bool {
97
3
        a.latitude_deg.to_bits() == b.latitude_deg.to_bits()
98
1
            && a.longitude_deg.to_bits() == b.longitude_deg.to_bits()
99
1
            && a.accuracy_m.to_bits() == b.accuracy_m.to_bits()
100
1
            && a.altitude_m.to_bits() == b.altitude_m.to_bits()
101
1
            && a.altitude_accuracy_m.to_bits() == b.altitude_accuracy_m.to_bits()
102
1
            && a.heading_deg.to_bits() == b.heading_deg.to_bits()
103
1
            && a.speed_mps.to_bits() == b.speed_mps.to_bits()
104
1
            && a.timestamp_ms == b.timestamp_ms
105
3
    }
106

            
107
    /// Drain queued diff events. Platform backend calls this once per
108
    /// frame.
109
7
    pub fn take_pending_events(&mut self) -> Vec<GeolocationDiffEvent> {
110
7
        core::mem::take(&mut self.pending_events)
111
7
    }
112

            
113
    /// Diff entry point. The layout pass walks the styled DOM for
114
    /// `GeolocationProbe` nodes and feeds each `(config, node_id)`
115
    /// pair to the closure. The manager bumps the refcount, watches
116
    /// for config drift, and enqueues the right Subscribe / Release /
117
    /// Reconfigure event.
118
7
    pub fn diff_layout<F>(&mut self, mut for_each_probe: F)
119
7
    where
120
7
        F: FnMut(&mut dyn FnMut(GeolocationProbeConfig)),
121
    {
122
7
        let mut new_count: u32 = 0;
123
7
        let mut next_config: Option<GeolocationProbeConfig> = None;
124
7
        for_each_probe(&mut |cfg| {
125
6
            new_count += 1;
126
            // First probe's config wins. Subsequent probes that
127
            // disagree are accepted silently — a real app shouldn't
128
            // mount two `GeolocationProbe`s with different configs
129
            // but the framework can't assert that here.
130
6
            if next_config.is_none() {
131
6
                next_config = Some(cfg);
132
6
            }
133
6
        });
134

            
135
7
        let old_count = self.refcount;
136
7
        self.refcount = new_count;
137

            
138
7
        match (old_count, new_count) {
139
4
            (0, n) if n > 0 => {
140
4
                let config = next_config.unwrap_or_default();
141
4
                self.active_config = Some(config);
142
4
                self.pending_events
143
4
                    .push(GeolocationDiffEvent::Subscribe { config });
144
4
            }
145
1
            (m, 0) if m > 0 => {
146
1
                self.active_config = None;
147
1
                self.latest_fix = None;
148
1
                self.pending_events.push(GeolocationDiffEvent::Release);
149
1
            }
150
2
            (m, n) if m > 0 && n > 0 => {
151
                // Both frames have probes. Emit Reconfigure if the
152
                // config actually drifted.
153
2
                let new_config = next_config.unwrap_or_default();
154
2
                if Some(new_config) != self.active_config {
155
1
                    self.active_config = Some(new_config);
156
1
                    self.pending_events
157
1
                        .push(GeolocationDiffEvent::Reconfigure { config: new_config });
158
1
                }
159
            }
160
            _ => {
161
                // 0 → 0 — nothing to do.
162
            }
163
        }
164
7
    }
165
}
166

            
167
// ────────── Async fix channel (platform backend → manager) ────────────
168
//
169
// A native location callback (Android `FusedLocationProvider`
170
// `onLocationResult`, iOS `CLLocationManagerDelegate`) fires on an
171
// arbitrary thread with no handle to the live `GeolocationManager` (it
172
// lives inside the window's `LayoutWindow`). The backend parks each fix
173
// here; the layout pass drains it once per frame via
174
// [`drain_location_fixes`] and applies the latest through
175
// [`GeolocationManager::set_latest_fix`]. Pure Rust — no platform
176
// dependency (SUPER_PLAN_2 §0.5). Mirrors the permission manager's
177
// async-result channel.
178

            
179
static PENDING_FIXES: std::sync::Mutex<Vec<LocationFix>> =
180
    std::sync::Mutex::new(Vec::new());
181

            
182
/// Park a location fix delivered by a platform backend (in the dll).
183
/// Thread-safe; recovers from a poisoned lock so one panicking applier
184
/// can't wedge delivery forever.
185
37
pub fn push_location_fix(fix: LocationFix) {
186
37
    let mut q = PENDING_FIXES.lock().unwrap_or_else(|e| e.into_inner());
187
37
    q.push(fix);
188
37
}
189

            
190
/// Drain every fix parked by [`push_location_fix`], in arrival order.
191
/// Called once per layout pass; the caller applies them through
192
/// [`GeolocationManager::set_latest_fix`] (the last one wins).
193
73
pub fn drain_location_fixes() -> Vec<LocationFix> {
194
73
    let mut q = PENDING_FIXES.lock().unwrap_or_else(|e| e.into_inner());
195
73
    core::mem::take(&mut *q)
196
73
}
197

            
198
#[cfg(test)]
199
mod tests {
200
    use super::*;
201

            
202
5
    fn cfg() -> GeolocationProbeConfig {
203
5
        GeolocationProbeConfig::default()
204
5
    }
205

            
206
1
    fn high_accuracy_cfg() -> GeolocationProbeConfig {
207
1
        GeolocationProbeConfig {
208
1
            high_accuracy: true,
209
1
            ..GeolocationProbeConfig::default()
210
1
        }
211
1
    }
212

            
213
7
    fn fix(lat: f64, lon: f64) -> LocationFix {
214
7
        LocationFix {
215
7
            latitude_deg: lat,
216
7
            longitude_deg: lon,
217
7
            accuracy_m: 10.0,
218
7
            altitude_m: f32::NAN,
219
7
            altitude_accuracy_m: f32::NAN,
220
7
            heading_deg: f32::NAN,
221
7
            speed_mps: f32::NAN,
222
7
            timestamp_ms: 0,
223
7
        }
224
7
    }
225

            
226
    #[test]
227
1
    fn first_probe_emits_subscribe_with_config() {
228
1
        let mut mgr = GeolocationManager::new();
229
1
        mgr.diff_layout(|emit| emit(cfg()));
230
1
        assert_eq!(mgr.refcount(), 1);
231
1
        let events = mgr.take_pending_events();
232
1
        assert_eq!(events.len(), 1);
233
1
        assert!(matches!(events[0], GeolocationDiffEvent::Subscribe { .. }));
234
1
    }
235

            
236
    #[test]
237
1
    fn last_probe_drop_emits_release_and_clears_fix() {
238
1
        let mut mgr = GeolocationManager::new();
239
1
        mgr.diff_layout(|emit| emit(cfg()));
240
1
        mgr.set_latest_fix(fix(37.0, -122.0));
241
1
        let _ = mgr.take_pending_events();
242

            
243
1
        mgr.diff_layout(|_emit| {});
244
1
        assert_eq!(mgr.refcount(), 0);
245
1
        assert_eq!(mgr.latest_fix(), None);
246
1
        let events = mgr.take_pending_events();
247
1
        assert_eq!(events.len(), 1);
248
1
        assert!(matches!(events[0], GeolocationDiffEvent::Release));
249
1
    }
250

            
251
    #[test]
252
1
    fn config_drift_emits_reconfigure() {
253
1
        let mut mgr = GeolocationManager::new();
254
1
        mgr.diff_layout(|emit| emit(cfg()));
255
1
        let _ = mgr.take_pending_events();
256

            
257
1
        mgr.diff_layout(|emit| emit(high_accuracy_cfg()));
258
1
        let events = mgr.take_pending_events();
259
1
        assert_eq!(events.len(), 1);
260
1
        let ev = &events[0];
261
1
        match ev {
262
1
            GeolocationDiffEvent::Reconfigure { config } => {
263
1
                assert!(config.high_accuracy);
264
            }
265
            _ => panic!("expected Reconfigure, got {:?}", ev),
266
        }
267
1
    }
268

            
269
    #[test]
270
1
    fn stable_config_does_not_re_emit() {
271
1
        let mut mgr = GeolocationManager::new();
272
1
        mgr.diff_layout(|emit| emit(cfg()));
273
1
        let _ = mgr.take_pending_events();
274

            
275
        // Same config across frames — no events.
276
1
        mgr.diff_layout(|emit| emit(cfg()));
277
1
        assert!(mgr.take_pending_events().is_empty());
278
1
    }
279

            
280
    #[test]
281
1
    fn set_latest_fix_returns_change_flag() {
282
1
        let mut mgr = GeolocationManager::new();
283
1
        assert!(mgr.set_latest_fix(fix(37.0, -122.0)));
284
1
        assert!(!mgr.set_latest_fix(fix(37.0, -122.0)));
285
1
        assert!(mgr.set_latest_fix(fix(37.7749, -122.4194)));
286
1
    }
287

            
288
    #[test]
289
1
    fn missing_fields_decode_to_none() {
290
1
        let f = fix(0.0, 0.0);
291
1
        assert_eq!(f.altitude(), None);
292
1
        assert_eq!(f.heading(), None);
293
1
        assert_eq!(f.speed(), None);
294
1
    }
295

            
296
    #[test]
297
1
    fn async_fixes_round_trip_through_manager() {
298
        // The channel is process-global; clear any residue first.
299
1
        let _ = drain_location_fixes();
300

            
301
1
        push_location_fix(fix(37.0, -122.0));
302
1
        push_location_fix(fix(48.8566, 2.3522)); // Paris — last wins
303
1
        let drained = drain_location_fixes();
304
1
        assert_eq!(drained.len(), 2, "both parked fixes drain in order");
305
1
        assert_eq!(drained[0].latitude_deg, 37.0);
306
1
        assert_eq!(drained[1].latitude_deg, 48.8566);
307

            
308
        // Applying them reflects in latest_fix() — what the layout pass does.
309
1
        let mut mgr = GeolocationManager::new();
310
3
        for f in &drained {
311
2
            mgr.set_latest_fix(*f);
312
2
        }
313
1
        let got = mgr.latest_fix().expect("a fix was applied");
314
1
        assert_eq!(got.latitude_deg, 48.8566, "the last applied fix wins");
315

            
316
        // A second drain is empty — the queue was taken, not copied.
317
1
        assert!(drain_location_fixes().is_empty());
318
1
    }
319
}