1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
//! Photo/image picker: choose a single image from the user's photo library.
//!
//! On iOS "photos" and "files" are distinct — a scanner's document photos live
//! in the Photos library, which the file picker (`UIDocumentPicker`) cannot
//! reach. The iOS backend registers a `UIImagePickerController`-based photo
//! picker through [`set_platform_image_picker`]. The compiled-in default
//! delegates to the file picker with an image filter, which is the right
//! behavior on desktop (a file dialog), the web (`<input accept="image/*">`),
//! and Android (SAF), where a plain file picker already surfaces images.
use std::sync::Arc;
use cranpose_core::{CompositionLocal, CompositionLocalProvider, compositionLocalOfWithPolicy};
use cranpose_macros::composable;
use crate::{
file_picker::{FileFilter, FilePickerOptions, PickerFuture, default_file_picker},
registry::ServiceRegistry,
};
/// Image extensions offered when the picker falls back to the file picker.
pub const IMAGE_EXTENSIONS: &[&str] = &["jpg", "jpeg", "png", "webp", "bmp", "heic", "heif"];
/// Why an image could not be picked.
#[derive(thiserror::Error, Debug)]
pub enum ImagePickerError {
/// The platform has no way to pick an image.
#[error("image picking is not supported on this platform")]
Unsupported,
/// The platform failed to show the picker or to read the chosen image. The text describes the
/// cause.
#[error("failed to pick an image: {0}")]
Failed(String),
}
/// Where an image comes from.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum ImageSource {
/// The user's photo library (or a file dialog on platforms without one).
PhotoLibrary,
/// A photo captured with the device camera.
Camera,
}
/// Picks or captures a single image and returns its encoded bytes.
pub trait ImagePicker: Send + Sync {
/// Present a picker/camera for `source` and resolve to the chosen image's
/// encoded bytes, or `None` if the user cancelled.
fn pick_image(
&self,
source: ImageSource,
) -> PickerFuture<Result<Option<Vec<u8>>, ImagePickerError>>;
}
/// Shared handle to an [`ImagePicker`].
pub type ImagePickerRef = Arc<dyn ImagePicker>;
static PLATFORM_IMAGE_PICKER: ServiceRegistry<dyn ImagePicker> = ServiceRegistry::new();
/// Installs a platform image picker, replacing any previously installed one.
/// The iOS backend registers a photo-library picker here.
pub fn set_platform_image_picker(picker: ImagePickerRef) {
PLATFORM_IMAGE_PICKER.set(picker);
}
/// Removes any registered platform image picker (tests and teardown).
pub fn clear_platform_image_picker() {
PLATFORM_IMAGE_PICKER.clear();
}
fn registered_platform_image_picker() -> Option<ImagePickerRef> {
PLATFORM_IMAGE_PICKER.get_or_warn("image picker")
}
struct PlatformImagePicker;
impl ImagePicker for PlatformImagePicker {
fn pick_image(
&self,
source: ImageSource,
) -> PickerFuture<Result<Option<Vec<u8>>, ImagePickerError>> {
if let Some(picker) = registered_platform_image_picker() {
return picker.pick_image(source);
}
if source == ImageSource::Camera {
return Box::pin(async { Err(ImagePickerError::Unsupported) });
}
Box::pin(async {
let picker = default_file_picker();
let options = FilePickerOptions::default()
.with_title("Choose image")
.with_filter(FileFilter::new("Images", IMAGE_EXTENSIONS));
match picker.pick_file(options).await {
Ok(Some(entry)) => match entry.read_all().await {
Ok(bytes) => Ok(Some(bytes)),
Err(error) => Err(ImagePickerError::Failed(error.to_string())),
},
Ok(None) => Ok(None),
Err(error) => Err(ImagePickerError::Failed(error.to_string())),
}
})
}
}
/// Returns the image picker that forwards to the installed platform picker, or else to the file
/// picker with an image filter.
pub fn default_image_picker() -> ImagePickerRef {
Arc::new(PlatformImagePicker)
}
/// The image picker a composable reads. It is the platform picker unless a composition local
/// provider above it supplies another one.
pub fn local_image_picker() -> CompositionLocal<ImagePickerRef> {
crate::composition_locals::cached_local(
|locals| &locals.image_picker,
|| compositionLocalOfWithPolicy(default_image_picker, Arc::ptr_eq),
)
}
/// Provides the platform image picker to `content` through [`local_image_picker`].
#[composable]
pub fn ProvideImagePicker(content: impl FnOnce()) {
let picker = cranpose_core::remember(default_image_picker).with(|state| state.clone());
let local = local_image_picker();
CompositionLocalProvider([local.provides(picker)], move || {
content();
});
}
#[cfg(test)]
#[path = "tests/image_picker_tests.rs"]
mod tests;