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