use std::rc::Rc;
use inset_embedder::{
ClipboardEvent, ClipboardItem, ClipboardItemsFuture, ClipboardWriteFuture, Content,
CopyOptions, PastedItem,
};
use inset_foundation::{App, Handle, Task};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ClipboardData {
pub text: Option<String>,
}
impl ClipboardData {
pub fn new(text: impl Into<String>) -> ClipboardData {
ClipboardData {
text: Some(text.into()),
}
}
}
pub struct Clipboard;
impl Clipboard {
pub const K_TEXT_PLAIN: &'static str = "text/plain";
pub fn set_data(app: &mut App, data: ClipboardData) -> Task<()> {
let Some(text) = data.text else {
return Task::ready(());
};
let item = ClipboardItem {
representations: vec![(
Self::K_TEXT_PLAIN.to_owned(),
Content::Data(text.into_bytes()),
)],
};
match Self::write(app, vec![item]) {
Some(written) => app.spawn(async move |_cx| {
let _refused = written.await;
}),
None => Task::ready(()),
}
}
pub fn get_data(app: &mut App, format: &str) -> Task<Option<ClipboardData>> {
if format != Self::K_TEXT_PLAIN {
return Task::ready(None);
}
match Self::readable(app) {
Some(readable) => {
app.spawn(async move |_cx| readable.text().await.map(ClipboardData::new))
}
None => Task::ready(None),
}
}
pub fn has_strings(app: &mut App) -> Task<bool> {
match Self::readable(app) {
Some(readable) => app.spawn(async move |_cx| readable.holds_text().await),
None => Task::ready(false),
}
}
pub fn handle_event<R>(
app: &mut App,
event: ClipboardEvent,
handle: impl FnOnce(&mut App) -> R,
) -> R {
let in_hand = Self::event_in_hand(app);
let outer = app.get_mut(in_hand).0.replace(Rc::new(event));
let result = handle(app);
app.get_mut(in_hand).0 = outer;
result
}
fn write(app: &mut App, items: Vec<ClipboardItem>) -> Option<ClipboardWriteFuture> {
let in_hand = Self::event_in_hand(app);
match app.get(in_hand).0.as_deref() {
Some(ClipboardEvent::Copy(request) | ClipboardEvent::Cut(request)) => {
Some(request.write(items, CopyOptions::new()))
}
Some(ClipboardEvent::Paste(_)) | None => app
.platform()
.clipboard()
.map(|clipboard| clipboard.write(items, CopyOptions::new())),
}
}
fn readable(app: &mut App) -> Option<Readable> {
let in_hand = Self::event_in_hand(app);
match app.get(in_hand).0.clone() {
Some(event) if matches!(*event, ClipboardEvent::Paste(_)) => {
Some(Readable::Pasted(event))
}
_ => app
.platform()
.clipboard()
.map(|clipboard| Readable::Listing(clipboard.items())),
}
}
fn event_in_hand(app: &mut App) -> Handle<EventInHand> {
app.singleton()
}
}
#[derive(Default)]
struct EventInHand(Option<Rc<ClipboardEvent>>);
enum Readable {
Pasted(Rc<ClipboardEvent>),
Listing(ClipboardItemsFuture),
}
impl Readable {
async fn text(self) -> Option<String> {
match self {
Readable::Pasted(paste) => text_of(pasted_items(&paste)).await,
Readable::Listing(items) => text_of(&items.await).await,
}
}
async fn holds_text(self) -> bool {
match self {
Readable::Pasted(paste) => holds_text(pasted_items(&paste)),
Readable::Listing(items) => holds_text(&items.await),
}
}
}
fn pasted_items(paste: &ClipboardEvent) -> &[Box<dyn PastedItem>] {
match paste {
ClipboardEvent::Paste(items) => items,
ClipboardEvent::Copy(_) | ClipboardEvent::Cut(_) => &[],
}
}
fn offers_text(item: &dyn PastedItem) -> bool {
item.types()
.iter()
.any(|mime| mime == Clipboard::K_TEXT_PLAIN)
}
fn holds_text(items: &[Box<dyn PastedItem>]) -> bool {
items.iter().any(|item| offers_text(item.as_ref()))
}
async fn text_of(items: &[Box<dyn PastedItem>]) -> Option<String> {
let item = items.iter().find(|item| offers_text(item.as_ref()))?;
let bytes = item.read(Clipboard::K_TEXT_PLAIN).await.ok()?;
Some(String::from_utf8_lossy(&bytes).into_owned())
}
#[cfg(test)]
mod tests {
use std::cell::RefCell;
use std::rc::Rc;
use std::task::{Context, Poll, Waker};
use inset_embedder::{Clipboard as _, CopyRequest};
use inset_foundation::AppCell;
use inset_test::{MockClipboard, TestPlatform};
use super::*;
fn app_with(clipboard: &MockClipboard) -> Rc<AppCell> {
let clipboard: Rc<dyn inset_embedder::Clipboard> = Rc::new(clipboard.clone());
AppCell::with_platform(Rc::new(TestPlatform::new().with_clipboard(clipboard)))
}
fn awaited<T: 'static>(cell: &Rc<AppCell>, task: Task<T>) -> Option<T> {
let answer = Rc::new(RefCell::new(None));
let out = Rc::clone(&answer);
cell.borrow()
.spawn(async move |_cx| *out.borrow_mut() = Some(task.await));
cell.checkpoint();
answer.take()
}
fn listed(clipboard: &MockClipboard) -> Vec<Box<dyn PastedItem>> {
let mut items = clipboard.items();
match items.as_mut().poll(&mut Context::from_waker(Waker::noop())) {
Poll::Ready(items) => items,
Poll::Pending => panic!("a mock clipboard lists at once"),
}
}
#[test]
fn get_data_returns_text() {
let mock_clipboard = MockClipboard::new();
let cell = app_with(&mock_clipboard);
mock_clipboard.set_clipboard_data(Some("Hello world".to_owned()));
let data = Clipboard::get_data(&mut cell.borrow_mut(), Clipboard::K_TEXT_PLAIN);
let data = awaited(&cell, data).expect("the read completed");
assert_eq!(data, Some(ClipboardData::new("Hello world")));
}
#[test]
fn get_data_returns_none() {
let mock_clipboard = MockClipboard::new();
let cell = app_with(&mock_clipboard);
mock_clipboard.set_clipboard_data(None);
let data = Clipboard::get_data(&mut cell.borrow_mut(), Clipboard::K_TEXT_PLAIN);
let data = awaited(&cell, data).expect("the read completed");
assert_eq!(data, None);
}
#[test]
fn set_data_sets_text() {
let mock_clipboard = MockClipboard::new();
let cell = app_with(&mock_clipboard);
let written =
Clipboard::set_data(&mut cell.borrow_mut(), ClipboardData::new("Hello world"));
awaited(&cell, written).expect("the write completed");
assert_eq!(
mock_clipboard.clipboard_data().as_deref(),
Some("Hello world")
);
}
#[test]
fn the_text_is_the_first_item_offered_as_text() {
let mock_clipboard = MockClipboard::new();
let cell = app_with(&mock_clipboard);
let png = ClipboardItem {
representations: vec![("image/png".to_owned(), Content::Data(vec![0x89]))],
};
let text = ClipboardItem {
representations: vec![("text/plain".to_owned(), Content::Data(b"second".to_vec()))],
};
drop(mock_clipboard.write(vec![png, text], CopyOptions::new()));
let has_strings = Clipboard::has_strings(&mut cell.borrow_mut());
assert_eq!(awaited(&cell, has_strings), Some(true));
let data = Clipboard::get_data(&mut cell.borrow_mut(), Clipboard::K_TEXT_PLAIN);
assert_eq!(
awaited(&cell, data).expect("the read completed"),
Some(ClipboardData::new("second"))
);
}
#[test]
fn a_paste_being_handled_is_read_in_place_of_the_clipboard() {
let on_clipboard = MockClipboard::new();
on_clipboard.set_clipboard_data(Some("on the clipboard".to_owned()));
let pasted = MockClipboard::new();
pasted.set_clipboard_data(Some("pasted".to_owned()));
let cell = app_with(&on_clipboard);
let data = Clipboard::handle_event(
&mut cell.borrow_mut(),
ClipboardEvent::Paste(listed(&pasted)),
|app| Clipboard::get_data(app, Clipboard::K_TEXT_PLAIN),
);
assert_eq!(
awaited(&cell, data).expect("the read completed"),
Some(ClipboardData::new("pasted"))
);
let after = Clipboard::get_data(&mut cell.borrow_mut(), Clipboard::K_TEXT_PLAIN);
assert_eq!(
awaited(&cell, after).expect("the read completed"),
Some(ClipboardData::new("on the clipboard")),
"a read after the paste reads the clipboard"
);
}
#[test]
fn a_copy_being_handled_writes_through_its_request() {
let on_clipboard = MockClipboard::new();
let copied = MockClipboard::new();
let cell = app_with(&on_clipboard);
let into = copied.clone();
let request = CopyRequest::new(move |items, options| into.write(items, options));
let written = Clipboard::handle_event(
&mut cell.borrow_mut(),
ClipboardEvent::Copy(request),
|app| Clipboard::set_data(app, ClipboardData::new("copied")),
);
awaited(&cell, written).expect("the write completed");
assert_eq!(copied.clipboard_data().as_deref(), Some("copied"));
assert_eq!(on_clipboard.clipboard_data(), None);
}
}