use std::sync::{Arc, Mutex, OnceLock};
use arrow_array::ArrayRef;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ClipKind {
Text,
DataColumns,
Rows,
Image,
Custom(&'static str),
}
#[derive(Clone)]
pub struct ArrowColumnRef {
pub name: String,
pub array: ArrayRef,
}
impl std::fmt::Debug for ArrowColumnRef {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ArrowColumnRef")
.field("name", &self.name)
.field("len", &self.array.len())
.field("dtype", &self.array.data_type())
.finish()
}
}
#[derive(Clone, Debug)]
pub enum ClipPayload {
Text(String),
DataColumns(Vec<ArrowColumnRef>),
Rows(String),
Image(egui::ColorImage),
Custom { kind: &'static str, bytes: Arc<[u8]> },
}
impl ClipPayload {
pub fn kind(&self) -> ClipKind {
match self {
ClipPayload::Text(_) => ClipKind::Text,
ClipPayload::DataColumns(_) => ClipKind::DataColumns,
ClipPayload::Rows(_) => ClipKind::Rows,
ClipPayload::Image(_) => ClipKind::Image,
ClipPayload::Custom { kind, .. } => ClipKind::Custom(kind),
}
}
pub fn as_text(&self) -> String {
match self {
ClipPayload::Text(s) | ClipPayload::Rows(s) => s.clone(),
ClipPayload::DataColumns(cols) => columns_to_tsv(cols),
ClipPayload::Image(img) => format!("[image {}x{}]", img.size[0], img.size[1]),
ClipPayload::Custom { kind, bytes } => format!("[{kind}: {} bytes]", bytes.len()),
}
}
pub fn as_image(&self) -> Option<&egui::ColorImage> {
match self {
ClipPayload::Image(img) => Some(img),
_ => None,
}
}
}
#[cfg(feature = "os-image")]
pub mod os_image {
use super::*;
pub fn put_image(img: &egui::ColorImage) -> Result<(), String> {
let mut cb = arboard::Clipboard::new().map_err(|e| e.to_string())?;
let bytes: Vec<u8> = img.pixels.iter().flat_map(|p| [p.r(), p.g(), p.b(), p.a()]).collect();
cb.set_image(arboard::ImageData {
width: img.size[0],
height: img.size[1],
bytes: bytes.into(),
})
.map_err(|e| e.to_string())
}
pub fn get_image() -> Result<egui::ColorImage, String> {
let mut cb = arboard::Clipboard::new().map_err(|e| e.to_string())?;
let img = cb.get_image().map_err(|e| e.to_string())?;
let size = [img.width, img.height];
let pixels = img
.bytes
.chunks_exact(4)
.map(|c| egui::Color32::from_rgba_unmultiplied(c[0], c[1], c[2], c[3]))
.collect();
Ok(egui::ColorImage::new(size, pixels))
}
}
pub trait CopySource {
fn copy_kinds(&self) -> &[ClipKind];
fn copy_payload(&self) -> Option<ClipPayload>;
}
pub trait PasteTarget {
fn accepts(&self, k: ClipKind) -> bool;
fn paste_payload(&mut self, p: &ClipPayload);
}
fn registry() -> &'static Mutex<Option<ClipPayload>> {
static REG: OnceLock<Mutex<Option<ClipPayload>>> = OnceLock::new();
REG.get_or_init(|| Mutex::new(None))
}
pub fn put_typed(ctx: &egui::Context, payload: ClipPayload) {
let text = payload.as_text();
ctx.copy_text(text);
*registry().lock().unwrap() = Some(payload);
}
pub fn put_typed_inproc(payload: ClipPayload) {
*registry().lock().unwrap() = Some(payload);
}
pub fn last_typed() -> Option<ClipPayload> {
registry().lock().unwrap().clone()
}
pub fn clear_typed() {
*registry().lock().unwrap() = None;
}
pub fn paste_into(target: &mut dyn PasteTarget) -> bool {
let Some(payload) = last_typed() else { return false };
if target.accepts(payload.kind()) {
target.paste_payload(&payload);
true
} else if target.accepts(ClipKind::Text) {
target.paste_payload(&ClipPayload::Text(payload.as_text()));
true
} else {
false
}
}
pub fn columns_to_tsv(cols: &[ArrowColumnRef]) -> String {
if cols.is_empty() {
return String::new();
}
let nrows = cols.iter().map(|c| c.array.len()).max().unwrap_or(0);
let mut out = String::new();
out.push_str(&cols.iter().map(|c| c.name.as_str()).collect::<Vec<_>>().join("\t"));
for r in 0..nrows {
out.push('\n');
let line: Vec<String> = cols.iter().map(|c| arrow_cell_text(c.array.as_ref(), r)).collect();
out.push_str(&line.join("\t"));
}
out
}
fn arrow_cell_text(arr: &dyn arrow_array::Array, i: usize) -> String {
use arrow_array::{
BooleanArray, Float32Array, Float64Array, Int32Array, Int64Array, StringArray, UInt32Array, UInt64Array,
};
if i >= arr.len() || arr.is_null(i) {
return String::new();
}
macro_rules! tnum {
($($ty:ty),*) => {{ $(if let Some(a) = arr.as_any().downcast_ref::<$ty>() { return a.value(i).to_string(); })* }};
}
tnum!(Int64Array, Int32Array, UInt64Array, UInt32Array, Float64Array, Float32Array, BooleanArray);
if let Some(a) = arr.as_any().downcast_ref::<StringArray>() {
return a.value(i).to_string();
}
String::new()
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use arrow_array::{Int64Array, StringArray};
use super::*;
fn cols() -> Vec<ArrowColumnRef> {
vec![
ArrowColumnRef { name: "name".into(), array: Arc::new(StringArray::from(vec!["knut", "korp"])) },
ArrowColumnRef { name: "n".into(), array: Arc::new(Int64Array::from(vec![1, 2])) },
]
}
#[test]
fn datacolumns_render_as_tsv_for_the_text_fallback() {
let p = ClipPayload::DataColumns(cols());
assert_eq!(p.kind(), ClipKind::DataColumns);
assert_eq!(p.as_text(), "name\tn\nknut\t1\nkorp\t2");
}
#[test]
fn datacolumns_are_zero_copy_arc_shared() {
let original = cols();
let arr_ptr = Arc::as_ptr(&original[0].array) as *const ();
let payload = ClipPayload::DataColumns(original);
if let ClipPayload::DataColumns(c) = &payload {
assert_eq!(Arc::as_ptr(&c[0].array) as *const (), arr_ptr, "column array must be Arc-shared, not copied");
} else {
panic!("wrong kind");
}
}
struct ColTarget {
last: Option<usize>,
text: Option<String>,
}
impl PasteTarget for ColTarget {
fn accepts(&self, k: ClipKind) -> bool {
matches!(k, ClipKind::DataColumns | ClipKind::Text)
}
fn paste_payload(&mut self, p: &ClipPayload) {
match p {
ClipPayload::DataColumns(c) => self.last = Some(c.len()),
ClipPayload::Text(s) => self.text = Some(s.clone()),
_ => {}
}
}
}
#[test]
fn typed_transfer_between_components_in_process() {
clear_typed();
put_typed_inproc(ClipPayload::DataColumns(cols()));
let mut t = ColTarget { last: None, text: None };
assert!(paste_into(&mut t));
assert_eq!(t.last, Some(2), "target received 2 typed columns");
assert!(t.text.is_none(), "richest accepted kind used, not the text fallback");
clear_typed();
}
#[test]
fn image_payload_transfers_to_a_viewer_in_process() {
clear_typed();
let img = egui::ColorImage::new(
[2, 2],
vec![egui::Color32::RED, egui::Color32::GREEN, egui::Color32::BLUE, egui::Color32::WHITE],
);
put_typed_inproc(ClipPayload::Image(img));
struct Viewer(Option<[usize; 2]>);
impl PasteTarget for Viewer {
fn accepts(&self, k: ClipKind) -> bool {
matches!(k, ClipKind::Image | ClipKind::Text)
}
fn paste_payload(&mut self, p: &ClipPayload) {
if let Some(img) = p.as_image() {
self.0 = Some(img.size);
}
}
}
let mut v = Viewer(None);
assert!(paste_into(&mut v));
assert_eq!(v.0, Some([2, 2]), "viewer received the typed image (a copied frame)");
clear_typed();
}
#[test]
fn text_is_the_universal_fallback() {
clear_typed();
put_typed_inproc(ClipPayload::DataColumns(cols()));
struct TextOnly(String);
impl PasteTarget for TextOnly {
fn accepts(&self, k: ClipKind) -> bool {
matches!(k, ClipKind::Text)
}
fn paste_payload(&mut self, p: &ClipPayload) {
if let ClipPayload::Text(s) = p {
self.0 = s.clone();
}
}
}
let mut t = TextOnly(String::new());
assert!(paste_into(&mut t));
assert_eq!(t.0, "name\tn\nknut\t1\nkorp\t2", "fell back to the TSV text view");
clear_typed();
}
}