cranpose_services/
image_picker.rs1use crate::file_picker::{default_file_picker, FileFilter, FilePickerOptions, PickerFuture};
12use crate::registry::ServiceRegistry;
13use cranpose_core::compositionLocalOfWithPolicy;
14use cranpose_core::CompositionLocal;
15use cranpose_core::CompositionLocalProvider;
16use cranpose_macros::composable;
17use std::cell::RefCell;
18use std::sync::Arc;
19
20pub const IMAGE_EXTENSIONS: &[&str] = &["jpg", "jpeg", "png", "webp", "bmp", "heic", "heif"];
22
23#[derive(thiserror::Error, Debug)]
24pub enum ImagePickerError {
25 #[error("image picking is not supported on this platform")]
26 Unsupported,
27 #[error("failed to pick an image: {0}")]
28 Failed(String),
29}
30
31#[derive(Clone, Copy, PartialEq, Eq, Debug)]
33pub enum ImageSource {
34 PhotoLibrary,
36 Camera,
38}
39
40pub trait ImagePicker: Send + Sync {
42 fn pick_image(
45 &self,
46 source: ImageSource,
47 ) -> PickerFuture<Result<Option<Vec<u8>>, ImagePickerError>>;
48}
49
50pub type ImagePickerRef = Arc<dyn ImagePicker>;
51
52static PLATFORM_IMAGE_PICKER: ServiceRegistry<dyn ImagePicker> = ServiceRegistry::new();
53
54pub fn set_platform_image_picker(picker: ImagePickerRef) {
57 PLATFORM_IMAGE_PICKER.set(picker);
58}
59
60pub fn clear_platform_image_picker() {
62 PLATFORM_IMAGE_PICKER.clear();
63}
64
65fn registered_platform_image_picker() -> Option<ImagePickerRef> {
66 PLATFORM_IMAGE_PICKER.get_or_warn("image picker")
67}
68
69struct PlatformImagePicker;
70
71impl ImagePicker for PlatformImagePicker {
72 fn pick_image(
73 &self,
74 source: ImageSource,
75 ) -> PickerFuture<Result<Option<Vec<u8>>, ImagePickerError>> {
76 if let Some(picker) = registered_platform_image_picker() {
77 return picker.pick_image(source);
78 }
79 if source == ImageSource::Camera {
82 return Box::pin(async { Err(ImagePickerError::Unsupported) });
83 }
84 Box::pin(async {
85 let picker = default_file_picker();
86 let options = FilePickerOptions::default()
87 .with_title("Choose image")
88 .with_filter(FileFilter::new("Images", IMAGE_EXTENSIONS));
89 match picker.pick_file(options).await {
90 Ok(Some(entry)) => match entry.read_bytes().await {
91 Ok(bytes) => Ok(Some(bytes)),
92 Err(error) => Err(ImagePickerError::Failed(error.to_string())),
93 },
94 Ok(None) => Ok(None),
95 Err(error) => Err(ImagePickerError::Failed(error.to_string())),
96 }
97 })
98 }
99}
100
101pub fn default_image_picker() -> ImagePickerRef {
102 Arc::new(PlatformImagePicker)
103}
104
105pub fn local_image_picker() -> CompositionLocal<ImagePickerRef> {
106 thread_local! {
107 static LOCAL_IMAGE_PICKER: RefCell<Option<CompositionLocal<ImagePickerRef>>> = const { RefCell::new(None) };
108 }
109
110 LOCAL_IMAGE_PICKER.with(|cell| {
111 let mut local = cell.borrow_mut();
112 local
113 .get_or_insert_with(|| compositionLocalOfWithPolicy(default_image_picker, Arc::ptr_eq))
114 .clone()
115 })
116}
117
118#[allow(non_snake_case)]
119#[composable]
120pub fn ProvideImagePicker(content: impl FnOnce()) {
121 let picker = cranpose_core::remember(default_image_picker).with(|state| state.clone());
122 let local = local_image_picker();
123
124 CompositionLocalProvider(vec![local.provides(picker)], move || {
125 content();
126 });
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132 use std::sync::RwLock;
133
134 struct FixedImagePicker {
135 bytes: RwLock<Option<Vec<u8>>>,
136 }
137
138 impl ImagePicker for FixedImagePicker {
139 fn pick_image(
140 &self,
141 _source: ImageSource,
142 ) -> PickerFuture<Result<Option<Vec<u8>>, ImagePickerError>> {
143 let bytes = self.bytes.read().unwrap().clone();
144 Box::pin(async move { Ok(bytes) })
145 }
146 }
147
148 #[test]
149 fn registered_image_picker_takes_precedence() {
150 let _guard = crate::registry::test_service_guard();
151 clear_platform_image_picker();
152 set_platform_image_picker(Arc::new(FixedImagePicker {
153 bytes: RwLock::new(Some(vec![1, 2, 3])),
154 }));
155 let picker = default_image_picker();
156 let result = pollster::block_on(picker.pick_image(ImageSource::Camera));
157 assert_eq!(result.unwrap(), Some(vec![1, 2, 3]));
158 clear_platform_image_picker();
159 }
160
161 #[test]
162 fn camera_is_unsupported_without_a_platform_picker() {
163 let _guard = crate::registry::test_service_guard();
164 clear_platform_image_picker();
165 let result = pollster::block_on(default_image_picker().pick_image(ImageSource::Camera));
166 assert!(matches!(result, Err(ImagePickerError::Unsupported)));
167 }
168}