use crate::geometry::{PhysicalSize, Point, ScaleFactor};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct WindowId(pub(crate) u64);
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ButtonState {
Pressed,
Released,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PointerButton {
Left,
Right,
Middle,
Other(u16),
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Modifiers {
pub shift: bool,
pub ctrl: bool,
pub alt: bool,
pub meta: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum KeyCode {
Enter,
Escape,
Backspace,
Tab,
Space,
ArrowLeft,
ArrowRight,
ArrowUp,
ArrowDown,
Home,
End,
PageUp,
PageDown,
Insert,
Delete,
F1,
F2,
F3,
F4,
F5,
F6,
F7,
F8,
F9,
F10,
F11,
F12,
Alt,
ContextMenu,
Char(char),
Unidentified(u32),
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ScrollDelta {
pub dx: f64,
pub dy: f64,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TouchPhase {
Started,
Moved,
Ended,
Cancelled,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Touch {
pub id: u64,
pub phase: TouchPhase,
pub position: Point,
}
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub enum Event {
RedrawRequested,
Resized(PhysicalSize),
ScaleFactorChanged(ScaleFactor),
CloseRequested,
Wake,
FocusChanged(bool),
ThemeChanged,
PointerMoved { position: Point },
PointerLeft,
PointerButton {
button: PointerButton,
state: ButtonState,
position: Point,
},
Scroll { delta: ScrollDelta },
Touch(Touch),
Key {
code: KeyCode,
state: ButtonState,
modifiers: Modifiers,
},
Text(String),
Ime(Ime),
MenuCommand(u32),
TrayActivated,
DragEnter { position: Point, offer: DragOffer },
DragOver { position: Point },
DragLeave,
Drop { position: Point, data: DropData },
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct DragOffer {
pub files: bool,
pub text: bool,
pub internal: bool,
}
impl DragOffer {
pub const FILES: DragOffer = DragOffer {
files: true,
text: false,
internal: false,
};
pub const TEXT: DragOffer = DragOffer {
files: false,
text: true,
internal: false,
};
pub const INTERNAL: DragOffer = DragOffer {
files: false,
text: false,
internal: true,
};
pub const ANY: DragOffer = DragOffer {
files: true,
text: true,
internal: true,
};
pub fn accepts(self, offer: DragOffer) -> bool {
(self.files && offer.files)
|| (self.text && offer.text)
|| (self.internal && offer.internal)
}
pub fn is_empty(self) -> bool {
!(self.files || self.text || self.internal)
}
pub fn union(self, other: DragOffer) -> DragOffer {
DragOffer {
files: self.files || other.files,
text: self.text || other.text,
internal: self.internal || other.internal,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum DropData {
Files(Vec<std::path::PathBuf>),
Text(String),
Internal(String),
}
impl DropData {
pub fn kind(&self) -> DragOffer {
match self {
DropData::Files(_) => DragOffer::FILES,
DropData::Text(_) => DragOffer::TEXT,
DropData::Internal(_) => DragOffer::INTERNAL,
}
}
pub fn can_leave_app(&self) -> bool {
!matches!(self, DropData::Internal(_))
}
}
pub fn uri_list(paths: &[std::path::PathBuf]) -> String {
paths.iter().map(|p| path_to_file_uri(p) + "\r\n").collect()
}
pub fn path_to_file_uri(path: &std::path::Path) -> String {
#[cfg(unix)]
let bytes = {
use std::os::unix::ffi::OsStrExt;
path.as_os_str().as_bytes().to_vec()
};
#[cfg(not(unix))]
let bytes = {
let s = path.to_string_lossy().replace('\\', "/");
let s = if s.starts_with('/') {
s
} else {
format!("/{s}")
};
s.into_bytes()
};
let mut uri = String::from("file://");
for b in bytes {
if b.is_ascii_alphanumeric() || b"-._~/".contains(&b) || (cfg!(not(unix)) && b == b':') {
uri.push(b as char);
} else {
uri.push_str(&format!("%{b:02X}"));
}
}
uri
}
pub fn parse_uri_list(list: &str) -> Vec<std::path::PathBuf> {
list.lines()
.map(|l| l.trim_matches(|c: char| c.is_whitespace() || c == '\0'))
.filter(|l| !l.is_empty() && !l.starts_with('#'))
.filter_map(file_uri_to_path)
.collect()
}
pub fn file_uri_to_path(uri: &str) -> Option<std::path::PathBuf> {
let rest = uri.strip_prefix("file:")?;
let path = match rest.strip_prefix("//") {
Some(after) => {
let slash = after.find('/')?;
let host = &after[..slash];
if !(host.is_empty() || host.eq_ignore_ascii_case("localhost")) {
return None;
}
&after[slash..]
}
None => rest,
};
let bytes = path.as_bytes();
let hex = |b: Option<&u8>| b.and_then(|&b| (b as char).to_digit(16)).map(|d| d as u8);
let mut out = Vec::with_capacity(bytes.len());
let mut i = 0;
while i < bytes.len() {
match (bytes[i], hex(bytes.get(i + 1)), hex(bytes.get(i + 2))) {
(b'%', Some(h), Some(l)) => {
out.push(h << 4 | l);
i += 3;
}
(b, _, _) => {
out.push(b);
i += 1;
}
}
}
String::from_utf8(out).ok().map(std::path::PathBuf::from)
}
#[cfg_attr(not(windows), allow(dead_code))]
pub(crate) fn hdrop_bytes(paths: &[Vec<u16>]) -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(&20u32.to_le_bytes()); out.extend_from_slice(&[0; 12]); out.extend_from_slice(&1u32.to_le_bytes()); for unit in paths
.iter()
.flat_map(|p| p.iter().copied().chain([0]))
.chain([0])
{
out.extend_from_slice(&unit.to_le_bytes());
}
out
}
#[cfg_attr(not(windows), allow(dead_code))]
pub(crate) fn unicode_text_bytes(text: &str) -> Vec<u8> {
text.encode_utf16()
.chain([0])
.flat_map(u16::to_le_bytes)
.collect()
}
#[cfg(test)]
mod dnd_tests {
use super::*;
use std::path::PathBuf;
#[test]
fn uri_lists_encode_and_round_trip() {
let paths = vec![
PathBuf::from("/tmp/a b.txt"),
PathBuf::from("/tmp/h\u{e9}llo%#?.txt"),
];
let list = uri_list(&paths);
assert_eq!(
list,
"file:///tmp/a%20b.txt\r\nfile:///tmp/h%C3%A9llo%25%23%3F.txt\r\n"
);
assert_eq!(parse_uri_list(&list), paths);
assert!(DropData::Files(paths).can_leave_app());
assert!(DropData::Text("x".into()).can_leave_app());
assert!(!DropData::Internal("x".into()).can_leave_app());
}
#[test]
fn windows_formats_are_laid_out() {
let wide = |s: &str| s.encode_utf16().collect::<Vec<u16>>();
let block = hdrop_bytes(&[wide("C:\\a"), wide("D:\\\u{e9}")]);
assert_eq!(&block[..4], &20u32.to_le_bytes());
assert_eq!(&block[16..20], &1u32.to_le_bytes());
let units: Vec<u16> = block[20..]
.chunks(2)
.map(|c| u16::from_le_bytes([c[0], c[1]]))
.collect();
let mut want = wide("C:\\a");
want.push(0);
want.extend(wide("D:\\\u{e9}"));
want.extend([0, 0]);
assert_eq!(units, want);
assert_eq!(unicode_text_bytes("h\u{e9}"), vec![b'h', 0, 0xe9, 0, 0, 0]);
}
#[test]
fn uri_list_parses_file_uris() {
let list = "# comment\r\nfile:///tmp/a%20b.txt\r\nhttp://example.com/x\r\n\
file://localhost/etc/hosts\nfile:/x/y\nfile://remote/z\n\0";
assert_eq!(
parse_uri_list(list),
vec![
PathBuf::from("/tmp/a b.txt"),
PathBuf::from("/etc/hosts"),
PathBuf::from("/x/y"),
]
);
}
#[test]
fn offers_filter_kinds() {
assert!(DragOffer::ANY.accepts(DragOffer::FILES));
assert!(!DragOffer::TEXT.accepts(DragOffer::FILES));
assert!(
DragOffer::FILES
.union(DragOffer::TEXT)
.accepts(DragOffer::TEXT)
);
assert!(DragOffer::default().is_empty());
assert_eq!(DropData::Text("x".into()).kind(), DragOffer::TEXT);
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Ime {
Preedit {
text: String,
cursor: Option<(usize, usize)>,
},
Commit(String),
DeleteSurrounding { before: usize, after: usize },
}