slint_evdev_input/
lib.rs

1//! Convert linux evdev input events from a touchscreen into slint WindowEvents
2//!
3//! # Why?
4//!
5//! For small embedded linux devices it may not make sense to run the normal slint event loop, but
6//! to use the MinimalSoftwareRenderer to draw to a frame buffer, and implement the event loop as
7//! part of the application. Most linux touch drivers provide an events interface, e.g.
8//! `/etc/input/event0`, from which events can be read.
9//!
10//! This is a wrapper around the [`evdev` crate](https://crates.io/crates/evdev) to convert
11//! the input events into WindowEvent structs which can be passed to slint via the
12//! `dispatch_event()` method on a MinimalSoftwareWindow.
13//!
14//! # Caveats
15//!
16//! This only supports touch events: PointerPressed, PointerMoved, and PointedReleased.
17//!
18//! # Usage
19//!
20//! Can be used as a blocking call via [`fetch_events()`](SlintEventsWrapper::fetch_events), or via
21//! async stream by enabling the `tokio` feature and using
22//! [`into_event_stream()`](SlintEventsWrapper::into_event_stream) to create an
23//! [`EventStream`](tokio::EventStream).
24//!
25#![warn(missing_docs)]
26#![cfg_attr(docsrs, feature(doc_cfg))]
27
28use std::path::Path;
29
30use evdev::{AbsoluteAxisCode, EventSummary, FetchEventsSynced, KeyCode};
31use slint::{
32    LogicalPosition, PhysicalPosition,
33    platform::{PointerEventButton, WindowEvent},
34};
35
36#[derive(Clone, Copy, Debug, Default, PartialEq)]
37enum ButtonChange {
38    #[default]
39    None,
40    Up,
41    Down,
42}
43
44/// Collect evdev events and convert them to slint events
45struct Collector {
46    last_position: (i32, i32),
47    scale_factor: f32,
48    button_change: ButtonChange,
49}
50
51impl Collector {
52    pub fn new(scale_factor: f32, last_position: (i32, i32)) -> Self {
53        Self {
54            last_position,
55            scale_factor,
56            button_change: ButtonChange::None,
57        }
58    }
59
60    pub fn push(&mut self, event: evdev::EventSummary) -> Option<WindowEvent> {
61        match event {
62            EventSummary::Synchronization(_, _, _) => {
63                let button_change = self.button_change;
64                self.button_change = ButtonChange::None;
65                if button_change == ButtonChange::Down {
66                    return Some(WindowEvent::PointerPressed {
67                        position: self.last_logical_position(),
68                        button: PointerEventButton::Left,
69                    });
70                } else if button_change == ButtonChange::Up {
71                    return Some(WindowEvent::PointerReleased {
72                        position: self.last_logical_position(),
73                        button: PointerEventButton::Left,
74                    });
75                } else {
76                    return Some(WindowEvent::PointerMoved {
77                        position: self.last_logical_position(),
78                    });
79                };
80            }
81            EventSummary::AbsoluteAxis(_event, code, value) => match code {
82                AbsoluteAxisCode::ABS_X => self.last_position.0 = value,
83                AbsoluteAxisCode::ABS_Y => self.last_position.1 = value,
84                _ => (),
85            },
86            EventSummary::Key(_event, key, value) => {
87                if matches!(key, KeyCode::BTN_TOUCH) {
88                    if value == 1 {
89                        self.button_change = ButtonChange::Down
90                    } else {
91                        self.button_change = ButtonChange::Up;
92                    }
93                }
94            }
95            _ => (),
96        }
97        None
98    }
99
100    fn last_logical_position(&self) -> LogicalPosition {
101        let (x, y) = self.last_position;
102        LogicalPosition::from_physical(PhysicalPosition::new(x, y), self.scale_factor)
103    }
104}
105
106/// A wrapper for evdev::Device to convert events to slint WindowEvents
107///
108/// Only supports single-touch touch screens
109///
110/// # Example
111///
112/// ```no_run
113///  use slint_evdev_input::SlintEventsWrapper;
114///
115///  // Scale factor from `slint::Window::scale_factor()` for converting logical to physical pixel
116///  // coordinates
117///  let scale_factor = 1.0;
118///  let mut slint_device = SlintEventsWrapper::new("/dev/input/event0", scale_factor).unwrap();
119///
120///  loop {
121///      for event in slint_device.fetch_events() {
122///          println!("{:?}", event);
123///      }
124///  }
125/// ```
126pub struct SlintEventsWrapper {
127    device: evdev::Device,
128    last_position: (i32, i32),
129    scale_factor: f32,
130}
131
132impl SlintEventsWrapper {
133    /// Create a new SlintEventsWrapper using the given event device path
134    ///
135    /// # Arguments
136    ///
137    /// - `device`: A path to the device (e.g. '/dev/input/event0')
138    /// - `scale_factor`: The scale factor from slint for converting between logical and physical
139    ///   coordinates.
140    pub fn new(device: impl AsRef<Path>, scale_factor: f32) -> std::io::Result<Self> {
141        let device = evdev::Device::open(device)?;
142        Ok(Self {
143            device,
144            last_position: (0, 0),
145            scale_factor,
146        })
147    }
148
149    /// Fetches and returns event. This will block until events are ready.
150    pub fn fetch_events<'a>(&'a mut self) -> SlintEventsIterator<'a> {
151        SlintEventsIterator {
152            inner: self.device.fetch_events().unwrap(),
153            collector: Collector::new(self.scale_factor, self.last_position),
154        }
155    }
156
157    /// Convert the wrapper into an [`EventStream`] for async reading
158    ///
159    /// Requires the `tokio` feature
160    #[cfg(feature = "tokio")]
161    #[cfg_attr(docsrs, doc(cfg(feature = "tokio")))]
162    pub fn into_event_stream(self) -> std::io::Result<tokio::EventStream> {
163        Ok(tokio::EventStream {
164            evdev_stream: self.device.into_event_stream()?,
165            collector: Collector::new(self.scale_factor, self.last_position),
166        })
167    }
168}
169
170/// An iterator over window events which will block until a new event is ready
171pub struct SlintEventsIterator<'a> {
172    inner: FetchEventsSynced<'a>,
173    collector: Collector,
174}
175
176impl Iterator for SlintEventsIterator<'_> {
177    type Item = WindowEvent;
178
179    fn next(&mut self) -> Option<Self::Item> {
180        // Read to sync event
181        loop {
182            match self.inner.next() {
183                Some(event) => {
184                    if let Some(window_event) = self.collector.push(event.destructure()) {
185                        return Some(window_event);
186                    }
187                }
188                None => return None,
189            }
190        }
191    }
192}
193
194#[cfg(feature = "tokio")]
195#[cfg_attr(docsrs, doc(cfg(feature = "tokio")))]
196mod tokio {
197    use super::*;
198    /// A async stream of input events
199    pub struct EventStream {
200        pub(super) evdev_stream: evdev::EventStream,
201        pub(super) collector: Collector,
202    }
203
204    impl EventStream {
205        /// Get a future for the next available event in the stream
206        pub async fn next_event(&mut self) -> Result<WindowEvent, std::io::Error> {
207            loop {
208                let event = self.evdev_stream.next_event().await?;
209                if let Some(ret) = self.collector.push(event.destructure()) {
210                    return Ok(ret);
211                }
212            }
213        }
214    }
215}