use cranpose_core::compositionLocalOfWithPolicy;
use cranpose_core::CompositionLocal;
use cranpose_core::CompositionLocalProvider;
use cranpose_macros::composable;
use std::cell::RefCell;
use std::rc::Rc;
#[derive(thiserror::Error, Debug)]
pub enum ShareError {
#[error("sharing is not supported on this platform")]
Unsupported,
#[error("failed to present the share sheet: {0}")]
Failed(String),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ShareContent {
pub file_name: String,
pub mime_type: String,
pub bytes: Vec<u8>,
pub text: Option<String>,
}
impl ShareContent {
pub fn file(
file_name: impl Into<String>,
mime_type: impl Into<String>,
bytes: Vec<u8>,
) -> Self {
Self {
file_name: file_name.into(),
mime_type: mime_type.into(),
bytes,
text: None,
}
}
pub fn with_text(mut self, text: impl Into<String>) -> Self {
self.text = Some(text.into());
self
}
}
pub trait ShareSheet {
fn share(&self, content: ShareContent) -> Result<(), ShareError>;
fn is_supported(&self) -> bool;
}
pub type ShareSheetRef = Rc<dyn ShareSheet>;
thread_local! {
static PLATFORM_SHARE_SHEET: RefCell<Option<ShareSheetRef>> = const { RefCell::new(None) };
}
pub fn set_platform_share_sheet(share_sheet: ShareSheetRef) {
PLATFORM_SHARE_SHEET.with(|cell| *cell.borrow_mut() = Some(share_sheet));
}
pub fn clear_platform_share_sheet() {
PLATFORM_SHARE_SHEET.with(|cell| *cell.borrow_mut() = None);
}
fn registered_platform_share_sheet() -> Option<ShareSheetRef> {
PLATFORM_SHARE_SHEET.with(|cell| cell.borrow().clone())
}
struct PlatformShareSheet;
impl ShareSheet for PlatformShareSheet {
fn share(&self, content: ShareContent) -> Result<(), ShareError> {
match registered_platform_share_sheet() {
Some(handler) => handler.share(content),
None => Err(ShareError::Unsupported),
}
}
fn is_supported(&self) -> bool {
registered_platform_share_sheet().is_some_and(|handler| handler.is_supported())
}
}
pub fn default_share_sheet() -> ShareSheetRef {
Rc::new(PlatformShareSheet)
}
pub fn local_share_sheet() -> CompositionLocal<ShareSheetRef> {
thread_local! {
static LOCAL_SHARE_SHEET: RefCell<Option<CompositionLocal<ShareSheetRef>>> = const { RefCell::new(None) };
}
LOCAL_SHARE_SHEET.with(|cell| {
let mut local = cell.borrow_mut();
local
.get_or_insert_with(|| compositionLocalOfWithPolicy(default_share_sheet, Rc::ptr_eq))
.clone()
})
}
#[allow(non_snake_case)]
#[composable]
pub fn ProvideShareSheet(content: impl FnOnce()) {
let share_sheet = cranpose_core::remember(default_share_sheet).with(|state| state.clone());
let local = local_share_sheet();
CompositionLocalProvider(vec![local.provides(share_sheet)], move || {
content();
});
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::RefCell;
struct RecordingShareSheet {
shared: RefCell<Option<ShareContent>>,
supported: bool,
}
impl ShareSheet for RecordingShareSheet {
fn share(&self, content: ShareContent) -> Result<(), ShareError> {
*self.shared.borrow_mut() = Some(content);
Ok(())
}
fn is_supported(&self) -> bool {
self.supported
}
}
#[test]
fn default_share_sheet_is_unsupported_without_a_backend() {
clear_platform_share_sheet();
let sheet = default_share_sheet();
assert!(!sheet.is_supported());
let error = sheet
.share(ShareContent::file(
"a.pdf",
"application/pdf",
vec![1, 2, 3],
))
.expect_err("no backend installed");
assert!(matches!(error, ShareError::Unsupported));
}
#[test]
fn registered_share_sheet_takes_precedence() {
let recorder = Rc::new(RecordingShareSheet {
shared: RefCell::new(None),
supported: true,
});
set_platform_share_sheet(recorder.clone());
let sheet = default_share_sheet();
assert!(sheet.is_supported());
sheet
.share(ShareContent::file("r.pdf", "application/pdf", vec![9]).with_text("hi"))
.expect("registered backend shares");
let shared = recorder.shared.borrow();
let shared = shared.as_ref().expect("content recorded");
assert_eq!(shared.file_name, "r.pdf");
assert_eq!(shared.text.as_deref(), Some("hi"));
clear_platform_share_sheet();
assert!(!default_share_sheet().is_supported());
}
}