use std::path::PathBuf;
use crcbl_core::EventTime;
use crcbl_core::input::{
ButtonState, ContactId, DeviceId, Keysym, Modifiers, PointerButton, Scancode, ScrollDelta,
TouchPhase,
};
use crate::{
ClipboardContent, ClipboardRequestId, KeyCode, MonitorId, PhysicalPoint, PhysicalSize,
ReceivedMime, WindowId,
};
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub enum ShellEvent {
Resized {
window: WindowId,
size: PhysicalSize,
scale_factor: f64,
},
ScaleFactorChanged {
window: WindowId,
scale_factor: f64,
size: PhysicalSize,
},
CloseRequested {
window: WindowId,
},
WindowDestroyed {
window: WindowId,
},
Focus {
window: WindowId,
focused: bool,
},
Key {
window: WindowId,
device: DeviceId,
time: EventTime,
scancode: Scancode,
key_code: Option<KeyCode>,
keysym: Keysym,
state: ButtonState,
repeat: bool,
modifiers: Modifiers,
},
PointerMotion {
window: WindowId,
device: DeviceId,
time: EventTime,
abs: Option<PhysicalPoint>,
raw_delta: Option<(f64, f64)>,
},
PointerFocus {
window: WindowId,
device: DeviceId,
time: EventTime,
entered: bool,
position: Option<PhysicalPoint>,
},
Button {
window: WindowId,
device: DeviceId,
time: EventTime,
button: PointerButton,
state: ButtonState,
position: Option<PhysicalPoint>,
modifiers: Modifiers,
},
Wheel {
window: WindowId,
device: DeviceId,
time: EventTime,
delta: ScrollDelta,
position: Option<PhysicalPoint>,
modifiers: Modifiers,
},
Touch {
window: WindowId,
device: DeviceId,
time: EventTime,
contact: ContactId,
phase: TouchPhase,
position: PhysicalPoint,
},
TextCommit {
window: WindowId,
time: EventTime,
text: String,
},
TextPreedit {
window: WindowId,
time: EventTime,
text: String,
cursor: Option<usize>,
},
MonitorsChanged,
MonitorRemoved {
monitor: MonitorId,
},
FocusRefused {
window: WindowId,
},
FullScreenApp {
window: WindowId,
active: bool,
},
FrameReady {
window: WindowId,
},
DroppedFile {
window: WindowId,
time: EventTime,
path: PathBuf,
position: Option<PhysicalPoint>,
},
ClipboardData {
window: WindowId,
request: ClipboardRequestId,
mime: ReceivedMime,
content: ClipboardContent,
},
#[cfg(feature = "accesskit")]
AccessibilityActivated {
window: WindowId,
},
#[cfg(feature = "accesskit")]
AccessibilityAction {
window: WindowId,
request: accesskit::ActionRequest,
},
#[cfg(feature = "accesskit")]
AccessibilityDeactivated {
window: WindowId,
},
}
impl ShellEvent {
#[must_use]
pub const fn window(&self) -> Option<WindowId> {
match self {
Self::Resized { window, .. }
| Self::ScaleFactorChanged { window, .. }
| Self::CloseRequested { window }
| Self::WindowDestroyed { window }
| Self::Focus { window, .. }
| Self::Key { window, .. }
| Self::PointerMotion { window, .. }
| Self::PointerFocus { window, .. }
| Self::Button { window, .. }
| Self::Wheel { window, .. }
| Self::Touch { window, .. }
| Self::TextCommit { window, .. }
| Self::TextPreedit { window, .. }
| Self::DroppedFile { window, .. }
| Self::FocusRefused { window }
| Self::FullScreenApp { window, .. }
| Self::FrameReady { window }
| Self::ClipboardData { window, .. } => Some(*window),
#[cfg(feature = "accesskit")]
Self::AccessibilityActivated { window }
| Self::AccessibilityAction { window, .. }
| Self::AccessibilityDeactivated { window } => Some(*window),
Self::MonitorsChanged | Self::MonitorRemoved { .. } => None,
}
}
#[must_use]
pub const fn time(&self) -> Option<EventTime> {
match self {
Self::Key { time, .. }
| Self::PointerMotion { time, .. }
| Self::PointerFocus { time, .. }
| Self::Button { time, .. }
| Self::Wheel { time, .. }
| Self::Touch { time, .. }
| Self::TextCommit { time, .. }
| Self::TextPreedit { time, .. }
| Self::DroppedFile { time, .. } => Some(*time),
Self::Resized { .. }
| Self::ScaleFactorChanged { .. }
| Self::CloseRequested { .. }
| Self::WindowDestroyed { .. }
| Self::Focus { .. }
| Self::MonitorsChanged
| Self::MonitorRemoved { .. }
| Self::FocusRefused { .. }
| Self::FullScreenApp { .. }
| Self::FrameReady { .. }
| Self::ClipboardData { .. } => None,
#[cfg(feature = "accesskit")]
Self::AccessibilityActivated { .. }
| Self::AccessibilityAction { .. }
| Self::AccessibilityDeactivated { .. } => None,
}
}
#[inline]
#[must_use]
pub const fn is_input(&self) -> bool {
self.time().is_some()
}
#[must_use]
pub const fn name(&self) -> &'static str {
match self {
Self::Resized { .. } => "Resized",
Self::ScaleFactorChanged { .. } => "ScaleFactorChanged",
Self::CloseRequested { .. } => "CloseRequested",
Self::WindowDestroyed { .. } => "WindowDestroyed",
Self::Focus { .. } => "Focus",
Self::Key { .. } => "Key",
Self::PointerMotion { .. } => "PointerMotion",
Self::PointerFocus { .. } => "PointerFocus",
Self::Button { .. } => "Button",
Self::Wheel { .. } => "Wheel",
Self::Touch { .. } => "Touch",
Self::TextCommit { .. } => "TextCommit",
Self::TextPreedit { .. } => "TextPreedit",
Self::MonitorsChanged => "MonitorsChanged",
Self::MonitorRemoved { .. } => "MonitorRemoved",
Self::FocusRefused { .. } => "FocusRefused",
Self::FullScreenApp { .. } => "FullScreenApp",
Self::FrameReady { .. } => "FrameReady",
Self::DroppedFile { .. } => "DroppedFile",
Self::ClipboardData { .. } => "ClipboardData",
#[cfg(feature = "accesskit")]
Self::AccessibilityActivated { .. } => "AccessibilityActivated",
#[cfg(feature = "accesskit")]
Self::AccessibilityAction { .. } => "AccessibilityAction",
#[cfg(feature = "accesskit")]
Self::AccessibilityDeactivated { .. } => "AccessibilityDeactivated",
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use core::time::Duration;
use crcbl_core::Pool;
fn window_id() -> WindowId {
let mut pool: Pool<u8> = Pool::new();
pool.insert(0).cast()
}
fn key_event(time: EventTime) -> ShellEvent {
ShellEvent::Key {
window: window_id(),
device: DeviceId(1),
time,
scancode: Scancode(57),
key_code: Some(KeyCode::Space),
keysym: Keysym::from_char(' '),
state: ButtonState::Pressed,
repeat: false,
modifiers: Modifiers::empty(),
}
}
#[test]
fn a_clipboard_answer_carries_the_peers_spelling() {
let event = ShellEvent::ClipboardData {
window: window_id(),
request: ClipboardRequestId(3),
mime: ReceivedMime::new("text/plain"),
content: ClipboardContent::Bytes(b"pasted".to_vec()),
};
let ShellEvent::ClipboardData { mime, content, .. } = &event else {
panic!("wrong variant");
};
assert_eq!(mime.as_str(), "text/plain");
assert!(mime.matches(crate::MimeType::TextUtf8));
assert_eq!(content.bytes(), Some(&b"pasted"[..]));
assert_eq!(content.text(), Some("pasted"));
assert_eq!(ClipboardContent::Bytes(Vec::new()).bytes(), Some(&[][..]));
assert_ne!(ClipboardContent::Bytes(Vec::new()), ClipboardContent::Empty);
assert_eq!(ClipboardContent::Empty.bytes(), None);
assert_eq!(ClipboardContent::Unavailable.bytes(), None);
assert_ne!(ClipboardContent::Empty, ClipboardContent::Unavailable);
assert_eq!(ClipboardContent::Unavailable.name(), "Unavailable");
assert_eq!(ClipboardContent::Bytes(vec![0xff]).text(), None);
assert_eq!(
event.time(),
None,
"a clipboard answer is not an input sample"
);
}
#[test]
fn input_events_carry_a_usable_timestamp_and_lifecycle_events_do_not() {
let pressed = key_event(EventTime::from_millis(1_000));
let released = key_event(EventTime::from_millis(1_180));
let held = released
.time()
.expect("input")
.saturating_since(pressed.time().expect("input"));
assert_eq!(held, Duration::from_millis(180));
assert!(pressed.is_input());
assert!(!ShellEvent::MonitorsChanged.is_input());
assert_eq!(
ShellEvent::Resized {
window: window_id(),
size: PhysicalSize::new(1, 1),
scale_factor: 1.0,
}
.time(),
None
);
}
#[test]
fn every_variant_reports_its_window_and_a_stable_name() {
let window = window_id();
let events = [
ShellEvent::Resized {
window,
size: PhysicalSize::new(640, 360),
scale_factor: 1.0,
},
ShellEvent::ScaleFactorChanged {
window,
scale_factor: 2.0,
size: PhysicalSize::new(1280, 720),
},
ShellEvent::CloseRequested { window },
ShellEvent::WindowDestroyed { window },
ShellEvent::Focus {
window,
focused: true,
},
key_event(EventTime::ZERO),
ShellEvent::PointerMotion {
window,
device: DeviceId::UNKNOWN,
time: EventTime::ZERO,
abs: Some(PhysicalPoint::new(1.0, 2.0)),
raw_delta: Some((0.5, -0.5)),
},
ShellEvent::PointerFocus {
window,
device: DeviceId::UNKNOWN,
time: EventTime::ZERO,
entered: true,
position: None,
},
ShellEvent::Button {
window,
device: DeviceId::UNKNOWN,
time: EventTime::ZERO,
button: PointerButton::Left,
state: ButtonState::Pressed,
position: None,
modifiers: Modifiers::CTRL,
},
ShellEvent::Wheel {
window,
device: DeviceId::UNKNOWN,
time: EventTime::ZERO,
delta: ScrollDelta::Lines { x: 0.0, y: 1.0 },
position: None,
modifiers: Modifiers::empty(),
},
ShellEvent::Touch {
window,
device: DeviceId::UNKNOWN,
time: EventTime::ZERO,
contact: ContactId(1),
phase: TouchPhase::Began,
position: PhysicalPoint::new(4.0, 5.0),
},
ShellEvent::TextCommit {
window,
time: EventTime::ZERO,
text: "は".to_string(),
},
ShellEvent::DroppedFile {
window,
time: EventTime::ZERO,
path: PathBuf::from("/tmp/scene.ron"),
position: None,
},
ShellEvent::ClipboardData {
window,
request: ClipboardRequestId(1),
mime: ReceivedMime::from(crate::MimeType::TextUtf8),
content: ClipboardContent::Empty,
},
ShellEvent::MonitorsChanged,
ShellEvent::MonitorRemoved {
monitor: MonitorId(2),
},
ShellEvent::FocusRefused { window },
ShellEvent::FullScreenApp {
window,
active: true,
},
ShellEvent::FrameReady { window },
];
#[cfg(feature = "accesskit")]
let events = {
let mut events = events.to_vec();
events.extend([
ShellEvent::AccessibilityActivated { window },
ShellEvent::AccessibilityAction {
window,
request: accesskit::ActionRequest {
action: accesskit::Action::Click,
target_tree: accesskit::TreeId::ROOT,
target_node: accesskit::NodeId(1),
data: None,
},
},
ShellEvent::AccessibilityDeactivated { window },
]);
events
};
let mut names = Vec::new();
for event in &events {
names.push(event.name());
match event {
ShellEvent::MonitorsChanged | ShellEvent::MonitorRemoved { .. } => {
assert_eq!(event.window(), None);
}
_ => assert_eq!(event.window(), Some(window), "{}", event.name()),
}
}
let count = names.len();
names.sort_unstable();
names.dedup();
assert_eq!(names.len(), count, "event names must be unique");
}
#[test]
fn a_batch_of_contacts_keeps_the_two_fingers_apart() {
let window = window_id();
let touch = |contact: u32, phase: TouchPhase, x: f64, y: f64| ShellEvent::Touch {
window,
device: DeviceId(3),
time: EventTime::from_millis(1),
contact: ContactId(contact),
phase,
position: PhysicalPoint::new(x, y),
};
let batch = [
touch(7, TouchPhase::Began, 10.0, 20.0),
touch(9, TouchPhase::Began, 300.0, 400.0),
touch(9, TouchPhase::Moved, 305.0, 410.0),
touch(7, TouchPhase::Ended, 10.0, 20.0),
];
let mut live: Vec<(ContactId, TouchPhase, PhysicalPoint)> = Vec::new();
for event in &batch {
let ShellEvent::Touch {
contact,
phase,
position,
..
} = event
else {
panic!("wrong variant");
};
live.retain(|(id, ..)| id != contact);
if !phase.ends_contact() {
live.push((*contact, *phase, *position));
}
}
assert_eq!(
live,
vec![(
ContactId(9),
TouchPhase::Moved,
PhysicalPoint::new(305.0, 410.0)
)]
);
assert!(TouchPhase::Ended.ends_contact());
assert!(TouchPhase::Cancelled.ends_contact());
assert!(!TouchPhase::Began.ends_contact());
assert!(!TouchPhase::Moved.ends_contact());
assert_ne!(TouchPhase::Ended, TouchPhase::Cancelled);
assert!(batch[0].is_input(), "a contact is an input sample");
assert_eq!(batch[0].time(), Some(EventTime::from_millis(1)));
}
#[test]
fn locked_pointer_motion_has_no_absolute_position() {
let event = ShellEvent::PointerMotion {
window: window_id(),
device: DeviceId(2),
time: EventTime::from_micros(16_666),
abs: None,
raw_delta: Some((12.0, -3.0)),
};
let ShellEvent::PointerMotion { abs, raw_delta, .. } = event else {
panic!("wrong variant");
};
assert_eq!(abs, None);
assert_eq!(raw_delta, Some((12.0, -3.0)));
}
}