use alloc::collections::btree_map::BTreeMap;
use alloc::vec::Vec;
use azul_core::dom::DomNodeId;
use azul_core::events::{
EventData, EventProvider, EventSource as CoreEventSource, EventType, SyntheticEvent,
};
use azul_core::task::Instant;
pub use azul_core::geolocation::{GeolocationProbeConfig, LocationFix};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(C, u8)]
pub enum GeolocationDiffEvent {
Subscribe { config: GeolocationProbeConfig },
Release,
Reconfigure { config: GeolocationProbeConfig },
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct GeolocationManager {
pub latest_fix: Option<LocationFix>,
pub active_config: Option<GeolocationProbeConfig>,
pending_events: Vec<GeolocationDiffEvent>,
refcount: u32,
pending_event: bool,
pub last_error: Option<LocationError>,
pending_error_event: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct LocationError {
pub code: u32,
pub message: String,
}
impl GeolocationManager {
#[must_use] pub fn new() -> Self {
Self::default()
}
#[must_use] pub const fn latest_fix(&self) -> Option<LocationFix> {
self.latest_fix
}
#[must_use] pub const fn refcount(&self) -> u32 {
self.refcount
}
pub fn set_latest_fix(&mut self, fix: LocationFix) -> bool {
let changed = self
.latest_fix
.is_none_or(|prev| !Self::location_fix_bitwise_eq(&prev, &fix));
self.latest_fix = Some(fix);
if changed {
self.pending_event = true;
self.last_error = None;
}
changed
}
pub fn set_last_error(&mut self, error: LocationError) {
self.last_error = Some(error);
self.pending_error_event = true;
}
pub const fn clear_pending_event(&mut self) {
self.pending_event = false;
self.pending_error_event = false;
}
#[must_use] pub const fn has_active_subscription(&self) -> bool {
self.refcount > 0
}
const fn location_fix_bitwise_eq(a: &LocationFix, b: &LocationFix) -> bool {
a.latitude_deg.to_bits() == b.latitude_deg.to_bits()
&& a.longitude_deg.to_bits() == b.longitude_deg.to_bits()
&& a.accuracy_m.to_bits() == b.accuracy_m.to_bits()
&& a.altitude_m.to_bits() == b.altitude_m.to_bits()
&& a.altitude_accuracy_m.to_bits() == b.altitude_accuracy_m.to_bits()
&& a.heading_deg.to_bits() == b.heading_deg.to_bits()
&& a.speed_mps.to_bits() == b.speed_mps.to_bits()
&& a.timestamp_ms == b.timestamp_ms
}
pub fn take_pending_events(&mut self) -> Vec<GeolocationDiffEvent> {
core::mem::take(&mut self.pending_events)
}
pub fn diff_layout<F>(&mut self, mut for_each_probe: F)
where
F: FnMut(&mut dyn FnMut(GeolocationProbeConfig)),
{
let mut new_count: u32 = 0;
let mut next_config: Option<GeolocationProbeConfig> = None;
for_each_probe(&mut |cfg| {
new_count += 1;
if next_config.is_none() {
next_config = Some(cfg);
}
});
let old_count = self.refcount;
self.refcount = new_count;
match (old_count, new_count) {
(0, n) if n > 0 => {
let config = next_config.unwrap_or_default();
self.active_config = Some(config);
self.pending_events
.push(GeolocationDiffEvent::Subscribe { config });
}
(m, 0) if m > 0 => {
self.active_config = None;
self.latest_fix = None;
self.pending_events.push(GeolocationDiffEvent::Release);
}
(m, n) if m > 0 && n > 0 => {
let new_config = next_config.unwrap_or_default();
if Some(new_config) != self.active_config {
self.active_config = Some(new_config);
self.pending_events
.push(GeolocationDiffEvent::Reconfigure { config: new_config });
}
}
_ => {
}
}
}
}
impl EventProvider for GeolocationManager {
fn get_pending_events(&self, timestamp: Instant) -> Vec<SyntheticEvent> {
let mut events = Vec::new();
if self.pending_event {
events.push(SyntheticEvent::new(
EventType::GeolocationFix,
CoreEventSource::User,
DomNodeId::ROOT,
timestamp.clone(),
EventData::None,
));
}
if self.pending_error_event {
events.push(SyntheticEvent::new(
EventType::GeolocationError,
CoreEventSource::User,
DomNodeId::ROOT,
timestamp,
EventData::None,
));
}
events
}
}
static PENDING_FIXES: std::sync::Mutex<Vec<LocationFix>> =
std::sync::Mutex::new(Vec::new());
pub fn push_location_fix(fix: LocationFix) {
let mut q = PENDING_FIXES.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
q.push(fix);
}
pub fn drain_location_fixes() -> Vec<LocationFix> {
let mut q = PENDING_FIXES.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
core::mem::take(&mut *q)
}
static PENDING_ERRORS: std::sync::Mutex<Vec<LocationError>> =
std::sync::Mutex::new(Vec::new());
pub fn push_location_error(error: LocationError) {
let mut q = PENDING_ERRORS.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
q.push(error);
}
pub fn drain_location_errors() -> Vec<LocationError> {
let mut q = PENDING_ERRORS.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
core::mem::take(&mut *q)
}
#[cfg(test)]
mod tests {
use super::*;
fn cfg() -> GeolocationProbeConfig {
GeolocationProbeConfig::default()
}
fn high_accuracy_cfg() -> GeolocationProbeConfig {
GeolocationProbeConfig {
high_accuracy: true,
..GeolocationProbeConfig::default()
}
}
fn fix(lat: f64, lon: f64) -> LocationFix {
LocationFix {
latitude_deg: lat,
longitude_deg: lon,
accuracy_m: 10.0,
altitude_m: f32::NAN,
altitude_accuracy_m: f32::NAN,
heading_deg: f32::NAN,
speed_mps: f32::NAN,
timestamp_ms: 0,
}
}
#[test]
fn first_probe_emits_subscribe_with_config() {
let mut mgr = GeolocationManager::new();
mgr.diff_layout(|emit| emit(cfg()));
assert_eq!(mgr.refcount(), 1);
let events = mgr.take_pending_events();
assert_eq!(events.len(), 1);
assert!(matches!(events[0], GeolocationDiffEvent::Subscribe { .. }));
}
#[test]
fn last_probe_drop_emits_release_and_clears_fix() {
let mut mgr = GeolocationManager::new();
mgr.diff_layout(|emit| emit(cfg()));
mgr.set_latest_fix(fix(37.0, -122.0));
drop(mgr.take_pending_events());
mgr.diff_layout(|_emit| {});
assert_eq!(mgr.refcount(), 0);
assert_eq!(mgr.latest_fix(), None);
let events = mgr.take_pending_events();
assert_eq!(events.len(), 1);
assert!(matches!(events[0], GeolocationDiffEvent::Release));
}
#[test]
fn config_drift_emits_reconfigure() {
let mut mgr = GeolocationManager::new();
mgr.diff_layout(|emit| emit(cfg()));
drop(mgr.take_pending_events());
mgr.diff_layout(|emit| emit(high_accuracy_cfg()));
let events = mgr.take_pending_events();
assert_eq!(events.len(), 1);
let ev = &events[0];
match ev {
GeolocationDiffEvent::Reconfigure { config } => {
assert!(config.high_accuracy);
}
_ => panic!("expected Reconfigure, got {ev:?}"),
}
}
#[test]
fn stable_config_does_not_re_emit() {
let mut mgr = GeolocationManager::new();
mgr.diff_layout(|emit| emit(cfg()));
drop(mgr.take_pending_events());
mgr.diff_layout(|emit| emit(cfg()));
assert!(mgr.take_pending_events().is_empty());
}
#[test]
fn set_latest_fix_returns_change_flag() {
let mut mgr = GeolocationManager::new();
assert!(mgr.set_latest_fix(fix(37.0, -122.0)));
assert!(!mgr.set_latest_fix(fix(37.0, -122.0)));
assert!(mgr.set_latest_fix(fix(37.7749, -122.4194)));
}
#[test]
fn missing_fields_decode_to_none() {
let f = fix(0.0, 0.0);
assert_eq!(f.altitude(), None);
assert_eq!(f.heading(), None);
assert_eq!(f.speed(), None);
}
#[test]
fn provider_yields_fix_event_then_clears() {
use azul_core::task::{Instant, SystemTick};
let ts = Instant::Tick(SystemTick::new(0));
let mut mgr = GeolocationManager::new();
assert!(mgr.get_pending_events(ts.clone()).is_empty(), "no fix yet");
mgr.set_latest_fix(fix(37.0, -122.0));
let events = mgr.get_pending_events(ts.clone());
assert_eq!(events.len(), 1);
assert_eq!(events[0].event_type, EventType::GeolocationFix);
mgr.clear_pending_event();
assert!(mgr.get_pending_events(ts.clone()).is_empty(), "cleared after dispatch");
mgr.set_latest_fix(fix(37.0, -122.0));
assert!(mgr.get_pending_events(ts).is_empty());
}
#[test]
fn error_channel_and_provider_event() {
use azul_core::task::{Instant, SystemTick};
drop(drain_location_errors());
push_location_error(LocationError { code: 1, message: "denied".into() });
let errs = drain_location_errors();
assert_eq!(errs.len(), 1);
assert!(drain_location_errors().is_empty());
let ts = Instant::Tick(SystemTick::new(0));
let mut mgr = GeolocationManager::new();
mgr.set_last_error(errs[0].clone());
let events = mgr.get_pending_events(ts.clone());
assert_eq!(events.len(), 1);
assert_eq!(
events[0].event_type,
EventType::GeolocationError
);
mgr.clear_pending_event();
assert!(mgr.get_pending_events(ts).is_empty());
mgr.set_latest_fix(fix(1.0, 2.0));
assert!(mgr.last_error.is_none());
}
#[test]
fn subscription_flag_follows_probe_refcount() {
let mut mgr = GeolocationManager::new();
assert!(!mgr.has_active_subscription());
mgr.diff_layout(|emit| emit(cfg()));
assert!(mgr.has_active_subscription());
mgr.diff_layout(|_emit| {});
assert!(!mgr.has_active_subscription());
}
#[test]
#[allow(clippy::float_cmp)] fn async_fixes_round_trip_through_manager() {
drop(drain_location_fixes());
push_location_fix(fix(37.0, -122.0));
push_location_fix(fix(48.8566, 2.3522)); let drained = drain_location_fixes();
assert_eq!(drained.len(), 2, "both parked fixes drain in order");
assert_eq!(drained[0].latitude_deg, 37.0);
assert_eq!(drained[1].latitude_deg, 48.8566);
let mut mgr = GeolocationManager::new();
for f in &drained {
mgr.set_latest_fix(*f);
}
let got = mgr.latest_fix().expect("a fix was applied");
assert_eq!(got.latitude_deg, 48.8566, "the last applied fix wins");
assert!(drain_location_fixes().is_empty());
}
}