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 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(test)]
mod dnd_tests {
use super::*;
use std::path::PathBuf;
#[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 },
}