use crate::file_picker::{default_file_picker, FileFilter, FilePickerOptions, PickerFuture};
use cranpose_core::compositionLocalOfWithPolicy;
use cranpose_core::CompositionLocal;
use cranpose_core::CompositionLocalProvider;
use cranpose_macros::composable;
use std::cell::RefCell;
use std::rc::Rc;
pub const IMAGE_EXTENSIONS: &[&str] = &["jpg", "jpeg", "png", "webp", "bmp", "heic", "heif"];
#[derive(thiserror::Error, Debug)]
pub enum ImagePickerError {
#[error("image picking is not supported on this platform")]
Unsupported,
#[error("failed to pick an image: {0}")]
Failed(String),
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum ImageSource {
PhotoLibrary,
Camera,
}
pub trait ImagePicker {
fn pick_image(
&self,
source: ImageSource,
) -> PickerFuture<Result<Option<Vec<u8>>, ImagePickerError>>;
}
pub type ImagePickerRef = Rc<dyn ImagePicker>;
thread_local! {
static PLATFORM_IMAGE_PICKER: RefCell<Option<ImagePickerRef>> = const { RefCell::new(None) };
}
pub fn set_platform_image_picker(picker: ImagePickerRef) {
PLATFORM_IMAGE_PICKER.with(|cell| *cell.borrow_mut() = Some(picker));
}
pub fn clear_platform_image_picker() {
PLATFORM_IMAGE_PICKER.with(|cell| *cell.borrow_mut() = None);
}
fn registered_platform_image_picker() -> Option<ImagePickerRef> {
PLATFORM_IMAGE_PICKER.with(|cell| cell.borrow().clone())
}
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_bytes().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())),
}
})
}
}
pub fn default_image_picker() -> ImagePickerRef {
Rc::new(PlatformImagePicker)
}
pub fn local_image_picker() -> CompositionLocal<ImagePickerRef> {
thread_local! {
static LOCAL_IMAGE_PICKER: RefCell<Option<CompositionLocal<ImagePickerRef>>> = const { RefCell::new(None) };
}
LOCAL_IMAGE_PICKER.with(|cell| {
let mut local = cell.borrow_mut();
local
.get_or_insert_with(|| compositionLocalOfWithPolicy(default_image_picker, Rc::ptr_eq))
.clone()
})
}
#[allow(non_snake_case)]
#[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(vec![local.provides(picker)], move || {
content();
});
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::RefCell;
struct FixedImagePicker {
bytes: RefCell<Option<Vec<u8>>>,
}
impl ImagePicker for FixedImagePicker {
fn pick_image(
&self,
_source: ImageSource,
) -> PickerFuture<Result<Option<Vec<u8>>, ImagePickerError>> {
let bytes = self.bytes.borrow().clone();
Box::pin(async move { Ok(bytes) })
}
}
#[test]
fn registered_image_picker_takes_precedence() {
clear_platform_image_picker();
set_platform_image_picker(Rc::new(FixedImagePicker {
bytes: RefCell::new(Some(vec![1, 2, 3])),
}));
let picker = default_image_picker();
let result = pollster::block_on(picker.pick_image(ImageSource::Camera));
assert_eq!(result.unwrap(), Some(vec![1, 2, 3]));
clear_platform_image_picker();
}
#[test]
fn camera_is_unsupported_without_a_platform_picker() {
clear_platform_image_picker();
let result = pollster::block_on(default_image_picker().pick_image(ImageSource::Camera));
assert!(matches!(result, Err(ImagePickerError::Unsupported)));
}
}