use std::{collections::HashMap, path::PathBuf, sync::mpsc::channel};
use serde::{ser::Serializer, Deserialize, Deserializer, Serialize};
use tauri::{command, ipc::Channel, AppHandle, Runtime, Window};
type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error(transparent)]
Drag(#[from] drag::Error),
#[error(transparent)]
Tauri(#[from] tauri::Error),
#[error(transparent)]
Base64(#[from] base64::DecodeError),
}
impl Serialize for Error {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.to_string().as_ref())
}
}
pub struct Base64Image(String);
impl<'de> Deserialize<'de> for Base64Image {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let value = String::deserialize(deserializer)?;
if let Some(data) = value.strip_prefix("data:image/png;base64,") {
return Ok(Self(data.into()));
}
Err(serde::de::Error::custom(
"expected an image/png base64 image string",
))
}
}
#[derive(Deserialize)]
#[serde(untagged)]
pub enum Image {
Base64(Base64Image),
Raw(drag::Image),
}
#[derive(Deserialize)]
#[serde(untagged)]
pub enum DragItem {
Files(Vec<PathBuf>),
Data {
data: SharedData,
types: Vec<String>,
},
}
#[derive(Deserialize)]
#[serde(untagged)]
pub enum SharedData {
Fixed(String),
Map(HashMap<String, String>),
}
#[derive(Serialize, Clone)]
pub struct CallbackResult {
result: drag::DragResult,
#[serde(rename = "cursorPos")]
cursor_pos: drag::CursorPosition,
}
#[derive(Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub enum DragMode {
#[default]
Copy,
Move,
}
impl From<DragMode> for drag::DragMode {
fn from(value: DragMode) -> Self {
match value {
DragMode::Copy => Self::Copy,
DragMode::Move => Self::Move,
}
}
}
#[derive(Default, Deserialize)]
pub struct DragOptions {
#[serde(default)]
mode: DragMode,
}
impl From<DragOptions> for drag::Options {
fn from(options: DragOptions) -> Self {
Self {
skip_animatation_on_cancel_or_failure: false,
mode: options.mode.into(),
}
}
}
#[command]
pub async fn start_drag<R: Runtime>(
app: AppHandle<R>,
window: Window<R>,
item: DragItem,
image: Image,
options: Option<DragOptions>,
on_event: Channel<CallbackResult>,
) -> Result<()> {
let (tx, rx) = channel();
let image = match image {
Image::Raw(r) => r,
Image::Base64(b) => {
use base64::Engine;
drag::Image::Raw(base64::engine::general_purpose::STANDARD.decode(b.0)?)
}
};
app.run_on_main_thread(move || {
#[cfg(target_os = "linux")]
let raw_window = window.gtk_window();
#[cfg(not(target_os = "linux"))]
let raw_window = tauri::Result::Ok(window.clone());
let r = match raw_window {
Ok(w) => drag::start_drag(
&w,
match item {
DragItem::Files(f) => drag::DragItem::Files(f),
DragItem::Data { data, types } => drag::DragItem::Data {
provider: Box::new(move |data_type| match &data {
SharedData::Fixed(d) => Some(d.as_bytes().to_vec()),
SharedData::Map(m) => m.get(data_type).map(|d| d.as_bytes().to_vec()),
}),
types,
},
},
image,
move |result, cursor_pos| {
let callback_result = CallbackResult { result, cursor_pos };
let _ = on_event.send(callback_result);
},
options.unwrap_or_default().into(),
)
.map_err(Into::into),
Err(e) => Err(e.into()),
};
tx.send(r).unwrap();
})?;
rx.recv().unwrap()
}