#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ClipAction {
Copy,
Cut,
Paste(String),
}
pub fn poll(ctx: &egui::Context) -> Vec<ClipAction> {
ctx.input(|i| {
i.events
.iter()
.filter_map(|e| match e {
egui::Event::Copy => Some(ClipAction::Copy),
egui::Event::Cut => Some(ClipAction::Cut),
egui::Event::Paste(s) => Some(ClipAction::Paste(s.clone())),
_ => None,
})
.collect()
})
}
pub fn put(ctx: &egui::Context, text: impl Into<String>) {
ctx.copy_text(text.into());
}
pub fn rows_to_tsv<S: AsRef<str>>(header: &[S], rows: impl IntoIterator<Item = Vec<String>>) -> String {
let mut out = String::new();
if !header.is_empty() {
out.push_str(&header.iter().map(|c| c.as_ref()).collect::<Vec<_>>().join("\t"));
}
for row in rows {
if !out.is_empty() {
out.push('\n');
}
out.push_str(&row.join("\t"));
}
out
}
const ENVELOPE_KIND: &str = "facett.kind";
pub const ENVELOPE_VERSION: u64 = 1;
pub fn encode_component(kind: &str, state: &serde_json::Value) -> String {
let env = serde_json::json!({
ENVELOPE_KIND: kind,
"v": ENVELOPE_VERSION,
"state": state,
});
serde_json::to_string(&env).unwrap_or_default()
}
pub fn decode_component(text: &str) -> Option<(String, serde_json::Value)> {
let env: serde_json::Value = serde_json::from_str(text).ok()?;
let obj = env.as_object()?;
if obj.get("v").and_then(|v| v.as_u64())? != ENVELOPE_VERSION {
return None;
}
let kind = obj.get(ENVELOPE_KIND)?.as_str()?.to_string();
if kind.is_empty() {
return None;
}
let state = obj.get("state")?.clone();
Some((kind, state))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rows_to_tsv_joins_header_and_rows() {
let tsv = rows_to_tsv(
&["name", "ver"],
vec![vec!["knut".to_string(), "0.1".to_string()], vec!["korp".to_string(), "0.2".to_string()]],
);
assert_eq!(tsv, "name\tver\nknut\t0.1\nkorp\t0.2");
}
#[test]
fn rows_to_tsv_no_header() {
let tsv = rows_to_tsv::<&str>(&[], vec![vec!["a".to_string(), "b".to_string()]]);
assert_eq!(tsv, "a\tb");
}
#[test]
fn poll_decodes_events_in_order() {
let ctx = egui::Context::default();
let input = egui::RawInput {
events: vec![
egui::Event::Copy,
egui::Event::Paste("hello".to_string()),
egui::Event::Cut,
],
..Default::default()
};
let mut got = Vec::new();
let _ = ctx.run(input, |ctx| {
got = poll(ctx);
});
assert_eq!(got, vec![ClipAction::Copy, ClipAction::Paste("hello".to_string()), ClipAction::Cut]);
}
#[test]
fn component_envelope_round_trips_kind_and_state() {
let state = serde_json::json!({ "pan": [3.0, 4.0], "zoom": 1.5, "selected": "b" });
let text = encode_component("graphpan", &state);
assert!(text.contains("facett.kind"));
assert!(text.contains("graphpan"));
let (kind, back) = decode_component(&text).expect("decodes its own envelope");
assert_eq!(kind, "graphpan");
assert_eq!(back, state, "state survives the round-trip byte-for-byte");
}
#[test]
fn decode_rejects_unknown_version_cleanly() {
let bad = serde_json::json!({ "facett.kind": "table", "v": 99, "state": {} }).to_string();
assert_eq!(decode_component(&bad), None, "an unknown v is rejected, not adopted");
}
#[test]
fn decode_rejects_non_envelope_text() {
assert_eq!(decode_component("name\tver\nknut\t0.1"), None);
assert_eq!(decode_component("just some prose"), None);
let empty = serde_json::json!({ "facett.kind": "", "v": 1, "state": {} }).to_string();
assert_eq!(decode_component(&empty), None);
}
}