1use alloc::collections::btree_map::BTreeMap;
24use alloc::vec::Vec;
25
26use azul_core::dom::DomNodeId;
27use azul_core::events::{
28 EventData, EventProvider, EventSource as CoreEventSource, EventType, SyntheticEvent,
29};
30use azul_core::task::Instant;
31
32pub use azul_core::geolocation::{GeolocationProbeConfig, LocationFix};
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43#[repr(C, u8)]
44pub enum GeolocationDiffEvent {
45 Subscribe { config: GeolocationProbeConfig },
48 Release,
50 Reconfigure { config: GeolocationProbeConfig },
54}
55
56#[derive(Debug, Clone, PartialEq, Default)]
59pub struct GeolocationManager {
60 pub latest_fix: Option<LocationFix>,
63 pub active_config: Option<GeolocationProbeConfig>,
66 pending_events: Vec<GeolocationDiffEvent>,
68 refcount: u32,
70 pending_event: bool,
76 pub last_error: Option<LocationError>,
79 pending_error_event: bool,
82}
83
84#[derive(Debug, Clone, PartialEq, Eq, Default)]
91pub struct LocationError {
92 pub code: u32,
94 pub message: String,
96}
97
98impl GeolocationManager {
99 #[must_use] pub fn new() -> Self {
100 Self::default()
101 }
102
103 #[must_use] pub const fn latest_fix(&self) -> Option<LocationFix> {
104 self.latest_fix
105 }
106
107 #[must_use] pub const fn refcount(&self) -> u32 {
108 self.refcount
109 }
110
111 pub fn set_latest_fix(&mut self, fix: LocationFix) -> bool {
120 let changed = self
121 .latest_fix
122 .is_none_or(|prev| !Self::location_fix_bitwise_eq(&prev, &fix));
123 self.latest_fix = Some(fix);
124 if changed {
125 self.pending_event = true;
126 self.last_error = None;
128 }
129 changed
130 }
131
132 pub fn set_last_error(&mut self, error: LocationError) {
136 self.last_error = Some(error);
137 self.pending_error_event = true;
138 }
139
140 pub const fn clear_pending_event(&mut self) {
143 self.pending_event = false;
144 self.pending_error_event = false;
145 }
146
147 #[must_use] pub const fn has_active_subscription(&self) -> bool {
152 self.refcount > 0
153 }
154
155 const fn location_fix_bitwise_eq(a: &LocationFix, b: &LocationFix) -> bool {
156 a.latitude_deg.to_bits() == b.latitude_deg.to_bits()
157 && a.longitude_deg.to_bits() == b.longitude_deg.to_bits()
158 && a.accuracy_m.to_bits() == b.accuracy_m.to_bits()
159 && a.altitude_m.to_bits() == b.altitude_m.to_bits()
160 && a.altitude_accuracy_m.to_bits() == b.altitude_accuracy_m.to_bits()
161 && a.heading_deg.to_bits() == b.heading_deg.to_bits()
162 && a.speed_mps.to_bits() == b.speed_mps.to_bits()
163 && a.timestamp_ms == b.timestamp_ms
164 }
165
166 pub fn take_pending_events(&mut self) -> Vec<GeolocationDiffEvent> {
169 core::mem::take(&mut self.pending_events)
170 }
171
172 pub fn diff_layout<F>(&mut self, mut for_each_probe: F)
178 where
179 F: FnMut(&mut dyn FnMut(GeolocationProbeConfig)),
180 {
181 let mut new_count: u32 = 0;
182 let mut next_config: Option<GeolocationProbeConfig> = None;
183 for_each_probe(&mut |cfg| {
184 new_count += 1;
185 if next_config.is_none() {
190 next_config = Some(cfg);
191 }
192 });
193
194 let old_count = self.refcount;
195 self.refcount = new_count;
196
197 match (old_count, new_count) {
198 (0, n) if n > 0 => {
199 let config = next_config.unwrap_or_default();
200 self.active_config = Some(config);
201 self.pending_events
202 .push(GeolocationDiffEvent::Subscribe { config });
203 }
204 (m, 0) if m > 0 => {
205 self.active_config = None;
206 self.latest_fix = None;
207 self.pending_events.push(GeolocationDiffEvent::Release);
208 }
209 (m, n) if m > 0 && n > 0 => {
210 let new_config = next_config.unwrap_or_default();
213 if Some(new_config) != self.active_config {
214 self.active_config = Some(new_config);
215 self.pending_events
216 .push(GeolocationDiffEvent::Reconfigure { config: new_config });
217 }
218 }
219 _ => {
220 }
222 }
223 }
224}
225
226impl EventProvider for GeolocationManager {
227 fn get_pending_events(&self, timestamp: Instant) -> Vec<SyntheticEvent> {
233 let mut events = Vec::new();
234 if self.pending_event {
235 events.push(SyntheticEvent::new(
236 EventType::GeolocationFix,
237 CoreEventSource::User,
238 DomNodeId::ROOT,
239 timestamp.clone(),
240 EventData::None,
241 ));
242 }
243 if self.pending_error_event {
244 events.push(SyntheticEvent::new(
245 EventType::GeolocationError,
246 CoreEventSource::User,
247 DomNodeId::ROOT,
248 timestamp,
249 EventData::None,
250 ));
251 }
252 events
253 }
254}
255
256static PENDING_FIXES: std::sync::Mutex<Vec<LocationFix>> =
269 std::sync::Mutex::new(Vec::new());
270
271pub fn push_location_fix(fix: LocationFix) {
275 let mut q = PENDING_FIXES.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
276 q.push(fix);
277}
278
279pub fn drain_location_fixes() -> Vec<LocationFix> {
283 let mut q = PENDING_FIXES.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
284 core::mem::take(&mut *q)
285}
286
287static PENDING_ERRORS: std::sync::Mutex<Vec<LocationError>> =
292 std::sync::Mutex::new(Vec::new());
293
294pub fn push_location_error(error: LocationError) {
297 let mut q = PENDING_ERRORS.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
298 q.push(error);
299}
300
301pub fn drain_location_errors() -> Vec<LocationError> {
303 let mut q = PENDING_ERRORS.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
304 core::mem::take(&mut *q)
305}
306
307#[cfg(test)]
308mod tests {
309 use super::*;
310
311 fn cfg() -> GeolocationProbeConfig {
312 GeolocationProbeConfig::default()
313 }
314
315 fn high_accuracy_cfg() -> GeolocationProbeConfig {
316 GeolocationProbeConfig {
317 high_accuracy: true,
318 ..GeolocationProbeConfig::default()
319 }
320 }
321
322 fn fix(lat: f64, lon: f64) -> LocationFix {
323 LocationFix {
324 latitude_deg: lat,
325 longitude_deg: lon,
326 accuracy_m: 10.0,
327 altitude_m: f32::NAN,
328 altitude_accuracy_m: f32::NAN,
329 heading_deg: f32::NAN,
330 speed_mps: f32::NAN,
331 timestamp_ms: 0,
332 }
333 }
334
335 #[test]
336 fn first_probe_emits_subscribe_with_config() {
337 let mut mgr = GeolocationManager::new();
338 mgr.diff_layout(|emit| emit(cfg()));
339 assert_eq!(mgr.refcount(), 1);
340 let events = mgr.take_pending_events();
341 assert_eq!(events.len(), 1);
342 assert!(matches!(events[0], GeolocationDiffEvent::Subscribe { .. }));
343 }
344
345 #[test]
346 fn last_probe_drop_emits_release_and_clears_fix() {
347 let mut mgr = GeolocationManager::new();
348 mgr.diff_layout(|emit| emit(cfg()));
349 mgr.set_latest_fix(fix(37.0, -122.0));
350 drop(mgr.take_pending_events());
351
352 mgr.diff_layout(|_emit| {});
353 assert_eq!(mgr.refcount(), 0);
354 assert_eq!(mgr.latest_fix(), None);
355 let events = mgr.take_pending_events();
356 assert_eq!(events.len(), 1);
357 assert!(matches!(events[0], GeolocationDiffEvent::Release));
358 }
359
360 #[test]
361 fn config_drift_emits_reconfigure() {
362 let mut mgr = GeolocationManager::new();
363 mgr.diff_layout(|emit| emit(cfg()));
364 drop(mgr.take_pending_events());
365
366 mgr.diff_layout(|emit| emit(high_accuracy_cfg()));
367 let events = mgr.take_pending_events();
368 assert_eq!(events.len(), 1);
369 let ev = &events[0];
370 match ev {
371 GeolocationDiffEvent::Reconfigure { config } => {
372 assert!(config.high_accuracy);
373 }
374 _ => panic!("expected Reconfigure, got {ev:?}"),
375 }
376 }
377
378 #[test]
379 fn stable_config_does_not_re_emit() {
380 let mut mgr = GeolocationManager::new();
381 mgr.diff_layout(|emit| emit(cfg()));
382 drop(mgr.take_pending_events());
383
384 mgr.diff_layout(|emit| emit(cfg()));
386 assert!(mgr.take_pending_events().is_empty());
387 }
388
389 #[test]
390 fn set_latest_fix_returns_change_flag() {
391 let mut mgr = GeolocationManager::new();
392 assert!(mgr.set_latest_fix(fix(37.0, -122.0)));
393 assert!(!mgr.set_latest_fix(fix(37.0, -122.0)));
394 assert!(mgr.set_latest_fix(fix(37.7749, -122.4194)));
395 }
396
397 #[test]
398 fn missing_fields_decode_to_none() {
399 let f = fix(0.0, 0.0);
400 assert_eq!(f.altitude(), None);
401 assert_eq!(f.heading(), None);
402 assert_eq!(f.speed(), None);
403 }
404
405 #[test]
406 fn provider_yields_fix_event_then_clears() {
407 use azul_core::task::{Instant, SystemTick};
408
409 let ts = Instant::Tick(SystemTick::new(0));
410 let mut mgr = GeolocationManager::new();
411 assert!(mgr.get_pending_events(ts.clone()).is_empty(), "no fix yet");
412
413 mgr.set_latest_fix(fix(37.0, -122.0));
414 let events = mgr.get_pending_events(ts.clone());
415 assert_eq!(events.len(), 1);
416 assert_eq!(events[0].event_type, EventType::GeolocationFix);
417
418 mgr.clear_pending_event();
419 assert!(mgr.get_pending_events(ts.clone()).is_empty(), "cleared after dispatch");
420
421 mgr.set_latest_fix(fix(37.0, -122.0));
423 assert!(mgr.get_pending_events(ts).is_empty());
424 }
425
426 #[test]
427 fn error_channel_and_provider_event() {
428 use azul_core::task::{Instant, SystemTick};
429
430 drop(drain_location_errors());
431 push_location_error(LocationError { code: 1, message: "denied".into() });
432 let errs = drain_location_errors();
433 assert_eq!(errs.len(), 1);
434 assert!(drain_location_errors().is_empty());
435
436 let ts = Instant::Tick(SystemTick::new(0));
437 let mut mgr = GeolocationManager::new();
438 mgr.set_last_error(errs[0].clone());
439 let events = mgr.get_pending_events(ts.clone());
440 assert_eq!(events.len(), 1);
441 assert_eq!(
442 events[0].event_type,
443 EventType::GeolocationError
444 );
445 mgr.clear_pending_event();
446 assert!(mgr.get_pending_events(ts).is_empty());
447
448 mgr.set_latest_fix(fix(1.0, 2.0));
450 assert!(mgr.last_error.is_none());
451 }
452
453 #[test]
454 fn subscription_flag_follows_probe_refcount() {
455 let mut mgr = GeolocationManager::new();
456 assert!(!mgr.has_active_subscription());
457 mgr.diff_layout(|emit| emit(cfg()));
458 assert!(mgr.has_active_subscription());
459 mgr.diff_layout(|_emit| {});
460 assert!(!mgr.has_active_subscription());
461 }
462
463 #[test]
464 #[allow(clippy::float_cmp)] fn async_fixes_round_trip_through_manager() {
466 drop(drain_location_fixes());
468
469 push_location_fix(fix(37.0, -122.0));
470 push_location_fix(fix(48.8566, 2.3522)); let drained = drain_location_fixes();
472 assert_eq!(drained.len(), 2, "both parked fixes drain in order");
473 assert_eq!(drained[0].latitude_deg, 37.0);
474 assert_eq!(drained[1].latitude_deg, 48.8566);
475
476 let mut mgr = GeolocationManager::new();
478 for f in &drained {
479 mgr.set_latest_fix(*f);
480 }
481 let got = mgr.latest_fix().expect("a fix was applied");
482 assert_eq!(got.latitude_deg, 48.8566, "the last applied fix wins");
483
484 assert!(drain_location_fixes().is_empty());
486 }
487}