#[cfg(not(feature = "mini"))]
use std::path::PathBuf;
#[derive(Debug, Clone, PartialEq)]
pub enum ClipboardContent {
Text(String),
Html { html: String, plain: String },
Rtf(Vec<u8>),
Image { width: u32, height: u32, data: Vec<u8> },
Files(Vec<PathBuf>),
}
impl ClipboardContent {
pub fn content_type(&self) -> &'static str {
match self {
Self::Text(_) => "text/plain",
Self::Html { .. } => "text/html",
Self::Rtf(_) => "text/rtf",
Self::Image { .. } => "image/png",
Self::Files(_) => "text/uri-list",
}
}
}
impl From<String> for ClipboardContent {
fn from(text: String) -> Self {
Self::Text(text)
}
}
impl From<&str> for ClipboardContent {
fn from(text: &str) -> Self {
Self::Text(text.to_string())
}
}
pub trait RichClipboardBackend: Send + Sync {
fn set_contents(&self, content: ClipboardContent) -> bool;
fn get_contents(&self) -> Option<ClipboardContent>;
fn has_format(&self, content_type: &str) -> bool;
fn set_clipboard_html(&self, html: &str, plain_text: &str) {
self.set_contents(ClipboardContent::Html {
html: html.to_string(),
plain: plain_text.to_string(),
});
}
fn set_clipboard_image(&self, rgba: &[u8], width: u32, height: u32) {
self.set_contents(ClipboardContent::Image { width, height, data: rgba.to_vec() });
}
}
#[derive(Debug, Default)]
pub struct MockClipboard {
content: crate::compat::Mutex<Option<ClipboardContent>>,
}
impl MockClipboard {
pub fn new() -> Self {
Self::default()
}
}
impl RichClipboardBackend for MockClipboard {
fn set_contents(&self, content: ClipboardContent) -> bool {
*self.content.lock().unwrap() = Some(content);
true
}
fn get_contents(&self) -> Option<ClipboardContent> {
self.content.lock().unwrap().clone()
}
fn has_format(&self, content_type: &str) -> bool {
self.content.lock().unwrap().as_ref().is_some_and(|c| c.content_type() == content_type)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mock_set_get_text() {
let clip = MockClipboard::new();
assert!(clip.set_contents(ClipboardContent::Text("hello".into())));
assert_eq!(clip.get_contents(), Some(ClipboardContent::Text("hello".into())));
}
#[test]
fn test_mock_set_get_html() {
let clip = MockClipboard::new();
let html = ClipboardContent::Html { html: "<b>bold</b>".into(), plain: "bold".into() };
assert!(clip.set_contents(html.clone()));
assert_eq!(clip.get_contents(), Some(html));
}
#[test]
fn test_mock_has_format() {
let clip = MockClipboard::new();
clip.set_contents(ClipboardContent::Text("test".into()));
assert!(clip.has_format("text/plain"));
assert!(!clip.has_format("text/html"));
}
#[test]
fn test_clipboard_content_from_string() {
let c: ClipboardContent = "hello".into();
assert_eq!(c, ClipboardContent::Text("hello".into()));
}
#[test]
fn test_clipboard_content_type() {
assert_eq!(ClipboardContent::Text("a".into()).content_type(), "text/plain");
assert_eq!(
ClipboardContent::Html { html: "".into(), plain: "".into() }.content_type(),
"text/html"
);
assert_eq!(ClipboardContent::Rtf(vec![]).content_type(), "text/rtf");
}
#[test]
fn test_mock_initial_empty() {
let clip = MockClipboard::new();
assert_eq!(clip.get_contents(), None);
assert!(!clip.has_format("text/plain"));
}
#[test]
fn test_clipboard_mock_thread_safety() {
let clip = alloc::sync::Arc::new(MockClipboard::new());
let clip2 = clip.clone();
std::thread::spawn(move || {
clip2.set_contents(ClipboardContent::Text("from thread".into()))
})
.join()
.unwrap();
assert_eq!(clip.get_contents(), Some(ClipboardContent::Text("from thread".to_string())));
}
#[test]
fn test_clipboard_files_format() {
let files = ClipboardContent::Files(vec!["/tmp/a.txt".into(), "/tmp/b.txt".into()]);
assert_eq!(files.content_type(), "text/uri-list");
}
#[test]
fn test_clipboard_image_roundtrip() {
let clip = MockClipboard::new();
let img = ClipboardContent::Image {
width: 2,
height: 2,
data: vec![255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 255, 255, 255, 255],
};
clip.set_contents(img.clone());
assert_eq!(clip.get_contents(), Some(img));
assert!(clip.has_format("image/png"));
}
}