Skip to main content

fission_shell_winit/
clipboard.rs

1use fission_core::env::Clipboard;
2use fission_core::{
3    ClipboardContent, ClipboardError, ClipboardText, ClipboardWriteTextRequest, CLEAR_CLIPBOARD,
4    READ_CLIPBOARD_CONTENT, READ_CLIPBOARD_TEXT, WRITE_CLIPBOARD_CONTENT, WRITE_CLIPBOARD_TEXT,
5};
6use fission_shell::async_host::AsyncRegistry;
7use std::sync::{Arc, Mutex};
8
9#[cfg(not(any(target_os = "android", target_os = "ios", target_arch = "wasm32")))]
10use arboard::Clipboard as Arboard;
11
12pub struct DesktopClipboard {
13    #[cfg(not(any(target_os = "android", target_os = "ios", target_arch = "wasm32")))]
14    system: Arc<Mutex<Option<Arboard>>>,
15    memory: Arc<Mutex<String>>,
16}
17
18impl DesktopClipboard {
19    pub fn new() -> Self {
20        Self {
21            #[cfg(not(any(target_os = "android", target_os = "ios", target_arch = "wasm32")))]
22            system: Arc::new(Mutex::new(Arboard::new().ok())),
23            memory: Arc::new(Mutex::new(String::new())),
24        }
25    }
26}
27
28impl Clipboard for DesktopClipboard {
29    fn get_text(&self) -> Option<String> {
30        #[cfg(not(any(target_os = "android", target_os = "ios", target_arch = "wasm32")))]
31        if let Ok(mut lock) = self.system.lock() {
32            if let Some(cb) = lock.as_mut() {
33                if let Ok(text) = cb.get_text() {
34                    return Some(text);
35                }
36            }
37        }
38        self.memory.lock().ok().map(|text| text.clone())
39    }
40
41    fn set_text(&self, text: &str) {
42        if let Ok(mut memory) = self.memory.lock() {
43            *memory = text.to_string();
44        }
45        #[cfg(not(any(target_os = "android", target_os = "ios", target_arch = "wasm32")))]
46        if let Ok(mut lock) = self.system.lock() {
47            if let Some(cb) = lock.as_mut() {
48                let _ = cb.set_text(text);
49            }
50        }
51    }
52}
53
54/// Host-side clipboard provider used by shell capability registration.
55pub trait ClipboardHost: Send + Sync + 'static {
56    /// Reads plain text from the host clipboard.
57    fn read_text(&self) -> Result<ClipboardText, ClipboardError>;
58    /// Writes plain text to the host clipboard.
59    fn write_text(&self, request: ClipboardWriteTextRequest) -> Result<(), ClipboardError>;
60    /// Reads typed clipboard items from the host clipboard.
61    fn read_content(&self) -> Result<ClipboardContent, ClipboardError>;
62    /// Writes typed clipboard items to the host clipboard.
63    fn write_content(&self, request: ClipboardContent) -> Result<(), ClipboardError>;
64    /// Clears clipboard content when the host allows apps to do that.
65    fn clear(&self) -> Result<(), ClipboardError>;
66}
67
68impl ClipboardHost for DesktopClipboard {
69    fn read_text(&self) -> Result<ClipboardText, ClipboardError> {
70        Ok(ClipboardText {
71            text: self.get_text(),
72        })
73    }
74
75    fn write_text(&self, request: ClipboardWriteTextRequest) -> Result<(), ClipboardError> {
76        self.set_text(&request.text);
77        Ok(())
78    }
79
80    fn read_content(&self) -> Result<ClipboardContent, ClipboardError> {
81        let text = self.get_text().unwrap_or_default();
82        Ok(ClipboardContent {
83            items: if text.is_empty() {
84                Vec::new()
85            } else {
86                vec![fission_core::ClipboardItem {
87                    content_type: "text/plain".into(),
88                    bytes: text.into_bytes(),
89                    suggested_name: None,
90                }]
91            },
92        })
93    }
94
95    fn write_content(&self, request: ClipboardContent) -> Result<(), ClipboardError> {
96        if let Some(item) = request
97            .items
98            .iter()
99            .find(|item| item.content_type.starts_with("text/plain"))
100        {
101            if let Ok(text) = String::from_utf8(item.bytes.clone()) {
102                self.set_text(&text);
103                return Ok(());
104            }
105        }
106        Err(ClipboardError::unsupported("write_content_non_text"))
107    }
108
109    fn clear(&self) -> Result<(), ClipboardError> {
110        self.set_text("");
111        Ok(())
112    }
113}
114
115/// In-process clipboard host for tests and non-OS environments.
116#[derive(Debug, Default)]
117pub struct MemoryClipboardHost {
118    content: Arc<Mutex<ClipboardContent>>,
119}
120
121impl ClipboardHost for MemoryClipboardHost {
122    fn read_text(&self) -> Result<ClipboardText, ClipboardError> {
123        let content = self.content.lock().map_err(|_| {
124            ClipboardError::new("lock_poisoned", "memory clipboard lock was poisoned")
125        })?;
126        let text = content
127            .items
128            .iter()
129            .find(|item| item.content_type.starts_with("text/plain"))
130            .and_then(|item| String::from_utf8(item.bytes.clone()).ok());
131        Ok(ClipboardText { text })
132    }
133
134    fn write_text(&self, request: ClipboardWriteTextRequest) -> Result<(), ClipboardError> {
135        let mut content = self.content.lock().map_err(|_| {
136            ClipboardError::new("lock_poisoned", "memory clipboard lock was poisoned")
137        })?;
138        *content = ClipboardContent {
139            items: vec![fission_core::ClipboardItem {
140                content_type: "text/plain".into(),
141                bytes: request.text.into_bytes(),
142                suggested_name: None,
143            }],
144        };
145        Ok(())
146    }
147
148    fn read_content(&self) -> Result<ClipboardContent, ClipboardError> {
149        self.content
150            .lock()
151            .map(|content| content.clone())
152            .map_err(|_| ClipboardError::new("lock_poisoned", "memory clipboard lock was poisoned"))
153    }
154
155    fn write_content(&self, request: ClipboardContent) -> Result<(), ClipboardError> {
156        let mut content = self.content.lock().map_err(|_| {
157            ClipboardError::new("lock_poisoned", "memory clipboard lock was poisoned")
158        })?;
159        *content = request;
160        Ok(())
161    }
162
163    fn clear(&self) -> Result<(), ClipboardError> {
164        let mut content = self.content.lock().map_err(|_| {
165            ClipboardError::new("lock_poisoned", "memory clipboard lock was poisoned")
166        })?;
167        content.items.clear();
168        Ok(())
169    }
170}
171
172pub(crate) fn register_clipboard_capabilities(
173    async_registry: &mut AsyncRegistry,
174    host: Arc<dyn ClipboardHost>,
175) {
176    let read_text_host = host.clone();
177    async_registry.register_operation_capability(READ_CLIPBOARD_TEXT, move |(), _| {
178        let host = read_text_host.clone();
179        async move { host.read_text() }
180    });
181
182    let write_text_host = host.clone();
183    async_registry.register_operation_capability(WRITE_CLIPBOARD_TEXT, move |request, _| {
184        let host = write_text_host.clone();
185        async move { host.write_text(request) }
186    });
187
188    let read_content_host = host.clone();
189    async_registry.register_operation_capability(READ_CLIPBOARD_CONTENT, move |(), _| {
190        let host = read_content_host.clone();
191        async move { host.read_content() }
192    });
193
194    let write_content_host = host.clone();
195    async_registry.register_operation_capability(WRITE_CLIPBOARD_CONTENT, move |request, _| {
196        let host = write_content_host.clone();
197        async move { host.write_content(request) }
198    });
199
200    async_registry.register_operation_capability(CLEAR_CLIPBOARD, move |(), _| {
201        let host = host.clone();
202        async move { host.clear() }
203    });
204}
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209
210    #[test]
211    fn memory_clipboard_reads_and_writes_text() {
212        let host = MemoryClipboardHost::default();
213        host.write_text(ClipboardWriteTextRequest {
214            text: "copied".into(),
215        })
216        .unwrap();
217        assert_eq!(host.read_text().unwrap().text.as_deref(), Some("copied"));
218        host.clear().unwrap();
219        assert_eq!(host.read_text().unwrap().text, None);
220    }
221
222    #[test]
223    fn desktop_clipboard_host_supports_text_content() {
224        let host = DesktopClipboard::new();
225        host.write_text(ClipboardWriteTextRequest {
226            text: "copied".into(),
227        })
228        .unwrap();
229        let content = ClipboardHost::read_content(&host).unwrap();
230        assert_eq!(content.items[0].content_type, "text/plain");
231    }
232}