add evfb (evdev + fbdev) backend
Outside of a terminal emulator or web browser, this typing experience can be useful outside the DE, such as in the TTY or when entering a password for full-disk encryption. Add a target that uses the bare hardware to allow the keyboard to be deployed in more flexible environments.
This commit is contained in:
parent
9b893e474d
commit
7b38edb656
12 changed files with 866 additions and 0 deletions
|
|
@ -6,6 +6,7 @@ license = "GPL-3.0-only"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
ashpd = { version = "0.11", features = ["wayland"] }
|
ashpd = { version = "0.11", features = ["wayland"] }
|
||||||
|
evdev = "0.13"
|
||||||
fontconfig = "0.9"
|
fontconfig = "0.9"
|
||||||
# Disable freetype-sys, as it vendors libfreetype2 while fontconfig dynamically
|
# Disable freetype-sys, as it vendors libfreetype2 while fontconfig dynamically
|
||||||
# links to it. Large dependencies should not be duplicated.
|
# links to it. Large dependencies should not be duplicated.
|
||||||
|
|
@ -13,10 +14,12 @@ freetype = { version = "0.7", default-features = false }
|
||||||
futures-util = "0.3"
|
futures-util = "0.3"
|
||||||
imgref = "1"
|
imgref = "1"
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
|
linux-raw-sys = { version = "0.9", features = ["ioctl"] }
|
||||||
memmap2 = "0.9"
|
memmap2 = "0.9"
|
||||||
polling = "3"
|
polling = "3"
|
||||||
reis = "0.4"
|
reis = "0.4"
|
||||||
rgb = "0.8"
|
rgb = "0.8"
|
||||||
|
signal-hook = "0.3"
|
||||||
tokio = { version = "1", features = ["macros", "rt"] }
|
tokio = { version = "1", features = ["macros", "rt"] }
|
||||||
wayland-backend = "0.3"
|
wayland-backend = "0.3"
|
||||||
wayland-client = "0.31"
|
wayland-client = "0.31"
|
||||||
|
|
@ -30,6 +33,10 @@ zbus = { version = "5", default-features = false, features = ["tokio"] }
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
bindgen = "0.71"
|
bindgen = "0.71"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "ufkbd-evfb"
|
||||||
|
path = "src/ufkbd_evfb.rs"
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "ufkbd-gnome"
|
name = "ufkbd-gnome"
|
||||||
path = "src/ufkbd_gnome.rs"
|
path = "src/ufkbd_gnome.rs"
|
||||||
|
|
|
||||||
13
build.rs
13
build.rs
|
|
@ -22,4 +22,17 @@ fn main()
|
||||||
println!("cargo::rustc-link-lib=expat");
|
println!("cargo::rustc-link-lib=expat");
|
||||||
|
|
||||||
bindings.write_to_file(out_file).expect("Writing failure");
|
bindings.write_to_file(out_file).expect("Writing failure");
|
||||||
|
|
||||||
|
let builder = bindgen::builder();
|
||||||
|
|
||||||
|
let bindings = builder.header("/usr/include/linux/fb.h")
|
||||||
|
.generate()
|
||||||
|
.expect("The Linux headers must be installed");
|
||||||
|
|
||||||
|
let out_dir: PathBuf = env::var("OUT_DIR")
|
||||||
|
.expect("Environment variable $OUT_DIR must be defined")
|
||||||
|
.into();
|
||||||
|
let out_file = out_dir.join("linuxfb.rs");
|
||||||
|
|
||||||
|
bindings.write_to_file(out_file).expect("Writing failure");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,7 @@ pub struct Configuration {
|
||||||
repeat: u64,
|
repeat: u64,
|
||||||
layout: String,
|
layout: String,
|
||||||
extra_keys: Vec<String>,
|
extra_keys: Vec<String>,
|
||||||
|
evfb_height: f64,
|
||||||
wayland_height: i32,
|
wayland_height: i32,
|
||||||
wayland_im_enable: bool,
|
wayland_im_enable: bool,
|
||||||
|
|
||||||
|
|
@ -67,6 +68,19 @@ pub struct Configuration {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Configuration {
|
impl Configuration {
|
||||||
|
fn load_evfb(&mut self, yaml: &Hash)
|
||||||
|
{
|
||||||
|
let height = yaml.get(&Yaml::String(String::from("height")));
|
||||||
|
if let Some(height) = height {
|
||||||
|
let height = match height.as_f64() {
|
||||||
|
Some(h) => h.try_into().ok(),
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
let height = height.expect("Linux evdev-fbdev height should be a fraction from 0 to 1");
|
||||||
|
self.evfb_height = height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn load_wayland(&mut self, yaml: &Hash)
|
fn load_wayland(&mut self, yaml: &Hash)
|
||||||
{
|
{
|
||||||
let height = yaml.get(&Yaml::String(String::from("height")));
|
let height = yaml.get(&Yaml::String(String::from("height")));
|
||||||
|
|
@ -135,6 +149,7 @@ impl Configuration {
|
||||||
extra_keys: vec![ String::from("alt"), String::from("meta") ],
|
extra_keys: vec![ String::from("alt"), String::from("meta") ],
|
||||||
wayland_height: 185,
|
wayland_height: 185,
|
||||||
wayland_im_enable: true,
|
wayland_im_enable: true,
|
||||||
|
evfb_height: 0.25,
|
||||||
|
|
||||||
colors: KeyboardColors::default(),
|
colors: KeyboardColors::default(),
|
||||||
};
|
};
|
||||||
|
|
@ -182,6 +197,12 @@ impl Configuration {
|
||||||
cfg.load_wayland(wl);
|
cfg.load_wayland(wl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let evfb = yaml.get(&Yaml::String(String::from("evfb")));
|
||||||
|
if let Some(evfb) = evfb {
|
||||||
|
let evfb = evfb.as_hash().expect("Linux evdev-fbdev configuration should be a YAML mapping");
|
||||||
|
cfg.load_evfb(evfb);
|
||||||
|
}
|
||||||
|
|
||||||
let colors = yaml.get(&Yaml::String(String::from("colors")));
|
let colors = yaml.get(&Yaml::String(String::from("colors")));
|
||||||
if let Some(colors) = colors {
|
if let Some(colors) = colors {
|
||||||
let colors = colors.as_hash().expect("Color configuration should be a YAML mapping");
|
let colors = colors.as_hash().expect("Color configuration should be a YAML mapping");
|
||||||
|
|
@ -228,6 +249,12 @@ impl Configuration {
|
||||||
self.wayland_im_enable
|
self.wayland_im_enable
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn evfb_height(&self) -> f64
|
||||||
|
{
|
||||||
|
self.evfb_height
|
||||||
|
}
|
||||||
|
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn colors(&self) -> &KeyboardColors
|
pub fn colors(&self) -> &KeyboardColors
|
||||||
{
|
{
|
||||||
|
|
|
||||||
10
src/evdev/mod.rs
Normal file
10
src/evdev/mod.rs
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2025, Richard Acayan. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
mod touch;
|
||||||
|
mod uinput;
|
||||||
|
|
||||||
|
pub use self::touch::Touchscreen;
|
||||||
|
pub use self::uinput::VirtualKeyboard;
|
||||||
190
src/evdev/touch.rs
Normal file
190
src/evdev/touch.rs
Normal file
|
|
@ -0,0 +1,190 @@
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024, Richard Acayan. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use crate::core::Button;
|
||||||
|
use crate::core::Configuration;
|
||||||
|
use crate::core::Display;
|
||||||
|
use crate::core::Keyboard;
|
||||||
|
use evdev::AbsoluteAxisCode;
|
||||||
|
use evdev::Device;
|
||||||
|
use evdev::EventSummary;
|
||||||
|
use std::fs;
|
||||||
|
use std::io::Error;
|
||||||
|
use std::os::fd::AsFd;
|
||||||
|
use std::os::fd::AsRawFd;
|
||||||
|
use std::os::fd::BorrowedFd;
|
||||||
|
use std::os::fd::RawFd;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum PressAction {
|
||||||
|
Pos,
|
||||||
|
Press,
|
||||||
|
Release,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct TouchAction {
|
||||||
|
id: usize,
|
||||||
|
act: PressAction,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Touchscreen<D: Display, K: Keyboard> {
|
||||||
|
dev: Device,
|
||||||
|
btn: Button<D, K>,
|
||||||
|
actions: Vec<TouchAction>,
|
||||||
|
pos: Vec<(f64, f64)>,
|
||||||
|
slot: usize,
|
||||||
|
y_off: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<D: Display, K: Keyboard> Touchscreen<D, K> {
|
||||||
|
fn open_device_if_supported(dent: Result<fs::DirEntry, Error>) -> Option<Device>
|
||||||
|
{
|
||||||
|
let path = dent.ok()?.path();
|
||||||
|
let dev = Device::open(path).ok()?;
|
||||||
|
|
||||||
|
let abs_props = dev.supported_absolute_axes()?;
|
||||||
|
|
||||||
|
if abs_props.contains(AbsoluteAxisCode::ABS_MT_SLOT)
|
||||||
|
&& abs_props.contains(AbsoluteAxisCode::ABS_MT_TRACKING_ID)
|
||||||
|
&& abs_props.contains(AbsoluteAxisCode::ABS_MT_POSITION_X)
|
||||||
|
&& abs_props.contains(AbsoluteAxisCode::ABS_MT_POSITION_Y) {
|
||||||
|
Some(dev)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new(mut btn: Button<D, K>, cfg: &Configuration) -> Result<Self, Error>
|
||||||
|
{
|
||||||
|
let mut devs = fs::read_dir("/dev/input")?;
|
||||||
|
let dev = devs.find_map(Self::open_device_if_supported).unwrap();
|
||||||
|
|
||||||
|
dev.set_nonblocking(true)?;
|
||||||
|
|
||||||
|
let props = dev.get_abs_state()?;
|
||||||
|
let max_slots = props[AbsoluteAxisCode::ABS_MT_SLOT.0 as usize].maximum as usize + 1;
|
||||||
|
|
||||||
|
// Dimensions of the touchscreen device
|
||||||
|
let d_width = props[AbsoluteAxisCode::ABS_MT_POSITION_X.0 as usize].maximum;
|
||||||
|
let d_width = d_width as f64 + 1.0;
|
||||||
|
let d_height = props[AbsoluteAxisCode::ABS_MT_POSITION_Y.0 as usize].maximum;
|
||||||
|
let d_height = d_height as f64 + 1.0;
|
||||||
|
|
||||||
|
// Dimensions of the keyboard
|
||||||
|
let c_height = d_height * cfg.evfb_height();
|
||||||
|
|
||||||
|
// Dimensions of the keyboard layout
|
||||||
|
let l_width = btn.layout().width();
|
||||||
|
let l_height = btn.layout().height();
|
||||||
|
|
||||||
|
btn.set_scale(l_width / d_width, l_height / c_height);
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
dev, btn,
|
||||||
|
actions: Vec::new(),
|
||||||
|
pos: vec![(0.0, 0.0); max_slots],
|
||||||
|
slot: 0,
|
||||||
|
y_off: d_height - c_height,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn button(&self) -> &Button<D, K>
|
||||||
|
{
|
||||||
|
&self.btn
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dispatch_timers(&mut self)
|
||||||
|
{
|
||||||
|
self.btn.dispatch_timers()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn commit(&mut self)
|
||||||
|
{
|
||||||
|
for act in &self.actions {
|
||||||
|
match act.act {
|
||||||
|
PressAction::Pos => {
|
||||||
|
self.btn.pos(act.id,
|
||||||
|
self.pos[act.id].0,
|
||||||
|
self.pos[act.id].1 - self.y_off);
|
||||||
|
},
|
||||||
|
PressAction::Press => {
|
||||||
|
if self.pos[act.id].1 >= self.y_off {
|
||||||
|
self.btn.press(act.id,
|
||||||
|
self.pos[act.id].0,
|
||||||
|
self.pos[act.id].1 - self.y_off);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
PressAction::Release => {
|
||||||
|
self.btn.release(act.id);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.actions.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_events(&mut self)
|
||||||
|
{
|
||||||
|
let evts: Vec<_> = self.dev.fetch_events().unwrap().collect();
|
||||||
|
|
||||||
|
for evt in evts {
|
||||||
|
match evt.destructure() {
|
||||||
|
EventSummary::AbsoluteAxis(_, AbsoluteAxisCode::ABS_MT_SLOT, val) => {
|
||||||
|
self.slot = val as usize;
|
||||||
|
},
|
||||||
|
EventSummary::AbsoluteAxis(_, AbsoluteAxisCode::ABS_MT_POSITION_X, val) => {
|
||||||
|
self.pos[self.slot].0 = val as f64;
|
||||||
|
let action = TouchAction {
|
||||||
|
id: self.slot,
|
||||||
|
act: PressAction::Pos,
|
||||||
|
};
|
||||||
|
self.actions.push(action);
|
||||||
|
},
|
||||||
|
EventSummary::AbsoluteAxis(_, AbsoluteAxisCode::ABS_MT_POSITION_Y, val) => {
|
||||||
|
self.pos[self.slot].1 = val as f64;
|
||||||
|
let action = TouchAction {
|
||||||
|
id: self.slot,
|
||||||
|
act: PressAction::Pos,
|
||||||
|
};
|
||||||
|
self.actions.push(action);
|
||||||
|
},
|
||||||
|
EventSummary::AbsoluteAxis(_, AbsoluteAxisCode::ABS_MT_TRACKING_ID, -1) => {
|
||||||
|
let action = TouchAction {
|
||||||
|
id: self.slot,
|
||||||
|
act: PressAction::Release,
|
||||||
|
};
|
||||||
|
self.actions.push(action);
|
||||||
|
},
|
||||||
|
EventSummary::AbsoluteAxis(_, AbsoluteAxisCode::ABS_MT_TRACKING_ID, _) => {
|
||||||
|
let action = TouchAction {
|
||||||
|
id: self.slot,
|
||||||
|
act: PressAction::Press,
|
||||||
|
};
|
||||||
|
self.actions.push(action);
|
||||||
|
},
|
||||||
|
EventSummary::Synchronization(_, _, _) => {
|
||||||
|
self.commit();
|
||||||
|
},
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<D: Display, K: Keyboard> AsFd for Touchscreen<D, K> {
|
||||||
|
fn as_fd(&self) -> BorrowedFd
|
||||||
|
{
|
||||||
|
self.dev.as_fd()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<D: Display, K: Keyboard> AsRawFd for Touchscreen<D, K> {
|
||||||
|
fn as_raw_fd(&self) -> RawFd
|
||||||
|
{
|
||||||
|
self.dev.as_raw_fd()
|
||||||
|
}
|
||||||
|
}
|
||||||
203
src/evdev/uinput.rs
Normal file
203
src/evdev/uinput.rs
Normal file
|
|
@ -0,0 +1,203 @@
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2025, Richard Acayan. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use crate::core::Keyboard;
|
||||||
|
use crate::core::Layout;
|
||||||
|
use crate::linux::Teletype;
|
||||||
|
use evdev::uinput::VirtualDevice;
|
||||||
|
use evdev::AttributeSet;
|
||||||
|
use evdev::EventType;
|
||||||
|
use evdev::InputEvent;
|
||||||
|
use evdev::KeyCode;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::io::Error;
|
||||||
|
use std::iter::FromIterator;
|
||||||
|
use xkeysym::Keysym;
|
||||||
|
|
||||||
|
const STATIC_KEYCODES: [(Keysym, KeyCode); 77] = [
|
||||||
|
(Keysym::space, KeyCode::KEY_SPACE),
|
||||||
|
(Keysym::apostrophe, KeyCode::KEY_APOSTROPHE),
|
||||||
|
(Keysym::comma, KeyCode::KEY_COMMA),
|
||||||
|
(Keysym::minus, KeyCode::KEY_MINUS),
|
||||||
|
(Keysym::period, KeyCode::KEY_DOT),
|
||||||
|
(Keysym::slash, KeyCode::KEY_SLASH),
|
||||||
|
(Keysym::_0, KeyCode::KEY_0),
|
||||||
|
(Keysym::_1, KeyCode::KEY_1),
|
||||||
|
(Keysym::_2, KeyCode::KEY_2),
|
||||||
|
(Keysym::_3, KeyCode::KEY_3),
|
||||||
|
(Keysym::_4, KeyCode::KEY_4),
|
||||||
|
(Keysym::_5, KeyCode::KEY_5),
|
||||||
|
(Keysym::_6, KeyCode::KEY_6),
|
||||||
|
(Keysym::_7, KeyCode::KEY_7),
|
||||||
|
(Keysym::_8, KeyCode::KEY_8),
|
||||||
|
(Keysym::_9, KeyCode::KEY_9),
|
||||||
|
(Keysym::semicolon, KeyCode::KEY_SEMICOLON),
|
||||||
|
(Keysym::equal, KeyCode::KEY_EQUAL),
|
||||||
|
(Keysym::bracketleft, KeyCode::KEY_LEFTBRACE),
|
||||||
|
(Keysym::backslash, KeyCode::KEY_BACKSLASH),
|
||||||
|
(Keysym::bracketright, KeyCode::KEY_RIGHTBRACE),
|
||||||
|
(Keysym::grave, KeyCode::KEY_GRAVE),
|
||||||
|
(Keysym::a, KeyCode::KEY_A),
|
||||||
|
(Keysym::b, KeyCode::KEY_B),
|
||||||
|
(Keysym::c, KeyCode::KEY_C),
|
||||||
|
(Keysym::d, KeyCode::KEY_D),
|
||||||
|
(Keysym::e, KeyCode::KEY_E),
|
||||||
|
(Keysym::f, KeyCode::KEY_F),
|
||||||
|
(Keysym::g, KeyCode::KEY_G),
|
||||||
|
(Keysym::h, KeyCode::KEY_H),
|
||||||
|
(Keysym::i, KeyCode::KEY_I),
|
||||||
|
(Keysym::j, KeyCode::KEY_J),
|
||||||
|
(Keysym::k, KeyCode::KEY_K),
|
||||||
|
(Keysym::l, KeyCode::KEY_L),
|
||||||
|
(Keysym::m, KeyCode::KEY_M),
|
||||||
|
(Keysym::n, KeyCode::KEY_N),
|
||||||
|
(Keysym::o, KeyCode::KEY_O),
|
||||||
|
(Keysym::p, KeyCode::KEY_P),
|
||||||
|
(Keysym::q, KeyCode::KEY_Q),
|
||||||
|
(Keysym::r, KeyCode::KEY_R),
|
||||||
|
(Keysym::s, KeyCode::KEY_S),
|
||||||
|
(Keysym::t, KeyCode::KEY_T),
|
||||||
|
(Keysym::u, KeyCode::KEY_U),
|
||||||
|
(Keysym::v, KeyCode::KEY_V),
|
||||||
|
(Keysym::w, KeyCode::KEY_W),
|
||||||
|
(Keysym::x, KeyCode::KEY_X),
|
||||||
|
(Keysym::y, KeyCode::KEY_Y),
|
||||||
|
(Keysym::z, KeyCode::KEY_Z),
|
||||||
|
(Keysym::BackSpace, KeyCode::KEY_BACKSPACE),
|
||||||
|
(Keysym::Tab, KeyCode::KEY_TAB),
|
||||||
|
(Keysym::Return, KeyCode::KEY_ENTER),
|
||||||
|
(Keysym::Escape, KeyCode::KEY_ESC),
|
||||||
|
(Keysym::Home, KeyCode::KEY_HOME),
|
||||||
|
(Keysym::Left, KeyCode::KEY_LEFT),
|
||||||
|
(Keysym::Up, KeyCode::KEY_UP),
|
||||||
|
(Keysym::Right, KeyCode::KEY_RIGHT),
|
||||||
|
(Keysym::Down, KeyCode::KEY_DOWN),
|
||||||
|
(Keysym::Prior, KeyCode::KEY_PAGEUP),
|
||||||
|
(Keysym::Next, KeyCode::KEY_PAGEDOWN),
|
||||||
|
(Keysym::End, KeyCode::KEY_END),
|
||||||
|
(Keysym::Insert, KeyCode::KEY_INSERT),
|
||||||
|
(Keysym::F1, KeyCode::KEY_F1),
|
||||||
|
(Keysym::F2, KeyCode::KEY_F2),
|
||||||
|
(Keysym::F3, KeyCode::KEY_F3),
|
||||||
|
(Keysym::F4, KeyCode::KEY_F4),
|
||||||
|
(Keysym::F5, KeyCode::KEY_F5),
|
||||||
|
(Keysym::F6, KeyCode::KEY_F6),
|
||||||
|
(Keysym::F7, KeyCode::KEY_F7),
|
||||||
|
(Keysym::F8, KeyCode::KEY_F8),
|
||||||
|
(Keysym::F9, KeyCode::KEY_F9),
|
||||||
|
(Keysym::F10, KeyCode::KEY_F10),
|
||||||
|
(Keysym::F11, KeyCode::KEY_F11),
|
||||||
|
(Keysym::F12, KeyCode::KEY_F12),
|
||||||
|
(Keysym::Shift_L, KeyCode::KEY_LEFTSHIFT),
|
||||||
|
(Keysym::Control_L, KeyCode::KEY_LEFTCTRL),
|
||||||
|
(Keysym::Alt_L, KeyCode::KEY_LEFTALT),
|
||||||
|
(Keysym::Delete, KeyCode::KEY_DELETE),
|
||||||
|
];
|
||||||
|
|
||||||
|
pub struct VirtualKeyboard {
|
||||||
|
dev: VirtualDevice,
|
||||||
|
tty: Teletype,
|
||||||
|
keycodes: HashMap<Keysym, u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VirtualKeyboard {
|
||||||
|
pub fn new(tty: Teletype) -> Result<Self, Error>
|
||||||
|
{
|
||||||
|
let keycodes = (1..=255).into_iter().map(|c| KeyCode(c));
|
||||||
|
let keycodes = AttributeSet::from_iter(keycodes);
|
||||||
|
|
||||||
|
let builder = VirtualDevice::builder()?;
|
||||||
|
let builder = builder.name("Unfettered Keyboard");
|
||||||
|
let builder = builder.with_keys(&keycodes)?;
|
||||||
|
let dev = builder.build()?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
dev,
|
||||||
|
tty,
|
||||||
|
keycodes: HashMap::with_capacity(256),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Keyboard for VirtualKeyboard {
|
||||||
|
fn key_supported(&self, sym: Keysym) -> bool
|
||||||
|
{
|
||||||
|
Teletype::keysym_value(sym).is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn press(&mut self, sym: Keysym)
|
||||||
|
{
|
||||||
|
let code = *self.keycodes.get(&sym).unwrap();
|
||||||
|
let evt = InputEvent::new_now(EventType::KEY.0, code as u16, 1);
|
||||||
|
self.dev.emit(&[evt]).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn release(&mut self, sym: Keysym)
|
||||||
|
{
|
||||||
|
let code = *self.keycodes.get(&sym).unwrap();
|
||||||
|
let evt = InputEvent::new_now(EventType::KEY.0, code as u16, 0);
|
||||||
|
self.dev.emit(&[evt]).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn text(&mut self, text: &str)
|
||||||
|
{
|
||||||
|
self.tty.write_text(text);
|
||||||
|
self.press(Keysym::Shift_R);
|
||||||
|
self.release(Keysym::Shift_R);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn change_layout(&mut self, layout: &Layout)
|
||||||
|
{
|
||||||
|
let mut keycode = 1;
|
||||||
|
let mut used_codes = [false; 256];
|
||||||
|
|
||||||
|
self.keycodes.clear();
|
||||||
|
for row in layout.rows() {
|
||||||
|
for key in row {
|
||||||
|
for part in &key.parts {
|
||||||
|
if !part.key_available() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(idx) = STATIC_KEYCODES.binary_search_by_key(&part.sym(), |(s, _)| *s) {
|
||||||
|
let (sym, KeyCode(code)) = STATIC_KEYCODES[idx];
|
||||||
|
self.keycodes.insert(sym, code as u8);
|
||||||
|
self.tty.write_key(part, code as u8);
|
||||||
|
used_codes[code as usize] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for row in layout.rows() {
|
||||||
|
for key in row {
|
||||||
|
for part in &key.parts {
|
||||||
|
if !part.key_available() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.keycodes.contains_key(&part.sym()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
while used_codes[keycode as usize] {
|
||||||
|
keycode += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.keycodes.insert(part.sym(), keycode);
|
||||||
|
self.tty.write_key(part, keycode);
|
||||||
|
keycode += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
while used_codes[keycode as usize] {
|
||||||
|
keycode += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.keycodes.insert(Keysym::Shift_R, keycode);
|
||||||
|
self.tty.write_text_key(keycode);
|
||||||
|
}
|
||||||
|
}
|
||||||
143
src/fbdev/framebuffer.rs
Normal file
143
src/fbdev/framebuffer.rs
Normal file
|
|
@ -0,0 +1,143 @@
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2025, Richard Acayan. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use crate::core::Configuration;
|
||||||
|
use crate::core::Display;
|
||||||
|
use crate::fbdev::linuxfb;
|
||||||
|
use imgref::ImgRefMut;
|
||||||
|
use rgb::FromSlice;
|
||||||
|
use rgb::alt::BGRA;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::fs::OpenOptions;
|
||||||
|
use std::io::Error;
|
||||||
|
use std::io::Seek;
|
||||||
|
use std::io::SeekFrom;
|
||||||
|
use std::io::Write;
|
||||||
|
use std::os::fd::AsRawFd;
|
||||||
|
|
||||||
|
pub struct Framebuffer {
|
||||||
|
f: File,
|
||||||
|
y: u32,
|
||||||
|
size: (u32, u32),
|
||||||
|
stride: u32,
|
||||||
|
img: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Framebuffer {
|
||||||
|
pub fn new(cfg: &Configuration) -> Result<Self, Error>
|
||||||
|
{
|
||||||
|
let mut open = OpenOptions::new();
|
||||||
|
open.read(true);
|
||||||
|
open.write(true);
|
||||||
|
let f = open.open("/dev/fb0")?;
|
||||||
|
|
||||||
|
let mut finfo = linuxfb::fb_fix_screeninfo {
|
||||||
|
id: [0; 16],
|
||||||
|
smem_start: 0,
|
||||||
|
smem_len: 0,
|
||||||
|
type_: 0,
|
||||||
|
type_aux: 0,
|
||||||
|
visual: 0,
|
||||||
|
xpanstep: 0,
|
||||||
|
ypanstep: 0,
|
||||||
|
ywrapstep: 0,
|
||||||
|
line_length: 0,
|
||||||
|
mmio_start: 0,
|
||||||
|
mmio_len: 0,
|
||||||
|
accel: 0,
|
||||||
|
capabilities: 0,
|
||||||
|
reserved: [0; 2],
|
||||||
|
};
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
libc::ioctl(f.as_raw_fd(), linuxfb::FBIOGET_FSCREENINFO as i32, &mut finfo as *mut _);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut vinfo = linuxfb::fb_var_screeninfo {
|
||||||
|
xres: 0,
|
||||||
|
yres: 0,
|
||||||
|
xres_virtual: 0,
|
||||||
|
yres_virtual: 0,
|
||||||
|
xoffset: 0,
|
||||||
|
yoffset: 0,
|
||||||
|
bits_per_pixel: 0,
|
||||||
|
grayscale: 0,
|
||||||
|
red: linuxfb::fb_bitfield { offset: 0, length: 0, msb_right: 0 },
|
||||||
|
green: linuxfb::fb_bitfield { offset: 0, length: 0, msb_right: 0 },
|
||||||
|
blue: linuxfb::fb_bitfield { offset: 0, length: 0, msb_right: 0 },
|
||||||
|
transp: linuxfb::fb_bitfield { offset: 0, length: 0, msb_right: 0 },
|
||||||
|
nonstd: 0,
|
||||||
|
activate: 0,
|
||||||
|
height: 0,
|
||||||
|
width: 0,
|
||||||
|
accel_flags: 0,
|
||||||
|
pixclock: 0,
|
||||||
|
left_margin: 0,
|
||||||
|
right_margin: 0,
|
||||||
|
upper_margin: 0,
|
||||||
|
lower_margin: 0,
|
||||||
|
hsync_len: 0,
|
||||||
|
vsync_len: 0,
|
||||||
|
sync: 0,
|
||||||
|
vmode: 0,
|
||||||
|
rotate: 0,
|
||||||
|
colorspace: 0,
|
||||||
|
reserved: [0; 4],
|
||||||
|
};
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
libc::ioctl(f.as_raw_fd(), linuxfb::FBIOGET_VSCREENINFO as i32, &mut vinfo as *mut _);
|
||||||
|
}
|
||||||
|
|
||||||
|
let height = (vinfo.yres_virtual as f64 * cfg.evfb_height()) as u32;
|
||||||
|
let y = vinfo.yres_virtual - height;
|
||||||
|
|
||||||
|
let size = (vinfo.xres_virtual, height as u32);
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
f,
|
||||||
|
y,
|
||||||
|
size,
|
||||||
|
stride: finfo.line_length,
|
||||||
|
img: vec![0; size.0 as usize * finfo.line_length as usize * 4],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Framebuffer {
|
||||||
|
fn size(&self) -> (u32, u32)
|
||||||
|
{
|
||||||
|
self.size
|
||||||
|
}
|
||||||
|
|
||||||
|
fn begin(&mut self) -> ImgRefMut<BGRA<u8>>
|
||||||
|
{
|
||||||
|
// The ARGB format is in little endian.
|
||||||
|
let ptr = &mut self.img;
|
||||||
|
let ptr = ptr.as_bgra_mut();
|
||||||
|
let img = ImgRefMut::new_stride(ptr,
|
||||||
|
self.size.0 as usize,
|
||||||
|
self.size.1 as usize,
|
||||||
|
(self.stride / 4) as usize);
|
||||||
|
|
||||||
|
img
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resize(&mut self, _width: u32, _height: u32)
|
||||||
|
{
|
||||||
|
// Nothing to do here, we resize once on startup and everything is working.
|
||||||
|
}
|
||||||
|
|
||||||
|
fn end(&mut self, _x: u32, y: u32, _width: u32, height: u32)
|
||||||
|
{
|
||||||
|
let kbd_top = self.y as u64 * self.stride as u64;
|
||||||
|
let target_top = y as usize * self.stride as usize;
|
||||||
|
let target_bottom = (y as usize + height as usize) * self.stride as usize;
|
||||||
|
let subimg = &self.img[target_top..target_bottom];
|
||||||
|
|
||||||
|
self.f.seek(SeekFrom::Start(kbd_top + target_top as u64)).unwrap();
|
||||||
|
self.f.write(subimg).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
6
src/fbdev/linuxfb.rs
Normal file
6
src/fbdev/linuxfb.rs
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
#![allow(dead_code,
|
||||||
|
improper_ctypes,
|
||||||
|
non_camel_case_types,
|
||||||
|
non_snake_case,
|
||||||
|
non_upper_case_globals)]
|
||||||
|
include!(concat!(env!("OUT_DIR"), "/linuxfb.rs"));
|
||||||
9
src/fbdev/mod.rs
Normal file
9
src/fbdev/mod.rs
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2025, Richard Acayan. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
mod framebuffer;
|
||||||
|
mod linuxfb;
|
||||||
|
|
||||||
|
pub use self::framebuffer::Framebuffer;
|
||||||
8
src/linux/mod.rs
Normal file
8
src/linux/mod.rs
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2025, Richard Acayan. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
mod tty;
|
||||||
|
|
||||||
|
pub use self::tty::Teletype;
|
||||||
170
src/linux/tty.rs
Normal file
170
src/linux/tty.rs
Normal file
|
|
@ -0,0 +1,170 @@
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2025, Richard Acayan. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use crate::core::Configuration;
|
||||||
|
use crate::core::Part;
|
||||||
|
use linux_raw_sys::ioctl::KDSKBENT;
|
||||||
|
use linux_raw_sys::ioctl::KDSKBSENT;
|
||||||
|
use linux_raw_sys::ioctl::TIOCGWINSZ;
|
||||||
|
use linux_raw_sys::ioctl::TIOCSWINSZ;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::Error;
|
||||||
|
use std::os::fd::AsRawFd;
|
||||||
|
use xkeysym::Keysym;
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
struct KbEntry {
|
||||||
|
table: u8,
|
||||||
|
index: u8,
|
||||||
|
value: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
struct KbSEntry {
|
||||||
|
func: u8,
|
||||||
|
string: [u8; 512],
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct WinSize {
|
||||||
|
row: u16,
|
||||||
|
col: u16,
|
||||||
|
xpixel: u16,
|
||||||
|
ypixel: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Teletype {
|
||||||
|
f: File,
|
||||||
|
orig_ws: WinSize,
|
||||||
|
}
|
||||||
|
|
||||||
|
const KT_LATIN: u8 = 0;
|
||||||
|
const KT_FN: u8 = 1;
|
||||||
|
const KT_SPEC: u8 = 2;
|
||||||
|
const KT_CUR: u8 = 6;
|
||||||
|
const KT_SHIFT: u8 = 6;
|
||||||
|
|
||||||
|
fn key(kt: u8, kv: u8) -> u16
|
||||||
|
{
|
||||||
|
((kt as u16) << 8) | (kv as u16)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Teletype {
|
||||||
|
pub fn keysym_value(sym: Keysym) -> Option<u16>
|
||||||
|
{
|
||||||
|
if sym.raw() >= 0x20 && sym.raw() <= 0x7e {
|
||||||
|
return Some(sym.raw() as u16);
|
||||||
|
}
|
||||||
|
|
||||||
|
match sym {
|
||||||
|
Keysym::BackSpace => Some(key(KT_LATIN, 0x7f)),
|
||||||
|
Keysym::Tab => Some(key(KT_LATIN, 0x09)),
|
||||||
|
Keysym::Return => Some(key(KT_SPEC, 0x01)),
|
||||||
|
Keysym::Escape => Some(key(KT_LATIN, 0x1b)),
|
||||||
|
Keysym::Home => Some(key(KT_FN, 0x14)),
|
||||||
|
Keysym::Left => Some(key(KT_CUR, 0x01)),
|
||||||
|
Keysym::Up => Some(key(KT_CUR, 0x03)),
|
||||||
|
Keysym::Right => Some(key(KT_CUR, 0x02)),
|
||||||
|
Keysym::Down => Some(key(KT_CUR, 0x00)),
|
||||||
|
Keysym::Prior => Some(key(KT_FN, 0x18)),
|
||||||
|
Keysym::Next => Some(key(KT_FN, 0x19)),
|
||||||
|
Keysym::End => Some(key(KT_FN, 0x18)),
|
||||||
|
Keysym::Insert => Some(key(KT_FN, 0x15)),
|
||||||
|
Keysym::F1 => Some(key(KT_FN, 0x00)),
|
||||||
|
Keysym::F2 => Some(key(KT_FN, 0x01)),
|
||||||
|
Keysym::F3 => Some(key(KT_FN, 0x02)),
|
||||||
|
Keysym::F4 => Some(key(KT_FN, 0x03)),
|
||||||
|
Keysym::F5 => Some(key(KT_FN, 0x04)),
|
||||||
|
Keysym::F6 => Some(key(KT_FN, 0x05)),
|
||||||
|
Keysym::F7 => Some(key(KT_FN, 0x06)),
|
||||||
|
Keysym::F8 => Some(key(KT_FN, 0x07)),
|
||||||
|
Keysym::F9 => Some(key(KT_FN, 0x08)),
|
||||||
|
Keysym::F10 => Some(key(KT_FN, 0x09)),
|
||||||
|
Keysym::F11 => Some(key(KT_FN, 0x0a)),
|
||||||
|
Keysym::F12 => Some(key(KT_FN, 0x0b)),
|
||||||
|
Keysym::Shift_L => Some(key(KT_SHIFT, 0x00)),
|
||||||
|
Keysym::Control_L => Some(key(KT_SHIFT, 0x02)),
|
||||||
|
Keysym::Alt_L => Some(key(KT_SHIFT, 0x03)),
|
||||||
|
Keysym::Delete => Some(key(KT_FN, 0x16)),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new(cfg: &Configuration) -> Result<Self, Error>
|
||||||
|
{
|
||||||
|
let f = File::open("/dev/tty0")?;
|
||||||
|
|
||||||
|
let mut ws = WinSize { row: 0, col: 0, xpixel: 0, ypixel: 0 };
|
||||||
|
unsafe {
|
||||||
|
libc::ioctl(f.as_raw_fd(), TIOCGWINSZ as i32, &mut ws as *mut _);
|
||||||
|
}
|
||||||
|
|
||||||
|
let orig_ws = ws.clone();
|
||||||
|
ws.row = (ws.row as f64 - cfg.evfb_height() * ws.row as f64) as u16;
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
libc::ioctl(f.as_raw_fd(), TIOCSWINSZ as i32, &ws as *const _);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self { f, orig_ws })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write_key(&self, part: &Part, keycode: u8)
|
||||||
|
{
|
||||||
|
let lower = part.sym();
|
||||||
|
let upper = Part::modify_shift(lower);
|
||||||
|
|
||||||
|
let val = Self::keysym_value(lower).unwrap();
|
||||||
|
let ent = KbEntry { table: 0, index: keycode, value: val };
|
||||||
|
unsafe {
|
||||||
|
libc::ioctl(self.f.as_raw_fd(), KDSKBENT as i32, &ent as *const _);
|
||||||
|
}
|
||||||
|
|
||||||
|
let val = Self::keysym_value(upper).unwrap();
|
||||||
|
let ent = KbEntry { table: 1, index: keycode, value: val };
|
||||||
|
unsafe {
|
||||||
|
libc::ioctl(self.f.as_raw_fd(), KDSKBENT as i32, &ent as *const _);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write_text_key(&self, keycode: u8)
|
||||||
|
{
|
||||||
|
let ent = KbEntry { table: 0, index: keycode, value: 0x1fe };
|
||||||
|
unsafe {
|
||||||
|
libc::ioctl(self.f.as_raw_fd(), KDSKBENT as i32, &ent as *const _);
|
||||||
|
}
|
||||||
|
|
||||||
|
let ent = KbEntry { table: 1, index: keycode, value: 0x1fe };
|
||||||
|
unsafe {
|
||||||
|
libc::ioctl(self.f.as_raw_fd(), KDSKBENT as i32, &ent as *const _);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write_text(&self, text: &str)
|
||||||
|
{
|
||||||
|
let text = text.as_bytes();
|
||||||
|
|
||||||
|
let mut ent = KbSEntry { func: 254, string: [0; 512] };
|
||||||
|
let dest = &mut ent.string[..text.len()];
|
||||||
|
dest.copy_from_slice(text);
|
||||||
|
ent.string[511] = 0;
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
libc::ioctl(self.f.as_raw_fd(), KDSKBSENT as i32, &ent as *const _);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for Teletype {
|
||||||
|
fn drop(&mut self)
|
||||||
|
{
|
||||||
|
unsafe {
|
||||||
|
libc::ioctl(self.f.as_raw_fd(),
|
||||||
|
TIOCSWINSZ as i32,
|
||||||
|
&self.orig_ws as *const _);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
80
src/ufkbd_evfb.rs
Normal file
80
src/ufkbd_evfb.rs
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2025, Richard Acayan. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
mod core;
|
||||||
|
mod evdev;
|
||||||
|
mod fbdev;
|
||||||
|
mod linux;
|
||||||
|
|
||||||
|
use crate::core::Button;
|
||||||
|
use crate::core::Configuration;
|
||||||
|
use crate::core::Display;
|
||||||
|
use crate::core::Graphics;
|
||||||
|
use crate::core::Layout;
|
||||||
|
use crate::evdev::Touchscreen;
|
||||||
|
use crate::evdev::VirtualKeyboard;
|
||||||
|
use crate::fbdev::Framebuffer;
|
||||||
|
use crate::linux::Teletype;
|
||||||
|
use polling::Event;
|
||||||
|
use polling::Events;
|
||||||
|
use polling::Poller;
|
||||||
|
use signal_hook::consts::signal;
|
||||||
|
use signal_hook::iterator::Signals;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::sync::Mutex;
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
|
fn main()
|
||||||
|
{
|
||||||
|
let cfg = Configuration::load().unwrap();
|
||||||
|
let disp = Framebuffer::new(&cfg).unwrap();
|
||||||
|
|
||||||
|
let gfx = Graphics::new(disp, &cfg);
|
||||||
|
let gfx = Mutex::new(gfx);
|
||||||
|
let gfx = Arc::new(gfx);
|
||||||
|
|
||||||
|
let tty = Teletype::new(&cfg).unwrap();
|
||||||
|
let kbd = VirtualKeyboard::new(tty).unwrap();
|
||||||
|
let layout = Layout::load(&cfg).unwrap();
|
||||||
|
let mut btn = Button::new(&cfg, layout, kbd, gfx.clone());
|
||||||
|
btn.set_text_supported(true);
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut gfx = gfx.lock().unwrap();
|
||||||
|
let (width, height) = gfx.display().size();
|
||||||
|
gfx.resize(btn.layout(), btn.mod_state(), width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut touchscreen = Touchscreen::new(btn, &cfg).unwrap();
|
||||||
|
|
||||||
|
let mut events = Events::new();
|
||||||
|
let poller = Poller::new().unwrap();
|
||||||
|
let ts_evt = Event::readable(0);
|
||||||
|
|
||||||
|
let mut sigs = Signals::new([
|
||||||
|
signal::SIGHUP,
|
||||||
|
signal::SIGINT,
|
||||||
|
signal::SIGPIPE,
|
||||||
|
signal::SIGTERM,
|
||||||
|
]).unwrap();
|
||||||
|
|
||||||
|
while sigs.pending().next().is_none() {
|
||||||
|
let timer = touchscreen.button().next_time().map(|t| t - Instant::now());
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
poller.add(&touchscreen, ts_evt).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
events.clear();
|
||||||
|
poller.wait(&mut events, timer).unwrap();
|
||||||
|
poller.delete(&touchscreen).unwrap();
|
||||||
|
|
||||||
|
if !events.is_empty() {
|
||||||
|
touchscreen.handle_events();
|
||||||
|
}
|
||||||
|
|
||||||
|
touchscreen.dispatch_timers();
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue