use super::ffi::WlProxy;
use crate::{MimeType, PhysicalPoint, WindowId};
#[derive(Debug)]
pub(super) struct Offer {
pub proxy: *mut WlProxy,
pub mimes: Vec<String>,
}
impl Offer {
pub(super) const MAX_OFFER_MIMES: usize = 32;
pub(super) const fn new(proxy: *mut WlProxy) -> Self {
Self {
proxy,
mimes: Vec::new(),
}
}
pub(super) fn note_mime(&mut self, mime: String) {
if self.mimes.len() < Self::MAX_OFFER_MIMES {
self.mimes.push(mime);
}
}
pub(super) fn pick(&self, want: MimeType) -> Option<&str> {
let exact = self.mimes.iter().find(|mime| *mime == want.as_str());
exact
.or_else(|| {
self.mimes
.iter()
.find(|mime| MimeType::recognize(mime) == Some(want))
})
.map(String::as_str)
}
}
#[derive(Debug)]
pub(super) struct Drag {
pub offer: Offer,
pub window: Option<WindowId>,
pub position: PhysicalPoint,
pub mime: Option<String>,
}
#[derive(Debug)]
pub(super) struct Source {
pub proxy: *mut WlProxy,
pub payload: Vec<(String, Vec<u8>)>,
}
impl Source {
pub(super) fn bytes_for(&self, mime: &str) -> Option<&[u8]> {
if let Some((_, bytes)) = self.payload.iter().find(|(offered, _)| offered == mime) {
return Some(bytes);
}
let wanted = MimeType::recognize(mime)?;
self.payload
.iter()
.find(|(offered, _)| MimeType::recognize(offered) == Some(wanted))
.map(|(_, bytes)| bytes.as_slice())
}
}
#[derive(Debug)]
pub(super) struct Device {
pub proxy: *mut WlProxy,
pub incoming: Vec<Offer>,
pub selection: Option<Offer>,
pub drag: Option<Drag>,
pub source: Option<Source>,
pub selection_seen: bool,
}
impl Device {
pub(super) const MAX_PENDING_OFFERS: usize = 8;
pub(super) const fn new(proxy: *mut WlProxy) -> Self {
Self {
proxy,
incoming: Vec::new(),
selection: None,
drag: None,
source: None,
selection_seen: false,
}
}
pub(super) fn push_incoming(&mut self, offer: Offer) -> Option<Offer> {
if self.incoming.len() >= Self::MAX_PENDING_OFFERS {
let evicted = self.incoming.remove(0);
self.incoming.push(offer);
return Some(evicted);
}
self.incoming.push(offer);
None
}
pub(super) fn claim(&mut self, proxy: usize) -> Option<Offer> {
if proxy == 0 {
return None;
}
let index = self
.incoming
.iter()
.position(|offer| offer.proxy as usize == proxy)?;
Some(self.incoming.remove(index))
}
pub(super) fn announced_mut(&mut self, proxy: usize) -> Option<&mut Offer> {
if let Some(index) = self
.incoming
.iter()
.position(|offer| offer.proxy as usize == proxy)
{
return self.incoming.get_mut(index);
}
if self
.selection
.as_ref()
.is_some_and(|offer| offer.proxy as usize == proxy)
{
return self.selection.as_mut();
}
self.drag
.as_mut()
.map(|drag| &mut drag.offer)
.filter(|offer| offer.proxy as usize == proxy)
}
}
#[derive(Clone, Debug, PartialEq)]
pub(super) enum Delivery {
Clipboard {
window: WindowId,
request: crate::ClipboardRequestId,
mime: crate::ReceivedMime,
},
Drop {
window: WindowId,
position: PhysicalPoint,
device: usize,
offer: usize,
},
}
impl Delivery {
pub(super) const fn window(&self) -> WindowId {
match self {
Self::Clipboard { window, .. } | Self::Drop { window, .. } => *window,
}
}
pub(super) const fn drop_offer(&self) -> Option<usize> {
match self {
Self::Drop { offer, .. } => Some(*offer),
Self::Clipboard { .. } => None,
}
}
}
#[derive(Debug)]
pub(super) struct Transfer {
pub reading: super::fd::Reading,
pub delivery: Delivery,
}
#[derive(Clone, Debug)]
pub(super) struct HeldRead {
pub window: WindowId,
pub request: crate::ClipboardRequestId,
pub mime: MimeType,
pub deadline_nanos: u64,
}
#[derive(Debug)]
pub(super) enum Resolution {
Ready {
offer: *mut super::WlProxy,
spelling: String,
},
Empty,
Unknown,
}
pub(super) const ACTION_COPY: u32 = 1;
#[cfg(test)]
mod tests {
use core::ptr;
use super::*;
fn offer_with(mimes: &[&str]) -> Offer {
Offer {
proxy: ptr::null_mut(),
mimes: mimes.iter().map(|mime| (*mime).to_string()).collect(),
}
}
#[test]
fn an_exact_spelling_wins_and_an_equivalent_one_still_matches() {
let both = offer_with(&["text/plain", "text/plain;charset=utf-8"]);
assert_eq!(
both.pick(MimeType::TextUtf8),
Some("text/plain;charset=utf-8")
);
let bare = offer_with(&["TARGETS", "text/plain", "text/html"]);
assert_eq!(
bare.pick(MimeType::TextUtf8),
Some("text/plain"),
"asking for utf-8 text must accept the peer's spelling of it"
);
assert_eq!(bare.pick(MimeType::CrcblRon), None);
assert_eq!(bare.pick(MimeType::UriList), None);
let foreign = offer_with(&["application/vnd.some-editor.scene+json"]);
assert_eq!(
foreign.pick(MimeType::Other("application/vnd.some-editor.scene+json")),
Some("application/vnd.some-editor.scene+json")
);
assert_eq!(foreign.pick(MimeType::Other("image/png")), None);
assert_eq!(offer_with(&[]).pick(MimeType::TextUtf8), None);
}
#[test]
fn a_source_answers_a_peers_spelling_of_a_format_it_published() {
let source = Source {
proxy: ptr::null_mut(),
payload: vec![
(MimeType::TextUtf8.as_str().to_string(), b"plain".to_vec()),
(
MimeType::CrcblRon.as_str().to_string(),
b"(node:1)".to_vec(),
),
],
};
assert_eq!(
source.bytes_for("text/plain;charset=utf-8"),
Some(&b"plain"[..])
);
assert_eq!(
source.bytes_for("text/plain"),
Some(&b"plain"[..]),
"a peer may ask for the spelling it knows, not the one we advertised"
);
assert_eq!(source.bytes_for("UTF8_STRING"), Some(&b"plain"[..]));
assert_eq!(
source.bytes_for("application/x-crcbl+ron"),
Some(&b"(node:1)"[..])
);
assert_eq!(source.bytes_for("image/png"), None);
}
#[test]
fn an_announced_offer_is_claimed_exactly_once() {
let mut device = Device::new(ptr::null_mut());
let first = 0x1000 as *mut WlProxy;
let second = 0x2000 as *mut WlProxy;
device.incoming.push(Offer::new(first));
device.incoming.push(Offer::new(second));
device
.announced_mut(first as usize)
.expect("announced")
.mimes
.push("text/plain".to_string());
let claimed = device.claim(first as usize).expect("claimed");
assert_eq!(claimed.mimes, ["text/plain"]);
assert!(
device.claim(first as usize).is_none(),
"claiming twice would destroy an offer somebody else owns"
);
assert_eq!(device.incoming.len(), 1);
assert!(device.claim(0).is_none());
device.selection = Some(claimed);
assert!(device.announced_mut(first as usize).is_some());
assert!(device.announced_mut(0xdead).is_none());
}
#[test]
fn push_incoming_evicts_the_oldest_offer_at_the_cap() {
let mut device = Device::new(ptr::null_mut());
let proxies: Vec<*mut WlProxy> = (0..Device::MAX_PENDING_OFFERS)
.map(|i| ((i + 1) * 0x1000) as *mut WlProxy)
.collect();
for &proxy in &proxies {
device.push_incoming(Offer::new(proxy));
}
let evicted = device
.push_incoming(Offer::new(0xdead as *mut WlProxy))
.expect("past the cap");
assert_eq!(evicted.proxy, proxies[0]);
assert_eq!(device.incoming.len(), Device::MAX_PENDING_OFFERS);
assert!(device.claim(proxies[0] as usize).is_none());
assert!(device.claim(proxies[1] as usize).is_some());
}
#[test]
fn note_mime_caps_the_format_list() {
let mut offer = Offer::new(ptr::null_mut());
let mimes: Vec<String> = (0..Offer::MAX_OFFER_MIMES + 8)
.map(|i| format!("text/plain;charset=utf-8;part={i}"))
.collect();
for mime in &mimes {
offer.note_mime(mime.clone());
}
assert_eq!(offer.mimes.len(), Offer::MAX_OFFER_MIMES);
assert_eq!(offer.mimes, mimes[..Offer::MAX_OFFER_MIMES]);
}
}