use std::cell::{Cell, RefCell};
use std::collections::{HashMap, VecDeque};
use std::convert::{TryFrom, TryInto};
use std::os::unix::io::RawFd;
use std::rc::Rc;
use std::time::{Duration, Instant};
use anyhow::{anyhow, Context, Error};
use x11rb::connection::{Connection, RequestConnection};
use x11rb::protocol::present::ConnectionExt as _;
use x11rb::protocol::render::{self, ConnectionExt as _, Pictformat};
use x11rb::protocol::xfixes::ConnectionExt as _;
use x11rb::protocol::xproto::{
self, ConnectionExt, CreateWindowAux, EventMask, Timestamp, Visualtype, WindowClass,
};
use x11rb::protocol::Event;
use x11rb::resource_manager::{
new_from_default as new_resource_db_from_default, Database as ResourceDb,
};
use x11rb::xcb_ffi::XCBConnection;
use crate::application::AppHandler;
use super::clipboard::Clipboard;
use super::util;
use super::window::Window;
use crate::backend::shared::linux;
use crate::backend::shared::xkb;
x11rb::atom_manager! {
pub(crate) AppAtoms: AppAtomsCookie {
WM_PROTOCOLS,
WM_DELETE_WINDOW,
_NET_WM_PID,
_NET_WM_NAME,
UTF8_STRING,
_NET_WM_WINDOW_TYPE,
_NET_WM_WINDOW_TYPE_NORMAL,
_NET_WM_WINDOW_TYPE_DROPDOWN_MENU,
_NET_WM_WINDOW_TYPE_TOOLTIP,
_NET_WM_WINDOW_TYPE_DIALOG,
CLIPBOARD,
PRIMARY,
TARGETS,
INCR,
}
}
#[derive(Clone)]
pub(crate) struct Application {
connection: Rc<XCBConnection>,
marker: std::marker::PhantomData<*mut XCBConnection>,
root_visual_type: Visualtype,
argb_visual_type: Option<Visualtype>,
pending_events: Rc<RefCell<VecDeque<Event>>>,
atoms: Rc<AppAtoms>,
pub(crate) rdb: Rc<ResourceDb>,
pub(crate) cursors: Cursors,
clipboard: Clipboard,
primary: Clipboard,
screen_num: usize, window_id: u32,
state: Rc<RefCell<State>>,
idle_read: RawFd,
idle_write: RawFd,
present_opcode: Option<u8>,
render_argb32_pictformat_cursor: Option<Pictformat>,
timestamp: Rc<Cell<Timestamp>>,
}
struct State {
quitting: bool,
windows: HashMap<u32, Rc<Window>>,
xkb_state: xkb::State,
}
#[derive(Clone, Debug)]
pub(crate) struct Cursors {
pub default: Option<xproto::Cursor>,
pub text: Option<xproto::Cursor>,
pub pointer: Option<xproto::Cursor>,
pub crosshair: Option<xproto::Cursor>,
pub not_allowed: Option<xproto::Cursor>,
pub row_resize: Option<xproto::Cursor>,
pub col_resize: Option<xproto::Cursor>,
}
impl Application {
pub fn new() -> Result<Application, Error> {
let (conn, screen_num) = XCBConnection::connect(None)?;
let rdb = Rc::new(new_resource_db_from_default(&conn)?);
let xkb_context = xkb::Context::new();
xkb_context.set_log_level(tracing::Level::DEBUG);
use x11rb::protocol::xkb::ConnectionExt;
conn.xkb_use_extension(1, 0)?
.reply()
.context("init xkb extension")?;
let device_id = xkb_context
.core_keyboard_device_id(&conn)
.context("get core keyboard device id")?;
let keymap = xkb_context
.keymap_from_device(&conn, device_id)
.context("key map from device")?;
let xkb_state = keymap.state();
let connection = Rc::new(conn);
let window_id = Application::create_event_window(&connection, screen_num)?;
let state = Rc::new(RefCell::new(State {
quitting: false,
windows: HashMap::new(),
xkb_state,
}));
let (idle_read, idle_write) = nix::unistd::pipe2(nix::fcntl::OFlag::O_NONBLOCK)?;
let present_opcode = if std::env::var_os("DRUID_SHELL_DISABLE_X11_PRESENT").is_some() {
None
} else {
match Application::query_present_opcode(&connection) {
Ok(p) => p,
Err(e) => {
tracing::info!("failed to find Present extension: {}", e);
None
}
}
};
let pictformats = connection.render_query_pict_formats()?;
let render_create_cursor_supported = matches!(connection
.extension_information(render::X11_EXTENSION_NAME)?
.and_then(|_| connection.render_query_version(0, 5).ok())
.map(|cookie| cookie.reply())
.transpose()?,
Some(version) if version.major_version >= 1 || version.minor_version >= 5);
let render_argb32_pictformat_cursor = if render_create_cursor_supported {
pictformats
.reply()?
.formats
.iter()
.find(|format| {
format.type_ == render::PictType::DIRECT
&& format.depth == 32
&& format.direct.red_shift == 16
&& format.direct.red_mask == 0xff
&& format.direct.green_shift == 8
&& format.direct.green_mask == 0xff
&& format.direct.blue_shift == 0
&& format.direct.blue_mask == 0xff
&& format.direct.alpha_shift == 24
&& format.direct.alpha_mask == 0xff
})
.map(|format| format.id)
} else {
drop(pictformats);
None
};
let handle = x11rb::cursor::Handle::new(connection.as_ref(), screen_num, &rdb)?.reply()?;
let load_cursor = |cursor| {
handle
.load_cursor(connection.as_ref(), cursor)
.map_err(|e| tracing::warn!("Unable to load cursor {}, error: {}", cursor, e))
.ok()
};
let cursors = Cursors {
default: load_cursor("default"),
text: load_cursor("text"),
pointer: load_cursor("pointer"),
crosshair: load_cursor("crosshair"),
not_allowed: load_cursor("not-allowed"),
row_resize: load_cursor("row-resize"),
col_resize: load_cursor("col-resize"),
};
let atoms = Rc::new(
AppAtoms::new(&*connection)?
.reply()
.context("get X11 atoms")?,
);
let screen = connection
.setup()
.roots
.get(screen_num)
.ok_or_else(|| anyhow!("Invalid screen num: {}", screen_num))?;
let root_visual_type = util::get_visual_from_screen(screen)
.ok_or_else(|| anyhow!("Couldn't get visual from screen"))?;
let argb_visual_type = util::get_argb_visual_type(&connection, screen)?;
let timestamp = Rc::new(Cell::new(x11rb::CURRENT_TIME));
let pending_events = Default::default();
let clipboard = Clipboard::new(
Rc::clone(&connection),
screen_num,
Rc::clone(&atoms),
atoms.CLIPBOARD,
Rc::clone(&pending_events),
Rc::clone(×tamp),
);
let primary = Clipboard::new(
Rc::clone(&connection),
screen_num,
Rc::clone(&atoms),
atoms.PRIMARY,
Rc::clone(&pending_events),
Rc::clone(×tamp),
);
Ok(Application {
connection,
rdb,
screen_num,
window_id,
state,
idle_read,
cursors,
clipboard,
primary,
idle_write,
present_opcode,
root_visual_type,
argb_visual_type,
atoms,
pending_events: Default::default(),
marker: std::marker::PhantomData,
render_argb32_pictformat_cursor,
timestamp,
})
}
fn query_present_opcode(conn: &Rc<XCBConnection>) -> Result<Option<u8>, Error> {
let query = conn
.query_extension(b"Present")?
.reply()
.context("query Present extension")?;
if !query.present {
return Ok(None);
}
let opcode = Some(query.major_opcode);
let version = conn
.present_query_version(1, 0)?
.reply()
.context("query Present version")?;
tracing::info!(
"X server supports Present version {}.{}",
version.major_version,
version.minor_version,
);
let version = conn
.xfixes_query_version(5, 0)?
.reply()
.context("query XFIXES version")?;
tracing::info!(
"X server supports XFIXES version {}.{}",
version.major_version,
version.minor_version,
);
Ok(opcode)
}
#[inline]
pub(crate) fn present_opcode(&self) -> Option<u8> {
self.present_opcode
}
#[inline]
pub(crate) fn render_argb32_pictformat_cursor(&self) -> Option<Pictformat> {
self.render_argb32_pictformat_cursor
}
fn create_event_window(conn: &Rc<XCBConnection>, screen_num: usize) -> Result<u32, Error> {
let id = conn.generate_id()?;
let setup = conn.setup();
let screen = setup
.roots
.get(screen_num)
.ok_or_else(|| anyhow!("invalid screen num: {}", screen_num))?;
conn.create_window(
x11rb::COPY_FROM_PARENT.try_into().unwrap(),
id,
screen.root,
0,
0,
1,
1,
0,
WindowClass::INPUT_ONLY,
x11rb::COPY_FROM_PARENT,
&CreateWindowAux::new().event_mask(EventMask::STRUCTURE_NOTIFY),
)?
.check()
.context("create input-only window")?;
Ok(id)
}
pub(crate) fn add_window(&self, id: u32, window: Rc<Window>) -> Result<(), Error> {
borrow_mut!(self.state)?.windows.insert(id, window);
Ok(())
}
fn remove_window(&self, id: u32) -> Result<usize, Error> {
let mut state = borrow_mut!(self.state)?;
state.windows.remove(&id);
Ok(state.windows.len())
}
fn window(&self, id: u32) -> Result<Rc<Window>, Error> {
borrow!(self.state)?
.windows
.get(&id)
.cloned()
.ok_or_else(|| anyhow!("No window with id {}", id))
}
#[inline]
pub(crate) fn connection(&self) -> &Rc<XCBConnection> {
&self.connection
}
#[inline]
pub(crate) fn screen_num(&self) -> usize {
self.screen_num
}
#[inline]
pub(crate) fn argb_visual_type(&self) -> Option<Visualtype> {
let atom_name = format!("_NET_WM_CM_S{}", self.screen_num);
let owner = self
.connection
.intern_atom(false, atom_name.as_bytes())
.ok()
.and_then(|cookie| cookie.reply().ok())
.map(|reply| reply.atom)
.and_then(|atom| self.connection.get_selection_owner(atom).ok())
.and_then(|cookie| cookie.reply().ok())
.map(|reply| reply.owner);
if Some(x11rb::NONE) == owner {
tracing::debug!("_NET_WM_CM_Sn selection is unowned, not providing ARGB visual");
None
} else {
self.argb_visual_type
}
}
#[inline]
pub(crate) fn root_visual_type(&self) -> Visualtype {
self.root_visual_type
}
#[inline]
pub(crate) fn atoms(&self) -> &AppAtoms {
&self.atoms
}
fn handle_event(&self, ev: &Event) -> Result<bool, Error> {
if ev.server_generated() {
let timestamp = match ev {
Event::KeyPress(ev) => ev.time,
Event::KeyRelease(ev) => ev.time,
Event::ButtonPress(ev) => ev.time,
Event::ButtonRelease(ev) => ev.time,
Event::MotionNotify(ev) => ev.time,
Event::EnterNotify(ev) => ev.time,
Event::LeaveNotify(ev) => ev.time,
Event::PropertyNotify(ev) => ev.time,
_ => self.timestamp.get(),
};
self.timestamp.set(timestamp);
}
match ev {
Event::Expose(ev) => {
let w = self
.window(ev.window)
.context("EXPOSE - failed to get window")?;
w.handle_expose(ev).context("EXPOSE - failed to handle")?;
}
Event::KeyPress(ev) => {
let w = self
.window(ev.event)
.context("KEY_PRESS - failed to get window")?;
let hw_keycode = ev.detail;
let mut state = borrow_mut!(self.state)?;
let key_event = state.xkb_state.key_event(
hw_keycode as _,
keyboard_types::KeyState::Down,
false,
);
w.handle_key_event(key_event);
}
Event::KeyRelease(ev) => {
let w = self
.window(ev.event)
.context("KEY_PRESS - failed to get window")?;
let hw_keycode = ev.detail;
let mut state = borrow_mut!(self.state)?;
let key_event =
state
.xkb_state
.key_event(hw_keycode as _, keyboard_types::KeyState::Up, false);
w.handle_key_event(key_event);
}
Event::ButtonPress(ev) => {
let w = self
.window(ev.event)
.context("BUTTON_PRESS - failed to get window")?;
if ev.detail >= 4 && ev.detail <= 7 {
w.handle_wheel(ev)
.context("BUTTON_PRESS - failed to handle wheel")?;
} else {
w.handle_button_press(ev)?;
}
}
Event::ButtonRelease(ev) => {
let w = self
.window(ev.event)
.context("BUTTON_RELEASE - failed to get window")?;
if ev.detail >= 4 && ev.detail <= 7 {
} else {
w.handle_button_release(ev)?;
}
}
Event::MotionNotify(ev) => {
let w = self
.window(ev.event)
.context("MOTION_NOTIFY - failed to get window")?;
w.handle_motion_notify(ev)?;
}
Event::ClientMessage(ev) => {
let w = self
.window(ev.window)
.context("CLIENT_MESSAGE - failed to get window")?;
w.handle_client_message(ev);
}
Event::DestroyNotify(ev) => {
if ev.window == self.window_id {
return Ok(true);
}
let w = self
.window(ev.window)
.context("DESTROY_NOTIFY - failed to get window")?;
w.handle_destroy_notify(ev);
let windows_left = self
.remove_window(ev.window)
.context("DESTROY_NOTIFY - failed to remove window")?;
if windows_left == 0 && borrow!(self.state)?.quitting {
self.finalize_quit();
}
}
Event::ConfigureNotify(ev) => {
if ev.window != self.window_id {
let w = self
.window(ev.window)
.context("CONFIGURE_NOTIFY - failed to get window")?;
w.handle_configure_notify(ev)
.context("CONFIGURE_NOTIFY - failed to handle")?;
}
}
Event::PresentCompleteNotify(ev) => {
let w = self
.window(ev.window)
.context("COMPLETE_NOTIFY - failed to get window")?;
w.handle_complete_notify(ev)
.context("COMPLETE_NOTIFY - failed to handle")?;
}
Event::PresentIdleNotify(ev) => {
let w = self
.window(ev.window)
.context("IDLE_NOTIFY - failed to get window")?;
w.handle_idle_notify(ev)
.context("IDLE_NOTIFY - failed to handle")?;
}
Event::SelectionClear(ev) => {
self.clipboard
.handle_clear(*ev)
.context("SELECTION_CLEAR event handling for clipboard")?;
self.primary
.handle_clear(*ev)
.context("SELECTION_CLEAR event handling for primary")?;
}
Event::SelectionRequest(ev) => {
self.clipboard
.handle_request(ev)
.context("SELECTION_REQUEST event handling for clipboard")?;
self.primary
.handle_request(ev)
.context("SELECTION_REQUEST event handling for primary")?;
}
Event::PropertyNotify(ev) => {
self.clipboard
.handle_property_notify(*ev)
.context("PROPERTY_NOTIFY event handling for clipboard")?;
self.primary
.handle_property_notify(*ev)
.context("PROPERTY_NOTIFY event handling for primary")?;
}
Event::FocusIn(ev) => {
let w = self
.window(ev.event)
.context("FOCUS_IN - failed to get window")?;
w.handle_got_focus();
}
Event::FocusOut(ev) => {
let w = self
.window(ev.event)
.context("FOCUS_OUT - failed to get window")?;
w.handle_lost_focus();
}
Event::Error(e) => {
return Err(x11rb::errors::ReplyError::from(e.clone()).into());
}
_ => {}
}
Ok(false)
}
fn run_inner(self) -> Result<(), Error> {
let refresh_rate = util::refresh_rate(self.connection(), self.window_id).unwrap_or(60.0);
let timeout = Duration::from_millis((1000.0 / refresh_rate) as u64);
let mut last_idle_time = Instant::now();
loop {
let next_timeout = if let Ok(state) = self.state.try_borrow() {
state
.windows
.values()
.filter_map(|w| w.next_timeout())
.min()
} else {
tracing::error!("Getting next timeout, application state already borrowed");
None
};
let next_idle_time = last_idle_time + timeout;
self.connection.flush()?;
let mut event = self.pending_events.borrow_mut().pop_front();
if event.is_none() {
event = self.connection.poll_for_event()?;
}
if event.is_none() {
poll_with_timeout(
&self.connection,
self.idle_read,
next_timeout,
next_idle_time,
)
.context("Error while waiting for X11 connection")?;
}
while let Some(ev) = event {
match self.handle_event(&ev) {
Ok(quit) => {
if quit {
return Ok(());
}
}
Err(e) => {
tracing::error!("Error handling event: {:#}", e);
}
}
event = self.connection.poll_for_event()?;
}
let now = Instant::now();
if let Some(timeout) = next_timeout {
if timeout <= now {
if let Ok(state) = self.state.try_borrow() {
let values = state.windows.values().cloned().collect::<Vec<_>>();
drop(state);
for w in values {
w.run_timers(now);
}
} else {
tracing::error!("In timer loop, application state already borrowed");
}
}
}
if now >= next_idle_time {
last_idle_time = now;
drain_idle_pipe(self.idle_read)?;
if let Ok(state) = self.state.try_borrow() {
for w in state.windows.values() {
w.run_idle();
}
} else {
tracing::error!("In idle loop, application state already borrowed");
}
}
}
}
pub fn run(self, _handler: Option<Box<dyn AppHandler>>) {
if let Err(e) = self.run_inner() {
tracing::error!("{}", e);
}
}
pub fn quit(&self) {
if let Ok(mut state) = self.state.try_borrow_mut() {
if !state.quitting {
state.quitting = true;
if state.windows.is_empty() {
self.finalize_quit();
} else {
for window in state.windows.values() {
window.destroy();
}
}
}
} else {
tracing::error!("Application state already borrowed");
}
}
fn finalize_quit(&self) {
log_x11!(self.connection.destroy_window(self.window_id));
if let Err(e) = nix::unistd::close(self.idle_read) {
tracing::error!("Error closing idle_read: {}", e);
}
if let Err(e) = nix::unistd::close(self.idle_write) {
tracing::error!("Error closing idle_write: {}", e);
}
}
pub fn clipboard(&self) -> Clipboard {
self.clipboard.clone()
}
pub fn get_locale() -> String {
linux::env::locale()
}
pub(crate) fn idle_pipe(&self) -> RawFd {
self.idle_write
}
}
impl crate::platform::linux::ApplicationExt for crate::Application {
fn primary_clipboard(&self) -> crate::Clipboard {
self.backend_app.primary.clone().into()
}
}
fn drain_idle_pipe(idle_read: RawFd) -> Result<(), Error> {
let mut read_buf = [0u8; 16];
loop {
match nix::unistd::read(idle_read, &mut read_buf[..]) {
Err(nix::errno::Errno::EINTR) => {}
Err(nix::errno::Errno::EAGAIN) => {
break;
}
Err(e) => {
return Err(e).context("Failed to read from idle pipe");
}
Ok(0) => {
break;
}
Ok(_) => {}
}
}
Ok(())
}
fn poll_with_timeout(
conn: &Rc<XCBConnection>,
idle: RawFd,
timer_timeout: Option<Instant>,
idle_timeout: Instant,
) -> Result<(), Error> {
use nix::poll::{poll, PollFd, PollFlags};
use std::os::raw::c_int;
use std::os::unix::io::AsRawFd;
let mut now = Instant::now();
let earliest_timeout = idle_timeout.min(timer_timeout.unwrap_or(idle_timeout));
let fd = conn.as_raw_fd();
let mut both_poll_fds = [
PollFd::new(fd, PollFlags::POLLIN),
PollFd::new(idle, PollFlags::POLLIN),
];
let mut just_connection = [PollFd::new(fd, PollFlags::POLLIN)];
let mut poll_fds = &mut both_poll_fds[..];
let mut honor_idle_timeout = false;
loop {
fn readable(p: PollFd) -> bool {
p.revents()
.unwrap_or_else(PollFlags::empty)
.contains(PollFlags::POLLIN)
}
let deadline = if honor_idle_timeout {
Some(earliest_timeout)
} else {
timer_timeout
};
let poll_timeout = if let Some(deadline) = deadline {
if deadline <= now {
break;
} else {
let millis = c_int::try_from(deadline.duration_since(now).as_millis())
.unwrap_or(c_int::max_value() - 1);
millis + 1
}
} else {
-1
};
match poll(poll_fds, poll_timeout) {
Ok(_) => {
if readable(poll_fds[0]) {
break;
}
now = Instant::now();
if timer_timeout.is_some() && now >= timer_timeout.unwrap() {
break;
}
if poll_fds.len() == 1 || readable(poll_fds[1]) {
poll_fds = &mut just_connection;
honor_idle_timeout = true;
if now >= idle_timeout {
break;
}
}
}
Err(nix::errno::Errno::EINTR) => {
now = Instant::now();
}
Err(e) => return Err(e.into()),
}
}
Ok(())
}