veridian_kernel/desktop/desktop_ext/
clipboard.rs1#[cfg(feature = "alloc")]
7use alloc::{collections::BTreeMap, vec::Vec};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum ClipboardError {
12 Empty,
14 MimeNotFound,
16 HistoryFull,
18 InvalidSelection,
20 DataTooLarge,
22 SourceDestroyed,
24}
25
26impl core::fmt::Display for ClipboardError {
27 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
28 match self {
29 Self::Empty => write!(f, "clipboard is empty"),
30 Self::MimeNotFound => write!(f, "MIME type not found"),
31 Self::HistoryFull => write!(f, "clipboard history full"),
32 Self::InvalidSelection => write!(f, "invalid selection type"),
33 Self::DataTooLarge => write!(f, "data too large"),
34 Self::SourceDestroyed => write!(f, "source destroyed"),
35 }
36 }
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
41pub enum ClipboardMime {
42 TextPlain,
44 TextPlainUtf8,
46 TextHtml,
48 TextUriList,
50 ImagePng,
52 ImageBmp,
54 Custom(u32),
56}
57
58impl ClipboardMime {
59 pub fn as_str(&self) -> &'static str {
61 match self {
62 Self::TextPlain => "text/plain",
63 Self::TextPlainUtf8 => "text/plain;charset=utf-8",
64 Self::TextHtml => "text/html",
65 Self::TextUriList => "text/uri-list",
66 Self::ImagePng => "image/png",
67 Self::ImageBmp => "image/bmp",
68 Self::Custom(_) => "application/octet-stream",
69 }
70 }
71
72 pub fn is_text(&self) -> bool {
74 matches!(
75 self,
76 Self::TextPlain | Self::TextPlainUtf8 | Self::TextHtml | Self::TextUriList
77 )
78 }
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
83pub enum SelectionType {
84 #[default]
86 Clipboard,
87 Primary,
89}
90
91#[derive(Debug, Clone, PartialEq, Eq)]
93pub struct ClipboardEntry {
94 #[cfg(feature = "alloc")]
96 pub mime_data: BTreeMap<ClipboardMime, Vec<u8>>,
97 pub timestamp: u64,
99 pub source_surface: u32,
101}
102
103#[cfg(feature = "alloc")]
104impl ClipboardEntry {
105 pub fn new(source_surface: u32, timestamp: u64) -> Self {
107 Self {
108 mime_data: BTreeMap::new(),
109 timestamp,
110 source_surface,
111 }
112 }
113
114 pub fn set_data(&mut self, mime: ClipboardMime, data: Vec<u8>) {
116 self.mime_data.insert(mime, data);
117 }
118
119 pub fn get_data(&self, mime: ClipboardMime) -> Option<&[u8]> {
121 self.mime_data.get(&mime).map(|v| v.as_slice())
122 }
123
124 pub fn offered_mimes(&self) -> Vec<ClipboardMime> {
126 self.mime_data.keys().copied().collect()
127 }
128
129 pub fn offers(&self, mime: ClipboardMime) -> bool {
131 self.mime_data.contains_key(&mime)
132 }
133
134 pub fn total_size(&self) -> usize {
136 self.mime_data.values().map(|v| v.len()).sum()
137 }
138}
139
140pub(crate) const CLIPBOARD_HISTORY_MAX: usize = 8;
142
143pub(crate) const CLIPBOARD_MAX_DATA_SIZE: usize = 65536;
145
146#[derive(Debug)]
148#[cfg(feature = "alloc")]
149pub struct ClipboardManager {
150 clipboard: Option<ClipboardEntry>,
152 primary: Option<ClipboardEntry>,
154 history: Vec<ClipboardEntry>,
156 history_enabled: bool,
158 tick: u64,
160}
161
162#[cfg(feature = "alloc")]
163impl Default for ClipboardManager {
164 fn default() -> Self {
165 Self::new()
166 }
167}
168
169#[cfg(feature = "alloc")]
170impl ClipboardManager {
171 pub fn new() -> Self {
173 Self {
174 clipboard: None,
175 primary: None,
176 history: Vec::new(),
177 history_enabled: true,
178 tick: 0,
179 }
180 }
181
182 pub fn copy(
184 &mut self,
185 selection: SelectionType,
186 source_surface: u32,
187 mime: ClipboardMime,
188 data: Vec<u8>,
189 ) -> Result<(), ClipboardError> {
190 if data.len() > CLIPBOARD_MAX_DATA_SIZE {
191 return Err(ClipboardError::DataTooLarge);
192 }
193
194 self.tick += 1;
195 let mut entry = ClipboardEntry::new(source_surface, self.tick);
196 entry.set_data(mime, data);
197
198 match selection {
199 SelectionType::Clipboard => {
200 if self.history_enabled {
202 if let Some(old) = self.clipboard.take() {
203 self.push_history(old);
204 }
205 }
206 self.clipboard = Some(entry);
207 }
208 SelectionType::Primary => {
209 self.primary = Some(entry);
210 }
211 }
212
213 Ok(())
214 }
215
216 pub fn copy_multi(
218 &mut self,
219 selection: SelectionType,
220 source_surface: u32,
221 data: &[(ClipboardMime, Vec<u8>)],
222 ) -> Result<(), ClipboardError> {
223 let total_size: usize = data.iter().map(|(_, d)| d.len()).sum();
224 if total_size > CLIPBOARD_MAX_DATA_SIZE {
225 return Err(ClipboardError::DataTooLarge);
226 }
227
228 self.tick += 1;
229 let mut entry = ClipboardEntry::new(source_surface, self.tick);
230 for (mime, d) in data {
231 entry.set_data(*mime, d.clone());
232 }
233
234 match selection {
235 SelectionType::Clipboard => {
236 if self.history_enabled {
237 if let Some(old) = self.clipboard.take() {
238 self.push_history(old);
239 }
240 }
241 self.clipboard = Some(entry);
242 }
243 SelectionType::Primary => {
244 self.primary = Some(entry);
245 }
246 }
247
248 Ok(())
249 }
250
251 pub fn paste(
253 &self,
254 selection: SelectionType,
255 mime: ClipboardMime,
256 ) -> Result<&[u8], ClipboardError> {
257 let entry = match selection {
258 SelectionType::Clipboard => self.clipboard.as_ref(),
259 SelectionType::Primary => self.primary.as_ref(),
260 };
261
262 let entry = entry.ok_or(ClipboardError::Empty)?;
263 entry.get_data(mime).ok_or(ClipboardError::MimeNotFound)
264 }
265
266 pub fn available_mimes(&self, selection: SelectionType) -> Vec<ClipboardMime> {
268 match selection {
269 SelectionType::Clipboard => self
270 .clipboard
271 .as_ref()
272 .map(|e| e.offered_mimes())
273 .unwrap_or_default(),
274 SelectionType::Primary => self
275 .primary
276 .as_ref()
277 .map(|e| e.offered_mimes())
278 .unwrap_or_default(),
279 }
280 }
281
282 pub fn clear(&mut self, selection: SelectionType) {
284 match selection {
285 SelectionType::Clipboard => {
286 self.clipboard = None;
287 }
288 SelectionType::Primary => {
289 self.primary = None;
290 }
291 }
292 }
293
294 pub fn history(&self) -> &[ClipboardEntry] {
296 &self.history
297 }
298
299 pub fn restore_from_history(&mut self, index: usize) -> Result<(), ClipboardError> {
301 if index >= self.history.len() {
302 return Err(ClipboardError::Empty);
303 }
304 let entry = self.history.remove(index);
305 if let Some(old) = self.clipboard.take() {
306 self.push_history(old);
307 }
308 self.clipboard = Some(entry);
309 Ok(())
310 }
311
312 pub fn clear_history(&mut self) {
314 self.history.clear();
315 }
316
317 pub fn set_history_enabled(&mut self, enabled: bool) {
319 self.history_enabled = enabled;
320 if !enabled {
321 self.history.clear();
322 }
323 }
324
325 pub fn has_data(&self, selection: SelectionType) -> bool {
327 match selection {
328 SelectionType::Clipboard => self.clipboard.is_some(),
329 SelectionType::Primary => self.primary.is_some(),
330 }
331 }
332
333 pub fn negotiate_mime(
335 &self,
336 selection: SelectionType,
337 requested: &[ClipboardMime],
338 ) -> Option<ClipboardMime> {
339 let available = self.available_mimes(selection);
340 requested.iter().find(|r| available.contains(r)).copied()
342 }
343
344 fn push_history(&mut self, entry: ClipboardEntry) {
346 if self.history.len() >= CLIPBOARD_HISTORY_MAX {
347 self.history.pop();
348 }
349 self.history.insert(0, entry);
350 }
351
352 pub fn current_tick(&self) -> u64 {
354 self.tick
355 }
356}