commit 267c083d8889c91848f030da6ee8726c56ee3486
parent d6793efde079a565c9c964e630c3be6034658d72
Author: triesap <tyson@radroots.org>
Date: Sun, 22 Mar 2026 11:47:50 +0000
desktop: add async reverse lookup backend
- add a dedicated desktop reverse-lookup tracker that runs geocoder queries off the ui thread
- expose begin and poll hooks from the desktop backend without changing the current home lookup ui yet
- keep query results typed and one-shot so the core consumer can adopt the new poll contract cleanly
- cover the tracker behavior with focused tests and verify the full desktop crate still builds
Diffstat:
2 files changed, 158 insertions(+), 2 deletions(-)
diff --git a/crates/desktop/src/main.rs b/crates/desktop/src/main.rs
@@ -13,7 +13,8 @@ use radroots_app_core::{
ImportActionState, RadrootsApp, RadrootsAppBackend, RadrootsLocationCountry,
RadrootsLocationPoint, RadrootsLocationResolverError, RadrootsLocationReverseOptions,
RadrootsOfflineGeocoderPlatform, RadrootsOfflineGeocoderState,
- RadrootsOfflineGeocoderUnavailableKind, RadrootsResolvedLocation, SetupActionState,
+ RadrootsOfflineGeocoderUnavailableKind, RadrootsResolvedLocation,
+ RadrootsReverseLocationLookupResult, SetupActionState,
};
#[cfg(target_os = "macos")]
use radroots_identity::RadrootsIdentity;
@@ -28,8 +29,10 @@ use std::sync::Arc;
use zeroize::Zeroizing;
mod offline_geocoder;
+mod reverse_lookup;
use offline_geocoder::DesktopOfflineGeocoder;
+use reverse_lookup::DesktopReverseLookup;
const RADROOTS_DESKTOP_ICON_BYTES: &[u8] = include_bytes!("../assets/icons/radroots-logo.ico");
@@ -59,6 +62,7 @@ fn desktop_icon() -> Option<egui::IconData> {
struct DesktopBackend {
offline_geocoder: DesktopOfflineGeocoder,
+ reverse_lookup: DesktopReverseLookup,
}
impl DesktopBackend {
@@ -83,7 +87,10 @@ impl DesktopBackend {
"desktop offline geocoder initialization is only wired for macos",
));
- Self { offline_geocoder }
+ Self {
+ offline_geocoder,
+ reverse_lookup: DesktopReverseLookup::new(),
+ }
}
#[cfg(target_os = "macos")]
@@ -317,6 +324,36 @@ impl RadrootsAppBackend for DesktopBackend {
}
}
+ fn request_reverse_location_lookup(
+ &self,
+ point: RadrootsLocationPoint,
+ options: Option<RadrootsLocationReverseOptions>,
+ ) -> Result<(), RadrootsLocationResolverError> {
+ #[cfg(target_os = "macos")]
+ {
+ let app_data_root = Self::app_data_root()
+ .map_err(|message| RadrootsLocationResolverError::QueryFailed { message })?;
+ return self.reverse_lookup.begin(
+ app_data_root,
+ self.offline_geocoder.current_state(),
+ point,
+ options,
+ );
+ }
+
+ #[cfg(not(target_os = "macos"))]
+ {
+ let _ = (point, options);
+ Err(RadrootsLocationResolverError::Unsupported)
+ }
+ }
+
+ fn poll_reverse_location_lookup_result(
+ &self,
+ ) -> Result<Option<RadrootsReverseLocationLookupResult>, String> {
+ Ok(self.reverse_lookup.take_update())
+ }
+
fn list_location_countries(
&self,
) -> Result<Vec<RadrootsLocationCountry>, RadrootsLocationResolverError> {
diff --git a/crates/desktop/src/reverse_lookup.rs b/crates/desktop/src/reverse_lookup.rs
@@ -0,0 +1,119 @@
+use crate::offline_geocoder;
+use radroots_app_core::{
+ RadrootsLocationPoint, RadrootsLocationResolverError, RadrootsLocationReverseOptions,
+ RadrootsOfflineGeocoderState, RadrootsReverseLocationLookupResult,
+};
+#[cfg(target_os = "macos")]
+use std::path::PathBuf;
+use std::sync::atomic::{AtomicBool, Ordering};
+use std::sync::{Arc, Mutex};
+
+#[derive(Clone, Default)]
+pub(crate) struct DesktopReverseLookup {
+ result: Arc<Mutex<Option<RadrootsReverseLocationLookupResult>>>,
+ changed: Arc<AtomicBool>,
+ pending: Arc<AtomicBool>,
+}
+
+impl DesktopReverseLookup {
+ pub(crate) fn new() -> Self {
+ Self::default()
+ }
+
+ #[cfg(target_os = "macos")]
+ pub(crate) fn begin(
+ &self,
+ app_data_root: PathBuf,
+ geocoder_state: RadrootsOfflineGeocoderState,
+ point: RadrootsLocationPoint,
+ options: Option<RadrootsLocationReverseOptions>,
+ ) -> Result<(), RadrootsLocationResolverError> {
+ if self.pending.swap(true, Ordering::AcqRel) {
+ return Err(RadrootsLocationResolverError::QueryFailed {
+ message: "offline location query is already running".to_owned(),
+ });
+ }
+
+ if let Ok(mut slot) = self.result.lock() {
+ *slot = None;
+ }
+
+ let result = Arc::clone(&self.result);
+ let changed = Arc::clone(&self.changed);
+ let pending = Arc::clone(&self.pending);
+ std::thread::spawn(move || {
+ let lookup_result = offline_geocoder::reverse_location(
+ app_data_root.as_path(),
+ &geocoder_state,
+ point,
+ options,
+ );
+ if let Ok(mut slot) = result.lock() {
+ *slot = Some(lookup_result);
+ changed.store(true, Ordering::Release);
+ }
+ pending.store(false, Ordering::Release);
+ });
+
+ Ok(())
+ }
+
+ #[cfg(not(target_os = "macos"))]
+ pub(crate) fn begin(
+ &self,
+ _app_data_root: std::path::PathBuf,
+ _geocoder_state: RadrootsOfflineGeocoderState,
+ _point: RadrootsLocationPoint,
+ _options: Option<RadrootsLocationReverseOptions>,
+ ) -> Result<(), RadrootsLocationResolverError> {
+ Err(RadrootsLocationResolverError::Unsupported)
+ }
+
+ pub(crate) fn take_update(&self) -> Option<RadrootsReverseLocationLookupResult> {
+ if !self.changed.swap(false, Ordering::AcqRel) {
+ return None;
+ }
+
+ match self.result.lock() {
+ Ok(mut slot) => slot.take(),
+ Err(_) => Some(Err(RadrootsLocationResolverError::QueryFailed {
+ message: "desktop reverse lookup result lock poisoned".to_owned(),
+ })),
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use radroots_app_core::RadrootsResolvedLocation;
+
+ fn sample_result() -> RadrootsReverseLocationLookupResult {
+ Ok(vec![RadrootsResolvedLocation {
+ id: 7,
+ name: "example".to_owned(),
+ admin1_id: None,
+ admin1_name: None,
+ country_id: "US".to_owned(),
+ country_name: Some("United States".to_owned()),
+ point: RadrootsLocationPoint { lat: 1.0, lng: 2.0 },
+ }])
+ }
+
+ #[test]
+ fn take_update_is_none_until_tracker_changes() {
+ let tracker = DesktopReverseLookup::new();
+
+ assert_eq!(tracker.take_update(), None);
+ }
+
+ #[test]
+ fn take_update_returns_queued_result_once() {
+ let tracker = DesktopReverseLookup::new();
+ *tracker.result.lock().unwrap() = Some(sample_result());
+ tracker.changed.store(true, Ordering::Release);
+
+ assert!(matches!(tracker.take_update(), Some(Ok(results)) if results.len() == 1));
+ assert_eq!(tracker.take_update(), None);
+ }
+}