cranpose_services/
image_picker.rs1use std::{cell::RefCell, 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)]
25pub enum ImagePickerError {
26 #[error("image picking is not supported on this platform")]
27 Unsupported,
28 #[error("failed to pick an image: {0}")]
29 Failed(String),
30}
31
32#[derive(Clone, Copy, PartialEq, Eq, Debug)]
34pub enum ImageSource {
35 PhotoLibrary,
37 Camera,
39}
40
41pub trait ImagePicker: Send + Sync {
43 fn pick_image(
46 &self,
47 source: ImageSource,
48 ) -> PickerFuture<Result<Option<Vec<u8>>, ImagePickerError>>;
49}
50
51pub type ImagePickerRef = Arc<dyn ImagePicker>;
52
53static PLATFORM_IMAGE_PICKER: ServiceRegistry<dyn ImagePicker> = ServiceRegistry::new();
54
55pub fn set_platform_image_picker(picker: ImagePickerRef) {
58 PLATFORM_IMAGE_PICKER.set(picker);
59}
60
61pub fn clear_platform_image_picker() {
63 PLATFORM_IMAGE_PICKER.clear();
64}
65
66fn registered_platform_image_picker() -> Option<ImagePickerRef> {
67 PLATFORM_IMAGE_PICKER.get_or_warn("image picker")
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 {
81 return Box::pin(async { Err(ImagePickerError::Unsupported) });
82 }
83 Box::pin(async {
84 let picker = default_file_picker();
85 let options = FilePickerOptions::default()
86 .with_title("Choose image")
87 .with_filter(FileFilter::new("Images", IMAGE_EXTENSIONS));
88 match picker.pick_file(options).await {
89 Ok(Some(entry)) => match entry.read_all().await {
90 Ok(bytes) => Ok(Some(bytes)),
91 Err(error) => Err(ImagePickerError::Failed(error.to_string())),
92 },
93 Ok(None) => Ok(None),
94 Err(error) => Err(ImagePickerError::Failed(error.to_string())),
95 }
96 })
97 }
98}
99
100pub fn default_image_picker() -> ImagePickerRef {
101 Arc::new(PlatformImagePicker)
102}
103
104pub fn local_image_picker() -> CompositionLocal<ImagePickerRef> {
105 thread_local! {
106 static LOCAL_IMAGE_PICKER: RefCell<Option<CompositionLocal<ImagePickerRef>>> = const { RefCell::new(None) };
107 }
108
109 LOCAL_IMAGE_PICKER.with(|cell| {
110 let mut local = cell.borrow_mut();
111 local
112 .get_or_insert_with(|| compositionLocalOfWithPolicy(default_image_picker, Arc::ptr_eq))
113 .clone()
114 })
115}
116
117#[composable]
118pub fn ProvideImagePicker(content: impl FnOnce()) {
119 let picker = cranpose_core::remember(default_image_picker).with(|state| state.clone());
120 let local = local_image_picker();
121
122 CompositionLocalProvider(vec![local.provides(picker)], move || {
123 content();
124 });
125}
126
127#[cfg(test)]
128#[path = "tests/image_picker_tests.rs"]
129mod tests;