1use raw_window_handle::RawDisplayHandle;
2
3pub struct Clipboard {
8 #[cfg(all(
9 not(any(target_os = "android", target_os = "ios")),
10 feature = "arboard",
11 ))]
12 arboard: Option<arboard::Clipboard>,
13
14 #[cfg(all(
15 any(
16 target_os = "linux",
17 target_os = "dragonfly",
18 target_os = "freebsd",
19 target_os = "netbsd",
20 target_os = "openbsd"
21 ),
22 feature = "smithay-clipboard"
23 ))]
24 smithay: Option<smithay_clipboard::Clipboard>,
25
26 clipboard: String,
28}
29
30impl Clipboard {
31 #[expect(unsafe_code)]
38 pub unsafe fn new(_raw_display_handle: Option<RawDisplayHandle>) -> Self {
39 Self {
40 #[cfg(all(
41 not(any(target_os = "android", target_os = "ios")),
42 feature = "arboard",
43 ))]
44 arboard: init_arboard(),
45
46 #[cfg(all(
47 any(
48 target_os = "linux",
49 target_os = "dragonfly",
50 target_os = "freebsd",
51 target_os = "netbsd",
52 target_os = "openbsd"
53 ),
54 feature = "smithay-clipboard"
55 ))]
56 smithay: unsafe { init_smithay_clipboard(_raw_display_handle) },
58
59 clipboard: Default::default(),
60 }
61 }
62
63 #[cfg_attr(
64 not(feature = "clipboard"),
65 expect(clippy::needless_pass_by_ref_mut, clippy::unnecessary_wraps)
66 )]
67 pub fn get(&mut self) -> Option<String> {
68 #[cfg(all(
69 any(
70 target_os = "linux",
71 target_os = "dragonfly",
72 target_os = "freebsd",
73 target_os = "netbsd",
74 target_os = "openbsd"
75 ),
76 feature = "smithay-clipboard"
77 ))]
78 if let Some(clipboard) = &mut self.smithay {
79 match clipboard.load() {
80 Ok(text) => return Some(text),
81 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
83 Err(err) => {
84 log::debug!("smithay paste error: {err}");
86 }
87 }
88 }
89
90 #[cfg(all(
91 not(any(target_os = "android", target_os = "ios")),
92 feature = "arboard",
93 ))]
94 if let Some(clipboard) = &mut self.arboard {
95 return match clipboard.get_text() {
96 Ok(text) => Some(text),
97 Err(err) => {
98 if !is_expected_content_absence(&err) {
103 log::error!("arboard paste error: {err}");
104 }
105 None
106 }
107 };
108 }
109
110 Some(self.clipboard.clone())
111 }
112
113 pub fn set_text(&mut self, text: String) {
114 #[cfg(all(
115 any(
116 target_os = "linux",
117 target_os = "dragonfly",
118 target_os = "freebsd",
119 target_os = "netbsd",
120 target_os = "openbsd"
121 ),
122 feature = "smithay-clipboard"
123 ))]
124 if let Some(clipboard) = &mut self.smithay {
125 clipboard.store(text);
126 return;
127 }
128
129 #[cfg(all(
130 not(any(target_os = "android", target_os = "ios")),
131 feature = "arboard",
132 ))]
133 if let Some(clipboard) = &mut self.arboard {
134 if let Err(err) = clipboard.set_text(text) {
135 log::error!("arboard copy/cut error: {err}");
136 }
137 return;
138 }
139
140 self.clipboard = text;
141 }
142
143 #[cfg_attr(
149 not(feature = "clipboard"),
150 expect(clippy::needless_pass_by_ref_mut, clippy::unused_self)
151 )]
152 pub fn get_image(&mut self) -> Option<egui::ColorImage> {
153 #[cfg(all(
154 not(any(target_os = "android", target_os = "ios")),
155 feature = "arboard",
156 ))]
157 if let Some(clipboard) = &mut self.arboard {
158 return match clipboard.get_image() {
159 Ok(image) => Some(color_image_from_arboard(&image)),
160 Err(err) => {
161 if !is_expected_content_absence(&err) {
165 log::error!("arboard paste-image error: {err}");
166 }
167 None
168 }
169 };
170 }
171
172 None
173 }
174
175 #[cfg_attr(
176 not(feature = "clipboard"),
177 expect(clippy::needless_pass_by_ref_mut, clippy::unused_self)
178 )]
179 pub fn set_image(&mut self, image: &egui::ColorImage) {
180 #[cfg(all(
181 not(any(target_os = "android", target_os = "ios")),
182 feature = "arboard",
183 ))]
184 if let Some(clipboard) = &mut self.arboard {
185 if let Err(err) = clipboard.set_image(arboard_from_color_image(image)) {
186 log::error!("arboard copy/cut error: {err}");
187 }
188 log::debug!("Copied image to clipboard");
189 return;
190 }
191
192 log::error!(
193 "Copying images is not supported. Enable the 'clipboard' feature of `egui-winit` to enable it."
194 );
195 _ = image;
196 }
197}
198
199cfg_select! {
201 any(
202 target_os = "linux",
203 target_os = "dragonfly",
204 target_os = "freebsd",
205 target_os = "netbsd",
206 target_os = "openbsd"
207 ) => {
208 #[cfg_attr(
209 not(any(feature = "arboard", feature = "smithay-clipboard")),
210 expect(
211 clippy::unused_self,
212 clippy::needless_pass_by_ref_mut,
213 clippy::needless_pass_by_value,
214 reason = "no backend to use"
215 )
216 )]
217 impl Clipboard {
218 pub fn get_primary_text(&mut self) -> Option<String> {
220 #[cfg(feature = "smithay-clipboard")]
221 if let Some(clipboard) = &mut self.smithay {
222 return clipboard
223 .load_primary()
224 .inspect_err(|err| log::debug!("smithay primary paste error: {err}"))
225 .ok();
226 }
227
228 #[cfg(feature = "arboard")]
229 if let Some(clipboard) = &mut self.arboard {
230 use arboard::GetExtLinux as _;
231 return clipboard
232 .get()
233 .clipboard(arboard::LinuxClipboardKind::Primary)
234 .text()
235 .inspect_err(|err| log::debug!("arboard primary paste error: {err}"))
236 .ok();
237 }
238
239 None
240 }
241
242 pub fn set_primary_text(&mut self, text: String) {
244 #[cfg(feature = "smithay-clipboard")]
245 if let Some(clipboard) = &mut self.smithay {
246 clipboard.store_primary(text);
247 return;
248 }
249
250 #[cfg(feature = "arboard")]
251 if let Some(clipboard) = &mut self.arboard {
252 use arboard::SetExtLinux as _;
253 if let Err(err) = clipboard
254 .set()
255 .clipboard(arboard::LinuxClipboardKind::Primary)
256 .text(text)
257 {
258 log::error!("arboard primary copy error: {err}");
259 }
260 return;
261 }
262
263 _ = text;
264 }
265 }
266 }
267 _ => {
268 #[expect(
269 clippy::unused_self,
270 clippy::needless_pass_by_ref_mut,
271 reason = "there is no PRIMARY selection on this platform"
272 )]
273 impl Clipboard {
274 pub fn get_primary_text(&mut self) -> Option<String> {
276 None
277 }
278
279 pub fn set_primary_text(&mut self, _text: String) {}
281 }
282 }
283}
284
285#[cfg(all(
293 not(any(target_os = "android", target_os = "ios")),
294 feature = "arboard",
295))]
296fn is_expected_content_absence(err: &arboard::Error) -> bool {
297 matches!(err, arboard::Error::ContentNotAvailable)
298}
299
300#[cfg(all(
301 not(any(target_os = "android", target_os = "ios")),
302 feature = "arboard",
303))]
304fn color_image_from_arboard(image: &arboard::ImageData<'_>) -> egui::ColorImage {
305 egui::ColorImage::from_rgba_unmultiplied([image.width, image.height], &image.bytes)
306}
307
308#[cfg(all(
310 not(any(target_os = "android", target_os = "ios")),
311 feature = "arboard",
312))]
313fn arboard_from_color_image(image: &egui::ColorImage) -> arboard::ImageData<'static> {
314 let bytes: Vec<u8> = image
315 .pixels
316 .iter()
317 .flat_map(|color| color.to_srgba_unmultiplied())
318 .collect();
319 arboard::ImageData {
320 width: image.width(),
321 height: image.height(),
322 bytes: bytes.into(),
323 }
324}
325
326#[cfg(all(
327 not(any(target_os = "android", target_os = "ios")),
328 feature = "arboard",
329))]
330fn init_arboard() -> Option<arboard::Clipboard> {
331 profiling::function_scope!();
332
333 log::trace!("Initializing arboard clipboard…");
334 match arboard::Clipboard::new() {
335 Ok(clipboard) => Some(clipboard),
336 Err(err) => {
337 log::warn!("Failed to initialize arboard clipboard: {err}");
338 None
339 }
340 }
341}
342
343#[cfg(all(
348 any(
349 target_os = "linux",
350 target_os = "dragonfly",
351 target_os = "freebsd",
352 target_os = "netbsd",
353 target_os = "openbsd"
354 ),
355 feature = "smithay-clipboard"
356))]
357#[expect(unsafe_code)]
358unsafe fn init_smithay_clipboard(
359 raw_display_handle: Option<RawDisplayHandle>,
360) -> Option<smithay_clipboard::Clipboard> {
361 profiling::function_scope!();
362
363 if let Some(RawDisplayHandle::Wayland(display)) = raw_display_handle {
364 log::trace!("Initializing smithay clipboard…");
365 Some(unsafe { smithay_clipboard::Clipboard::new(display.display.as_ptr()) })
367 } else {
368 #[cfg(feature = "wayland")]
369 log::debug!("Cannot init smithay clipboard without a Wayland display handle");
370 #[cfg(not(feature = "wayland"))]
371 log::debug!(
372 "Cannot init smithay clipboard: the 'wayland' feature of 'egui-winit' is not enabled"
373 );
374 None
375 }
376}
377
378#[cfg(all(
379 not(any(target_os = "android", target_os = "ios")),
380 feature = "arboard",
381))]
382#[cfg(test)]
383mod tests {
384 use super::{arboard_from_color_image, color_image_from_arboard, is_expected_content_absence};
385
386 #[test]
392 fn only_content_not_available_is_treated_as_expected() {
393 assert!(is_expected_content_absence(
394 &arboard::Error::ContentNotAvailable
395 ));
396
397 assert!(!is_expected_content_absence(
398 &arboard::Error::ClipboardNotSupported
399 ));
400 assert!(!is_expected_content_absence(
401 &arboard::Error::ClipboardOccupied
402 ));
403 assert!(!is_expected_content_absence(
404 &arboard::Error::ConversionFailure
405 ));
406 assert!(!is_expected_content_absence(&arboard::Error::Unknown {
407 description: "anything".to_owned(),
408 }));
409 }
410
411 #[test]
412 fn color_image_from_arboard_converts_straight_to_premultiplied_alpha() {
413 let image = arboard::ImageData {
416 width: 2,
417 height: 1,
418 bytes: std::borrow::Cow::Borrowed(&[255, 0, 0, 255, 255, 255, 255, 128]),
419 };
420 let color_image = color_image_from_arboard(&image);
421 assert_eq!(color_image.size, [2, 1]);
422 assert_eq!(
423 color_image.pixels[0],
424 egui::Color32::from_rgba_unmultiplied(255, 0, 0, 255)
425 );
426 assert_eq!(
427 color_image.pixels[1],
428 egui::Color32::from_rgba_unmultiplied(255, 255, 255, 128)
429 );
430 }
431
432 #[test]
433 fn arboard_from_color_image_converts_premultiplied_to_straight_alpha() {
434 let color_image = egui::ColorImage::new(
435 [3, 1],
436 vec![
437 egui::Color32::from_rgba_unmultiplied(255, 0, 0, 255),
438 egui::Color32::from_rgba_unmultiplied(255, 255, 255, 128),
439 egui::Color32::TRANSPARENT,
440 ],
441 );
442 let image = arboard_from_color_image(&color_image);
443 assert_eq!([image.width, image.height], [3, 1]);
444 assert_eq!(
445 &*image.bytes,
446 &[255, 0, 0, 255, 255, 255, 255, 128, 0, 0, 0, 0]
447 );
448 }
449}