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
134
135
//! System share sheet: hand a file (and optional text) to the OS share UI so
//! the user can send it to another app.
//!
//! Like [`crate::uri_handler`], the compiled-in default is a no-op that reports
//! [`ShareError::Unsupported`]; platform backends install a real implementation
//! through [`set_platform_share_sheet`] (iOS `UIActivityViewController`, Android
//! `ACTION_SEND`, the Web Share API). Desktop has no system share sheet — apps
//! export through a save dialog instead — so `is_supported` is `false` there and
//! the UI can offer "Save…" rather than "Share".
use std::rc::Rc;
use cranpose_core::{CompositionLocal, CompositionLocalProvider, compositionLocalOfWithPolicy};
use cranpose_macros::composable;
/// Why the share sheet could not be shown.
#[derive(thiserror::Error, Debug)]
pub enum ShareError {
/// The platform has no share sheet.
#[error("sharing is not supported on this platform")]
Unsupported,
/// The platform failed to present the share sheet. The text describes the cause.
#[error("failed to present the share sheet: {0}")]
Failed(String),
}
/// A single file to share, with an optional accompanying message/subject.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ShareContent {
/// Suggested file name shown to the user and used by the receiving app.
pub file_name: String,
/// MIME type of `bytes` (e.g. `application/pdf`, `image/jpeg`).
pub mime_type: String,
/// File payload. Backends stage it to a temporary URL to share.
pub bytes: Vec<u8>,
/// Optional text/subject shared alongside the file.
pub text: Option<String>,
}
impl ShareContent {
/// A file to share, identified by name and MIME type.
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,
}
}
/// Attach accompanying text (subject / message) to the shared file.
pub fn with_text(mut self, text: impl Into<String>) -> Self {
self.text = Some(text.into());
self
}
}
/// The OS share sheet. Installed by the platform backend; the default is a
/// no-op that reports [`ShareError::Unsupported`].
pub trait ShareSheet {
/// Present the system share sheet for `content`. Fire-and-forget: it
/// resolves once presented and does not report the user's choice.
fn share(&self, content: ShareContent) -> Result<(), ShareError>;
/// Whether a real system share sheet is available (so UI can choose between
/// "Share" and "Save…").
fn is_supported(&self) -> bool;
}
/// Shared handle to a [`ShareSheet`].
pub type ShareSheetRef = Rc<dyn ShareSheet>;
/// Installs a platform share sheet, replacing any previously installed one.
/// Backends with main-thread UIKit / Activity access register here.
pub fn set_platform_share_sheet(share_sheet: ShareSheetRef) {
crate::thread_state::with(|state| *state.share_sheet.borrow_mut() = Some(share_sheet));
}
/// Removes any registered platform share sheet (tests and teardown).
pub fn clear_platform_share_sheet() {
crate::thread_state::with(|state| *state.share_sheet.borrow_mut() = None);
}
fn registered_platform_share_sheet() -> Option<ShareSheetRef> {
crate::thread_state::with(|state| state.share_sheet.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())
}
}
/// Returns the share sheet that forwards to the installed platform share sheet. Without one,
/// every share reports [`ShareError::Unsupported`].
pub fn default_share_sheet() -> ShareSheetRef {
Rc::new(PlatformShareSheet)
}
/// The share sheet a composable reads. It is the platform share sheet unless a composition
/// local provider above it supplies another one.
pub fn local_share_sheet() -> CompositionLocal<ShareSheetRef> {
crate::composition_locals::cached_local(
|locals| &locals.share_sheet,
|| compositionLocalOfWithPolicy(default_share_sheet, Rc::ptr_eq),
)
}
/// Provides the platform share sheet to `content` through [`local_share_sheet`].
#[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([local.provides(share_sheet)], move || {
content();
});
}
#[cfg(test)]
#[path = "tests/share_sheet_tests.rs"]
mod tests;