use gtk4::gdk;
use gtk4::glib;
use gtk4::prelude::*;
use crate::{Key, Modifiers, MouseButton, NamedKey, UiEvent};
pub fn gdk_key_to_uievent(
key: gdk::Key,
modifiers: gdk::ModifierType,
repeat: bool,
) -> Option<UiEvent> {
let key = gdk_key_to_quadraui_key(key)?;
let modifiers = gdk_modifiers_to_quadraui(modifiers);
Some(UiEvent::KeyPressed {
key,
modifiers,
repeat,
})
}
pub fn gdk_button_to_mouse_down(
button: u32,
x: f64,
y: f64,
modifiers: gdk::ModifierType,
) -> UiEvent {
crate::event::mouse_down(
gdk_button_to_quadraui(button),
x as f32,
y as f32,
gdk_modifiers_to_quadraui(modifiers),
)
}
pub fn gdk_button_to_mouse_up(button: u32, x: f64, y: f64) -> UiEvent {
crate::event::mouse_up(gdk_button_to_quadraui(button), x as f32, y as f32)
}
pub fn gdk_motion_to_uievent(x: f64, y: f64, buttons: crate::ButtonMask) -> UiEvent {
crate::event::mouse_moved(x as f32, y as f32, buttons)
}
pub fn gdk_scroll_to_uievent(dx: f64, dy: f64, x: f64, y: f64) -> UiEvent {
gdk_scroll_to_uievent_with_direction(dx, dy, x, y, false)
}
pub fn gdk_scroll_to_uievent_with_direction(
dx: f64,
dy: f64,
x: f64,
y: f64,
natural_scroll: bool,
) -> UiEvent {
let dy_native_down_positive = if natural_scroll { -dy } else { dy };
crate::event::scroll(
dx as f32,
dy_native_down_positive as f32,
x as f32,
y as f32,
)
}
pub fn gdk_resize_to_uievent(width: i32, height: i32, scale: f32) -> UiEvent {
crate::event::window_resized(width as f32, height as f32, scale)
}
pub fn gtk_drop_to_uievent(paths: Vec<std::path::PathBuf>, x: f64, y: f64) -> UiEvent {
crate::event::files_dropped(paths, x as f32, y as f32)
}
pub fn gtk_open_to_uievent(files: &[gtk4::gio::File]) -> UiEvent {
let mut local_paths = Vec::new();
let mut urls = Vec::new();
for file in files {
match file.path() {
Some(path) => local_paths.push(path),
None => urls.push(file.uri().to_string()),
}
}
UiEvent::OpenRequested {
urls,
files: local_paths,
}
}
pub fn gdk_modifiers_to_quadraui(m: gdk::ModifierType) -> Modifiers {
Modifiers {
shift: m.contains(gdk::ModifierType::SHIFT_MASK),
ctrl: m.contains(gdk::ModifierType::CONTROL_MASK),
alt: m.contains(gdk::ModifierType::ALT_MASK),
cmd: m.contains(gdk::ModifierType::SUPER_MASK) || m.contains(gdk::ModifierType::META_MASK),
}
}
pub fn gdk_key_to_quadraui_key(key: gdk::Key) -> Option<Key> {
if let Some(c) = key.to_unicode() {
if !c.is_control() {
return Some(Key::Char(c));
}
if let Some(name) = key.name() {
let bytes = name.as_bytes();
if bytes.len() == 1 && bytes[0].is_ascii_alphabetic() {
return Some(Key::Char(bytes[0] as char));
}
}
}
let name = key.name()?;
let named = gdk_keyname_to_named_key(&name)?;
Some(Key::Named(named))
}
pub fn gdk_keyname_to_named_key(name: &str) -> Option<NamedKey> {
let named = match name {
"Escape" => NamedKey::Escape,
"Tab" => NamedKey::Tab,
"ISO_Left_Tab" => NamedKey::BackTab,
"Return" | "KP_Enter" => NamedKey::Enter,
"BackSpace" => NamedKey::Backspace,
"Delete" | "KP_Delete" => NamedKey::Delete,
"Insert" | "KP_Insert" => NamedKey::Insert,
"Home" | "KP_Home" => NamedKey::Home,
"End" | "KP_End" => NamedKey::End,
"Page_Up" | "KP_Page_Up" => NamedKey::PageUp,
"Page_Down" | "KP_Page_Down" => NamedKey::PageDown,
"Up" | "KP_Up" => NamedKey::Up,
"Down" | "KP_Down" => NamedKey::Down,
"Left" | "KP_Left" => NamedKey::Left,
"Right" | "KP_Right" => NamedKey::Right,
"Caps_Lock" => NamedKey::CapsLock,
"Num_Lock" => NamedKey::NumLock,
"Scroll_Lock" => NamedKey::ScrollLock,
"Menu" => NamedKey::Menu,
s if s.starts_with('F') && s[1..].chars().all(|c| c.is_ascii_digit()) => {
let n: u8 = s[1..].parse().ok()?;
if (1..=24).contains(&n) {
NamedKey::F(n)
} else {
return None;
}
}
_ => return None,
};
Some(named)
}
pub fn gdk_button_to_quadraui(button: u32) -> MouseButton {
match button {
1 => MouseButton::Left,
2 => MouseButton::Middle,
3 => MouseButton::Right,
8 => MouseButton::X1,
9 => MouseButton::X2,
n => MouseButton::Other(n.min(255) as u8),
}
}
pub fn wire_da_events<F>(da: >k4::DrawingArea, on_event: F)
where
F: Fn(UiEvent) + Clone + 'static,
{
wire_da_events_with_scroll_direction(da, on_event, false);
}
pub fn wire_da_events_with_scroll_direction<F>(
da: >k4::DrawingArea,
on_event: F,
natural_scroll: bool,
) where
F: Fn(UiEvent) + Clone + 'static,
{
use gtk4::{
EventControllerMotion, EventControllerScroll, EventControllerScrollFlags, GestureClick,
};
let click = GestureClick::builder().button(0).build();
{
let on_event = on_event.clone();
let double_click = std::rc::Rc::new(std::cell::RefCell::new(
crate::dispatch::DoubleClickDetector::with_radius(
super::backend::GTK_DOUBLE_CLICK_RADIUS,
),
));
click.connect_pressed(move |gesture, _n_press, x, y| {
let button = gesture.current_button();
let modifier = gesture.current_event_state();
let mut ev = gdk_button_to_mouse_down(button, x, y, modifier);
double_click
.borrow_mut()
.process(std::slice::from_mut(&mut ev));
on_event(ev);
});
}
{
let on_event = on_event.clone();
click.connect_released(move |gesture, _n_press, x, y| {
on_event(gdk_button_to_mouse_up(gesture.current_button(), x, y));
});
}
da.add_controller(click);
let cursor_pos = std::rc::Rc::new(std::cell::Cell::new((0.0_f64, 0.0_f64)));
let motion = EventControllerMotion::new();
{
let on_event = on_event.clone();
let cursor_pos = cursor_pos.clone();
motion.connect_motion(move |ctrl, x, y| {
cursor_pos.set((x, y));
let modifier = ctrl.current_event_state();
let buttons = crate::ButtonMask {
left: modifier.contains(gdk::ModifierType::BUTTON1_MASK),
middle: modifier.contains(gdk::ModifierType::BUTTON2_MASK),
right: modifier.contains(gdk::ModifierType::BUTTON3_MASK),
};
on_event(gdk_motion_to_uievent(x, y, buttons));
});
}
da.add_controller(motion);
let scroll = EventControllerScroll::new(EventControllerScrollFlags::BOTH_AXES);
{
let on_event = on_event.clone();
scroll.connect_scroll(move |_ctrl, dx, dy| {
let (x, y) = cursor_pos.get();
on_event(gdk_scroll_to_uievent_with_direction(
dx,
dy,
x,
y,
natural_scroll,
));
glib::Propagation::Stop
});
}
da.add_controller(scroll);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn drop_to_uievent_carries_paths_and_position() {
let paths = vec![
std::path::PathBuf::from("/tmp/a.txt"),
std::path::PathBuf::from("/tmp/b.txt"),
];
let ev = gtk_drop_to_uievent(paths.clone(), 12.0, 34.0);
match ev {
UiEvent::FilesDropped {
paths: got,
position,
} => {
assert_eq!(got, paths);
assert_eq!((position.x, position.y), (12.0, 34.0));
}
other => panic!("expected UiEvent::FilesDropped, got {other:?}"),
}
}
#[test]
fn open_to_uievent_splits_local_files_from_uris() {
let files = vec![
gtk4::gio::File::for_path("/tmp/report.pdf"),
gtk4::gio::File::for_uri("https://example.com/oauth/callback"),
];
let ev = gtk_open_to_uievent(&files);
match ev {
UiEvent::OpenRequested { urls, files } => {
assert_eq!(files, vec![std::path::PathBuf::from("/tmp/report.pdf")]);
assert_eq!(urls, vec!["https://example.com/oauth/callback".to_string()]);
}
other => panic!("expected UiEvent::OpenRequested, got {other:?}"),
}
}
#[test]
fn open_to_uievent_handles_empty_list() {
let ev = gtk_open_to_uievent(&[]);
assert_eq!(
ev,
UiEvent::OpenRequested {
urls: vec![],
files: vec![],
}
);
}
#[test]
fn modifiers_translate() {
let m = gdk::ModifierType::CONTROL_MASK | gdk::ModifierType::SHIFT_MASK;
let q = gdk_modifiers_to_quadraui(m);
assert!(q.ctrl);
assert!(q.shift);
assert!(!q.alt);
assert!(!q.cmd);
}
#[test]
fn alt_modifier_translates() {
let q = gdk_modifiers_to_quadraui(gdk::ModifierType::ALT_MASK);
assert!(q.alt);
assert!(!q.ctrl);
}
#[test]
fn super_or_meta_maps_to_cmd() {
assert!(gdk_modifiers_to_quadraui(gdk::ModifierType::SUPER_MASK).cmd);
assert!(gdk_modifiers_to_quadraui(gdk::ModifierType::META_MASK).cmd);
}
#[test]
fn keyname_named_keys() {
for (name, expected) in &[
("Escape", NamedKey::Escape),
("Tab", NamedKey::Tab),
("ISO_Left_Tab", NamedKey::BackTab),
("Return", NamedKey::Enter),
("KP_Enter", NamedKey::Enter),
("BackSpace", NamedKey::Backspace),
("Delete", NamedKey::Delete),
("Up", NamedKey::Up),
("Down", NamedKey::Down),
("Left", NamedKey::Left),
("Right", NamedKey::Right),
("Page_Up", NamedKey::PageUp),
("Page_Down", NamedKey::PageDown),
("Home", NamedKey::Home),
("End", NamedKey::End),
("F1", NamedKey::F(1)),
("F12", NamedKey::F(12)),
] {
assert_eq!(
gdk_keyname_to_named_key(name),
Some(*expected),
"for {name}"
);
}
}
#[test]
fn keyname_function_out_of_range() {
assert_eq!(gdk_keyname_to_named_key("F0"), None);
assert_eq!(gdk_keyname_to_named_key("F25"), None);
assert_eq!(gdk_keyname_to_named_key("F99"), None);
}
#[test]
fn keyname_unknown() {
assert_eq!(gdk_keyname_to_named_key("Unknown_Foo"), None);
assert_eq!(gdk_keyname_to_named_key(""), None);
}
#[test]
fn button_translates() {
assert_eq!(gdk_button_to_quadraui(1), MouseButton::Left);
assert_eq!(gdk_button_to_quadraui(2), MouseButton::Middle);
assert_eq!(gdk_button_to_quadraui(3), MouseButton::Right);
assert_eq!(gdk_button_to_quadraui(8), MouseButton::X1);
assert_eq!(gdk_button_to_quadraui(9), MouseButton::X2);
assert_eq!(gdk_button_to_quadraui(7), MouseButton::Other(7));
}
#[test]
fn mouse_down_translation() {
let ev = gdk_button_to_mouse_down(1, 50.0, 100.0, gdk::ModifierType::CONTROL_MASK);
match ev {
UiEvent::MouseDown {
widget,
button,
position,
modifiers,
} => {
assert!(widget.is_none());
assert_eq!(button, MouseButton::Left);
assert_eq!(position.x, 50.0);
assert_eq!(position.y, 100.0);
assert!(modifiers.ctrl);
}
other => panic!("expected MouseDown, got {other:?}"),
}
}
#[test]
fn scroll_negates_dy_for_quadraui_convention() {
let ev = gdk_scroll_to_uievent(0.0, 1.0, 10.0, 20.0);
match ev {
UiEvent::Scroll {
delta, position, ..
} => {
assert_eq!(delta.y, -1.0);
assert_eq!(delta.x, 0.0);
assert_eq!(position.x, 10.0);
assert_eq!(position.y, 20.0);
}
_ => panic!(),
}
}
#[test]
fn scroll_with_direction_false_matches_default_translator() {
let a = gdk_scroll_to_uievent(0.0, 1.0, 10.0, 20.0);
let b = gdk_scroll_to_uievent_with_direction(0.0, 1.0, 10.0, 20.0, false);
match (a, b) {
(UiEvent::Scroll { delta: da, .. }, UiEvent::Scroll { delta: db, .. }) => {
assert_eq!(da, db);
}
_ => panic!(),
}
}
#[test]
fn scroll_with_direction_true_flips_sign() {
let ev = gdk_scroll_to_uievent_with_direction(0.0, 1.0, 10.0, 20.0, true);
match ev {
UiEvent::Scroll { delta, .. } => assert_eq!(delta.y, 1.0),
_ => panic!(),
}
}
#[test]
fn resize_translation() {
let ev = gdk_resize_to_uievent(1920, 1080, 2.0);
match ev {
UiEvent::WindowResized { viewport } => {
assert_eq!(viewport.width, 1920.0);
assert_eq!(viewport.height, 1080.0);
assert_eq!(viewport.scale, 2.0);
}
_ => panic!(),
}
}
}