cranpose_services/
image_picker.rs1use std::sync::Arc;
12
13use cranpose_core::{CompositionLocal, CompositionLocalProvider, compositionLocalOfWithPolicy};
14use cranpose_macros::composable;
15
16use crate::{
17 file_picker::{FileFilter, FilePickerOptions, PickerFuture, default_file_picker},
18 registry::ServiceRegistry,
19};
20
21pub const IMAGE_EXTENSIONS: &[&str] = &["jpg", "jpeg", "png", "webp", "bmp", "heic", "heif"];
23
24#[derive(thiserror::Error, Debug)]
26pub enum ImagePickerError {
27 #[error("image picking is not supported on this platform")]
29 Unsupported,
30 #[error("failed to pick an image: {0}")]
33 Failed(String),
34}
35
36#[derive(Clone, Copy, PartialEq, Eq, Debug)]
38pub enum ImageSource {
39 PhotoLibrary,
41 Camera,
43}
44
45pub trait ImagePicker: Send + Sync {
47 fn pick_image(
50 &self,
51 source: ImageSource,
52 ) -> PickerFuture<Result<Option<Vec<u8>>, ImagePickerError>>;
53}
54
55pub type ImagePickerRef = Arc<dyn ImagePicker>;
57
58static PLATFORM_IMAGE_PICKER: ServiceRegistry<dyn ImagePicker> = ServiceRegistry::new();
59
60pub fn set_platform_image_picker(picker: ImagePickerRef) {
63 PLATFORM_IMAGE_PICKER.set(picker);
64}
65
66pub fn clear_platform_image_picker() {
68 PLATFORM_IMAGE_PICKER.clear();
69}
70
71fn registered_platform_image_picker() -> Option<ImagePickerRef> {
72 PLATFORM_IMAGE_PICKER.get_or_warn("image picker")
73}
74
75struct PlatformImagePicker;
76
77impl ImagePicker for PlatformImagePicker {
78 fn pick_image(
79 &self,
80 source: ImageSource,
81 ) -> PickerFuture<Result<Option<Vec<u8>>, ImagePickerError>> {
82 if let Some(picker) = registered_platform_image_picker() {
83 return picker.pick_image(source);
84 }
85 if source == ImageSource::Camera {
86 return Box::pin(async { Err(ImagePickerError::Unsupported) });
87 }
88 Box::pin(async {
89 let picker = default_file_picker();
90 let options = FilePickerOptions::default()
91 .with_title("Choose image")
92 .with_filter(FileFilter::new("Images", IMAGE_EXTENSIONS));
93 match picker.pick_file(options).await {
94 Ok(Some(entry)) => match entry.read_all().await {
95 Ok(bytes) => Ok(Some(bytes)),
96 Err(error) => Err(ImagePickerError::Failed(error.to_string())),
97 },
98 Ok(None) => Ok(None),
99 Err(error) => Err(ImagePickerError::Failed(error.to_string())),
100 }
101 })
102 }
103}
104
105pub fn default_image_picker() -> ImagePickerRef {
108 Arc::new(PlatformImagePicker)
109}
110
111pub fn local_image_picker() -> CompositionLocal<ImagePickerRef> {
114 crate::composition_locals::cached_local(
115 |locals| &locals.image_picker,
116 || compositionLocalOfWithPolicy(default_image_picker, Arc::ptr_eq),
117 )
118}
119
120#[composable]
122pub fn ProvideImagePicker(content: impl FnOnce()) {
123 let picker = cranpose_core::remember(default_image_picker).with(|state| state.clone());
124 let local = local_image_picker();
125
126 CompositionLocalProvider([local.provides(picker)], move || {
127 content();
128 });
129}
130
131#[cfg(test)]
132#[path = "tests/image_picker_tests.rs"]
133mod tests;