Move X11 backend to winit-x11 (#4253)
This commit is contained in:
parent
1126e9ea2f
commit
256bbe949e
42 changed files with 232 additions and 227 deletions
195
winit-x11/src/activation.rs
Normal file
195
winit-x11/src/activation.rs
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//! X11 activation handling.
|
||||
//!
|
||||
//! X11 has a "startup notification" specification similar to Wayland's, see this URL:
|
||||
//! <https://specifications.freedesktop.org/startup-notification-spec/startup-notification-latest.txt>
|
||||
|
||||
use std::ffi::CString;
|
||||
use std::fmt::Write;
|
||||
|
||||
use x11rb::protocol::xproto::{self, ConnectionExt as _};
|
||||
|
||||
use crate::atoms::*;
|
||||
use crate::event_loop::{VoidCookie, X11Error};
|
||||
use crate::xdisplay::XConnection;
|
||||
|
||||
impl XConnection {
|
||||
/// "Request" a new activation token from the server.
|
||||
pub(crate) fn request_activation_token(&self, window_title: &str) -> Result<String, X11Error> {
|
||||
// The specification recommends the format "hostname+pid+"_TIME"+current time"
|
||||
let uname = rustix::system::uname();
|
||||
let pid = rustix::process::getpid();
|
||||
let time = self.timestamp();
|
||||
|
||||
let activation_token = format!(
|
||||
"{}{}_TIME{}",
|
||||
uname.nodename().to_str().unwrap_or("winit"),
|
||||
pid.as_raw_nonzero(),
|
||||
time
|
||||
);
|
||||
|
||||
// Set up the new startup notification.
|
||||
let notification = {
|
||||
let mut buffer = Vec::new();
|
||||
buffer.extend_from_slice(b"new: ID=");
|
||||
quote_string(&activation_token, &mut buffer);
|
||||
buffer.extend_from_slice(b" NAME=");
|
||||
quote_string(window_title, &mut buffer);
|
||||
buffer.extend_from_slice(b" SCREEN=");
|
||||
push_display(&mut buffer, &self.default_screen_index());
|
||||
|
||||
CString::new(buffer)
|
||||
.map_err(|err| X11Error::InvalidActivationToken(err.into_vec()))?
|
||||
.into_bytes_with_nul()
|
||||
};
|
||||
self.send_message(¬ification)?;
|
||||
|
||||
Ok(activation_token)
|
||||
}
|
||||
|
||||
/// Finish launching a window with the given startup ID.
|
||||
pub(crate) fn remove_activation_token(
|
||||
&self,
|
||||
window: xproto::Window,
|
||||
startup_id: &str,
|
||||
) -> Result<(), X11Error> {
|
||||
let atoms = self.atoms();
|
||||
|
||||
// Set the _NET_STARTUP_ID property on the window.
|
||||
self.xcb_connection()
|
||||
.change_property(
|
||||
xproto::PropMode::REPLACE,
|
||||
window,
|
||||
atoms[_NET_STARTUP_ID],
|
||||
xproto::AtomEnum::STRING,
|
||||
8,
|
||||
startup_id.len().try_into().unwrap(),
|
||||
startup_id.as_bytes(),
|
||||
)?
|
||||
.check()?;
|
||||
|
||||
// Send the message indicating that the startup is over.
|
||||
let message = {
|
||||
const MESSAGE_ROOT: &str = "remove: ID=";
|
||||
|
||||
let mut buffer = Vec::with_capacity(
|
||||
MESSAGE_ROOT
|
||||
.len()
|
||||
.checked_add(startup_id.len())
|
||||
.and_then(|x| x.checked_add(1))
|
||||
.unwrap(),
|
||||
);
|
||||
buffer.extend_from_slice(MESSAGE_ROOT.as_bytes());
|
||||
quote_string(startup_id, &mut buffer);
|
||||
CString::new(buffer)
|
||||
.map_err(|err| X11Error::InvalidActivationToken(err.into_vec()))?
|
||||
.into_bytes_with_nul()
|
||||
};
|
||||
|
||||
self.send_message(&message)
|
||||
}
|
||||
|
||||
/// Send a startup notification message to the window manager.
|
||||
fn send_message(&self, message: &[u8]) -> Result<(), X11Error> {
|
||||
let atoms = self.atoms();
|
||||
|
||||
// Create a new window to send the message over.
|
||||
let screen = self.default_root();
|
||||
let window = xproto::WindowWrapper::create_window(
|
||||
self.xcb_connection(),
|
||||
screen.root_depth,
|
||||
screen.root,
|
||||
-100,
|
||||
-100,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
xproto::WindowClass::INPUT_OUTPUT,
|
||||
screen.root_visual,
|
||||
&xproto::CreateWindowAux::new().override_redirect(1).event_mask(
|
||||
xproto::EventMask::STRUCTURE_NOTIFY | xproto::EventMask::PROPERTY_CHANGE,
|
||||
),
|
||||
)?;
|
||||
|
||||
// Serialize the messages in 20-byte chunks.
|
||||
let mut message_type = atoms[_NET_STARTUP_INFO_BEGIN];
|
||||
message
|
||||
.chunks(20)
|
||||
.map(|chunk| {
|
||||
let mut buffer = [0u8; 20];
|
||||
buffer[..chunk.len()].copy_from_slice(chunk);
|
||||
let event =
|
||||
xproto::ClientMessageEvent::new(8, window.window(), message_type, buffer);
|
||||
|
||||
// Set the message type to the continuation atom for the next chunk.
|
||||
message_type = atoms[_NET_STARTUP_INFO];
|
||||
|
||||
event
|
||||
})
|
||||
.try_for_each(|event| {
|
||||
// Send each event in order.
|
||||
self.xcb_connection()
|
||||
.send_event(false, screen.root, xproto::EventMask::PROPERTY_CHANGE, event)
|
||||
.map(VoidCookie::ignore_error)
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Quote a literal string as per the startup notification specification.
|
||||
fn quote_string(s: &str, target: &mut Vec<u8>) {
|
||||
let total_len = s.len().checked_add(3).expect("quote string overflow");
|
||||
target.reserve(total_len);
|
||||
|
||||
// Add the opening quote.
|
||||
target.push(b'"');
|
||||
|
||||
// Iterate over the string split by literal quotes.
|
||||
s.as_bytes().split(|&b| b == b'"').for_each(|part| {
|
||||
// Add the part.
|
||||
target.extend_from_slice(part);
|
||||
|
||||
// Escape the quote.
|
||||
target.push(b'\\');
|
||||
target.push(b'"');
|
||||
});
|
||||
|
||||
// Un-escape the last quote.
|
||||
target.remove(target.len() - 2);
|
||||
}
|
||||
|
||||
/// Push a `Display` implementation to the buffer.
|
||||
fn push_display(buffer: &mut Vec<u8>, display: &impl std::fmt::Display) {
|
||||
struct Writer<'a> {
|
||||
buffer: &'a mut Vec<u8>,
|
||||
}
|
||||
|
||||
impl std::fmt::Write for Writer<'_> {
|
||||
fn write_str(&mut self, s: &str) -> std::fmt::Result {
|
||||
self.buffer.extend_from_slice(s.as_bytes());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
write!(Writer { buffer }, "{display}").unwrap();
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn properly_escapes_x11_messages() {
|
||||
let assert_eq = |input: &str, output: &[u8]| {
|
||||
let mut buf = vec![];
|
||||
quote_string(input, &mut buf);
|
||||
assert_eq!(buf, output);
|
||||
};
|
||||
|
||||
assert_eq("", b"\"\"");
|
||||
assert_eq("foo", b"\"foo\"");
|
||||
assert_eq("foo\"bar", b"\"foo\\\"bar\"");
|
||||
}
|
||||
}
|
||||
120
winit-x11/src/atoms.rs
Normal file
120
winit-x11/src/atoms.rs
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
//! Collects every atom used by the platform implementation.
|
||||
|
||||
use core::ops::Index;
|
||||
|
||||
macro_rules! atom_manager {
|
||||
($($name:ident $(:$lit:literal)?),*) => {
|
||||
x11rb::atom_manager! {
|
||||
/// The atoms used by `winit`
|
||||
pub Atoms: AtomsCookie {
|
||||
$($name $(:$lit)?,)*
|
||||
}
|
||||
}
|
||||
|
||||
/// Indices into the `Atoms` struct.
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
#[allow(non_camel_case_types)]
|
||||
pub enum AtomName {
|
||||
$($name,)*
|
||||
}
|
||||
|
||||
impl AtomName {
|
||||
pub(crate) fn atom_from(
|
||||
self,
|
||||
atoms: &Atoms
|
||||
) -> &x11rb::protocol::xproto::Atom {
|
||||
match self {
|
||||
$(AtomName::$name => &atoms.$name,)*
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
atom_manager! {
|
||||
// General Use Atoms
|
||||
CARD32,
|
||||
UTF8_STRING,
|
||||
WM_CHANGE_STATE,
|
||||
WM_CLIENT_MACHINE,
|
||||
WM_DELETE_WINDOW,
|
||||
WM_PROTOCOLS,
|
||||
WM_STATE,
|
||||
XIM_SERVERS,
|
||||
|
||||
// Assorted ICCCM Atoms
|
||||
_NET_WM_ICON,
|
||||
_NET_WM_MOVERESIZE,
|
||||
_NET_WM_NAME,
|
||||
_NET_WM_PID,
|
||||
_NET_WM_PING,
|
||||
_NET_WM_SYNC_REQUEST,
|
||||
_NET_WM_SYNC_REQUEST_COUNTER,
|
||||
_NET_WM_STATE,
|
||||
_NET_WM_STATE_ABOVE,
|
||||
_NET_WM_STATE_BELOW,
|
||||
_NET_WM_STATE_FULLSCREEN,
|
||||
_NET_WM_STATE_HIDDEN,
|
||||
_NET_WM_STATE_MAXIMIZED_HORZ,
|
||||
_NET_WM_STATE_MAXIMIZED_VERT,
|
||||
_NET_WM_WINDOW_TYPE,
|
||||
|
||||
// Activation atoms.
|
||||
_NET_STARTUP_INFO_BEGIN,
|
||||
_NET_STARTUP_INFO,
|
||||
_NET_STARTUP_ID,
|
||||
|
||||
// WM window types.
|
||||
_NET_WM_WINDOW_TYPE_DESKTOP,
|
||||
_NET_WM_WINDOW_TYPE_DOCK,
|
||||
_NET_WM_WINDOW_TYPE_TOOLBAR,
|
||||
_NET_WM_WINDOW_TYPE_MENU,
|
||||
_NET_WM_WINDOW_TYPE_UTILITY,
|
||||
_NET_WM_WINDOW_TYPE_SPLASH,
|
||||
_NET_WM_WINDOW_TYPE_DIALOG,
|
||||
_NET_WM_WINDOW_TYPE_DROPDOWN_MENU,
|
||||
_NET_WM_WINDOW_TYPE_POPUP_MENU,
|
||||
_NET_WM_WINDOW_TYPE_TOOLTIP,
|
||||
_NET_WM_WINDOW_TYPE_NOTIFICATION,
|
||||
_NET_WM_WINDOW_TYPE_COMBO,
|
||||
_NET_WM_WINDOW_TYPE_DND,
|
||||
_NET_WM_WINDOW_TYPE_NORMAL,
|
||||
|
||||
// Drag-N-Drop Atoms
|
||||
XdndAware,
|
||||
XdndEnter,
|
||||
XdndLeave,
|
||||
XdndDrop,
|
||||
XdndPosition,
|
||||
XdndStatus,
|
||||
XdndActionPrivate,
|
||||
XdndSelection,
|
||||
XdndFinished,
|
||||
XdndTypeList,
|
||||
TextUriList: b"text/uri-list",
|
||||
None: b"None",
|
||||
|
||||
// Miscellaneous Atoms
|
||||
_GTK_THEME_VARIANT,
|
||||
_MOTIF_WM_HINTS,
|
||||
_NET_ACTIVE_WINDOW,
|
||||
_NET_CLIENT_LIST,
|
||||
_NET_FRAME_EXTENTS,
|
||||
_NET_SUPPORTED,
|
||||
_NET_SUPPORTING_WM_CHECK,
|
||||
_XEMBED,
|
||||
_XSETTINGS_SETTINGS
|
||||
}
|
||||
|
||||
impl Index<AtomName> for Atoms {
|
||||
type Output = x11rb::protocol::xproto::Atom;
|
||||
|
||||
fn index(&self, index: AtomName) -> &Self::Output {
|
||||
index.atom_from(self)
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure `None` is still defined.
|
||||
pub(crate) use core::option::Option::None;
|
||||
|
||||
pub(crate) use AtomName::*;
|
||||
191
winit-x11/src/dnd.rs
Normal file
191
winit-x11/src/dnd.rs
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
use std::io;
|
||||
use std::os::raw::*;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::str::Utf8Error;
|
||||
use std::sync::Arc;
|
||||
|
||||
use dpi::PhysicalPosition;
|
||||
use percent_encoding::percent_decode;
|
||||
use x11rb::protocol::xproto::{self, ConnectionExt};
|
||||
|
||||
use crate::atoms::AtomName::None as DndNone;
|
||||
use crate::atoms::*;
|
||||
use crate::event_loop::{CookieResultExt, X11Error};
|
||||
use crate::util;
|
||||
use crate::xdisplay::XConnection;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum DndState {
|
||||
Accepted,
|
||||
Rejected,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum DndDataParseError {
|
||||
EmptyData,
|
||||
InvalidUtf8(#[allow(dead_code)] Utf8Error),
|
||||
HostnameSpecified(#[allow(dead_code)] String),
|
||||
UnexpectedProtocol(#[allow(dead_code)] String),
|
||||
UnresolvablePath(#[allow(dead_code)] io::Error),
|
||||
}
|
||||
|
||||
impl From<Utf8Error> for DndDataParseError {
|
||||
fn from(e: Utf8Error) -> Self {
|
||||
DndDataParseError::InvalidUtf8(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<io::Error> for DndDataParseError {
|
||||
fn from(e: io::Error) -> Self {
|
||||
DndDataParseError::UnresolvablePath(e)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Dnd {
|
||||
xconn: Arc<XConnection>,
|
||||
// Populated by XdndEnter event handler
|
||||
pub version: Option<c_long>,
|
||||
pub type_list: Option<Vec<xproto::Atom>>,
|
||||
// Populated by XdndPosition event handler
|
||||
pub source_window: Option<xproto::Window>,
|
||||
// Populated by XdndPosition event handler
|
||||
pub position: PhysicalPosition<f64>,
|
||||
// Populated by SelectionNotify event handler (triggered by XdndPosition event handler)
|
||||
pub result: Option<Result<Vec<PathBuf>, DndDataParseError>>,
|
||||
// Populated by SelectionNotify event handler (triggered by XdndPosition event handler)
|
||||
pub dragging: bool,
|
||||
}
|
||||
|
||||
impl Dnd {
|
||||
pub fn new(xconn: Arc<XConnection>) -> Result<Self, X11Error> {
|
||||
Ok(Dnd {
|
||||
xconn,
|
||||
version: None,
|
||||
type_list: None,
|
||||
source_window: None,
|
||||
position: PhysicalPosition::default(),
|
||||
result: None,
|
||||
dragging: false,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn reset(&mut self) {
|
||||
self.version = None;
|
||||
self.type_list = None;
|
||||
self.source_window = None;
|
||||
self.result = None;
|
||||
self.dragging = false;
|
||||
}
|
||||
|
||||
pub unsafe fn send_status(
|
||||
&self,
|
||||
this_window: xproto::Window,
|
||||
target_window: xproto::Window,
|
||||
state: DndState,
|
||||
) -> Result<(), X11Error> {
|
||||
let atoms = self.xconn.atoms();
|
||||
let (accepted, action) = match state {
|
||||
DndState::Accepted => (1, atoms[XdndActionPrivate]),
|
||||
DndState::Rejected => (0, atoms[DndNone]),
|
||||
};
|
||||
self.xconn
|
||||
.send_client_msg(target_window, target_window, atoms[XdndStatus] as _, None, [
|
||||
this_window,
|
||||
accepted,
|
||||
0,
|
||||
0,
|
||||
action as _,
|
||||
])?
|
||||
.ignore_error();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub unsafe fn send_finished(
|
||||
&self,
|
||||
this_window: xproto::Window,
|
||||
target_window: xproto::Window,
|
||||
state: DndState,
|
||||
) -> Result<(), X11Error> {
|
||||
let atoms = self.xconn.atoms();
|
||||
let (accepted, action) = match state {
|
||||
DndState::Accepted => (1, atoms[XdndActionPrivate]),
|
||||
DndState::Rejected => (0, atoms[DndNone]),
|
||||
};
|
||||
self.xconn
|
||||
.send_client_msg(target_window, target_window, atoms[XdndFinished] as _, None, [
|
||||
this_window,
|
||||
accepted,
|
||||
action as _,
|
||||
0,
|
||||
0,
|
||||
])?
|
||||
.ignore_error();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub unsafe fn get_type_list(
|
||||
&self,
|
||||
source_window: xproto::Window,
|
||||
) -> Result<Vec<xproto::Atom>, util::GetPropertyError> {
|
||||
let atoms = self.xconn.atoms();
|
||||
self.xconn.get_property(
|
||||
source_window,
|
||||
atoms[XdndTypeList],
|
||||
xproto::Atom::from(xproto::AtomEnum::ATOM),
|
||||
)
|
||||
}
|
||||
|
||||
pub unsafe fn convert_selection(&self, window: xproto::Window, time: xproto::Timestamp) {
|
||||
let atoms = self.xconn.atoms();
|
||||
self.xconn
|
||||
.xcb_connection()
|
||||
.convert_selection(
|
||||
window,
|
||||
atoms[XdndSelection],
|
||||
atoms[TextUriList],
|
||||
atoms[XdndSelection],
|
||||
time,
|
||||
)
|
||||
.expect_then_ignore_error("Failed to send XdndSelection event")
|
||||
}
|
||||
|
||||
pub unsafe fn read_data(
|
||||
&self,
|
||||
window: xproto::Window,
|
||||
) -> Result<Vec<c_uchar>, util::GetPropertyError> {
|
||||
let atoms = self.xconn.atoms();
|
||||
self.xconn.get_property(window, atoms[XdndSelection], atoms[TextUriList])
|
||||
}
|
||||
|
||||
pub fn parse_data(&self, data: &mut [c_uchar]) -> Result<Vec<PathBuf>, DndDataParseError> {
|
||||
if !data.is_empty() {
|
||||
let mut path_list = Vec::new();
|
||||
let decoded = percent_decode(data).decode_utf8()?.into_owned();
|
||||
for uri in decoded.split("\r\n").filter(|u| !u.is_empty()) {
|
||||
// The format is specified as protocol://host/path
|
||||
// However, it's typically simply protocol:///path
|
||||
let path_str = if uri.starts_with("file://") {
|
||||
let path_str = uri.replace("file://", "");
|
||||
if !path_str.starts_with('/') {
|
||||
// A hostname is specified
|
||||
// Supporting this case is beyond the scope of my mental health
|
||||
return Err(DndDataParseError::HostnameSpecified(path_str));
|
||||
}
|
||||
path_str
|
||||
} else {
|
||||
// Only the file protocol is supported
|
||||
return Err(DndDataParseError::UnexpectedProtocol(uri.to_owned()));
|
||||
};
|
||||
|
||||
let path = Path::new(&path_str).canonicalize()?;
|
||||
path_list.push(path);
|
||||
}
|
||||
Ok(path_list)
|
||||
} else {
|
||||
Err(DndDataParseError::EmptyData)
|
||||
}
|
||||
}
|
||||
}
|
||||
1092
winit-x11/src/event_loop.rs
Normal file
1092
winit-x11/src/event_loop.rs
Normal file
File diff suppressed because it is too large
Load diff
1768
winit-x11/src/event_processor.rs
Normal file
1768
winit-x11/src/event_processor.rs
Normal file
File diff suppressed because it is too large
Load diff
4
winit-x11/src/ffi.rs
Normal file
4
winit-x11/src/ffi.rs
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
pub use x11_dl::error::OpenError;
|
||||
pub use x11_dl::xinput2::*;
|
||||
pub use x11_dl::xlib::*;
|
||||
pub use x11_dl::xlib_xcb::*;
|
||||
206
winit-x11/src/ime/callbacks.rs
Normal file
206
winit-x11/src/ime/callbacks.rs
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
use std::collections::HashMap;
|
||||
use std::os::raw::c_char;
|
||||
use std::ptr;
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::context::{ImeContext, ImeContextCreationError};
|
||||
use super::ffi;
|
||||
use super::inner::{close_im, ImeInner};
|
||||
use super::input_method::PotentialInputMethods;
|
||||
use crate::xdisplay::{XConnection, XError};
|
||||
|
||||
pub(crate) unsafe fn xim_set_callback(
|
||||
xconn: &Arc<XConnection>,
|
||||
xim: ffi::XIM,
|
||||
field: *const c_char,
|
||||
callback: *mut ffi::XIMCallback,
|
||||
) -> Result<(), XError> {
|
||||
// It's advisable to wrap variadic FFI functions in our own functions, as we want to minimize
|
||||
// access that isn't type-checked.
|
||||
unsafe { (xconn.xlib.XSetIMValues)(xim, field, callback, ptr::null_mut::<()>()) };
|
||||
xconn.check_errors()
|
||||
}
|
||||
|
||||
// Set a callback for when an input method matching the current locale modifiers becomes
|
||||
// available. Note that this has nothing to do with what input methods are open or able to be
|
||||
// opened, and simply uses the modifiers that are set when the callback is set.
|
||||
// * This is called per locale modifier, not per input method opened with that locale modifier.
|
||||
// * Trying to set this for multiple locale modifiers causes problems, i.e. one of the rebuilt input
|
||||
// contexts would always silently fail to use the input method.
|
||||
pub(crate) unsafe fn set_instantiate_callback(
|
||||
xconn: &Arc<XConnection>,
|
||||
client_data: ffi::XPointer,
|
||||
) -> Result<(), XError> {
|
||||
unsafe {
|
||||
(xconn.xlib.XRegisterIMInstantiateCallback)(
|
||||
xconn.display,
|
||||
ptr::null_mut(),
|
||||
ptr::null_mut(),
|
||||
ptr::null_mut(),
|
||||
Some(xim_instantiate_callback),
|
||||
client_data,
|
||||
)
|
||||
};
|
||||
xconn.check_errors()
|
||||
}
|
||||
|
||||
pub(crate) unsafe fn unset_instantiate_callback(
|
||||
xconn: &Arc<XConnection>,
|
||||
client_data: ffi::XPointer,
|
||||
) -> Result<(), XError> {
|
||||
unsafe {
|
||||
(xconn.xlib.XUnregisterIMInstantiateCallback)(
|
||||
xconn.display,
|
||||
ptr::null_mut(),
|
||||
ptr::null_mut(),
|
||||
ptr::null_mut(),
|
||||
Some(xim_instantiate_callback),
|
||||
client_data,
|
||||
)
|
||||
};
|
||||
xconn.check_errors()
|
||||
}
|
||||
|
||||
pub(crate) unsafe fn set_destroy_callback(
|
||||
xconn: &Arc<XConnection>,
|
||||
im: ffi::XIM,
|
||||
inner: &ImeInner,
|
||||
) -> Result<(), XError> {
|
||||
unsafe {
|
||||
xim_set_callback(
|
||||
xconn,
|
||||
im,
|
||||
ffi::XNDestroyCallback_0.as_ptr() as *const _,
|
||||
&inner.destroy_callback as *const _ as *mut _,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
enum ReplaceImError {
|
||||
// Boxed to prevent large error type
|
||||
MethodOpenFailed(#[allow(dead_code)] Box<PotentialInputMethods>),
|
||||
ContextCreationFailed(#[allow(dead_code)] ImeContextCreationError),
|
||||
SetDestroyCallbackFailed(#[allow(dead_code)] XError),
|
||||
}
|
||||
|
||||
// Attempt to replace current IM (which may or may not be presently valid) with a new one. This
|
||||
// includes replacing all existing input contexts and free'ing resources as necessary. This only
|
||||
// modifies existing state if all operations succeed.
|
||||
unsafe fn replace_im(inner: *mut ImeInner) -> Result<(), ReplaceImError> {
|
||||
let xconn = unsafe { &(*inner).xconn };
|
||||
|
||||
let (new_im, is_fallback) = {
|
||||
let new_im = unsafe { (*inner).potential_input_methods.open_im(xconn, None) };
|
||||
let is_fallback = new_im.is_fallback();
|
||||
(
|
||||
new_im.ok().ok_or_else(|| {
|
||||
ReplaceImError::MethodOpenFailed(Box::new(unsafe {
|
||||
(*inner).potential_input_methods.clone()
|
||||
}))
|
||||
})?,
|
||||
is_fallback,
|
||||
)
|
||||
};
|
||||
|
||||
// It's important to always set a destroy callback, since there's otherwise potential for us
|
||||
// to try to use or free a resource that's already been destroyed on the server.
|
||||
{
|
||||
let result = unsafe { set_destroy_callback(xconn, new_im.im, &*inner) };
|
||||
if result.is_err() {
|
||||
let _ = unsafe { close_im(xconn, new_im.im) };
|
||||
}
|
||||
result
|
||||
}
|
||||
.map_err(ReplaceImError::SetDestroyCallbackFailed)?;
|
||||
|
||||
let mut new_contexts = HashMap::new();
|
||||
for (window, old_context) in unsafe { (*inner).contexts.iter() } {
|
||||
let area = old_context.as_ref().map(|old_context| old_context.ic_area);
|
||||
|
||||
// Check if the IME was allowed on that context.
|
||||
let is_allowed =
|
||||
old_context.as_ref().map(|old_context| old_context.is_allowed()).unwrap_or_default();
|
||||
|
||||
let new_context = {
|
||||
let result = unsafe {
|
||||
ImeContext::new(
|
||||
xconn,
|
||||
&new_im,
|
||||
*window,
|
||||
area,
|
||||
(*inner).event_sender.clone(),
|
||||
is_allowed,
|
||||
)
|
||||
};
|
||||
if result.is_err() {
|
||||
let _ = unsafe { close_im(xconn, new_im.im) };
|
||||
}
|
||||
result.map_err(ReplaceImError::ContextCreationFailed)?
|
||||
};
|
||||
new_contexts.insert(*window, Some(new_context));
|
||||
}
|
||||
|
||||
// If we've made it this far, everything succeeded.
|
||||
unsafe {
|
||||
let _ = (*inner).destroy_all_contexts_if_necessary();
|
||||
let _ = (*inner).close_im_if_necessary();
|
||||
(*inner).im = Some(new_im);
|
||||
(*inner).contexts = new_contexts;
|
||||
(*inner).is_destroyed = false;
|
||||
(*inner).is_fallback = is_fallback;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub unsafe extern "C" fn xim_instantiate_callback(
|
||||
_display: *mut ffi::Display,
|
||||
client_data: ffi::XPointer,
|
||||
// This field is unsupplied.
|
||||
_call_data: ffi::XPointer,
|
||||
) {
|
||||
let inner: *mut ImeInner = client_data as _;
|
||||
if !inner.is_null() {
|
||||
let xconn = unsafe { &(*inner).xconn };
|
||||
match unsafe { replace_im(inner) } {
|
||||
Ok(()) => unsafe {
|
||||
let _ = unset_instantiate_callback(xconn, client_data);
|
||||
(*inner).is_fallback = false;
|
||||
},
|
||||
Err(err) => unsafe {
|
||||
if (*inner).is_destroyed {
|
||||
// We have no usable input methods!
|
||||
panic!("Failed to reopen input method: {err:?}");
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This callback is triggered when the input method is closed on the server end. When this
|
||||
// happens, XCloseIM/XDestroyIC doesn't need to be called, as the resources have already been
|
||||
// free'd (attempting to do so causes our connection to freeze).
|
||||
pub unsafe extern "C" fn xim_destroy_callback(
|
||||
_xim: ffi::XIM,
|
||||
client_data: ffi::XPointer,
|
||||
// This field is unsupplied.
|
||||
_call_data: ffi::XPointer,
|
||||
) {
|
||||
let inner: *mut ImeInner = client_data as _;
|
||||
if !inner.is_null() {
|
||||
unsafe { (*inner).is_destroyed = true };
|
||||
let xconn = unsafe { &(*inner).xconn };
|
||||
if unsafe { !(*inner).is_fallback } {
|
||||
let _ = unsafe { set_instantiate_callback(xconn, client_data) };
|
||||
// Attempt to open fallback input method.
|
||||
match unsafe { replace_im(inner) } {
|
||||
Ok(()) => unsafe { (*inner).is_fallback = true },
|
||||
Err(err) => {
|
||||
// We have no usable input methods!
|
||||
panic!("Failed to open fallback input method: {err:?}");
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
405
winit-x11/src/ime/context.rs
Normal file
405
winit-x11/src/ime/context.rs
Normal file
|
|
@ -0,0 +1,405 @@
|
|||
use std::error::Error;
|
||||
use std::ffi::CStr;
|
||||
use std::sync::Arc;
|
||||
use std::{fmt, mem, ptr};
|
||||
|
||||
use x11_dl::xlib::{XIMCallback, XIMPreeditCaretCallbackStruct, XIMPreeditDrawCallbackStruct};
|
||||
|
||||
use super::input_method::{InputMethod, Style, XIMStyle};
|
||||
use super::{ffi, util, ImeEvent, ImeEventSender};
|
||||
use crate::xdisplay::{XConnection, XError};
|
||||
|
||||
/// IME creation error.
|
||||
#[derive(Debug)]
|
||||
pub enum ImeContextCreationError {
|
||||
/// Got the error from Xlib.
|
||||
XError(XError),
|
||||
|
||||
/// Got null pointer from Xlib but without exact reason.
|
||||
Null,
|
||||
}
|
||||
|
||||
impl fmt::Display for ImeContextCreationError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
ImeContextCreationError::XError(err) => err.fmt(f),
|
||||
ImeContextCreationError::Null => {
|
||||
write!(f, "got null pointer from Xlib without exact reason")
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for ImeContextCreationError {}
|
||||
|
||||
/// The callback used by XIM preedit functions.
|
||||
type XIMProcNonnull = unsafe extern "C" fn(ffi::XIM, ffi::XPointer, ffi::XPointer);
|
||||
|
||||
/// Wrapper for creating XIM callbacks.
|
||||
#[inline]
|
||||
fn create_xim_callback(client_data: ffi::XPointer, callback: XIMProcNonnull) -> ffi::XIMCallback {
|
||||
XIMCallback { client_data, callback: Some(callback) }
|
||||
}
|
||||
|
||||
/// The server started preedit.
|
||||
extern "C" fn preedit_start_callback(
|
||||
_xim: ffi::XIM,
|
||||
client_data: ffi::XPointer,
|
||||
_call_data: ffi::XPointer,
|
||||
) -> i32 {
|
||||
let client_data = unsafe { &mut *(client_data as *mut ImeContextClientData) };
|
||||
|
||||
client_data.text.clear();
|
||||
client_data.cursor_pos = 0;
|
||||
client_data
|
||||
.event_sender
|
||||
.send((client_data.window, ImeEvent::Start))
|
||||
.expect("failed to send preedit start event");
|
||||
-1
|
||||
}
|
||||
|
||||
/// Done callback is used when the preedit should be hidden.
|
||||
extern "C" fn preedit_done_callback(
|
||||
_xim: ffi::XIM,
|
||||
client_data: ffi::XPointer,
|
||||
_call_data: ffi::XPointer,
|
||||
) {
|
||||
let client_data = unsafe { &mut *(client_data as *mut ImeContextClientData) };
|
||||
|
||||
// Drop text buffer and reset cursor position on done.
|
||||
client_data.text = Vec::new();
|
||||
client_data.cursor_pos = 0;
|
||||
|
||||
client_data
|
||||
.event_sender
|
||||
.send((client_data.window, ImeEvent::End))
|
||||
.expect("failed to send preedit end event");
|
||||
}
|
||||
|
||||
fn calc_byte_position(text: &[char], pos: usize) -> usize {
|
||||
text.iter().take(pos).fold(0, |byte_pos, text| byte_pos + text.len_utf8())
|
||||
}
|
||||
|
||||
/// Preedit text information to be drawn inline by the client.
|
||||
extern "C" fn preedit_draw_callback(
|
||||
_xim: ffi::XIM,
|
||||
client_data: ffi::XPointer,
|
||||
call_data: ffi::XPointer,
|
||||
) {
|
||||
let client_data = unsafe { &mut *(client_data as *mut ImeContextClientData) };
|
||||
let call_data = unsafe { &mut *(call_data as *mut XIMPreeditDrawCallbackStruct) };
|
||||
client_data.cursor_pos = call_data.caret as usize;
|
||||
|
||||
let chg_range =
|
||||
call_data.chg_first as usize..(call_data.chg_first + call_data.chg_length) as usize;
|
||||
if chg_range.start > client_data.text.len() || chg_range.end > client_data.text.len() {
|
||||
tracing::warn!(
|
||||
"invalid chg range: buffer length={}, but chg_first={} chg_lengthg={}",
|
||||
client_data.text.len(),
|
||||
call_data.chg_first,
|
||||
call_data.chg_length
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// NULL indicate text deletion
|
||||
let mut new_chars = if call_data.text.is_null() {
|
||||
Vec::new()
|
||||
} else {
|
||||
let xim_text = unsafe { &mut *(call_data.text) };
|
||||
if xim_text.encoding_is_wchar > 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let new_text = unsafe { xim_text.string.multi_byte };
|
||||
|
||||
if new_text.is_null() {
|
||||
return;
|
||||
}
|
||||
|
||||
let new_text = unsafe { CStr::from_ptr(new_text) };
|
||||
|
||||
String::from(new_text.to_str().expect("Invalid UTF-8 String from IME")).chars().collect()
|
||||
};
|
||||
let mut old_text_tail = client_data.text.split_off(chg_range.end);
|
||||
client_data.text.truncate(chg_range.start);
|
||||
client_data.text.append(&mut new_chars);
|
||||
client_data.text.append(&mut old_text_tail);
|
||||
let cursor_byte_pos = calc_byte_position(&client_data.text, client_data.cursor_pos);
|
||||
|
||||
client_data
|
||||
.event_sender
|
||||
.send((
|
||||
client_data.window,
|
||||
ImeEvent::Update(client_data.text.iter().collect(), cursor_byte_pos),
|
||||
))
|
||||
.expect("failed to send preedit update event");
|
||||
}
|
||||
|
||||
/// Handling of cursor movements in preedit text.
|
||||
extern "C" fn preedit_caret_callback(
|
||||
_xim: ffi::XIM,
|
||||
client_data: ffi::XPointer,
|
||||
call_data: ffi::XPointer,
|
||||
) {
|
||||
let client_data = unsafe { &mut *(client_data as *mut ImeContextClientData) };
|
||||
let call_data = unsafe { &mut *(call_data as *mut XIMPreeditCaretCallbackStruct) };
|
||||
|
||||
if call_data.direction == ffi::XIMCaretDirection::XIMAbsolutePosition {
|
||||
client_data.cursor_pos = call_data.position as usize;
|
||||
let cursor_byte_pos = calc_byte_position(&client_data.text, client_data.cursor_pos);
|
||||
|
||||
client_data
|
||||
.event_sender
|
||||
.send((
|
||||
client_data.window,
|
||||
ImeEvent::Update(client_data.text.iter().collect(), cursor_byte_pos),
|
||||
))
|
||||
.expect("failed to send preedit update event");
|
||||
}
|
||||
}
|
||||
|
||||
/// Struct to simplify callback creation and latter passing into Xlib XIM.
|
||||
struct PreeditCallbacks {
|
||||
start_callback: ffi::XIMCallback,
|
||||
done_callback: ffi::XIMCallback,
|
||||
draw_callback: ffi::XIMCallback,
|
||||
caret_callback: ffi::XIMCallback,
|
||||
}
|
||||
|
||||
impl PreeditCallbacks {
|
||||
pub fn new(client_data: ffi::XPointer) -> PreeditCallbacks {
|
||||
let start_callback = create_xim_callback(client_data, unsafe {
|
||||
mem::transmute::<usize, unsafe extern "C" fn(ffi::XIM, ffi::XPointer, ffi::XPointer)>(
|
||||
preedit_start_callback as usize,
|
||||
)
|
||||
});
|
||||
let done_callback = create_xim_callback(client_data, preedit_done_callback);
|
||||
let caret_callback = create_xim_callback(client_data, preedit_caret_callback);
|
||||
let draw_callback = create_xim_callback(client_data, preedit_draw_callback);
|
||||
|
||||
PreeditCallbacks { start_callback, done_callback, caret_callback, draw_callback }
|
||||
}
|
||||
}
|
||||
|
||||
struct ImeContextClientData {
|
||||
window: ffi::Window,
|
||||
event_sender: ImeEventSender,
|
||||
text: Vec<char>,
|
||||
cursor_pos: usize,
|
||||
}
|
||||
|
||||
// XXX: this struct doesn't destroy its XIC resource when dropped.
|
||||
// This is intentional, as it doesn't have enough information to know whether or not the context
|
||||
// still exists on the server. Since `ImeInner` has that awareness, destruction must be handled
|
||||
// through `ImeInner`.
|
||||
pub struct ImeContext {
|
||||
pub(crate) ic: ffi::XIC,
|
||||
pub(crate) ic_area: ffi::XRectangle,
|
||||
pub(crate) allowed: bool,
|
||||
// Since the data is passed shared between X11 XIM callbacks, but couldn't be directly free
|
||||
// from there we keep the pointer to automatically deallocate it.
|
||||
_client_data: Box<ImeContextClientData>,
|
||||
}
|
||||
|
||||
impl ImeContext {
|
||||
pub(crate) unsafe fn new(
|
||||
xconn: &Arc<XConnection>,
|
||||
im: &InputMethod,
|
||||
window: ffi::Window,
|
||||
ic_area: Option<ffi::XRectangle>,
|
||||
event_sender: ImeEventSender,
|
||||
allowed: bool,
|
||||
) -> Result<Self, ImeContextCreationError> {
|
||||
let client_data = Box::into_raw(Box::new(ImeContextClientData {
|
||||
window,
|
||||
event_sender,
|
||||
text: Vec::new(),
|
||||
cursor_pos: 0,
|
||||
}));
|
||||
|
||||
let style = if allowed { im.preedit_style } else { im.none_style };
|
||||
|
||||
let ic = match style as _ {
|
||||
Style::Preedit(style) => unsafe {
|
||||
ImeContext::create_preedit_ic(
|
||||
xconn,
|
||||
im.im,
|
||||
style,
|
||||
window,
|
||||
client_data as ffi::XPointer,
|
||||
)
|
||||
},
|
||||
Style::Nothing(style) => unsafe {
|
||||
ImeContext::create_nothing_ic(xconn, im.im, style, window)
|
||||
},
|
||||
Style::None(style) => unsafe {
|
||||
ImeContext::create_none_ic(xconn, im.im, style, window)
|
||||
},
|
||||
}
|
||||
.ok_or(ImeContextCreationError::Null)?;
|
||||
|
||||
xconn.check_errors().map_err(ImeContextCreationError::XError)?;
|
||||
|
||||
let mut context = ImeContext {
|
||||
ic,
|
||||
ic_area: ffi::XRectangle { x: 0, y: 0, width: 0, height: 0 },
|
||||
allowed,
|
||||
_client_data: unsafe { Box::from_raw(client_data) },
|
||||
};
|
||||
|
||||
// Set the preedit cursor area, if it's present.
|
||||
if let Some(ic_area) = ic_area {
|
||||
context.set_area(xconn, ic_area.x, ic_area.y, ic_area.width, ic_area.height);
|
||||
}
|
||||
|
||||
Ok(context)
|
||||
}
|
||||
|
||||
unsafe fn create_none_ic(
|
||||
xconn: &Arc<XConnection>,
|
||||
im: ffi::XIM,
|
||||
style: XIMStyle,
|
||||
window: ffi::Window,
|
||||
) -> Option<ffi::XIC> {
|
||||
let ic = unsafe {
|
||||
(xconn.xlib.XCreateIC)(
|
||||
im,
|
||||
ffi::XNInputStyle_0.as_ptr() as *const _,
|
||||
style,
|
||||
ffi::XNClientWindow_0.as_ptr() as *const _,
|
||||
window,
|
||||
ptr::null_mut::<()>(),
|
||||
)
|
||||
};
|
||||
|
||||
(!ic.is_null()).then_some(ic)
|
||||
}
|
||||
|
||||
unsafe fn create_preedit_ic(
|
||||
xconn: &Arc<XConnection>,
|
||||
im: ffi::XIM,
|
||||
style: XIMStyle,
|
||||
window: ffi::Window,
|
||||
client_data: ffi::XPointer,
|
||||
) -> Option<ffi::XIC> {
|
||||
let preedit_callbacks = PreeditCallbacks::new(client_data);
|
||||
let preedit_attr = util::memory::XSmartPointer::new(xconn, unsafe {
|
||||
(xconn.xlib.XVaCreateNestedList)(
|
||||
0,
|
||||
ffi::XNPreeditStartCallback_0.as_ptr() as *const _,
|
||||
&(preedit_callbacks.start_callback) as *const _,
|
||||
ffi::XNPreeditDoneCallback_0.as_ptr() as *const _,
|
||||
&(preedit_callbacks.done_callback) as *const _,
|
||||
ffi::XNPreeditCaretCallback_0.as_ptr() as *const _,
|
||||
&(preedit_callbacks.caret_callback) as *const _,
|
||||
ffi::XNPreeditDrawCallback_0.as_ptr() as *const _,
|
||||
&(preedit_callbacks.draw_callback) as *const _,
|
||||
ptr::null_mut::<()>(),
|
||||
)
|
||||
})
|
||||
.expect("XVaCreateNestedList returned NULL");
|
||||
|
||||
let ic = unsafe {
|
||||
(xconn.xlib.XCreateIC)(
|
||||
im,
|
||||
ffi::XNInputStyle_0.as_ptr() as *const _,
|
||||
style,
|
||||
ffi::XNClientWindow_0.as_ptr() as *const _,
|
||||
window,
|
||||
ffi::XNPreeditAttributes_0.as_ptr() as *const _,
|
||||
preedit_attr.ptr,
|
||||
ptr::null_mut::<()>(),
|
||||
)
|
||||
};
|
||||
|
||||
(!ic.is_null()).then_some(ic)
|
||||
}
|
||||
|
||||
unsafe fn create_nothing_ic(
|
||||
xconn: &Arc<XConnection>,
|
||||
im: ffi::XIM,
|
||||
style: XIMStyle,
|
||||
window: ffi::Window,
|
||||
) -> Option<ffi::XIC> {
|
||||
let ic = unsafe {
|
||||
(xconn.xlib.XCreateIC)(
|
||||
im,
|
||||
ffi::XNInputStyle_0.as_ptr() as *const _,
|
||||
style,
|
||||
ffi::XNClientWindow_0.as_ptr() as *const _,
|
||||
window,
|
||||
ptr::null_mut::<()>(),
|
||||
)
|
||||
};
|
||||
|
||||
(!ic.is_null()).then_some(ic)
|
||||
}
|
||||
|
||||
pub(crate) fn focus(&self, xconn: &Arc<XConnection>) -> Result<(), XError> {
|
||||
unsafe {
|
||||
(xconn.xlib.XSetICFocus)(self.ic);
|
||||
}
|
||||
xconn.check_errors()
|
||||
}
|
||||
|
||||
pub(crate) fn unfocus(&self, xconn: &Arc<XConnection>) -> Result<(), XError> {
|
||||
unsafe {
|
||||
(xconn.xlib.XUnsetICFocus)(self.ic);
|
||||
}
|
||||
xconn.check_errors()
|
||||
}
|
||||
|
||||
pub fn is_allowed(&self) -> bool {
|
||||
self.allowed
|
||||
}
|
||||
|
||||
/// Set the spot and area for preedit text.
|
||||
///
|
||||
/// This functionality depends on the libx11 version.
|
||||
/// - Until libx11 1.8.2, XNSpotLocation was blocked by libx11 in On-The-Spot mode.
|
||||
/// - Until libx11 1.8.11, XNArea was blocked by libx11 in On-The-Spot mode.
|
||||
///
|
||||
/// Use of this information is discretionary by input method servers,
|
||||
/// and some may not use it by default, even if they have support.
|
||||
pub(crate) fn set_area(
|
||||
&mut self,
|
||||
xconn: &Arc<XConnection>,
|
||||
x: i16,
|
||||
y: i16,
|
||||
width: u16,
|
||||
height: u16,
|
||||
) {
|
||||
let ic_area = ffi::XRectangle { x, y, width, height };
|
||||
|
||||
if !self.is_allowed() || self.ic_area == ic_area {
|
||||
return;
|
||||
}
|
||||
|
||||
self.ic_area = ic_area;
|
||||
let ic_spot =
|
||||
ffi::XPoint { x: x.saturating_add(width as i16), y: y.saturating_add(height as i16) };
|
||||
|
||||
unsafe {
|
||||
let preedit_attr = util::memory::XSmartPointer::new(
|
||||
xconn,
|
||||
(xconn.xlib.XVaCreateNestedList)(
|
||||
0,
|
||||
ffi::XNSpotLocation_0.as_ptr(),
|
||||
&ic_spot,
|
||||
ffi::XNArea_0.as_ptr(),
|
||||
&self.ic_area,
|
||||
ptr::null_mut::<()>(),
|
||||
),
|
||||
)
|
||||
.expect("XVaCreateNestedList returned NULL");
|
||||
|
||||
(xconn.xlib.XSetICValues)(
|
||||
self.ic,
|
||||
ffi::XNPreeditAttributes_0.as_ptr() as *const _,
|
||||
preedit_attr.ptr,
|
||||
ptr::null_mut::<()>(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
74
winit-x11/src/ime/inner.rs
Normal file
74
winit-x11/src/ime/inner.rs
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
use std::collections::HashMap;
|
||||
use std::mem;
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::context::ImeContext;
|
||||
use super::input_method::{InputMethod, PotentialInputMethods};
|
||||
use super::{ffi, ImeEventSender};
|
||||
use crate::xdisplay::{XConnection, XError};
|
||||
|
||||
pub(crate) unsafe fn close_im(xconn: &Arc<XConnection>, im: ffi::XIM) -> Result<(), XError> {
|
||||
unsafe { (xconn.xlib.XCloseIM)(im) };
|
||||
xconn.check_errors()
|
||||
}
|
||||
|
||||
pub(crate) unsafe fn destroy_ic(xconn: &Arc<XConnection>, ic: ffi::XIC) -> Result<(), XError> {
|
||||
unsafe { (xconn.xlib.XDestroyIC)(ic) };
|
||||
xconn.check_errors()
|
||||
}
|
||||
|
||||
pub(crate) struct ImeInner {
|
||||
pub xconn: Arc<XConnection>,
|
||||
pub im: Option<InputMethod>,
|
||||
pub potential_input_methods: PotentialInputMethods,
|
||||
pub contexts: HashMap<ffi::Window, Option<ImeContext>>,
|
||||
// WARNING: this is initially zeroed!
|
||||
pub destroy_callback: ffi::XIMCallback,
|
||||
pub event_sender: ImeEventSender,
|
||||
// Indicates whether or not the input method was destroyed on the server end
|
||||
// (i.e. if ibus/fcitx/etc. was terminated/restarted)
|
||||
pub is_destroyed: bool,
|
||||
pub is_fallback: bool,
|
||||
}
|
||||
|
||||
impl ImeInner {
|
||||
pub(crate) fn new(
|
||||
xconn: Arc<XConnection>,
|
||||
potential_input_methods: PotentialInputMethods,
|
||||
event_sender: ImeEventSender,
|
||||
) -> Self {
|
||||
ImeInner {
|
||||
xconn,
|
||||
im: None,
|
||||
potential_input_methods,
|
||||
contexts: HashMap::new(),
|
||||
destroy_callback: unsafe { mem::zeroed() },
|
||||
event_sender,
|
||||
is_destroyed: false,
|
||||
is_fallback: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub unsafe fn close_im_if_necessary(&self) -> Result<bool, XError> {
|
||||
if !self.is_destroyed && self.im.is_some() {
|
||||
unsafe { close_im(&self.xconn, self.im.as_ref().unwrap().im) }.map(|_| true)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
pub unsafe fn destroy_ic_if_necessary(&self, ic: ffi::XIC) -> Result<bool, XError> {
|
||||
if !self.is_destroyed {
|
||||
unsafe { destroy_ic(&self.xconn, ic) }.map(|_| true)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
pub unsafe fn destroy_all_contexts_if_necessary(&self) -> Result<bool, XError> {
|
||||
for context in self.contexts.values().flatten() {
|
||||
unsafe { self.destroy_ic_if_necessary(context.ic)? };
|
||||
}
|
||||
Ok(!self.is_destroyed)
|
||||
}
|
||||
}
|
||||
347
winit-x11/src/ime/input_method.rs
Normal file
347
winit-x11/src/ime/input_method.rs
Normal file
|
|
@ -0,0 +1,347 @@
|
|||
use std::ffi::{CStr, CString, IntoStringError};
|
||||
use std::os::raw::{c_char, c_ulong, c_ushort};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::{env, fmt, ptr};
|
||||
|
||||
use x11rb::protocol::xproto;
|
||||
|
||||
use super::super::atoms::*;
|
||||
use super::{ffi, util};
|
||||
use crate::xdisplay::{XConnection, XError};
|
||||
|
||||
static GLOBAL_LOCK: Mutex<()> = Mutex::new(());
|
||||
|
||||
unsafe fn open_im(xconn: &Arc<XConnection>, locale_modifiers: &CStr) -> Option<ffi::XIM> {
|
||||
let _lock = GLOBAL_LOCK.lock();
|
||||
|
||||
// XSetLocaleModifiers returns...
|
||||
// * The current locale modifiers if it's given a NULL pointer.
|
||||
// * The new locale modifiers if we succeeded in setting them.
|
||||
// * NULL if the locale modifiers string is malformed or if the current locale is not supported
|
||||
// by Xlib.
|
||||
unsafe { (xconn.xlib.XSetLocaleModifiers)(locale_modifiers.as_ptr()) };
|
||||
|
||||
let im = unsafe {
|
||||
(xconn.xlib.XOpenIM)(xconn.display, ptr::null_mut(), ptr::null_mut(), ptr::null_mut())
|
||||
};
|
||||
|
||||
if im.is_null() {
|
||||
None
|
||||
} else {
|
||||
Some(im)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct InputMethod {
|
||||
pub im: ffi::XIM,
|
||||
pub preedit_style: Style,
|
||||
pub none_style: Style,
|
||||
_name: String,
|
||||
}
|
||||
|
||||
impl InputMethod {
|
||||
fn new(xconn: &Arc<XConnection>, im: ffi::XIM, name: String) -> Option<Self> {
|
||||
let mut styles: *mut XIMStyles = std::ptr::null_mut();
|
||||
|
||||
// Query the styles supported by the XIM.
|
||||
unsafe {
|
||||
if !(xconn.xlib.XGetIMValues)(
|
||||
im,
|
||||
ffi::XNQueryInputStyle_0.as_ptr() as *const _,
|
||||
(&mut styles) as *mut _,
|
||||
std::ptr::null_mut::<()>(),
|
||||
)
|
||||
.is_null()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
let mut preedit_style = None;
|
||||
let mut none_style = None;
|
||||
|
||||
unsafe {
|
||||
std::slice::from_raw_parts((*styles).supported_styles, (*styles).count_styles as _)
|
||||
.iter()
|
||||
.for_each(|style| match *style {
|
||||
XIM_PREEDIT_STYLE => {
|
||||
preedit_style = Some(Style::Preedit(*style));
|
||||
},
|
||||
XIM_NOTHING_STYLE if preedit_style.is_none() => {
|
||||
preedit_style = Some(Style::Nothing(*style))
|
||||
},
|
||||
XIM_NONE_STYLE => none_style = Some(Style::None(*style)),
|
||||
_ => (),
|
||||
});
|
||||
|
||||
(xconn.xlib.XFree)(styles.cast());
|
||||
};
|
||||
|
||||
if preedit_style.is_none() && none_style.is_none() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let preedit_style = preedit_style.unwrap_or_else(|| none_style.unwrap());
|
||||
let none_style = none_style.unwrap_or(preedit_style);
|
||||
|
||||
Some(InputMethod { im, _name: name, preedit_style, none_style })
|
||||
}
|
||||
}
|
||||
|
||||
const XIM_PREEDIT_STYLE: XIMStyle = (ffi::XIMPreeditCallbacks | ffi::XIMStatusNothing) as XIMStyle;
|
||||
const XIM_NOTHING_STYLE: XIMStyle = (ffi::XIMPreeditNothing | ffi::XIMStatusNothing) as XIMStyle;
|
||||
const XIM_NONE_STYLE: XIMStyle = (ffi::XIMPreeditNone | ffi::XIMStatusNone) as XIMStyle;
|
||||
|
||||
/// Style of the IME context.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Style {
|
||||
/// Preedit callbacks.
|
||||
Preedit(XIMStyle),
|
||||
|
||||
/// Nothing.
|
||||
Nothing(XIMStyle),
|
||||
|
||||
/// No IME.
|
||||
None(XIMStyle),
|
||||
}
|
||||
|
||||
impl Default for Style {
|
||||
fn default() -> Self {
|
||||
Style::None(XIM_NONE_STYLE)
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug)]
|
||||
struct XIMStyles {
|
||||
count_styles: c_ushort,
|
||||
supported_styles: *const XIMStyle,
|
||||
}
|
||||
|
||||
pub(crate) type XIMStyle = c_ulong;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum InputMethodResult {
|
||||
/// Input method used locale modifier from `XMODIFIERS` environment variable.
|
||||
XModifiers(InputMethod),
|
||||
/// Input method used internal fallback locale modifier.
|
||||
Fallback(InputMethod),
|
||||
/// Input method could not be opened using any locale modifier tried.
|
||||
Failure,
|
||||
}
|
||||
|
||||
impl InputMethodResult {
|
||||
pub fn is_fallback(&self) -> bool {
|
||||
matches!(self, InputMethodResult::Fallback(_))
|
||||
}
|
||||
|
||||
pub fn ok(self) -> Option<InputMethod> {
|
||||
use self::InputMethodResult::*;
|
||||
match self {
|
||||
XModifiers(im) | Fallback(im) => Some(im),
|
||||
Failure => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum GetXimServersError {
|
||||
XError(#[allow(dead_code)] XError),
|
||||
GetPropertyError(#[allow(dead_code)] util::GetPropertyError),
|
||||
InvalidUtf8(#[allow(dead_code)] IntoStringError),
|
||||
}
|
||||
|
||||
impl From<util::GetPropertyError> for GetXimServersError {
|
||||
fn from(error: util::GetPropertyError) -> Self {
|
||||
GetXimServersError::GetPropertyError(error)
|
||||
}
|
||||
}
|
||||
|
||||
// The root window has a property named XIM_SERVERS, which contains a list of atoms representing
|
||||
// the available XIM servers. For instance, if you're using ibus, it would contain an atom named
|
||||
// "@server=ibus". It's possible for this property to contain multiple atoms, though presumably
|
||||
// rare. Note that we replace "@server=" with "@im=" in order to match the format of locale
|
||||
// modifiers, since we don't want a user who's looking at logs to ask "am I supposed to set
|
||||
// XMODIFIERS to `@server=ibus`?!?"
|
||||
unsafe fn get_xim_servers(xconn: &Arc<XConnection>) -> Result<Vec<String>, GetXimServersError> {
|
||||
let atoms = xconn.atoms();
|
||||
let servers_atom = atoms[XIM_SERVERS];
|
||||
|
||||
let root = unsafe { (xconn.xlib.XDefaultRootWindow)(xconn.display) };
|
||||
|
||||
let mut atoms: Vec<ffi::Atom> = xconn
|
||||
.get_property::<xproto::Atom>(
|
||||
root as xproto::Window,
|
||||
servers_atom,
|
||||
xproto::Atom::from(xproto::AtomEnum::ATOM),
|
||||
)
|
||||
.map_err(GetXimServersError::GetPropertyError)?
|
||||
.into_iter()
|
||||
.map(|atom| atom as _)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut names: Vec<*const c_char> = Vec::with_capacity(atoms.len());
|
||||
unsafe {
|
||||
(xconn.xlib.XGetAtomNames)(
|
||||
xconn.display,
|
||||
atoms.as_mut_ptr(),
|
||||
atoms.len() as _,
|
||||
names.as_mut_ptr() as _,
|
||||
)
|
||||
};
|
||||
unsafe { names.set_len(atoms.len()) };
|
||||
|
||||
let mut formatted_names = Vec::with_capacity(names.len());
|
||||
for name in names {
|
||||
let string = unsafe { CStr::from_ptr(name) }
|
||||
.to_owned()
|
||||
.into_string()
|
||||
.map_err(GetXimServersError::InvalidUtf8)?;
|
||||
unsafe { (xconn.xlib.XFree)(name as _) };
|
||||
formatted_names.push(string.replace("@server=", "@im="));
|
||||
}
|
||||
xconn.check_errors().map_err(GetXimServersError::XError)?;
|
||||
Ok(formatted_names)
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct InputMethodName {
|
||||
c_string: CString,
|
||||
string: String,
|
||||
}
|
||||
|
||||
impl InputMethodName {
|
||||
pub fn from_string(string: String) -> Self {
|
||||
let c_string = CString::new(string.clone())
|
||||
.expect("String used to construct CString contained null byte");
|
||||
InputMethodName { c_string, string }
|
||||
}
|
||||
|
||||
pub fn from_str(string: &str) -> Self {
|
||||
let c_string =
|
||||
CString::new(string).expect("String used to construct CString contained null byte");
|
||||
InputMethodName { c_string, string: string.to_owned() }
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for InputMethodName {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
self.string.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct PotentialInputMethod {
|
||||
name: InputMethodName,
|
||||
successful: Option<bool>,
|
||||
}
|
||||
|
||||
impl PotentialInputMethod {
|
||||
pub fn from_string(string: String) -> Self {
|
||||
PotentialInputMethod { name: InputMethodName::from_string(string), successful: None }
|
||||
}
|
||||
|
||||
pub fn from_str(string: &str) -> Self {
|
||||
PotentialInputMethod { name: InputMethodName::from_str(string), successful: None }
|
||||
}
|
||||
|
||||
pub fn reset(&mut self) {
|
||||
self.successful = None;
|
||||
}
|
||||
|
||||
pub fn open_im(&mut self, xconn: &Arc<XConnection>) -> Option<InputMethod> {
|
||||
let im = unsafe { open_im(xconn, &self.name.c_string) };
|
||||
self.successful = Some(im.is_some());
|
||||
im.and_then(|im| InputMethod::new(xconn, im, self.name.string.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
// By logging this struct, you get a sequential listing of every locale modifier tried, where it
|
||||
// came from, and if it succeeded.
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct PotentialInputMethods {
|
||||
// On correctly configured systems, the XMODIFIERS environment variable tells us everything we
|
||||
// need to know.
|
||||
xmodifiers: Option<PotentialInputMethod>,
|
||||
// We have some standard options at our disposal that should ostensibly always work. For users
|
||||
// who only need compose sequences, this ensures that the program launches without a hitch
|
||||
// For users who need more sophisticated IME features, this is more or less a silent failure.
|
||||
// Logging features should be added in the future to allow both audiences to be effectively
|
||||
// served.
|
||||
fallbacks: [PotentialInputMethod; 2],
|
||||
// For diagnostic purposes, we include the list of XIM servers that the server reports as
|
||||
// being available.
|
||||
_xim_servers: Result<Vec<String>, GetXimServersError>,
|
||||
}
|
||||
|
||||
impl PotentialInputMethods {
|
||||
pub fn new(xconn: &Arc<XConnection>) -> Self {
|
||||
let xmodifiers = env::var("XMODIFIERS").ok().map(PotentialInputMethod::from_string);
|
||||
PotentialInputMethods {
|
||||
// Since passing "" to XSetLocaleModifiers results in it defaulting to the value of
|
||||
// XMODIFIERS, it's worth noting what happens if XMODIFIERS is also "". If simply
|
||||
// running the program with `XMODIFIERS="" cargo run`, then assuming XMODIFIERS is
|
||||
// defined in the profile (or parent environment) then that parent XMODIFIERS is used.
|
||||
// If that XMODIFIERS value is also "" (i.e. if you ran `export XMODIFIERS=""`), then
|
||||
// XSetLocaleModifiers uses the default local input method. Note that defining
|
||||
// XMODIFIERS as "" is different from XMODIFIERS not being defined at all, since in
|
||||
// that case, we get `None` and end up skipping ahead to the next method.
|
||||
xmodifiers,
|
||||
fallbacks: [
|
||||
// This is a standard input method that supports compose sequences, which should
|
||||
// always be available. `@im=none` appears to mean the same thing.
|
||||
PotentialInputMethod::from_str("@im=local"),
|
||||
// This explicitly specifies to use the implementation-dependent default, though
|
||||
// that seems to be equivalent to just using the local input method.
|
||||
PotentialInputMethod::from_str("@im="),
|
||||
],
|
||||
// The XIM_SERVERS property can have surprising values. For instance, when I exited
|
||||
// ibus to run fcitx, it retained the value denoting ibus. Even more surprising is
|
||||
// that the fcitx input method could only be successfully opened using "@im=ibus".
|
||||
// Presumably due to this quirk, it's actually possible to alternate between ibus and
|
||||
// fcitx in a running application.
|
||||
_xim_servers: unsafe { get_xim_servers(xconn) },
|
||||
}
|
||||
}
|
||||
|
||||
// This resets the `successful` field of every potential input method, ensuring we have
|
||||
// accurate information when this struct is re-used by the destruction/instantiation callbacks.
|
||||
fn reset(&mut self) {
|
||||
if let Some(ref mut input_method) = self.xmodifiers {
|
||||
input_method.reset();
|
||||
}
|
||||
|
||||
for input_method in &mut self.fallbacks {
|
||||
input_method.reset();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn open_im(
|
||||
&mut self,
|
||||
xconn: &Arc<XConnection>,
|
||||
callback: Option<&dyn Fn()>,
|
||||
) -> InputMethodResult {
|
||||
use self::InputMethodResult::*;
|
||||
|
||||
self.reset();
|
||||
|
||||
if let Some(ref mut input_method) = self.xmodifiers {
|
||||
let im = input_method.open_im(xconn);
|
||||
if let Some(im) = im {
|
||||
return XModifiers(im);
|
||||
} else if let Some(ref callback) = callback {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
for input_method in &mut self.fallbacks {
|
||||
let im = input_method.open_im(xconn);
|
||||
if let Some(im) = im {
|
||||
return Fallback(im);
|
||||
}
|
||||
}
|
||||
|
||||
Failure
|
||||
}
|
||||
}
|
||||
241
winit-x11/src/ime/mod.rs
Normal file
241
winit-x11/src/ime/mod.rs
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
// Important: all XIM calls need to happen from the same thread!
|
||||
|
||||
mod callbacks;
|
||||
mod context;
|
||||
mod inner;
|
||||
mod input_method;
|
||||
|
||||
use std::fmt;
|
||||
use std::sync::mpsc::{Receiver, Sender};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use self::callbacks::*;
|
||||
use self::context::ImeContext;
|
||||
pub use self::context::ImeContextCreationError;
|
||||
use self::inner::{close_im, ImeInner};
|
||||
use self::input_method::PotentialInputMethods;
|
||||
use crate::xdisplay::{XConnection, XError};
|
||||
use crate::{ffi, util};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
pub enum ImeEvent {
|
||||
Enabled,
|
||||
Start,
|
||||
Update(String, usize),
|
||||
End,
|
||||
Disabled,
|
||||
}
|
||||
|
||||
pub type ImeReceiver = Receiver<ImeRequest>;
|
||||
pub type ImeSender = Sender<ImeRequest>;
|
||||
pub type ImeEventReceiver = Receiver<(ffi::Window, ImeEvent)>;
|
||||
pub type ImeEventSender = Sender<(ffi::Window, ImeEvent)>;
|
||||
|
||||
/// Request to control XIM handler from the window.
|
||||
pub enum ImeRequest {
|
||||
/// Set IME preedit area for given `window_id`.
|
||||
Area(ffi::Window, i16, i16, u16, u16),
|
||||
|
||||
/// Allow IME input for the given `window_id`.
|
||||
Allow(ffi::Window, bool),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum ImeCreationError {
|
||||
// Boxed to prevent large error type
|
||||
OpenFailure(Box<PotentialInputMethods>),
|
||||
SetDestroyCallbackFailed(#[allow(dead_code)] XError),
|
||||
}
|
||||
|
||||
pub(crate) struct Ime {
|
||||
xconn: Arc<XConnection>,
|
||||
// The actual meat of this struct is boxed away, since it needs to have a fixed location in
|
||||
// memory so we can pass a pointer to it around.
|
||||
inner: Box<ImeInner>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for Ime {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("Ime").finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl Ime {
|
||||
pub fn new(
|
||||
xconn: Arc<XConnection>,
|
||||
event_sender: ImeEventSender,
|
||||
) -> Result<Self, ImeCreationError> {
|
||||
let potential_input_methods = PotentialInputMethods::new(&xconn);
|
||||
|
||||
let (mut inner, client_data) = {
|
||||
let mut inner = Box::new(ImeInner::new(xconn, potential_input_methods, event_sender));
|
||||
let inner_ptr = Box::into_raw(inner);
|
||||
let client_data = inner_ptr as _;
|
||||
let destroy_callback =
|
||||
ffi::XIMCallback { client_data, callback: Some(xim_destroy_callback) };
|
||||
inner = unsafe { Box::from_raw(inner_ptr) };
|
||||
inner.destroy_callback = destroy_callback;
|
||||
(inner, client_data)
|
||||
};
|
||||
|
||||
let xconn = Arc::clone(&inner.xconn);
|
||||
|
||||
let input_method = inner.potential_input_methods.open_im(
|
||||
&xconn,
|
||||
Some(&|| {
|
||||
let _ = unsafe { set_instantiate_callback(&xconn, client_data) };
|
||||
}),
|
||||
);
|
||||
|
||||
let is_fallback = input_method.is_fallback();
|
||||
if let Some(input_method) = input_method.ok() {
|
||||
inner.is_fallback = is_fallback;
|
||||
unsafe {
|
||||
let result = set_destroy_callback(&xconn, input_method.im, &inner)
|
||||
.map_err(ImeCreationError::SetDestroyCallbackFailed);
|
||||
if result.is_err() {
|
||||
let _ = close_im(&xconn, input_method.im);
|
||||
}
|
||||
result?;
|
||||
}
|
||||
inner.im = Some(input_method);
|
||||
Ok(Ime { xconn, inner })
|
||||
} else {
|
||||
Err(ImeCreationError::OpenFailure(Box::new(inner.potential_input_methods)))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_destroyed(&self) -> bool {
|
||||
self.inner.is_destroyed
|
||||
}
|
||||
|
||||
// This pattern is used for various methods here:
|
||||
// Ok(_) indicates that nothing went wrong internally
|
||||
// Ok(true) indicates that the action was actually performed
|
||||
// Ok(false) indicates that the action is not presently applicable
|
||||
pub fn create_context(
|
||||
&mut self,
|
||||
window: ffi::Window,
|
||||
with_ime: bool,
|
||||
) -> Result<bool, ImeContextCreationError> {
|
||||
let context = if self.is_destroyed() {
|
||||
// Create empty entry in map, so that when IME is rebuilt, this window has a context.
|
||||
None
|
||||
} else {
|
||||
let im = self.inner.im.as_ref().unwrap();
|
||||
|
||||
let context = unsafe {
|
||||
ImeContext::new(
|
||||
&self.inner.xconn,
|
||||
im,
|
||||
window,
|
||||
None,
|
||||
self.inner.event_sender.clone(),
|
||||
with_ime,
|
||||
)?
|
||||
};
|
||||
|
||||
let event = if context.is_allowed() { ImeEvent::Enabled } else { ImeEvent::Disabled };
|
||||
self.inner.event_sender.send((window, event)).expect("Failed to send enabled event");
|
||||
|
||||
Some(context)
|
||||
};
|
||||
|
||||
self.inner.contexts.insert(window, context);
|
||||
Ok(!self.is_destroyed())
|
||||
}
|
||||
|
||||
pub fn get_context(&self, window: ffi::Window) -> Option<ffi::XIC> {
|
||||
if self.is_destroyed() {
|
||||
return None;
|
||||
}
|
||||
if let Some(Some(context)) = self.inner.contexts.get(&window) {
|
||||
Some(context.ic)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove_context(&mut self, window: ffi::Window) -> Result<bool, XError> {
|
||||
if let Some(Some(context)) = self.inner.contexts.remove(&window) {
|
||||
unsafe {
|
||||
self.inner.destroy_ic_if_necessary(context.ic)?;
|
||||
}
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn focus(&mut self, window: ffi::Window) -> Result<bool, XError> {
|
||||
if self.is_destroyed() {
|
||||
return Ok(false);
|
||||
}
|
||||
if let Some(&mut Some(ref mut context)) = self.inner.contexts.get_mut(&window) {
|
||||
context.focus(&self.xconn).map(|_| true)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unfocus(&mut self, window: ffi::Window) -> Result<bool, XError> {
|
||||
if self.is_destroyed() {
|
||||
return Ok(false);
|
||||
}
|
||||
if let Some(&mut Some(ref mut context)) = self.inner.contexts.get_mut(&window) {
|
||||
context.unfocus(&self.xconn).map(|_| true)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_xim_area(&mut self, window: ffi::Window, x: i16, y: i16, w: u16, h: u16) {
|
||||
if self.is_destroyed() {
|
||||
return;
|
||||
}
|
||||
if let Some(&mut Some(ref mut context)) = self.inner.contexts.get_mut(&window) {
|
||||
context.set_area(&self.xconn, x as _, y as _, w as _, h as _);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_ime_allowed(&mut self, window: ffi::Window, allowed: bool) {
|
||||
if self.is_destroyed() {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(&mut Some(ref mut context)) = self.inner.contexts.get_mut(&window) {
|
||||
if allowed == context.is_allowed() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove context for that window.
|
||||
let _ = self.remove_context(window);
|
||||
|
||||
// Create new context supporting IME input.
|
||||
let _ = self.create_context(window, allowed);
|
||||
}
|
||||
|
||||
pub fn is_ime_allowed(&self, window: ffi::Window) -> bool {
|
||||
if self.is_destroyed() {
|
||||
false
|
||||
} else if let Some(Some(context)) = self.inner.contexts.get(&window) {
|
||||
context.is_allowed()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Ime {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
let _ = self.inner.destroy_all_contexts_if_necessary();
|
||||
let _ = self.inner.close_im_if_necessary();
|
||||
}
|
||||
}
|
||||
}
|
||||
267
winit-x11/src/lib.rs
Normal file
267
winit-x11/src/lib.rs
Normal file
|
|
@ -0,0 +1,267 @@
|
|||
//! # X11
|
||||
|
||||
use dpi::Size;
|
||||
#[cfg(feature = "serde")]
|
||||
use serde::{Deserialize, Serialize};
|
||||
use winit_core::event_loop::ActiveEventLoop as CoreActiveEventLoop;
|
||||
use winit_core::window::{ActivationToken, PlatformWindowAttributes, Window as CoreWindow};
|
||||
|
||||
pub use crate::event_loop::{ActiveEventLoop, EventLoop};
|
||||
pub use crate::window::Window;
|
||||
|
||||
macro_rules! os_error {
|
||||
($error:expr) => {{
|
||||
winit_core::error::OsError::new(line!(), file!(), $error)
|
||||
}};
|
||||
}
|
||||
|
||||
mod activation;
|
||||
mod atoms;
|
||||
mod dnd;
|
||||
mod event_loop;
|
||||
mod event_processor;
|
||||
pub mod ffi;
|
||||
mod ime;
|
||||
mod monitor;
|
||||
mod util;
|
||||
mod window;
|
||||
mod xdisplay;
|
||||
mod xsettings;
|
||||
|
||||
/// X window type. Maps directly to
|
||||
/// [`_NET_WM_WINDOW_TYPE`](https://specifications.freedesktop.org/wm-spec/wm-spec-1.5.html).
|
||||
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
pub enum WindowType {
|
||||
/// A desktop feature. This can include a single window containing desktop icons with the same
|
||||
/// dimensions as the screen, allowing the desktop environment to have full control of the
|
||||
/// desktop, without the need for proxying root window clicks.
|
||||
Desktop,
|
||||
/// A dock or panel feature. Typically a Window Manager would keep such windows on top of all
|
||||
/// other windows.
|
||||
Dock,
|
||||
/// Toolbar windows. "Torn off" from the main application.
|
||||
Toolbar,
|
||||
/// Pinnable menu windows. "Torn off" from the main application.
|
||||
Menu,
|
||||
/// A small persistent utility window, such as a palette or toolbox.
|
||||
Utility,
|
||||
/// The window is a splash screen displayed as an application is starting up.
|
||||
Splash,
|
||||
/// This is a dialog window.
|
||||
Dialog,
|
||||
/// A dropdown menu that usually appears when the user clicks on an item in a menu bar.
|
||||
/// This property is typically used on override-redirect windows.
|
||||
DropdownMenu,
|
||||
/// A popup menu that usually appears when the user right clicks on an object.
|
||||
/// This property is typically used on override-redirect windows.
|
||||
PopupMenu,
|
||||
/// A tooltip window. Usually used to show additional information when hovering over an object
|
||||
/// with the cursor. This property is typically used on override-redirect windows.
|
||||
Tooltip,
|
||||
/// The window is a notification.
|
||||
/// This property is typically used on override-redirect windows.
|
||||
Notification,
|
||||
/// This should be used on the windows that are popped up by combo boxes.
|
||||
/// This property is typically used on override-redirect windows.
|
||||
Combo,
|
||||
/// This indicates the window is being dragged.
|
||||
/// This property is typically used on override-redirect windows.
|
||||
Dnd,
|
||||
/// This is a normal, top-level window.
|
||||
#[default]
|
||||
Normal,
|
||||
}
|
||||
|
||||
/// The first argument in the provided hook will be the pointer to `XDisplay`
|
||||
/// and the second one the pointer to [`XErrorEvent`]. The returned `bool` is an
|
||||
/// indicator whether the error was handled by the callback.
|
||||
///
|
||||
/// [`XErrorEvent`]: https://linux.die.net/man/3/xerrorevent
|
||||
pub type XlibErrorHook =
|
||||
Box<dyn Fn(*mut std::ffi::c_void, *mut std::ffi::c_void) -> bool + Send + Sync>;
|
||||
|
||||
/// A unique identifier for an X11 visual.
|
||||
pub type XVisualID = u32;
|
||||
|
||||
/// A unique identifier for an X11 window.
|
||||
pub type XWindow = u32;
|
||||
|
||||
/// Hook to winit's xlib error handling callback.
|
||||
///
|
||||
/// This method is provided as a safe way to handle the errors coming from X11
|
||||
/// when using xlib in external crates, like glutin for GLX access. Trying to
|
||||
/// handle errors by speculating with `XSetErrorHandler` is [`unsafe`].
|
||||
///
|
||||
/// **Be aware that your hook is always invoked and returning `true` from it will
|
||||
/// prevent `winit` from getting the error itself. It's wise to always return
|
||||
/// `false` if you're not initiated the `Sync`.**
|
||||
///
|
||||
/// [`unsafe`]: https://www.remlab.net/op/xlib.shtml
|
||||
#[inline]
|
||||
pub fn register_xlib_error_hook(hook: XlibErrorHook) {
|
||||
// Append new hook.
|
||||
crate::event_loop::XLIB_ERROR_HOOKS.lock().unwrap().push(hook);
|
||||
}
|
||||
|
||||
/// Additional methods on [`ActiveEventLoop`] that are specific to X11.
|
||||
///
|
||||
/// [`ActiveEventLoop`]: winit_core::event_loop::ActiveEventLoop
|
||||
pub trait ActiveEventLoopExtX11 {
|
||||
/// True if the event loop uses X11.
|
||||
fn is_x11(&self) -> bool;
|
||||
}
|
||||
|
||||
impl ActiveEventLoopExtX11 for dyn CoreActiveEventLoop + '_ {
|
||||
#[inline]
|
||||
fn is_x11(&self) -> bool {
|
||||
self.cast_ref::<ActiveEventLoop>().is_some()
|
||||
}
|
||||
}
|
||||
|
||||
/// Additional methods on [`EventLoop`] that are specific to X11.
|
||||
pub trait EventLoopExtX11 {
|
||||
/// True if the [`EventLoop`] uses X11.
|
||||
fn is_x11(&self) -> bool;
|
||||
}
|
||||
|
||||
/// Additional methods on [`EventLoopBuilder`] that are specific to X11.
|
||||
pub trait EventLoopBuilderExtX11 {
|
||||
/// Force using X11.
|
||||
fn with_x11(&mut self) -> &mut Self;
|
||||
|
||||
/// Whether to allow the event loop to be created off of the main thread.
|
||||
///
|
||||
/// By default, the window is only allowed to be created on the main
|
||||
/// thread, to make platform compatibility easier.
|
||||
fn with_any_thread(&mut self, any_thread: bool) -> &mut Self;
|
||||
}
|
||||
|
||||
/// Additional methods on [`Window`] that are specific to X11.
|
||||
///
|
||||
/// [`Window`]: crate::window::Window
|
||||
pub trait WindowExtX11 {}
|
||||
|
||||
impl WindowExtX11 for dyn CoreWindow {}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) struct ApplicationName {
|
||||
pub(crate) general: String,
|
||||
pub(crate) instance: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct WindowAttributesX11 {
|
||||
pub(crate) name: Option<ApplicationName>,
|
||||
pub(crate) activation_token: Option<ActivationToken>,
|
||||
pub(crate) visual_id: Option<XVisualID>,
|
||||
pub(crate) screen_id: Option<i32>,
|
||||
pub(crate) base_size: Option<Size>,
|
||||
pub(crate) override_redirect: bool,
|
||||
pub(crate) x11_window_types: Vec<WindowType>,
|
||||
|
||||
/// The parent window to embed this window into.
|
||||
pub(crate) embed_window: Option<XWindow>,
|
||||
}
|
||||
|
||||
impl Default for WindowAttributesX11 {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
name: None,
|
||||
activation_token: None,
|
||||
visual_id: None,
|
||||
screen_id: None,
|
||||
base_size: None,
|
||||
override_redirect: false,
|
||||
x11_window_types: vec![WindowType::Normal],
|
||||
embed_window: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WindowAttributesX11 {
|
||||
/// Create this window with a specific X11 visual.
|
||||
pub fn with_x11_visual(mut self, visual_id: XVisualID) -> Self {
|
||||
self.visual_id = Some(visual_id);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_x11_screen(mut self, screen_id: i32) -> Self {
|
||||
self.screen_id = Some(screen_id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Build window with the given `general` and `instance` names.
|
||||
///
|
||||
/// The `general` sets general class of `WM_CLASS(STRING)`, while `instance` set the
|
||||
/// instance part of it. The resulted property looks like `WM_CLASS(STRING) = "instance",
|
||||
/// "general"`.
|
||||
///
|
||||
/// For details about application ID conventions, see the
|
||||
/// [Desktop Entry Spec](https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html#desktop-file-id)
|
||||
pub fn with_name(mut self, general: impl Into<String>, instance: impl Into<String>) -> Self {
|
||||
self.name = Some(ApplicationName { general: general.into(), instance: instance.into() });
|
||||
self
|
||||
}
|
||||
|
||||
/// Build window with override-redirect flag; defaults to false.
|
||||
pub fn with_override_redirect(mut self, override_redirect: bool) -> Self {
|
||||
self.override_redirect = override_redirect;
|
||||
self
|
||||
}
|
||||
|
||||
/// Build window with `_NET_WM_WINDOW_TYPE` hints; defaults to `Normal`.
|
||||
pub fn with_x11_window_type(mut self, x11_window_types: Vec<WindowType>) -> Self {
|
||||
self.x11_window_types = x11_window_types;
|
||||
self
|
||||
}
|
||||
|
||||
/// Build window with base size hint.
|
||||
///
|
||||
/// ```
|
||||
/// # use winit::dpi::{LogicalSize, PhysicalSize};
|
||||
/// # use winit::window::{Window, WindowAttributes};
|
||||
/// # use winit::platform::x11::WindowAttributesX11;
|
||||
/// // Specify the size in logical dimensions like this:
|
||||
/// WindowAttributesX11::default().with_base_size(LogicalSize::new(400.0, 200.0));
|
||||
///
|
||||
/// // Or specify the size in physical dimensions like this:
|
||||
/// WindowAttributesX11::default().with_base_size(PhysicalSize::new(400, 200));
|
||||
/// ```
|
||||
pub fn with_base_size<S: Into<Size>>(mut self, base_size: S) -> Self {
|
||||
self.base_size = Some(base_size.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Embed this window into another parent window.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```no_run
|
||||
/// use winit::window::{Window, WindowAttributes};
|
||||
/// use winit::event_loop::ActiveEventLoop;
|
||||
/// use winit::platform::x11::{XWindow, WindowAttributesX11};
|
||||
/// # fn create_window(event_loop: &dyn ActiveEventLoop) -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// let parent_window_id = std::env::args().nth(1).unwrap().parse::<XWindow>()?;
|
||||
/// let window_x11_attributes = WindowAttributesX11::default().with_embed_parent_window(parent_window_id);
|
||||
/// let window_attributes = WindowAttributes::default().with_platform_attributes(Box::new(window_x11_attributes));
|
||||
/// let window = event_loop.create_window(window_attributes)?;
|
||||
/// # Ok(()) }
|
||||
/// ```
|
||||
pub fn with_embed_parent_window(mut self, parent_window_id: XWindow) -> Self {
|
||||
self.embed_window = Some(parent_window_id);
|
||||
self
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn with_activation_token(mut self, token: ActivationToken) -> Self {
|
||||
self.activation_token = Some(token);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl PlatformWindowAttributes for WindowAttributesX11 {
|
||||
fn box_clone(&self) -> Box<dyn PlatformWindowAttributes> {
|
||||
Box::from(self.clone())
|
||||
}
|
||||
}
|
||||
315
winit-x11/src/monitor.rs
Normal file
315
winit-x11/src/monitor.rs
Normal file
|
|
@ -0,0 +1,315 @@
|
|||
use std::num::NonZeroU32;
|
||||
|
||||
use dpi::PhysicalPosition;
|
||||
use winit_core::monitor::{MonitorHandleProvider, VideoMode};
|
||||
use x11rb::connection::RequestConnection;
|
||||
use x11rb::protocol::randr::{self, ConnectionExt as _};
|
||||
use x11rb::protocol::xproto;
|
||||
|
||||
use crate::event_loop::X11Error;
|
||||
use crate::util;
|
||||
use crate::xdisplay::XConnection;
|
||||
|
||||
// Used for testing. This should always be committed as false.
|
||||
const DISABLE_MONITOR_LIST_CACHING: bool = false;
|
||||
|
||||
impl XConnection {
|
||||
pub fn invalidate_cached_monitor_list(&self) -> Option<Vec<MonitorHandle>> {
|
||||
// We update this lazily.
|
||||
self.monitor_handles.lock().unwrap().take()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct VideoModeHandle {
|
||||
pub(crate) current: bool,
|
||||
pub(crate) mode: VideoMode,
|
||||
pub(crate) native_mode: randr::Mode,
|
||||
}
|
||||
|
||||
impl From<VideoModeHandle> for VideoMode {
|
||||
fn from(handle: VideoModeHandle) -> Self {
|
||||
handle.mode
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MonitorHandle {
|
||||
/// The actual id
|
||||
pub(crate) id: randr::Crtc,
|
||||
/// The name of the monitor
|
||||
pub(crate) name: String,
|
||||
/// The position of the monitor in the X screen
|
||||
pub(crate) position: (i32, i32),
|
||||
/// If the monitor is the primary one
|
||||
primary: bool,
|
||||
/// The DPI scale factor
|
||||
pub(crate) scale_factor: f64,
|
||||
/// Used to determine which windows are on this monitor
|
||||
pub(crate) rect: util::AaRect,
|
||||
/// Supported video modes on this monitor
|
||||
pub(crate) video_modes: Vec<VideoModeHandle>,
|
||||
}
|
||||
|
||||
impl MonitorHandleProvider for MonitorHandle {
|
||||
fn id(&self) -> u128 {
|
||||
self.native_id() as _
|
||||
}
|
||||
|
||||
fn native_id(&self) -> u64 {
|
||||
self.id as _
|
||||
}
|
||||
|
||||
fn name(&self) -> Option<std::borrow::Cow<'_, str>> {
|
||||
Some(self.name.as_str().into())
|
||||
}
|
||||
|
||||
fn position(&self) -> Option<PhysicalPosition<i32>> {
|
||||
Some(self.position.into())
|
||||
}
|
||||
|
||||
fn scale_factor(&self) -> f64 {
|
||||
self.scale_factor
|
||||
}
|
||||
|
||||
fn current_video_mode(&self) -> Option<VideoMode> {
|
||||
self.video_modes.iter().find_map(|mode| mode.current.then(|| mode.clone().into()))
|
||||
}
|
||||
|
||||
fn video_modes(&self) -> Box<dyn Iterator<Item = VideoMode>> {
|
||||
Box::new(self.video_modes.clone().into_iter().map(|mode| mode.into()))
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for MonitorHandle {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.id == other.id
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for MonitorHandle {}
|
||||
|
||||
impl PartialOrd for MonitorHandle {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for MonitorHandle {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
self.id.cmp(&other.id)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::hash::Hash for MonitorHandle {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.id.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn mode_refresh_rate_millihertz(mode: &randr::ModeInfo) -> Option<NonZeroU32> {
|
||||
if mode.dot_clock > 0 && mode.htotal > 0 && mode.vtotal > 0 {
|
||||
#[allow(clippy::unnecessary_cast)]
|
||||
NonZeroU32::new(
|
||||
(mode.dot_clock as u64 * 1000 / (mode.htotal as u64 * mode.vtotal as u64)) as u32,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl MonitorHandle {
|
||||
fn new(
|
||||
xconn: &XConnection,
|
||||
resources: &ScreenResources,
|
||||
id: randr::Crtc,
|
||||
crtc: &randr::GetCrtcInfoReply,
|
||||
primary: bool,
|
||||
) -> Option<Self> {
|
||||
let (name, scale_factor, video_modes) = xconn.get_output_info(resources, crtc)?;
|
||||
let dimensions = (crtc.width as u32, crtc.height as u32);
|
||||
let position = (crtc.x as i32, crtc.y as i32);
|
||||
|
||||
let rect = util::AaRect::new(position, dimensions);
|
||||
|
||||
Some(MonitorHandle { id, name, scale_factor, position, primary, rect, video_modes })
|
||||
}
|
||||
|
||||
pub fn dummy() -> Self {
|
||||
MonitorHandle {
|
||||
id: 0,
|
||||
name: "<dummy monitor>".into(),
|
||||
scale_factor: 1.0,
|
||||
position: (0, 0),
|
||||
primary: true,
|
||||
rect: util::AaRect::new((0, 0), (1, 1)),
|
||||
video_modes: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn is_dummy(&self) -> bool {
|
||||
// Zero is an invalid XID value; no real monitor will have it
|
||||
self.id == 0
|
||||
}
|
||||
}
|
||||
|
||||
impl XConnection {
|
||||
pub fn get_monitor_for_window(
|
||||
&self,
|
||||
window_rect: Option<util::AaRect>,
|
||||
) -> Result<MonitorHandle, X11Error> {
|
||||
let monitors = self.available_monitors()?;
|
||||
|
||||
if monitors.is_empty() {
|
||||
// Return a dummy monitor to avoid panicking
|
||||
return Ok(MonitorHandle::dummy());
|
||||
}
|
||||
|
||||
let default = monitors.first().unwrap();
|
||||
|
||||
let window_rect = match window_rect {
|
||||
Some(rect) => rect,
|
||||
None => return Ok(default.to_owned()),
|
||||
};
|
||||
|
||||
let mut largest_overlap = 0;
|
||||
let mut matched_monitor = default;
|
||||
for monitor in &monitors {
|
||||
let overlapping_area = window_rect.get_overlapping_area(&monitor.rect);
|
||||
if overlapping_area > largest_overlap {
|
||||
largest_overlap = overlapping_area;
|
||||
matched_monitor = monitor;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(matched_monitor.to_owned())
|
||||
}
|
||||
|
||||
fn query_monitor_list(&self) -> Result<Vec<MonitorHandle>, X11Error> {
|
||||
let root = self.default_root();
|
||||
let resources =
|
||||
ScreenResources::from_connection(self.xcb_connection(), root, self.randr_version())?;
|
||||
|
||||
// Pipeline all of the get-crtc requests.
|
||||
let mut crtc_cookies = Vec::with_capacity(resources.crtcs().len());
|
||||
for &crtc in resources.crtcs() {
|
||||
crtc_cookies
|
||||
.push(self.xcb_connection().randr_get_crtc_info(crtc, x11rb::CURRENT_TIME)?);
|
||||
}
|
||||
|
||||
// Do this here so we do all of our requests in one shot.
|
||||
let primary = self.xcb_connection().randr_get_output_primary(root.root)?.reply()?.output;
|
||||
|
||||
let mut crtc_infos = Vec::with_capacity(crtc_cookies.len());
|
||||
for cookie in crtc_cookies {
|
||||
let reply = cookie.reply()?;
|
||||
crtc_infos.push(reply);
|
||||
}
|
||||
|
||||
let mut has_primary = false;
|
||||
let mut available_monitors = Vec::with_capacity(resources.crtcs().len());
|
||||
for (crtc_id, crtc) in resources.crtcs().iter().zip(crtc_infos.iter()) {
|
||||
if crtc.width == 0 || crtc.height == 0 || crtc.outputs.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let is_primary = crtc.outputs[0] == primary;
|
||||
has_primary |= is_primary;
|
||||
let monitor = MonitorHandle::new(self, &resources, *crtc_id, crtc, is_primary);
|
||||
available_monitors.extend(monitor);
|
||||
}
|
||||
|
||||
// If we don't have a primary monitor, just pick one ourselves!
|
||||
if !has_primary {
|
||||
if let Some(ref mut fallback) = available_monitors.first_mut() {
|
||||
// Setting this here will come in handy if we ever add an `is_primary` method.
|
||||
fallback.primary = true;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(available_monitors)
|
||||
}
|
||||
|
||||
pub fn available_monitors(&self) -> Result<Vec<MonitorHandle>, X11Error> {
|
||||
let mut monitors_lock = self.monitor_handles.lock().unwrap();
|
||||
match *monitors_lock {
|
||||
Some(ref monitors) => Ok(monitors.clone()),
|
||||
None => {
|
||||
let monitors = self.query_monitor_list()?;
|
||||
if !DISABLE_MONITOR_LIST_CACHING {
|
||||
*monitors_lock = Some(monitors.clone());
|
||||
}
|
||||
Ok(monitors)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn primary_monitor(&self) -> Result<MonitorHandle, X11Error> {
|
||||
Ok(self
|
||||
.available_monitors()?
|
||||
.into_iter()
|
||||
.find(|monitor| monitor.primary)
|
||||
.unwrap_or_else(MonitorHandle::dummy))
|
||||
}
|
||||
|
||||
pub fn select_xrandr_input(&self, root: xproto::Window) -> Result<u8, X11Error> {
|
||||
use randr::NotifyMask;
|
||||
|
||||
// Get extension info.
|
||||
let info = self
|
||||
.xcb_connection()
|
||||
.extension_information(randr::X11_EXTENSION_NAME)?
|
||||
.ok_or(X11Error::MissingExtension(randr::X11_EXTENSION_NAME))?;
|
||||
|
||||
// Select input data.
|
||||
let event_mask =
|
||||
NotifyMask::CRTC_CHANGE | NotifyMask::OUTPUT_PROPERTY | NotifyMask::SCREEN_CHANGE;
|
||||
self.xcb_connection().randr_select_input(root, event_mask)?;
|
||||
|
||||
Ok(info.first_event)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ScreenResources {
|
||||
/// List of attached modes.
|
||||
modes: Vec<randr::ModeInfo>,
|
||||
|
||||
/// List of attached CRTCs.
|
||||
crtcs: Vec<randr::Crtc>,
|
||||
}
|
||||
|
||||
impl ScreenResources {
|
||||
pub(crate) fn modes(&self) -> &[randr::ModeInfo] {
|
||||
&self.modes
|
||||
}
|
||||
|
||||
pub(crate) fn crtcs(&self) -> &[randr::Crtc] {
|
||||
&self.crtcs
|
||||
}
|
||||
|
||||
pub(crate) fn from_connection(
|
||||
conn: &impl x11rb::connection::Connection,
|
||||
root: &x11rb::protocol::xproto::Screen,
|
||||
(major_version, minor_version): (u32, u32),
|
||||
) -> Result<Self, X11Error> {
|
||||
if (major_version == 1 && minor_version >= 3) || major_version > 1 {
|
||||
let reply = conn.randr_get_screen_resources_current(root.root)?.reply()?;
|
||||
Ok(Self::from_get_screen_resources_current_reply(reply))
|
||||
} else {
|
||||
let reply = conn.randr_get_screen_resources(root.root)?.reply()?;
|
||||
Ok(Self::from_get_screen_resources_reply(reply))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn from_get_screen_resources_reply(reply: randr::GetScreenResourcesReply) -> Self {
|
||||
Self { modes: reply.modes, crtcs: reply.crtcs }
|
||||
}
|
||||
|
||||
pub(crate) fn from_get_screen_resources_current_reply(
|
||||
reply: randr::GetScreenResourcesCurrentReply,
|
||||
) -> Self {
|
||||
Self { modes: reply.modes, crtcs: reply.crtcs }
|
||||
}
|
||||
}
|
||||
1
winit-x11/src/tests/xsettings.dat
Normal file
1
winit-x11/src/tests/xsettings.dat
Normal file
File diff suppressed because one or more lines are too long
32
winit-x11/src/util/client_msg.rs
Normal file
32
winit-x11/src/util/client_msg.rs
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
use x11rb::x11_utils::Serialize;
|
||||
|
||||
use super::*;
|
||||
|
||||
impl XConnection {
|
||||
pub fn send_client_msg(
|
||||
&self,
|
||||
window: xproto::Window, // The window this is "about"; not necessarily this window
|
||||
target_window: xproto::Window, // The window we're sending to
|
||||
message_type: xproto::Atom,
|
||||
event_mask: Option<xproto::EventMask>,
|
||||
data: impl Into<xproto::ClientMessageData>,
|
||||
) -> Result<VoidCookie<'_>, X11Error> {
|
||||
let event = xproto::ClientMessageEvent {
|
||||
response_type: xproto::CLIENT_MESSAGE_EVENT,
|
||||
window,
|
||||
format: 32,
|
||||
data: data.into(),
|
||||
sequence: 0,
|
||||
type_: message_type,
|
||||
};
|
||||
|
||||
self.xcb_connection()
|
||||
.send_event(
|
||||
false,
|
||||
target_window,
|
||||
event_mask.unwrap_or(xproto::EventMask::NO_EVENT),
|
||||
event.serialize(),
|
||||
)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
55
winit-x11/src/util/cookie.rs
Normal file
55
winit-x11/src/util/cookie.rs
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
use std::ffi::c_int;
|
||||
use std::sync::Arc;
|
||||
|
||||
use x11_dl::xlib::{self, XEvent, XGenericEventCookie};
|
||||
|
||||
use crate::xdisplay::XConnection;
|
||||
|
||||
/// XEvents of type GenericEvent store their actual data in an XGenericEventCookie data structure.
|
||||
/// This is a wrapper to extract the cookie from a GenericEvent XEvent and release the cookie data
|
||||
/// once it has been processed
|
||||
pub struct GenericEventCookie {
|
||||
cookie: XGenericEventCookie,
|
||||
xconn: Arc<XConnection>,
|
||||
}
|
||||
|
||||
impl GenericEventCookie {
|
||||
pub fn from_event(xconn: Arc<XConnection>, event: XEvent) -> Option<GenericEventCookie> {
|
||||
unsafe {
|
||||
let mut cookie: XGenericEventCookie = From::from(event);
|
||||
if (xconn.xlib.XGetEventData)(xconn.display, &mut cookie) == xlib::True {
|
||||
Some(GenericEventCookie { cookie, xconn })
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn extension(&self) -> u8 {
|
||||
self.cookie.extension as u8
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn evtype(&self) -> c_int {
|
||||
self.cookie.evtype
|
||||
}
|
||||
|
||||
/// Borrow inner event data as `&T`.
|
||||
///
|
||||
/// ## SAFETY
|
||||
///
|
||||
/// The caller must ensure that the event has the `T` inside of it.
|
||||
#[inline]
|
||||
pub unsafe fn as_event<T>(&self) -> &T {
|
||||
unsafe { &*(self.cookie.data as *const _) }
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for GenericEventCookie {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
(self.xconn.xlib.XFreeEventData)(self.xconn.display, &mut self.cookie);
|
||||
}
|
||||
}
|
||||
}
|
||||
246
winit-x11/src/util/cursor.rs
Normal file
246
winit-x11/src/util/cursor.rs
Normal file
|
|
@ -0,0 +1,246 @@
|
|||
use std::collections::hash_map::Entry;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::iter;
|
||||
use std::sync::Arc;
|
||||
|
||||
use winit_core::cursor::{CursorIcon, CustomCursorProvider, CustomCursorSource};
|
||||
use winit_core::error::{NotSupportedError, RequestError};
|
||||
use x11rb::connection::Connection;
|
||||
use x11rb::protocol::render::{self, ConnectionExt as _};
|
||||
use x11rb::protocol::xproto;
|
||||
|
||||
use super::super::ActiveEventLoop;
|
||||
use super::*;
|
||||
|
||||
impl XConnection {
|
||||
pub fn set_cursor_icon(
|
||||
&self,
|
||||
window: xproto::Window,
|
||||
cursor: Option<CursorIcon>,
|
||||
) -> Result<(), X11Error> {
|
||||
let cursor = {
|
||||
let mut cache = self.cursor_cache.lock().unwrap_or_else(|e| e.into_inner());
|
||||
|
||||
match cache.entry(cursor) {
|
||||
Entry::Occupied(o) => *o.get(),
|
||||
Entry::Vacant(v) => *v.insert(self.get_cursor(cursor)?),
|
||||
}
|
||||
};
|
||||
|
||||
self.update_cursor(window, cursor)
|
||||
}
|
||||
|
||||
pub(crate) fn set_custom_cursor(
|
||||
&self,
|
||||
window: xproto::Window,
|
||||
cursor: &CustomCursor,
|
||||
) -> Result<(), X11Error> {
|
||||
self.update_cursor(window, cursor.cursor)
|
||||
}
|
||||
|
||||
/// Create a cursor from an image.
|
||||
fn create_cursor_from_image(
|
||||
&self,
|
||||
width: u16,
|
||||
height: u16,
|
||||
hotspot_x: u16,
|
||||
hotspot_y: u16,
|
||||
image: &[u8],
|
||||
) -> Result<xproto::Cursor, X11Error> {
|
||||
// Create a pixmap for the default root window.
|
||||
let root = self.default_root().root;
|
||||
let pixmap =
|
||||
xproto::PixmapWrapper::create_pixmap(self.xcb_connection(), 32, root, width, height)?;
|
||||
|
||||
// Create a GC to draw with.
|
||||
let gc = xproto::GcontextWrapper::create_gc(
|
||||
self.xcb_connection(),
|
||||
pixmap.pixmap(),
|
||||
&Default::default(),
|
||||
)?;
|
||||
|
||||
// Draw the data into it.
|
||||
self.xcb_connection()
|
||||
.put_image(
|
||||
xproto::ImageFormat::Z_PIXMAP,
|
||||
pixmap.pixmap(),
|
||||
gc.gcontext(),
|
||||
width,
|
||||
height,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
32,
|
||||
image,
|
||||
)?
|
||||
.ignore_error();
|
||||
drop(gc);
|
||||
|
||||
// Create the XRender picture.
|
||||
let picture = render::PictureWrapper::create_picture(
|
||||
self.xcb_connection(),
|
||||
pixmap.pixmap(),
|
||||
self.find_argb32_format()?,
|
||||
&Default::default(),
|
||||
)?;
|
||||
drop(pixmap);
|
||||
|
||||
// Create the cursor.
|
||||
let cursor = self.xcb_connection().generate_id()?;
|
||||
self.xcb_connection()
|
||||
.render_create_cursor(cursor, picture.picture(), hotspot_x, hotspot_y)?
|
||||
.check()?;
|
||||
|
||||
Ok(cursor)
|
||||
}
|
||||
|
||||
/// Find the render format that corresponds to ARGB32.
|
||||
fn find_argb32_format(&self) -> Result<render::Pictformat, X11Error> {
|
||||
macro_rules! direct {
|
||||
($format:expr, $shift_name:ident, $mask_name:ident, $shift:expr) => {{
|
||||
($format).direct.$shift_name == $shift && ($format).direct.$mask_name == 0xff
|
||||
}};
|
||||
}
|
||||
|
||||
self.render_formats()
|
||||
.formats
|
||||
.iter()
|
||||
.find(|format| {
|
||||
format.type_ == render::PictType::DIRECT
|
||||
&& format.depth == 32
|
||||
&& direct!(format, red_shift, red_mask, 16)
|
||||
&& direct!(format, green_shift, green_mask, 8)
|
||||
&& direct!(format, blue_shift, blue_mask, 0)
|
||||
&& direct!(format, alpha_shift, alpha_mask, 24)
|
||||
})
|
||||
.ok_or(X11Error::NoArgb32Format)
|
||||
.map(|format| format.id)
|
||||
}
|
||||
|
||||
fn create_empty_cursor(&self) -> Result<xproto::Cursor, X11Error> {
|
||||
self.create_cursor_from_image(1, 1, 0, 0, &[0, 0, 0, 0])
|
||||
}
|
||||
|
||||
fn get_cursor(&self, cursor: Option<CursorIcon>) -> Result<xproto::Cursor, X11Error> {
|
||||
let cursor = match cursor {
|
||||
Some(cursor) => cursor,
|
||||
None => return self.create_empty_cursor(),
|
||||
};
|
||||
|
||||
let database = self.database();
|
||||
let handle = x11rb::cursor::Handle::new(
|
||||
self.xcb_connection(),
|
||||
self.default_screen_index(),
|
||||
&database,
|
||||
)?
|
||||
.reply()?;
|
||||
|
||||
let mut last_error = None;
|
||||
for &name in iter::once(&cursor.name()).chain(cursor.alt_names().iter()) {
|
||||
match handle.load_cursor(self.xcb_connection(), name) {
|
||||
Ok(cursor) => return Ok(cursor),
|
||||
Err(err) => last_error = Some(err.into()),
|
||||
}
|
||||
}
|
||||
|
||||
Err(last_error.unwrap())
|
||||
}
|
||||
|
||||
fn update_cursor(
|
||||
&self,
|
||||
window: xproto::Window,
|
||||
cursor: xproto::Cursor,
|
||||
) -> Result<(), X11Error> {
|
||||
self.xcb_connection()
|
||||
.change_window_attributes(
|
||||
window,
|
||||
&xproto::ChangeWindowAttributesAux::new().cursor(cursor),
|
||||
)?
|
||||
.ignore_error();
|
||||
|
||||
self.xcb_connection().flush()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum SelectedCursor {
|
||||
Custom(CustomCursor),
|
||||
Named(CursorIcon),
|
||||
}
|
||||
|
||||
impl Default for SelectedCursor {
|
||||
fn default() -> Self {
|
||||
SelectedCursor::Named(Default::default())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CustomCursor {
|
||||
xconn: Arc<XConnection>,
|
||||
cursor: xproto::Cursor,
|
||||
}
|
||||
|
||||
impl Hash for CustomCursor {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.cursor.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for CustomCursor {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.cursor == other.cursor
|
||||
}
|
||||
}
|
||||
impl Eq for CustomCursor {}
|
||||
|
||||
impl CustomCursor {
|
||||
pub(crate) fn new(
|
||||
event_loop: &ActiveEventLoop,
|
||||
cursor: CustomCursorSource,
|
||||
) -> Result<CustomCursor, RequestError> {
|
||||
let mut cursor = match cursor {
|
||||
CustomCursorSource::Image(cursor_image) => cursor_image,
|
||||
CustomCursorSource::Animation { .. } | CustomCursorSource::Url { .. } => {
|
||||
return Err(NotSupportedError::new("unsupported cursor kind").into())
|
||||
},
|
||||
};
|
||||
|
||||
// Reverse RGBA order to BGRA.
|
||||
cursor.buffer_mut().chunks_mut(4).for_each(|chunk| {
|
||||
let chunk: &mut [u8; 4] = chunk.try_into().unwrap();
|
||||
chunk[0..3].reverse();
|
||||
|
||||
// Byteswap if we need to.
|
||||
if event_loop.xconn.needs_endian_swap() {
|
||||
let value = u32::from_ne_bytes(*chunk).swap_bytes();
|
||||
*chunk = value.to_ne_bytes();
|
||||
}
|
||||
});
|
||||
|
||||
let cursor = event_loop
|
||||
.xconn
|
||||
.create_cursor_from_image(
|
||||
cursor.width(),
|
||||
cursor.height(),
|
||||
cursor.hotspot_x(),
|
||||
cursor.hotspot_y(),
|
||||
cursor.buffer(),
|
||||
)
|
||||
.map_err(|err| os_error!(err))?;
|
||||
|
||||
Ok(Self { xconn: event_loop.xconn.clone(), cursor })
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for CustomCursor {
|
||||
fn drop(&mut self) {
|
||||
self.xconn.xcb_connection().free_cursor(self.cursor).map(|r| r.ignore_error()).ok();
|
||||
}
|
||||
}
|
||||
|
||||
impl CustomCursorProvider for CustomCursor {
|
||||
fn is_animated(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
283
winit-x11/src/util/geometry.rs
Normal file
283
winit-x11/src/util/geometry.rs
Normal file
|
|
@ -0,0 +1,283 @@
|
|||
use std::cmp;
|
||||
|
||||
use super::*;
|
||||
|
||||
// Friendly neighborhood axis-aligned rectangle
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct AaRect {
|
||||
x: i64,
|
||||
y: i64,
|
||||
width: i64,
|
||||
height: i64,
|
||||
}
|
||||
|
||||
impl AaRect {
|
||||
pub fn new((x, y): (i32, i32), (width, height): (u32, u32)) -> Self {
|
||||
let (x, y) = (x as i64, y as i64);
|
||||
let (width, height) = (width as i64, height as i64);
|
||||
AaRect { x, y, width, height }
|
||||
}
|
||||
|
||||
pub fn contains_point(&self, x: i64, y: i64) -> bool {
|
||||
x >= self.x && x <= self.x + self.width && y >= self.y && y <= self.y + self.height
|
||||
}
|
||||
|
||||
pub fn get_overlapping_area(&self, other: &Self) -> i64 {
|
||||
let x_overlap = cmp::max(
|
||||
0,
|
||||
cmp::min(self.x + self.width, other.x + other.width) - cmp::max(self.x, other.x),
|
||||
);
|
||||
let y_overlap = cmp::max(
|
||||
0,
|
||||
cmp::min(self.y + self.height, other.y + other.height) - cmp::max(self.y, other.y),
|
||||
);
|
||||
x_overlap * y_overlap
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FrameExtents {
|
||||
pub left: u32,
|
||||
pub right: u32,
|
||||
pub top: u32,
|
||||
pub bottom: u32,
|
||||
}
|
||||
|
||||
impl FrameExtents {
|
||||
pub fn new(left: u32, right: u32, top: u32, bottom: u32) -> Self {
|
||||
FrameExtents { left, right, top, bottom }
|
||||
}
|
||||
|
||||
pub fn from_border(border: u32) -> Self {
|
||||
Self::new(border, border, border, border)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum FrameExtentsHeuristicPath {
|
||||
Supported,
|
||||
UnsupportedNested,
|
||||
UnsupportedBordered,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FrameExtentsHeuristic {
|
||||
pub frame_extents: FrameExtents,
|
||||
pub heuristic_path: FrameExtentsHeuristicPath,
|
||||
}
|
||||
|
||||
impl FrameExtentsHeuristic {
|
||||
pub fn surface_position(&self) -> (i32, i32) {
|
||||
use self::FrameExtentsHeuristicPath::*;
|
||||
if self.heuristic_path != UnsupportedBordered {
|
||||
(self.frame_extents.left as i32, self.frame_extents.top as i32)
|
||||
} else {
|
||||
(0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn inner_pos_to_outer(&self, x: i32, y: i32) -> (i32, i32) {
|
||||
let (left, top) = self.surface_position();
|
||||
(x - left, y - top)
|
||||
}
|
||||
|
||||
pub fn surface_size_to_outer(&self, width: u32, height: u32) -> (u32, u32) {
|
||||
(
|
||||
width.saturating_add(
|
||||
self.frame_extents.left.saturating_add(self.frame_extents.right) as _
|
||||
),
|
||||
height.saturating_add(
|
||||
self.frame_extents.top.saturating_add(self.frame_extents.bottom) as _
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl XConnection {
|
||||
// This is adequate for inner_position
|
||||
pub fn translate_coords_root(
|
||||
&self,
|
||||
window: xproto::Window,
|
||||
root: xproto::Window,
|
||||
) -> Result<xproto::TranslateCoordinatesReply, X11Error> {
|
||||
self.xcb_connection().translate_coordinates(window, root, 0, 0)?.reply().map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn translate_coords(
|
||||
&self,
|
||||
src_w: xproto::Window,
|
||||
dst_w: xproto::Window,
|
||||
src_x: i16,
|
||||
src_y: i16,
|
||||
) -> Result<xproto::TranslateCoordinatesReply, X11Error> {
|
||||
self.xcb_connection()
|
||||
.translate_coordinates(src_w, dst_w, src_x, src_y)?
|
||||
.reply()
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
// This is adequate for surface_size
|
||||
pub fn get_geometry(
|
||||
&self,
|
||||
window: xproto::Window,
|
||||
) -> Result<xproto::GetGeometryReply, X11Error> {
|
||||
self.xcb_connection().get_geometry(window)?.reply().map_err(Into::into)
|
||||
}
|
||||
|
||||
fn get_frame_extents(&self, window: xproto::Window) -> Option<FrameExtents> {
|
||||
let atoms = self.atoms();
|
||||
let extents_atom = atoms[_NET_FRAME_EXTENTS];
|
||||
|
||||
if !hint_is_supported(extents_atom) {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Of the WMs tested, xmonad, i3, dwm, IceWM (1.3.x and earlier), and blackbox don't
|
||||
// support this. As this is part of EWMH (Extended Window Manager Hints), it's likely to
|
||||
// be unsupported by many smaller WMs.
|
||||
let extents: Option<Vec<u32>> = self
|
||||
.get_property(window, extents_atom, xproto::Atom::from(xproto::AtomEnum::CARDINAL))
|
||||
.ok();
|
||||
|
||||
extents.and_then(|extents| {
|
||||
if extents.len() >= 4 {
|
||||
Some(FrameExtents {
|
||||
left: extents[0],
|
||||
right: extents[1],
|
||||
top: extents[2],
|
||||
bottom: extents[3],
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn is_top_level(&self, window: xproto::Window, root: xproto::Window) -> Option<bool> {
|
||||
let atoms = self.atoms();
|
||||
let client_list_atom = atoms[_NET_CLIENT_LIST];
|
||||
|
||||
if !hint_is_supported(client_list_atom) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let client_list: Option<Vec<xproto::Window>> = self
|
||||
.get_property(root, client_list_atom, xproto::Atom::from(xproto::AtomEnum::WINDOW))
|
||||
.ok();
|
||||
|
||||
client_list.map(|client_list| client_list.contains(&(window as xproto::Window)))
|
||||
}
|
||||
|
||||
fn get_parent_window(&self, window: xproto::Window) -> Result<xproto::Window, X11Error> {
|
||||
let parent = self.xcb_connection().query_tree(window)?.reply()?.parent;
|
||||
Ok(parent)
|
||||
}
|
||||
|
||||
fn climb_hierarchy(
|
||||
&self,
|
||||
window: xproto::Window,
|
||||
root: xproto::Window,
|
||||
) -> Result<xproto::Window, X11Error> {
|
||||
let mut outer_window = window;
|
||||
loop {
|
||||
let candidate = self.get_parent_window(outer_window)?;
|
||||
if candidate == root {
|
||||
break;
|
||||
}
|
||||
outer_window = candidate;
|
||||
}
|
||||
Ok(outer_window)
|
||||
}
|
||||
|
||||
pub fn get_frame_extents_heuristic(
|
||||
&self,
|
||||
window: xproto::Window,
|
||||
root: xproto::Window,
|
||||
) -> FrameExtentsHeuristic {
|
||||
use self::FrameExtentsHeuristicPath::*;
|
||||
|
||||
// Position relative to root window.
|
||||
// With rare exceptions, this is the position of a nested window. Cases where the window
|
||||
// isn't nested are outlined in the comments throughout this function, but in addition to
|
||||
// that, fullscreen windows often aren't nested.
|
||||
let (inner_y_rel_root, child) = {
|
||||
let coords = self
|
||||
.translate_coords_root(window, root)
|
||||
.expect("Failed to translate window coordinates");
|
||||
(coords.dst_y, coords.child)
|
||||
};
|
||||
|
||||
let (width, height, border) = {
|
||||
let inner_geometry =
|
||||
self.get_geometry(window).expect("Failed to get inner window geometry");
|
||||
(inner_geometry.width, inner_geometry.height, inner_geometry.border_width)
|
||||
};
|
||||
|
||||
// The first condition is only false for un-nested windows, but isn't always false for
|
||||
// un-nested windows. Mutter/Muffin/Budgie and Marco present a mysterious discrepancy:
|
||||
// when y is on the range [0, 2] and if the window has been unfocused since being
|
||||
// undecorated (or was undecorated upon construction), the first condition is true,
|
||||
// requiring us to rely on the second condition.
|
||||
let nested = !(window == child || self.is_top_level(child, root) == Some(true));
|
||||
|
||||
// Hopefully the WM supports EWMH, allowing us to get exact info on the window frames.
|
||||
if let Some(mut frame_extents) = self.get_frame_extents(window) {
|
||||
// Mutter/Muffin/Budgie and Marco preserve their decorated frame extents when
|
||||
// decorations are disabled, but since the window becomes un-nested, it's easy to
|
||||
// catch.
|
||||
if !nested {
|
||||
frame_extents = FrameExtents::new(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
// The difference between the nested window's position and the outermost window's
|
||||
// position is equivalent to the frame size. In most scenarios, this is equivalent to
|
||||
// manually climbing the hierarchy as is done in the case below. Here's a list of
|
||||
// known discrepancies:
|
||||
// * Mutter/Muffin/Budgie gives decorated windows a margin of 9px (only 7px on top) in
|
||||
// addition to a 1px semi-transparent border. The margin can be easily observed by
|
||||
// using a screenshot tool to get a screenshot of a selected window, and is presumably
|
||||
// used for drawing drop shadows. Getting window geometry information via
|
||||
// hierarchy-climbing results in this margin being included in both the position and
|
||||
// outer size, so a window positioned at (0, 0) would be reported as having a position
|
||||
// (-10, -8).
|
||||
// * Compiz has a drop shadow margin just like Mutter/Muffin/Budgie, though it's 10px on
|
||||
// all sides, and there's no additional border.
|
||||
// * Enlightenment otherwise gets a y position equivalent to inner_y_rel_root. Without
|
||||
// decorations, there's no difference. This is presumably related to Enlightenment's
|
||||
// fairly unique concept of window position; it interprets positions given to
|
||||
// XMoveWindow as a client area position rather than a position of the overall window.
|
||||
|
||||
FrameExtentsHeuristic { frame_extents, heuristic_path: Supported }
|
||||
} else if nested {
|
||||
// If the position value we have is for a nested window used as the client area, we'll
|
||||
// just climb up the hierarchy and get the geometry of the outermost window we're
|
||||
// nested in.
|
||||
let outer_window =
|
||||
self.climb_hierarchy(window, root).expect("Failed to climb window hierarchy");
|
||||
let (outer_y, outer_width, outer_height) = {
|
||||
let outer_geometry =
|
||||
self.get_geometry(outer_window).expect("Failed to get outer window geometry");
|
||||
(outer_geometry.y, outer_geometry.width, outer_geometry.height)
|
||||
};
|
||||
|
||||
// Since we have the geometry of the outermost window and the geometry of the client
|
||||
// area, we can figure out what's in between.
|
||||
let diff_x = outer_width.saturating_sub(width) as u32;
|
||||
let diff_y = outer_height.saturating_sub(height) as u32;
|
||||
let offset_y = inner_y_rel_root.saturating_sub(outer_y) as u32;
|
||||
|
||||
let left = diff_x / 2;
|
||||
let right = left;
|
||||
let top = offset_y;
|
||||
let bottom = diff_y.saturating_sub(offset_y);
|
||||
|
||||
let frame_extents = FrameExtents::new(left, right, top, bottom);
|
||||
FrameExtentsHeuristic { frame_extents, heuristic_path: UnsupportedNested }
|
||||
} else {
|
||||
// This is the case for xmonad and dwm, AKA the only WMs tested that supplied a
|
||||
// border value. This is convenient, since we can use it to get an accurate frame.
|
||||
let frame_extents = FrameExtents::from_border(border.into());
|
||||
FrameExtentsHeuristic { frame_extents, heuristic_path: UnsupportedBordered }
|
||||
}
|
||||
}
|
||||
}
|
||||
169
winit-x11/src/util/hint.rs
Normal file
169
winit-x11/src/util/hint.rs
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use super::*;
|
||||
use crate::WindowType;
|
||||
|
||||
#[derive(Debug)]
|
||||
#[allow(dead_code)]
|
||||
pub enum StateOperation {
|
||||
Remove = 0, // _NET_WM_STATE_REMOVE
|
||||
Add = 1, // _NET_WM_STATE_ADD
|
||||
Toggle = 2, // _NET_WM_STATE_TOGGLE
|
||||
}
|
||||
|
||||
impl From<bool> for StateOperation {
|
||||
fn from(op: bool) -> Self {
|
||||
if op {
|
||||
StateOperation::Add
|
||||
} else {
|
||||
StateOperation::Remove
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WindowType {
|
||||
pub(crate) fn as_atom(&self, xconn: &Arc<XConnection>) -> xproto::Atom {
|
||||
use self::WindowType::*;
|
||||
let atom_name = match *self {
|
||||
Desktop => _NET_WM_WINDOW_TYPE_DESKTOP,
|
||||
Dock => _NET_WM_WINDOW_TYPE_DOCK,
|
||||
Toolbar => _NET_WM_WINDOW_TYPE_TOOLBAR,
|
||||
Menu => _NET_WM_WINDOW_TYPE_MENU,
|
||||
Utility => _NET_WM_WINDOW_TYPE_UTILITY,
|
||||
Splash => _NET_WM_WINDOW_TYPE_SPLASH,
|
||||
Dialog => _NET_WM_WINDOW_TYPE_DIALOG,
|
||||
DropdownMenu => _NET_WM_WINDOW_TYPE_DROPDOWN_MENU,
|
||||
PopupMenu => _NET_WM_WINDOW_TYPE_POPUP_MENU,
|
||||
Tooltip => _NET_WM_WINDOW_TYPE_TOOLTIP,
|
||||
Notification => _NET_WM_WINDOW_TYPE_NOTIFICATION,
|
||||
Combo => _NET_WM_WINDOW_TYPE_COMBO,
|
||||
Dnd => _NET_WM_WINDOW_TYPE_DND,
|
||||
Normal => _NET_WM_WINDOW_TYPE_NORMAL,
|
||||
};
|
||||
|
||||
let atoms = xconn.atoms();
|
||||
atoms[atom_name]
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MotifHints {
|
||||
hints: MwmHints,
|
||||
}
|
||||
|
||||
struct MwmHints {
|
||||
flags: u32,
|
||||
functions: u32,
|
||||
decorations: u32,
|
||||
input_mode: u32,
|
||||
status: u32,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
mod mwm {
|
||||
// Motif WM hints are obsolete, but still widely supported.
|
||||
// https://stackoverflow.com/a/1909708
|
||||
pub const MWM_HINTS_FUNCTIONS: u32 = 1 << 0;
|
||||
pub const MWM_HINTS_DECORATIONS: u32 = 1 << 1;
|
||||
|
||||
pub const MWM_FUNC_ALL: u32 = 1 << 0;
|
||||
pub const MWM_FUNC_RESIZE: u32 = 1 << 1;
|
||||
pub const MWM_FUNC_MOVE: u32 = 1 << 2;
|
||||
pub const MWM_FUNC_MINIMIZE: u32 = 1 << 3;
|
||||
pub const MWM_FUNC_MAXIMIZE: u32 = 1 << 4;
|
||||
pub const MWM_FUNC_CLOSE: u32 = 1 << 5;
|
||||
}
|
||||
|
||||
impl MotifHints {
|
||||
pub fn new() -> MotifHints {
|
||||
MotifHints {
|
||||
hints: MwmHints { flags: 0, functions: 0, decorations: 0, input_mode: 0, status: 0 },
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_decorations(&mut self, decorations: bool) {
|
||||
self.hints.flags |= mwm::MWM_HINTS_DECORATIONS;
|
||||
self.hints.decorations = decorations as u32;
|
||||
}
|
||||
|
||||
pub fn set_maximizable(&mut self, maximizable: bool) {
|
||||
if maximizable {
|
||||
self.add_func(mwm::MWM_FUNC_MAXIMIZE);
|
||||
} else {
|
||||
self.remove_func(mwm::MWM_FUNC_MAXIMIZE);
|
||||
}
|
||||
}
|
||||
|
||||
fn add_func(&mut self, func: u32) {
|
||||
if self.hints.flags & mwm::MWM_HINTS_FUNCTIONS != 0 {
|
||||
if self.hints.functions & mwm::MWM_FUNC_ALL != 0 {
|
||||
self.hints.functions &= !func;
|
||||
} else {
|
||||
self.hints.functions |= func;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_func(&mut self, func: u32) {
|
||||
if self.hints.flags & mwm::MWM_HINTS_FUNCTIONS == 0 {
|
||||
self.hints.flags |= mwm::MWM_HINTS_FUNCTIONS;
|
||||
self.hints.functions = mwm::MWM_FUNC_ALL;
|
||||
}
|
||||
|
||||
if self.hints.functions & mwm::MWM_FUNC_ALL != 0 {
|
||||
self.hints.functions |= func;
|
||||
} else {
|
||||
self.hints.functions &= !func;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MotifHints {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl XConnection {
|
||||
pub fn get_motif_hints(&self, window: xproto::Window) -> MotifHints {
|
||||
let atoms = self.atoms();
|
||||
let motif_hints = atoms[_MOTIF_WM_HINTS];
|
||||
|
||||
let mut hints = MotifHints::new();
|
||||
|
||||
if let Ok(props) = self.get_property::<u32>(window, motif_hints, motif_hints) {
|
||||
hints.hints.flags = props.first().cloned().unwrap_or(0);
|
||||
hints.hints.functions = props.get(1).cloned().unwrap_or(0);
|
||||
hints.hints.decorations = props.get(2).cloned().unwrap_or(0);
|
||||
hints.hints.input_mode = props.get(3).cloned().unwrap_or(0);
|
||||
hints.hints.status = props.get(4).cloned().unwrap_or(0);
|
||||
}
|
||||
|
||||
hints
|
||||
}
|
||||
|
||||
#[allow(clippy::unnecessary_cast)]
|
||||
pub fn set_motif_hints(
|
||||
&self,
|
||||
window: xproto::Window,
|
||||
hints: &MotifHints,
|
||||
) -> Result<VoidCookie<'_>, X11Error> {
|
||||
let atoms = self.atoms();
|
||||
let motif_hints = atoms[_MOTIF_WM_HINTS];
|
||||
|
||||
let hints_data: [u32; 5] = [
|
||||
hints.hints.flags as u32,
|
||||
hints.hints.functions as u32,
|
||||
hints.hints.decorations as u32,
|
||||
hints.hints.input_mode as u32,
|
||||
hints.hints.status as u32,
|
||||
];
|
||||
|
||||
self.change_property(
|
||||
window,
|
||||
motif_hints,
|
||||
motif_hints,
|
||||
xproto::PropMode::REPLACE,
|
||||
&hints_data,
|
||||
)
|
||||
}
|
||||
}
|
||||
46
winit-x11/src/util/icon.rs
Normal file
46
winit-x11/src/util/icon.rs
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
#![allow(clippy::assertions_on_constants)]
|
||||
|
||||
use winit_core::icon::RgbaIcon;
|
||||
|
||||
use super::*;
|
||||
|
||||
pub(crate) const PIXEL_SIZE: usize = mem::size_of::<Pixel>();
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Pixel {
|
||||
pub(crate) r: u8,
|
||||
pub(crate) g: u8,
|
||||
pub(crate) b: u8,
|
||||
pub(crate) a: u8,
|
||||
}
|
||||
|
||||
impl Pixel {
|
||||
pub fn to_packed_argb(&self) -> Cardinal {
|
||||
let mut cardinal = 0;
|
||||
assert!(CARDINAL_SIZE >= PIXEL_SIZE);
|
||||
let as_bytes = &mut cardinal as *mut _ as *mut u8;
|
||||
unsafe {
|
||||
*as_bytes.offset(0) = self.b;
|
||||
*as_bytes.offset(1) = self.g;
|
||||
*as_bytes.offset(2) = self.r;
|
||||
*as_bytes.offset(3) = self.a;
|
||||
}
|
||||
cardinal
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn rgba_to_cardinals(icon: &RgbaIcon) -> Vec<Cardinal> {
|
||||
assert_eq!(icon.buffer().len() % PIXEL_SIZE, 0);
|
||||
let pixel_count = icon.buffer().len() / PIXEL_SIZE;
|
||||
assert_eq!(pixel_count, (icon.width() * icon.height()) as usize);
|
||||
let mut data = Vec::with_capacity(pixel_count);
|
||||
data.push(icon.width() as Cardinal);
|
||||
data.push(icon.height() as Cardinal);
|
||||
let pixels = icon.buffer().as_ptr() as *const Pixel;
|
||||
for pixel_index in 0..pixel_count {
|
||||
let pixel = unsafe { &*pixels.add(pixel_index) };
|
||||
data.push(pixel.to_packed_argb());
|
||||
}
|
||||
data
|
||||
}
|
||||
106
winit-x11/src/util/input.rs
Normal file
106
winit-x11/src/util/input.rs
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
use std::{slice, str};
|
||||
|
||||
use x11rb::protocol::xinput::{self, ConnectionExt as _};
|
||||
use x11rb::protocol::xkb;
|
||||
|
||||
use super::*;
|
||||
|
||||
pub const VIRTUAL_CORE_POINTER: u16 = 2;
|
||||
|
||||
// A base buffer size of 1kB uses a negligible amount of RAM while preventing us from having to
|
||||
// re-allocate (and make another round-trip) in the *vast* majority of cases.
|
||||
// To test if `lookup_utf8` works correctly, set this to 1.
|
||||
const TEXT_BUFFER_SIZE: usize = 1024;
|
||||
|
||||
impl XConnection {
|
||||
pub fn select_xinput_events(
|
||||
&self,
|
||||
window: xproto::Window,
|
||||
device_id: u16,
|
||||
mask: xinput::XIEventMask,
|
||||
) -> Result<VoidCookie<'_>, X11Error> {
|
||||
self.xcb_connection()
|
||||
.xinput_xi_select_events(window, &[xinput::EventMask {
|
||||
deviceid: device_id,
|
||||
mask: vec![mask],
|
||||
}])
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn select_xkb_events(
|
||||
&self,
|
||||
device_id: xkb::DeviceSpec,
|
||||
mask: xkb::EventType,
|
||||
) -> Result<bool, X11Error> {
|
||||
let mask = u16::from(mask) as _;
|
||||
let status =
|
||||
unsafe { (self.xlib.XkbSelectEvents)(self.display, device_id as _, mask, mask) };
|
||||
|
||||
if status == ffi::True {
|
||||
self.flush_requests()?;
|
||||
Ok(true)
|
||||
} else {
|
||||
tracing::error!("Could not select XKB events: The XKB extension is not initialized!");
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn query_pointer(
|
||||
&self,
|
||||
window: xproto::Window,
|
||||
device_id: u16,
|
||||
) -> Result<xinput::XIQueryPointerReply, X11Error> {
|
||||
self.xcb_connection()
|
||||
.xinput_xi_query_pointer(window, device_id)?
|
||||
.reply()
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
fn lookup_utf8_inner(
|
||||
&self,
|
||||
ic: ffi::XIC,
|
||||
key_event: &mut ffi::XKeyEvent,
|
||||
buffer: *mut u8,
|
||||
size: usize,
|
||||
) -> (ffi::KeySym, ffi::Status, c_int) {
|
||||
let mut keysym: ffi::KeySym = 0;
|
||||
let mut status: ffi::Status = 0;
|
||||
let count = unsafe {
|
||||
(self.xlib.Xutf8LookupString)(
|
||||
ic,
|
||||
key_event,
|
||||
buffer as *mut c_char,
|
||||
size as c_int,
|
||||
&mut keysym,
|
||||
&mut status,
|
||||
)
|
||||
};
|
||||
(keysym, status, count)
|
||||
}
|
||||
|
||||
pub fn lookup_utf8(&self, ic: ffi::XIC, key_event: &mut ffi::XKeyEvent) -> String {
|
||||
// `assume_init` is safe here because the array consists of `MaybeUninit` values,
|
||||
// which do not require initialization.
|
||||
let mut buffer: [MaybeUninit<u8>; TEXT_BUFFER_SIZE] =
|
||||
unsafe { MaybeUninit::uninit().assume_init() };
|
||||
// If the buffer overflows, we'll make a new one on the heap.
|
||||
let mut vec;
|
||||
|
||||
let (_, status, count) =
|
||||
self.lookup_utf8_inner(ic, key_event, buffer.as_mut_ptr() as *mut u8, buffer.len());
|
||||
|
||||
let bytes = if status == ffi::XBufferOverflow {
|
||||
vec = Vec::with_capacity(count as usize);
|
||||
let (_, _, new_count) =
|
||||
self.lookup_utf8_inner(ic, key_event, vec.as_mut_ptr(), vec.capacity());
|
||||
debug_assert_eq!(count, new_count);
|
||||
|
||||
unsafe { vec.set_len(count as usize) };
|
||||
&vec[..count as usize]
|
||||
} else {
|
||||
unsafe { slice::from_raw_parts(buffer.as_ptr() as *const u8, count as usize) }
|
||||
};
|
||||
|
||||
str::from_utf8(bytes).unwrap_or("").to_string()
|
||||
}
|
||||
}
|
||||
75
winit-x11/src/util/keys.rs
Normal file
75
winit-x11/src/util/keys.rs
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
use std::iter::Enumerate;
|
||||
use std::slice::Iter;
|
||||
|
||||
use super::*;
|
||||
|
||||
pub struct Keymap {
|
||||
keys: [u8; 32],
|
||||
}
|
||||
|
||||
pub struct KeymapIter<'a> {
|
||||
iter: Enumerate<Iter<'a, u8>>,
|
||||
index: usize,
|
||||
item: Option<u8>,
|
||||
}
|
||||
|
||||
impl Keymap {
|
||||
pub fn iter(&self) -> KeymapIter<'_> {
|
||||
KeymapIter { iter: self.keys.iter().enumerate(), index: 0, item: None }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoIterator for &'a Keymap {
|
||||
type IntoIter = KeymapIter<'a>;
|
||||
type Item = ffi::KeyCode;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for KeymapIter<'_> {
|
||||
type Item = ffi::KeyCode;
|
||||
|
||||
fn next(&mut self) -> Option<ffi::KeyCode> {
|
||||
if self.item.is_none() {
|
||||
for (index, &item) in self.iter.by_ref() {
|
||||
if item != 0 {
|
||||
self.index = index;
|
||||
self.item = Some(item);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.item.take().map(|item| {
|
||||
debug_assert!(item != 0);
|
||||
|
||||
let bit = first_bit(item);
|
||||
|
||||
if item != bit {
|
||||
// Remove the first bit; save the rest for further iterations
|
||||
self.item = Some(item ^ bit);
|
||||
}
|
||||
|
||||
let shift = bit.trailing_zeros() + (self.index * 8) as u32;
|
||||
shift as ffi::KeyCode
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl XConnection {
|
||||
pub fn query_keymap(&self) -> Keymap {
|
||||
let mut keys = [0; 32];
|
||||
|
||||
unsafe {
|
||||
(self.xlib.XQueryKeymap)(self.display, keys.as_mut_ptr() as *mut c_char);
|
||||
}
|
||||
|
||||
Keymap { keys }
|
||||
}
|
||||
}
|
||||
|
||||
fn first_bit(b: u8) -> u8 {
|
||||
1 << b.trailing_zeros()
|
||||
}
|
||||
26
winit-x11/src/util/memory.rs
Normal file
26
winit-x11/src/util/memory.rs
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
use super::*;
|
||||
|
||||
pub(crate) struct XSmartPointer<'a, T> {
|
||||
xconn: &'a XConnection,
|
||||
pub ptr: *mut T,
|
||||
}
|
||||
|
||||
impl<'a, T> XSmartPointer<'a, T> {
|
||||
// You're responsible for only passing things to this that should be XFree'd.
|
||||
// Returns None if ptr is null.
|
||||
pub fn new(xconn: &'a XConnection, ptr: *mut T) -> Option<Self> {
|
||||
if !ptr.is_null() {
|
||||
Some(XSmartPointer { xconn, ptr })
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Drop for XSmartPointer<'_, T> {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
(self.xconn.xlib.XFree)(self.ptr as *mut _);
|
||||
}
|
||||
}
|
||||
}
|
||||
80
winit-x11/src/util/mod.rs
Normal file
80
winit-x11/src/util/mod.rs
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
// Welcome to the util module, where we try to keep you from shooting yourself in the foot.
|
||||
// *results may vary
|
||||
|
||||
use std::mem::{self, MaybeUninit};
|
||||
use std::ops::BitAnd;
|
||||
use std::os::raw::*;
|
||||
|
||||
mod client_msg;
|
||||
pub mod cookie;
|
||||
mod cursor;
|
||||
mod geometry;
|
||||
mod hint;
|
||||
mod icon;
|
||||
mod input;
|
||||
pub mod keys;
|
||||
pub(crate) mod memory;
|
||||
mod mouse;
|
||||
mod randr;
|
||||
mod window_property;
|
||||
mod wm;
|
||||
mod xmodmap;
|
||||
|
||||
use x11rb::protocol::xproto::{self, ConnectionExt as _};
|
||||
|
||||
pub use self::cursor::*;
|
||||
pub use self::geometry::*;
|
||||
pub use self::hint::*;
|
||||
pub(crate) use self::icon::rgba_to_cardinals;
|
||||
pub use self::input::*;
|
||||
pub use self::mouse::*;
|
||||
pub use self::window_property::*;
|
||||
pub use self::wm::*;
|
||||
pub use self::xmodmap::ModifierKeymap;
|
||||
use super::atoms::*;
|
||||
use super::ffi;
|
||||
use crate::event_loop::{VoidCookie, X11Error};
|
||||
use crate::xdisplay::{XConnection, XError};
|
||||
|
||||
pub fn maybe_change<T: PartialEq>(field: &mut Option<T>, value: T) -> bool {
|
||||
let wrapped = Some(value);
|
||||
if *field != wrapped {
|
||||
*field = wrapped;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn has_flag<T>(bitset: T, flag: T) -> bool
|
||||
where
|
||||
T: Copy + PartialEq + BitAnd<T, Output = T>,
|
||||
{
|
||||
bitset & flag == flag
|
||||
}
|
||||
|
||||
impl XConnection {
|
||||
// This is important, so pay attention!
|
||||
// Xlib has an output buffer, and tries to hide the async nature of X from you.
|
||||
// This buffer contains the requests you make, and is flushed under various circumstances:
|
||||
// 1. `XPending`, `XNextEvent`, and `XWindowEvent` flush "as needed"
|
||||
// 2. `XFlush` explicitly flushes
|
||||
// 3. `XSync` flushes and blocks until all requests are responded to
|
||||
// 4. Calls that have a return dependent on a response (i.e. `XGetWindowProperty`) sync
|
||||
// internally. When in doubt, check the X11 source; if a function calls `_XReply`, it flushes
|
||||
// and waits.
|
||||
// All util functions that abstract an async function will return a `Flusher`.
|
||||
pub fn flush_requests(&self) -> Result<(), XError> {
|
||||
unsafe { (self.xlib.XFlush)(self.display) };
|
||||
// println!("XFlush");
|
||||
// This isn't necessarily a useful time to check for errors (since our request hasn't
|
||||
// necessarily been processed yet)
|
||||
self.check_errors()
|
||||
}
|
||||
|
||||
pub fn sync_with_server(&self) -> Result<(), XError> {
|
||||
unsafe { (self.xlib.XSync)(self.display, ffi::False) };
|
||||
// println!("XSync");
|
||||
self.check_errors()
|
||||
}
|
||||
}
|
||||
187
winit-x11/src/util/modifiers.rs
Normal file
187
winit-x11/src/util/modifiers.rs
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
use std::{collections::HashMap, slice};
|
||||
|
||||
use super::*;
|
||||
|
||||
use winit_core::event::{ElementState, ModifiersState};
|
||||
|
||||
// Offsets within XModifierKeymap to each set of keycodes.
|
||||
// We are only interested in Shift, Control, Alt, and Logo.
|
||||
//
|
||||
// There are 8 sets total. The order of keycode sets is:
|
||||
// Shift, Lock, Control, Mod1 (Alt), Mod2, Mod3, Mod4 (Logo), Mod5
|
||||
//
|
||||
// https://tronche.com/gui/x/xlib/input/XSetModifierMapping.html
|
||||
const SHIFT_OFFSET: usize = 0;
|
||||
const CONTROL_OFFSET: usize = 2;
|
||||
const ALT_OFFSET: usize = 3;
|
||||
const LOGO_OFFSET: usize = 6;
|
||||
const NUM_MODS: usize = 8;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||
pub enum Modifier {
|
||||
Alt,
|
||||
Ctrl,
|
||||
Shift,
|
||||
Logo,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub(crate) struct ModifierKeymap {
|
||||
// Maps keycodes to modifiers
|
||||
keys: HashMap<ffi::KeyCode, Modifier>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub(crate) struct ModifierKeyState {
|
||||
// Contains currently pressed modifier keys and their corresponding modifiers
|
||||
keys: HashMap<ffi::KeyCode, Modifier>,
|
||||
state: ModifiersState,
|
||||
}
|
||||
|
||||
impl ModifierKeymap {
|
||||
pub fn new() -> ModifierKeymap {
|
||||
ModifierKeymap::default()
|
||||
}
|
||||
|
||||
pub fn get_modifier(&self, keycode: ffi::KeyCode) -> Option<Modifier> {
|
||||
self.keys.get(&keycode).cloned()
|
||||
}
|
||||
|
||||
pub fn reset_from_x_connection(&mut self, xconn: &XConnection) {
|
||||
{
|
||||
let keymap = xconn.xcb_connection().get_modifier_mapping().expect("get_modifier_mapping failed").reply().expect("get_modifier_mapping failed");
|
||||
|
||||
if keymap.is_null() {
|
||||
panic!("failed to allocate XModifierKeymap");
|
||||
}
|
||||
|
||||
self.reset_from_x_keymap(&*keymap);
|
||||
|
||||
(xconn.xlib.XFreeModifiermap)(keymap);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset_from_x_keymap(&mut self, keymap: &ffi::XModifierKeymap) {
|
||||
let keys_per_mod = keymap.max_keypermod as usize;
|
||||
|
||||
let keys = unsafe {
|
||||
slice::from_raw_parts(keymap.modifiermap as *const _, keys_per_mod * NUM_MODS)
|
||||
};
|
||||
|
||||
self.keys.clear();
|
||||
|
||||
self.read_x_keys(keys, SHIFT_OFFSET, keys_per_mod, Modifier::Shift);
|
||||
self.read_x_keys(keys, CONTROL_OFFSET, keys_per_mod, Modifier::Ctrl);
|
||||
self.read_x_keys(keys, ALT_OFFSET, keys_per_mod, Modifier::Alt);
|
||||
self.read_x_keys(keys, LOGO_OFFSET, keys_per_mod, Modifier::Logo);
|
||||
}
|
||||
|
||||
fn read_x_keys(
|
||||
&mut self,
|
||||
keys: &[ffi::KeyCode],
|
||||
offset: usize,
|
||||
keys_per_mod: usize,
|
||||
modifier: Modifier,
|
||||
) {
|
||||
let start = offset * keys_per_mod;
|
||||
let end = start + keys_per_mod;
|
||||
|
||||
for &keycode in &keys[start..end] {
|
||||
if keycode != 0 {
|
||||
self.keys.insert(keycode, modifier);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ModifierKeyState {
|
||||
pub fn update_keymap(&mut self, mods: &ModifierKeymap) {
|
||||
self.keys.retain(|k, v| {
|
||||
if let Some(m) = mods.get_modifier(*k) {
|
||||
*v = m;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
|
||||
self.reset_state();
|
||||
}
|
||||
|
||||
pub fn update_state(
|
||||
&mut self,
|
||||
state: &ModifiersState,
|
||||
except: Option<Modifier>,
|
||||
) -> Option<ModifiersState> {
|
||||
let mut new_state = *state;
|
||||
|
||||
match except {
|
||||
Some(Modifier::Alt) => new_state.set(ModifiersState::ALT, self.state.alt()),
|
||||
Some(Modifier::Ctrl) => new_state.set(ModifiersState::CTRL, self.state.ctrl()),
|
||||
Some(Modifier::Shift) => new_state.set(ModifiersState::SHIFT, self.state.shift()),
|
||||
Some(Modifier::Logo) => new_state.set(ModifiersState::LOGO, self.state.logo()),
|
||||
None => (),
|
||||
}
|
||||
|
||||
if self.state == new_state {
|
||||
None
|
||||
} else {
|
||||
self.keys.retain(|_k, v| get_modifier(&new_state, *v));
|
||||
self.state = new_state;
|
||||
Some(new_state)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn modifiers(&self) -> ModifiersState {
|
||||
self.state
|
||||
}
|
||||
|
||||
pub fn key_event(&mut self, state: ElementState, keycode: ffi::KeyCode, modifier: Modifier) {
|
||||
match state {
|
||||
ElementState::Pressed => self.key_press(keycode, modifier),
|
||||
ElementState::Released => self.key_release(keycode),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn key_press(&mut self, keycode: ffi::KeyCode, modifier: Modifier) {
|
||||
self.keys.insert(keycode, modifier);
|
||||
|
||||
set_modifier(&mut self.state, modifier, true);
|
||||
}
|
||||
|
||||
pub fn key_release(&mut self, keycode: ffi::KeyCode) {
|
||||
if let Some(modifier) = self.keys.remove(&keycode) {
|
||||
if !self.keys.values().any(|&m| m == modifier) {
|
||||
set_modifier(&mut self.state, modifier, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn reset_state(&mut self) {
|
||||
let mut new_state = ModifiersState::default();
|
||||
|
||||
for &m in self.keys.values() {
|
||||
set_modifier(&mut new_state, m, true);
|
||||
}
|
||||
|
||||
self.state = new_state;
|
||||
}
|
||||
}
|
||||
|
||||
fn get_modifier(state: &ModifiersState, modifier: Modifier) -> bool {
|
||||
match modifier {
|
||||
Modifier::Alt => state.alt(),
|
||||
Modifier::Ctrl => state.ctrl(),
|
||||
Modifier::Shift => state.shift(),
|
||||
Modifier::Logo => state.logo(),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_modifier(state: &mut ModifiersState, modifier: Modifier, value: bool) {
|
||||
match modifier {
|
||||
Modifier::Alt => state.set(ModifiersState::ALT, value),
|
||||
Modifier::Ctrl => state.set(ModifiersState::CTRL, value),
|
||||
Modifier::Shift => state.set(ModifiersState::SHIFT, value),
|
||||
Modifier::Logo => state.set(ModifiersState::LOGO, value),
|
||||
}
|
||||
}
|
||||
49
winit-x11/src/util/mouse.rs
Normal file
49
winit-x11/src/util/mouse.rs
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
//! Utilities for handling mouse events.
|
||||
|
||||
/// Recorded mouse delta designed to filter out noise.
|
||||
pub struct Delta<T> {
|
||||
x: T,
|
||||
y: T,
|
||||
}
|
||||
|
||||
impl<T: Default> Default for Delta<T> {
|
||||
fn default() -> Self {
|
||||
Self { x: Default::default(), y: Default::default() }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Default> Delta<T> {
|
||||
pub(crate) fn set_x(&mut self, x: T) {
|
||||
self.x = x;
|
||||
}
|
||||
|
||||
pub(crate) fn set_y(&mut self, y: T) {
|
||||
self.y = y;
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! consume {
|
||||
($this:expr, $ty:ty) => {{
|
||||
let this = $this;
|
||||
let (x, y) = match (this.x.abs() < <$ty>::EPSILON, this.y.abs() < <$ty>::EPSILON) {
|
||||
(true, true) => return None,
|
||||
(false, true) => (this.x, 0.0),
|
||||
(true, false) => (0.0, this.y),
|
||||
(false, false) => (this.x, this.y),
|
||||
};
|
||||
|
||||
Some((x, y))
|
||||
}};
|
||||
}
|
||||
|
||||
impl Delta<f32> {
|
||||
pub(crate) fn consume(self) -> Option<(f32, f32)> {
|
||||
consume!(self, f32)
|
||||
}
|
||||
}
|
||||
|
||||
impl Delta<f64> {
|
||||
pub(crate) fn consume(self) -> Option<(f64, f64)> {
|
||||
consume!(self, f64)
|
||||
}
|
||||
}
|
||||
186
winit-x11/src/util/randr.rs
Normal file
186
winit-x11/src/util/randr.rs
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
use std::num::NonZeroU16;
|
||||
use std::str::FromStr;
|
||||
use std::{env, str};
|
||||
|
||||
use dpi::validate_scale_factor;
|
||||
use tracing::warn;
|
||||
use winit_core::monitor::VideoMode;
|
||||
use x11rb::protocol::randr::{self, ConnectionExt as _};
|
||||
|
||||
use super::*;
|
||||
use crate::monitor::{self, VideoModeHandle};
|
||||
|
||||
/// Represents values of `WINIT_HIDPI_FACTOR`.
|
||||
pub enum EnvVarDPI {
|
||||
Randr,
|
||||
Scale(f64),
|
||||
NotSet,
|
||||
}
|
||||
|
||||
pub fn calc_dpi_factor(
|
||||
(width_px, height_px): (u32, u32),
|
||||
(width_mm, height_mm): (u64, u64),
|
||||
) -> f64 {
|
||||
// See http://xpra.org/trac/ticket/728 for more information.
|
||||
if width_mm == 0 || height_mm == 0 {
|
||||
warn!("XRandR reported that the display's 0mm in size, which is certifiably insane");
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
let ppmm = ((width_px as f64 * height_px as f64) / (width_mm as f64 * height_mm as f64)).sqrt();
|
||||
// Quantize 1/12 step size
|
||||
let dpi_factor = ((ppmm * (12.0 * 25.4 / 96.0)).round() / 12.0).max(1.0);
|
||||
assert!(validate_scale_factor(dpi_factor));
|
||||
if dpi_factor <= 20. {
|
||||
dpi_factor
|
||||
} else {
|
||||
1.
|
||||
}
|
||||
}
|
||||
|
||||
impl XConnection {
|
||||
// Retrieve DPI from Xft.dpi property
|
||||
pub fn get_xft_dpi(&self) -> Option<f64> {
|
||||
// Try to get it from XSETTINGS first.
|
||||
if let Some(xsettings_screen) = self.xsettings_screen() {
|
||||
match self.xsettings_dpi(xsettings_screen) {
|
||||
Ok(Some(dpi)) => return Some(dpi),
|
||||
Ok(None) => {},
|
||||
Err(err) => {
|
||||
tracing::warn!("failed to fetch XSettings: {err}");
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
self.database().get_string("Xft.dpi", "").and_then(|s| f64::from_str(s).ok())
|
||||
}
|
||||
|
||||
pub fn get_output_info(
|
||||
&self,
|
||||
resources: &monitor::ScreenResources,
|
||||
crtc: &randr::GetCrtcInfoReply,
|
||||
) -> Option<(String, f64, Vec<VideoModeHandle>)> {
|
||||
let output_info = match self
|
||||
.xcb_connection()
|
||||
.randr_get_output_info(crtc.outputs[0], x11rb::CURRENT_TIME)
|
||||
.map_err(X11Error::from)
|
||||
.and_then(|r| r.reply().map_err(X11Error::from))
|
||||
{
|
||||
Ok(output_info) => output_info,
|
||||
Err(err) => {
|
||||
warn!("Failed to get output info: {:?}", err);
|
||||
return None;
|
||||
},
|
||||
};
|
||||
|
||||
let bit_depth = self.default_root().root_depth;
|
||||
let output_modes = &output_info.modes;
|
||||
let resource_modes = resources.modes();
|
||||
let current_mode = crtc.mode;
|
||||
|
||||
let modes = resource_modes
|
||||
.iter()
|
||||
// XRROutputInfo contains an array of mode ids that correspond to
|
||||
// modes in the array in XRRScreenResources
|
||||
.filter(|x| output_modes.contains(&x.id))
|
||||
.map(|mode| VideoModeHandle {
|
||||
current: mode.id == current_mode,
|
||||
mode: VideoMode::new(
|
||||
(mode.width as u32, mode.height as u32).into(),
|
||||
NonZeroU16::new(bit_depth as u16),
|
||||
monitor::mode_refresh_rate_millihertz(mode),
|
||||
),
|
||||
native_mode: mode.id,
|
||||
})
|
||||
.collect();
|
||||
|
||||
let name = match str::from_utf8(&output_info.name) {
|
||||
Ok(name) => name.to_owned(),
|
||||
Err(err) => {
|
||||
warn!("Failed to get output name: {:?}", err);
|
||||
return None;
|
||||
},
|
||||
};
|
||||
// Override DPI if `WINIT_X11_SCALE_FACTOR` variable is set
|
||||
let deprecated_dpi_override = env::var("WINIT_HIDPI_FACTOR").ok();
|
||||
if deprecated_dpi_override.is_some() {
|
||||
warn!(
|
||||
"The WINIT_HIDPI_FACTOR environment variable is deprecated; use \
|
||||
WINIT_X11_SCALE_FACTOR"
|
||||
)
|
||||
}
|
||||
let dpi_env = env::var("WINIT_X11_SCALE_FACTOR").ok().map_or_else(
|
||||
|| EnvVarDPI::NotSet,
|
||||
|var| {
|
||||
if var.to_lowercase() == "randr" {
|
||||
EnvVarDPI::Randr
|
||||
} else if let Ok(dpi) = f64::from_str(&var) {
|
||||
EnvVarDPI::Scale(dpi)
|
||||
} else if var.is_empty() {
|
||||
EnvVarDPI::NotSet
|
||||
} else {
|
||||
panic!(
|
||||
"`WINIT_X11_SCALE_FACTOR` invalid; DPI factors must be either normal \
|
||||
floats greater than 0, or `randr`. Got `{var}`"
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
let scale_factor = match dpi_env {
|
||||
EnvVarDPI::Randr => calc_dpi_factor(
|
||||
(crtc.width.into(), crtc.height.into()),
|
||||
(output_info.mm_width as _, output_info.mm_height as _),
|
||||
),
|
||||
EnvVarDPI::Scale(dpi_override) => {
|
||||
if !validate_scale_factor(dpi_override) {
|
||||
panic!(
|
||||
"`WINIT_X11_SCALE_FACTOR` invalid; DPI factors must be either normal \
|
||||
floats greater than 0, or `randr`. Got `{dpi_override}`",
|
||||
);
|
||||
}
|
||||
dpi_override
|
||||
},
|
||||
EnvVarDPI::NotSet => {
|
||||
if let Some(dpi) = self.get_xft_dpi() {
|
||||
dpi / 96.
|
||||
} else {
|
||||
calc_dpi_factor(
|
||||
(crtc.width.into(), crtc.height.into()),
|
||||
(output_info.mm_width as _, output_info.mm_height as _),
|
||||
)
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
Some((name, scale_factor, modes))
|
||||
}
|
||||
|
||||
pub fn set_crtc_config(
|
||||
&self,
|
||||
crtc_id: randr::Crtc,
|
||||
mode_id: randr::Mode,
|
||||
) -> Result<(), X11Error> {
|
||||
let crtc =
|
||||
self.xcb_connection().randr_get_crtc_info(crtc_id, x11rb::CURRENT_TIME)?.reply()?;
|
||||
|
||||
self.xcb_connection()
|
||||
.randr_set_crtc_config(
|
||||
crtc_id,
|
||||
crtc.timestamp,
|
||||
x11rb::CURRENT_TIME,
|
||||
crtc.x,
|
||||
crtc.y,
|
||||
mode_id,
|
||||
crtc.rotation,
|
||||
&crtc.outputs,
|
||||
)?
|
||||
.reply()
|
||||
.map(|_| ())
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn get_crtc_mode(&self, crtc_id: randr::Crtc) -> Result<randr::Mode, X11Error> {
|
||||
Ok(self.xcb_connection().randr_get_crtc_info(crtc_id, x11rb::CURRENT_TIME)?.reply()?.mode)
|
||||
}
|
||||
}
|
||||
195
winit-x11/src/util/window_property.rs
Normal file
195
winit-x11/src/util/window_property.rs
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
use std::error::Error;
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
|
||||
use bytemuck::{NoUninit, Pod};
|
||||
use x11rb::connection::Connection;
|
||||
use x11rb::errors::ReplyError;
|
||||
|
||||
use super::*;
|
||||
|
||||
pub const CARDINAL_SIZE: usize = mem::size_of::<u32>();
|
||||
|
||||
pub type Cardinal = u32;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum GetPropertyError {
|
||||
X11rbError(Arc<ReplyError>),
|
||||
TypeMismatch(xproto::Atom),
|
||||
FormatMismatch(c_int),
|
||||
}
|
||||
|
||||
impl GetPropertyError {
|
||||
pub fn is_actual_property_type(&self, t: xproto::Atom) -> bool {
|
||||
if let GetPropertyError::TypeMismatch(actual_type) = *self {
|
||||
actual_type == t
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Into<ReplyError>> From<T> for GetPropertyError {
|
||||
fn from(e: T) -> Self {
|
||||
Self::X11rbError(Arc::new(e.into()))
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for GetPropertyError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
GetPropertyError::X11rbError(err) => err.fmt(f),
|
||||
GetPropertyError::TypeMismatch(err) => write!(f, "type mismatch: {err}"),
|
||||
GetPropertyError::FormatMismatch(err) => write!(f, "format mismatch: {err}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for GetPropertyError {}
|
||||
|
||||
// Number of 32-bit chunks to retrieve per iteration of get_property's inner loop.
|
||||
// To test if `get_property` works correctly, set this to 1.
|
||||
const PROPERTY_BUFFER_SIZE: u32 = 1024; // 4k of RAM ought to be enough for anyone!
|
||||
|
||||
impl XConnection {
|
||||
pub fn get_property<T: Pod>(
|
||||
&self,
|
||||
window: xproto::Window,
|
||||
property: xproto::Atom,
|
||||
property_type: xproto::Atom,
|
||||
) -> Result<Vec<T>, GetPropertyError> {
|
||||
let mut iter = PropIterator::new(self.xcb_connection(), window, property, property_type);
|
||||
let mut data = vec![];
|
||||
|
||||
loop {
|
||||
if !iter.next_window(&mut data)? {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
pub fn change_property<'a, T: NoUninit>(
|
||||
&'a self,
|
||||
window: xproto::Window,
|
||||
property: xproto::Atom,
|
||||
property_type: xproto::Atom,
|
||||
mode: xproto::PropMode,
|
||||
new_value: &[T],
|
||||
) -> Result<VoidCookie<'a>, X11Error> {
|
||||
assert!([1usize, 2, 4].contains(&mem::size_of::<T>()));
|
||||
self.xcb_connection()
|
||||
.change_property(
|
||||
mode,
|
||||
window,
|
||||
property,
|
||||
property_type,
|
||||
(mem::size_of::<T>() * 8) as u8,
|
||||
new_value.len().try_into().expect("too many items for property"),
|
||||
bytemuck::cast_slice::<T, u8>(new_value),
|
||||
)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
/// An iterator over the "windows" of the property that we are fetching.
|
||||
struct PropIterator<'a, C: ?Sized, T> {
|
||||
/// Handle to the connection.
|
||||
conn: &'a C,
|
||||
|
||||
/// The window that we're fetching the property from.
|
||||
window: xproto::Window,
|
||||
|
||||
/// The property that we're fetching.
|
||||
property: xproto::Atom,
|
||||
|
||||
/// The type of the property that we're fetching.
|
||||
property_type: xproto::Atom,
|
||||
|
||||
/// The offset of the next window, in 32-bit chunks.
|
||||
offset: u32,
|
||||
|
||||
/// The format of the type.
|
||||
format: u8,
|
||||
|
||||
/// Keep a reference to `T`.
|
||||
_phantom: std::marker::PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<'a, C: Connection + ?Sized, T: Pod> PropIterator<'a, C, T> {
|
||||
/// Create a new property iterator.
|
||||
fn new(
|
||||
conn: &'a C,
|
||||
window: xproto::Window,
|
||||
property: xproto::Atom,
|
||||
property_type: xproto::Atom,
|
||||
) -> Self {
|
||||
let format = match mem::size_of::<T>() {
|
||||
1 => 8,
|
||||
2 => 16,
|
||||
4 => 32,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
Self {
|
||||
conn,
|
||||
window,
|
||||
property,
|
||||
property_type,
|
||||
offset: 0,
|
||||
format,
|
||||
_phantom: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the next window and append it to `data`.
|
||||
///
|
||||
/// Returns whether there are more windows to fetch.
|
||||
fn next_window(&mut self, data: &mut Vec<T>) -> Result<bool, GetPropertyError> {
|
||||
// Send the request and wait for the reply.
|
||||
let reply = self
|
||||
.conn
|
||||
.get_property(
|
||||
false,
|
||||
self.window,
|
||||
self.property,
|
||||
self.property_type,
|
||||
self.offset,
|
||||
PROPERTY_BUFFER_SIZE,
|
||||
)?
|
||||
.reply()?;
|
||||
|
||||
// Make sure that the reply is of the correct type.
|
||||
if reply.type_ != self.property_type {
|
||||
return Err(GetPropertyError::TypeMismatch(reply.type_));
|
||||
}
|
||||
|
||||
// Make sure that the reply is of the correct format.
|
||||
if reply.format != self.format {
|
||||
return Err(GetPropertyError::FormatMismatch(reply.format.into()));
|
||||
}
|
||||
|
||||
// Append the data to the output.
|
||||
if mem::size_of::<T>() == 1 && mem::align_of::<T>() == 1 {
|
||||
// We can just do a bytewise append.
|
||||
data.extend_from_slice(bytemuck::cast_slice(&reply.value));
|
||||
} else {
|
||||
// Rust's borrowing and types system makes this a bit tricky.
|
||||
//
|
||||
// We need to make sure that the data is properly aligned. Unfortunately the best
|
||||
// safe way to do this is to copy the data to another buffer and then append.
|
||||
//
|
||||
// TODO(notgull): It may be worth it to use `unsafe` to copy directly from
|
||||
// `reply.value` to `data`; check if this is faster. Use benchmarks!
|
||||
let old_len = data.len();
|
||||
let added_len = reply.value.len() / mem::size_of::<T>();
|
||||
data.resize(old_len + added_len, T::zeroed());
|
||||
bytemuck::cast_slice_mut::<T, u8>(&mut data[old_len..]).copy_from_slice(&reply.value);
|
||||
}
|
||||
|
||||
// Check `bytes_after` to see if there are more windows to fetch.
|
||||
self.offset += PROPERTY_BUFFER_SIZE;
|
||||
Ok(reply.bytes_after != 0)
|
||||
}
|
||||
}
|
||||
137
winit-x11/src/util/wm.rs
Normal file
137
winit-x11/src/util/wm.rs
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
use std::sync::Mutex;
|
||||
|
||||
use super::*;
|
||||
|
||||
// https://specifications.freedesktop.org/wm-spec/latest/ar01s04.html#idm46075117309248
|
||||
pub const MOVERESIZE_TOPLEFT: isize = 0;
|
||||
pub const MOVERESIZE_TOP: isize = 1;
|
||||
pub const MOVERESIZE_TOPRIGHT: isize = 2;
|
||||
pub const MOVERESIZE_RIGHT: isize = 3;
|
||||
pub const MOVERESIZE_BOTTOMRIGHT: isize = 4;
|
||||
pub const MOVERESIZE_BOTTOM: isize = 5;
|
||||
pub const MOVERESIZE_BOTTOMLEFT: isize = 6;
|
||||
pub const MOVERESIZE_LEFT: isize = 7;
|
||||
pub const MOVERESIZE_MOVE: isize = 8;
|
||||
|
||||
// This info is global to the window manager.
|
||||
static SUPPORTED_HINTS: Mutex<Vec<xproto::Atom>> = Mutex::new(Vec::new());
|
||||
static WM_NAME: Mutex<Option<String>> = Mutex::new(None);
|
||||
|
||||
pub fn hint_is_supported(hint: xproto::Atom) -> bool {
|
||||
(*SUPPORTED_HINTS.lock().unwrap()).contains(&hint)
|
||||
}
|
||||
|
||||
pub fn wm_name_is_one_of(names: &[&str]) -> bool {
|
||||
if let Some(ref name) = *WM_NAME.lock().unwrap() {
|
||||
names.contains(&name.as_str())
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl XConnection {
|
||||
pub fn update_cached_wm_info(&self, root: xproto::Window) {
|
||||
*SUPPORTED_HINTS.lock().unwrap() = self.get_supported_hints(root);
|
||||
*WM_NAME.lock().unwrap() = self.get_wm_name(root);
|
||||
}
|
||||
|
||||
fn get_supported_hints(&self, root: xproto::Window) -> Vec<xproto::Atom> {
|
||||
let atoms = self.atoms();
|
||||
let supported_atom = atoms[_NET_SUPPORTED];
|
||||
self.get_property(root, supported_atom, xproto::Atom::from(xproto::AtomEnum::ATOM))
|
||||
.unwrap_or_else(|_| Vec::with_capacity(0))
|
||||
}
|
||||
|
||||
#[allow(clippy::useless_conversion)]
|
||||
fn get_wm_name(&self, root: xproto::Window) -> Option<String> {
|
||||
let atoms = self.atoms();
|
||||
let check_atom = atoms[_NET_SUPPORTING_WM_CHECK];
|
||||
let wm_name_atom = atoms[_NET_WM_NAME];
|
||||
|
||||
// Mutter/Muffin/Budgie doesn't have _NET_SUPPORTING_WM_CHECK in its _NET_SUPPORTED, despite
|
||||
// it working and being supported. This has been reported upstream, but due to the
|
||||
// inavailability of time machines, we'll just try to get _NET_SUPPORTING_WM_CHECK
|
||||
// regardless of whether or not the WM claims to support it.
|
||||
//
|
||||
// Blackbox 0.70 also incorrectly reports not supporting this, though that appears to be
|
||||
// fixed in 0.72.
|
||||
// if !supported_hints.contains(&check_atom) {
|
||||
// return None;
|
||||
// }
|
||||
|
||||
// IceWM (1.3.x and earlier) doesn't report supporting _NET_WM_NAME, but will nonetheless
|
||||
// provide us with a value for it. Note that the unofficial 1.4 fork of IceWM works fine.
|
||||
// if !supported_hints.contains(&wm_name_atom) {
|
||||
// return None;
|
||||
// }
|
||||
|
||||
// Of the WMs tested, only xmonad and dwm fail to provide a WM name.
|
||||
|
||||
// Querying this property on the root window will give us the ID of a child window created
|
||||
// by the WM.
|
||||
let root_window_wm_check = {
|
||||
let result = self.get_property::<xproto::Window>(
|
||||
root,
|
||||
check_atom,
|
||||
xproto::Atom::from(xproto::AtomEnum::WINDOW),
|
||||
);
|
||||
|
||||
let wm_check = result.ok().and_then(|wm_check| wm_check.first().cloned());
|
||||
|
||||
wm_check?
|
||||
};
|
||||
|
||||
// Querying the same property on the child window we were given, we should get this child
|
||||
// window's ID again.
|
||||
let child_window_wm_check = {
|
||||
let result = self.get_property::<xproto::Window>(
|
||||
root_window_wm_check.into(),
|
||||
check_atom,
|
||||
xproto::Atom::from(xproto::AtomEnum::WINDOW),
|
||||
);
|
||||
|
||||
let wm_check = result.ok().and_then(|wm_check| wm_check.first().cloned());
|
||||
|
||||
wm_check?
|
||||
};
|
||||
|
||||
// These values should be the same.
|
||||
if root_window_wm_check != child_window_wm_check {
|
||||
return None;
|
||||
}
|
||||
|
||||
// All of that work gives us a window ID that we can get the WM name from.
|
||||
let wm_name = {
|
||||
let atoms = self.atoms();
|
||||
let utf8_string_atom = atoms[UTF8_STRING];
|
||||
|
||||
let result =
|
||||
self.get_property(root_window_wm_check.into(), wm_name_atom, utf8_string_atom);
|
||||
|
||||
// IceWM requires this. IceWM was also the only WM tested that returns a null-terminated
|
||||
// string. For more fun trivia, IceWM is also unique in including version and uname
|
||||
// information in this string (this means you'll have to be careful if you want to match
|
||||
// against it, though).
|
||||
// The unofficial 1.4 fork of IceWM still includes the extra details, but properly
|
||||
// returns a UTF8 string that isn't null-terminated.
|
||||
let no_utf8 = if let Err(ref err) = result {
|
||||
err.is_actual_property_type(xproto::Atom::from(xproto::AtomEnum::STRING))
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
if no_utf8 {
|
||||
self.get_property(
|
||||
root_window_wm_check.into(),
|
||||
wm_name_atom,
|
||||
xproto::Atom::from(xproto::AtomEnum::STRING),
|
||||
)
|
||||
} else {
|
||||
result
|
||||
}
|
||||
}
|
||||
.ok();
|
||||
|
||||
wm_name.and_then(|wm_name| String::from_utf8(wm_name).ok())
|
||||
}
|
||||
}
|
||||
56
winit-x11/src/util/xmodmap.rs
Normal file
56
winit-x11/src/util/xmodmap.rs
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
use std::collections::HashSet;
|
||||
use std::slice;
|
||||
|
||||
use x11_dl::xlib::{KeyCode as XKeyCode, XModifierKeymap};
|
||||
|
||||
// Offsets within XModifierKeymap to each set of keycodes.
|
||||
// We are only interested in Shift, Control, Alt, and Logo.
|
||||
//
|
||||
// There are 8 sets total. The order of keycode sets is:
|
||||
// Shift, Lock, Control, Mod1 (Alt), Mod2, Mod3, Mod4 (Logo), Mod5
|
||||
//
|
||||
// https://tronche.com/gui/x/xlib/input/XSetModifierMapping.html
|
||||
const NUM_MODS: usize = 8;
|
||||
|
||||
/// Track which keys are modifiers, so we can properly replay them when they were filtered.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ModifierKeymap {
|
||||
// Maps keycodes to modifiers
|
||||
modifiers: HashSet<XKeyCode>,
|
||||
}
|
||||
|
||||
impl ModifierKeymap {
|
||||
pub fn new() -> ModifierKeymap {
|
||||
ModifierKeymap::default()
|
||||
}
|
||||
|
||||
pub fn is_modifier(&self, keycode: XKeyCode) -> bool {
|
||||
self.modifiers.contains(&keycode)
|
||||
}
|
||||
|
||||
pub fn reload_from_x_connection(&mut self, xconn: &super::XConnection) {
|
||||
unsafe {
|
||||
let keymap = (xconn.xlib.XGetModifierMapping)(xconn.display);
|
||||
|
||||
if keymap.is_null() {
|
||||
return;
|
||||
}
|
||||
|
||||
self.reset_from_x_keymap(&*keymap);
|
||||
|
||||
(xconn.xlib.XFreeModifiermap)(keymap);
|
||||
}
|
||||
}
|
||||
|
||||
fn reset_from_x_keymap(&mut self, keymap: &XModifierKeymap) {
|
||||
let keys_per_mod = keymap.max_keypermod as usize;
|
||||
|
||||
let keys = unsafe {
|
||||
slice::from_raw_parts(keymap.modifiermap as *const _, keys_per_mod * NUM_MODS)
|
||||
};
|
||||
self.modifiers.clear();
|
||||
for key in keys {
|
||||
self.modifiers.insert(*key);
|
||||
}
|
||||
}
|
||||
}
|
||||
2240
winit-x11/src/window.rs
Normal file
2240
winit-x11/src/window.rs
Normal file
File diff suppressed because it is too large
Load diff
408
winit-x11/src/xdisplay.rs
Normal file
408
winit-x11/src/xdisplay.rs
Normal file
|
|
@ -0,0 +1,408 @@
|
|||
use std::collections::HashMap;
|
||||
use std::error::Error;
|
||||
use std::ffi::c_int;
|
||||
use std::sync::atomic::{AtomicU32, Ordering};
|
||||
use std::sync::{Arc, Mutex, RwLock, RwLockReadGuard};
|
||||
use std::{fmt, ptr};
|
||||
|
||||
use rwh_06::HasDisplayHandle;
|
||||
use winit_core::cursor::CursorIcon;
|
||||
use x11rb::connection::Connection;
|
||||
use x11rb::protocol::randr::ConnectionExt as _;
|
||||
use x11rb::protocol::render;
|
||||
use x11rb::protocol::xproto::{self, ConnectionExt};
|
||||
use x11rb::resource_manager;
|
||||
use x11rb::xcb_ffi::XCBConnection;
|
||||
|
||||
use super::atoms::Atoms;
|
||||
use super::ffi;
|
||||
use super::monitor::MonitorHandle;
|
||||
use crate::event_loop::X11Error;
|
||||
|
||||
/// A connection to an X server.
|
||||
pub struct XConnection {
|
||||
pub xlib: ffi::Xlib,
|
||||
|
||||
// TODO(notgull): I'd like to remove this, but apparently Xlib and Xinput2 are tied together
|
||||
// for some reason.
|
||||
pub xinput2: ffi::XInput2,
|
||||
|
||||
pub display: *mut ffi::Display,
|
||||
|
||||
/// The manager for the XCB connection.
|
||||
///
|
||||
/// The `Option` ensures that we can drop it before we close the `Display`.
|
||||
xcb: Option<XCBConnection>,
|
||||
|
||||
/// The atoms used by `winit`.
|
||||
///
|
||||
/// This is a large structure, so I've elected to Box it to make accessing the fields of
|
||||
/// this struct easier. Feel free to unbox it if you like kicking puppies.
|
||||
atoms: Box<Atoms>,
|
||||
|
||||
/// The index of the default screen.
|
||||
default_screen: usize,
|
||||
|
||||
/// The last timestamp received by this connection.
|
||||
timestamp: AtomicU32,
|
||||
|
||||
/// List of monitor handles.
|
||||
pub monitor_handles: Mutex<Option<Vec<MonitorHandle>>>,
|
||||
|
||||
/// The resource database.
|
||||
database: RwLock<resource_manager::Database>,
|
||||
|
||||
/// RandR version.
|
||||
randr_version: (u32, u32),
|
||||
|
||||
/// Atom for the XSettings screen.
|
||||
xsettings_screen: Option<xproto::Atom>,
|
||||
|
||||
/// XRender format information.
|
||||
render_formats: render::QueryPictFormatsReply,
|
||||
|
||||
pub latest_error: Mutex<Option<XError>>,
|
||||
pub cursor_cache: Mutex<HashMap<Option<CursorIcon>, xproto::Cursor>>,
|
||||
}
|
||||
|
||||
impl HasDisplayHandle for XConnection {
|
||||
fn display_handle(&self) -> Result<rwh_06::DisplayHandle<'_>, rwh_06::HandleError> {
|
||||
let raw = self.raw_display_handle()?;
|
||||
unsafe { Ok(rwh_06::DisplayHandle::borrow_raw(raw)) }
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl Send for XConnection {}
|
||||
unsafe impl Sync for XConnection {}
|
||||
|
||||
pub type XErrorHandler =
|
||||
Option<unsafe extern "C" fn(*mut ffi::Display, *mut ffi::XErrorEvent) -> std::os::raw::c_int>;
|
||||
|
||||
impl XConnection {
|
||||
pub fn new(error_handler: XErrorHandler) -> Result<XConnection, XNotSupported> {
|
||||
// opening the libraries
|
||||
let xlib = ffi::Xlib::open()?;
|
||||
let xlib_xcb = ffi::Xlib_xcb::open()?;
|
||||
let xinput2 = ffi::XInput2::open()?;
|
||||
|
||||
unsafe { (xlib.XInitThreads)() };
|
||||
unsafe { (xlib.XSetErrorHandler)(error_handler) };
|
||||
|
||||
// calling XOpenDisplay
|
||||
let display = unsafe {
|
||||
let display = (xlib.XOpenDisplay)(ptr::null());
|
||||
if display.is_null() {
|
||||
return Err(XNotSupported::XOpenDisplayFailed);
|
||||
}
|
||||
display
|
||||
};
|
||||
|
||||
// Open the x11rb XCB connection.
|
||||
let xcb = {
|
||||
// Get a pointer to the underlying XCB connection
|
||||
let xcb_connection =
|
||||
unsafe { (xlib_xcb.XGetXCBConnection)(display as *mut ffi::Display) };
|
||||
assert!(!xcb_connection.is_null());
|
||||
|
||||
// Wrap the XCB connection in an x11rb XCB connection
|
||||
let conn =
|
||||
unsafe { XCBConnection::from_raw_xcb_connection(xcb_connection.cast(), false) };
|
||||
|
||||
conn.map_err(|e| XNotSupported::XcbConversionError(Arc::new(WrapConnectError(e))))?
|
||||
};
|
||||
|
||||
// Get the default screen.
|
||||
let default_screen = unsafe { (xlib.XDefaultScreen)(display) } as usize;
|
||||
|
||||
// Load the database.
|
||||
let database = resource_manager::new_from_default(&xcb)
|
||||
.map_err(|e| XNotSupported::XcbConversionError(Arc::new(e)))?;
|
||||
|
||||
// Load the RandR version.
|
||||
let randr_version = xcb
|
||||
.randr_query_version(1, 3)
|
||||
.expect("failed to request XRandR version")
|
||||
.reply()
|
||||
.expect("failed to query XRandR version");
|
||||
|
||||
let xsettings_screen = Self::new_xsettings_screen(&xcb, default_screen);
|
||||
if xsettings_screen.is_none() {
|
||||
tracing::warn!("error setting XSETTINGS; Xft options won't reload automatically")
|
||||
}
|
||||
|
||||
// Start getting the XRender formats.
|
||||
let formats_cookie = render::query_pict_formats(&xcb)
|
||||
.map_err(|e| XNotSupported::XcbConversionError(Arc::new(e)))?;
|
||||
|
||||
// Fetch atoms.
|
||||
let atoms = Atoms::new(&xcb)
|
||||
.map_err(|e| XNotSupported::XcbConversionError(Arc::new(e)))?
|
||||
.reply()
|
||||
.map_err(|e| XNotSupported::XcbConversionError(Arc::new(e)))?;
|
||||
|
||||
// Finish getting everything else.
|
||||
let formats =
|
||||
formats_cookie.reply().map_err(|e| XNotSupported::XcbConversionError(Arc::new(e)))?;
|
||||
|
||||
Ok(XConnection {
|
||||
xlib,
|
||||
xinput2,
|
||||
display,
|
||||
xcb: Some(xcb),
|
||||
atoms: Box::new(atoms),
|
||||
default_screen,
|
||||
timestamp: AtomicU32::new(0),
|
||||
latest_error: Mutex::new(None),
|
||||
monitor_handles: Mutex::new(None),
|
||||
database: RwLock::new(database),
|
||||
cursor_cache: Default::default(),
|
||||
randr_version: (randr_version.major_version, randr_version.minor_version),
|
||||
render_formats: formats,
|
||||
xsettings_screen,
|
||||
})
|
||||
}
|
||||
|
||||
fn new_xsettings_screen(xcb: &XCBConnection, default_screen: usize) -> Option<xproto::Atom> {
|
||||
// Fetch the _XSETTINGS_S[screen number] atom.
|
||||
let xsettings_screen = xcb
|
||||
.intern_atom(false, format!("_XSETTINGS_S{default_screen}").as_bytes())
|
||||
.ok()?
|
||||
.reply()
|
||||
.ok()?
|
||||
.atom;
|
||||
|
||||
// Get PropertyNotify events from the XSETTINGS window.
|
||||
// TODO: The XSETTINGS window here can change. In the future, listen for DestroyNotify on
|
||||
// this window in order to accommodate for a changed window here.
|
||||
let selector_window = xcb.get_selection_owner(xsettings_screen).ok()?.reply().ok()?.owner;
|
||||
|
||||
xcb.change_window_attributes(
|
||||
selector_window,
|
||||
&xproto::ChangeWindowAttributesAux::new()
|
||||
.event_mask(xproto::EventMask::PROPERTY_CHANGE),
|
||||
)
|
||||
.ok()?
|
||||
.check()
|
||||
.ok()?;
|
||||
|
||||
Some(xsettings_screen)
|
||||
}
|
||||
|
||||
/// Checks whether an error has been triggered by the previous function calls.
|
||||
#[inline]
|
||||
pub fn check_errors(&self) -> Result<(), XError> {
|
||||
let error = self.latest_error.lock().unwrap().take();
|
||||
if let Some(error) = error {
|
||||
Err(error)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn randr_version(&self) -> (u32, u32) {
|
||||
self.randr_version
|
||||
}
|
||||
|
||||
/// Get the underlying XCB connection.
|
||||
#[inline]
|
||||
pub fn xcb_connection(&self) -> &XCBConnection {
|
||||
self.xcb.as_ref().expect("xcb_connection somehow called after drop?")
|
||||
}
|
||||
|
||||
/// Get the list of atoms.
|
||||
#[inline]
|
||||
pub fn atoms(&self) -> &Atoms {
|
||||
&self.atoms
|
||||
}
|
||||
|
||||
/// Get the index of the default screen.
|
||||
#[inline]
|
||||
pub fn default_screen_index(&self) -> usize {
|
||||
self.default_screen
|
||||
}
|
||||
|
||||
/// Get the default screen.
|
||||
#[inline]
|
||||
pub fn default_root(&self) -> &xproto::Screen {
|
||||
&self.xcb_connection().setup().roots[self.default_screen]
|
||||
}
|
||||
|
||||
/// Get the resource database.
|
||||
#[inline]
|
||||
pub fn database(&self) -> RwLockReadGuard<'_, resource_manager::Database> {
|
||||
self.database.read().unwrap_or_else(|e| e.into_inner())
|
||||
}
|
||||
|
||||
/// Reload the resource database.
|
||||
#[inline]
|
||||
pub fn reload_database(&self) -> Result<(), X11Error> {
|
||||
let database = resource_manager::new_from_default(self.xcb_connection())?;
|
||||
*self.database.write().unwrap_or_else(|e| e.into_inner()) = database;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the latest timestamp.
|
||||
#[inline]
|
||||
pub fn timestamp(&self) -> u32 {
|
||||
self.timestamp.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// Set the last witnessed timestamp.
|
||||
#[inline]
|
||||
pub fn set_timestamp(&self, timestamp: u32) {
|
||||
// Store the timestamp in the slot if it's greater than the last one.
|
||||
let mut last_timestamp = self.timestamp.load(Ordering::Relaxed);
|
||||
loop {
|
||||
let wrapping_sub = |a: xproto::Timestamp, b: xproto::Timestamp| (a as i32) - (b as i32);
|
||||
|
||||
if wrapping_sub(timestamp, last_timestamp) <= 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
match self.timestamp.compare_exchange(
|
||||
last_timestamp,
|
||||
timestamp,
|
||||
Ordering::Relaxed,
|
||||
Ordering::Relaxed,
|
||||
) {
|
||||
Ok(_) => break,
|
||||
Err(x) => last_timestamp = x,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the atom for Xsettings.
|
||||
#[inline]
|
||||
pub fn xsettings_screen(&self) -> Option<xproto::Atom> {
|
||||
self.xsettings_screen
|
||||
}
|
||||
|
||||
/// Get the data containing our rendering formats.
|
||||
#[inline]
|
||||
pub fn render_formats(&self) -> &render::QueryPictFormatsReply {
|
||||
&self.render_formats
|
||||
}
|
||||
|
||||
/// Do we need to do an endian swap?
|
||||
#[inline]
|
||||
pub fn needs_endian_swap(&self) -> bool {
|
||||
#[cfg(target_endian = "big")]
|
||||
let endian = xproto::ImageOrder::MSB_FIRST;
|
||||
#[cfg(not(target_endian = "big"))]
|
||||
let endian = xproto::ImageOrder::LSB_FIRST;
|
||||
|
||||
self.xcb_connection().setup().image_byte_order != endian
|
||||
}
|
||||
|
||||
pub fn raw_display_handle(&self) -> Result<rwh_06::RawDisplayHandle, rwh_06::HandleError> {
|
||||
let display_handle = rwh_06::XlibDisplayHandle::new(
|
||||
// SAFETY: display will never be null
|
||||
Some(
|
||||
std::ptr::NonNull::new(self.display as *mut _)
|
||||
.expect("X11 display should never be null"),
|
||||
),
|
||||
self.default_screen_index() as c_int,
|
||||
);
|
||||
|
||||
Ok(display_handle.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for XConnection {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
self.display.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for XConnection {
|
||||
#[inline]
|
||||
fn drop(&mut self) {
|
||||
self.xcb = None;
|
||||
unsafe { (self.xlib.XCloseDisplay)(self.display) };
|
||||
}
|
||||
}
|
||||
|
||||
/// Error triggered by xlib.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct XError {
|
||||
pub description: String,
|
||||
pub error_code: u8,
|
||||
pub request_code: u8,
|
||||
pub minor_code: u8,
|
||||
}
|
||||
|
||||
impl Error for XError {}
|
||||
|
||||
impl fmt::Display for XError {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
|
||||
write!(
|
||||
formatter,
|
||||
"X error: {} (code: {}, request code: {}, minor code: {})",
|
||||
self.description, self.error_code, self.request_code, self.minor_code
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Error returned if this system doesn't have XLib or can't create an X connection.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum XNotSupported {
|
||||
/// Failed to load one or several shared libraries.
|
||||
LibraryOpenError(ffi::OpenError),
|
||||
|
||||
/// Connecting to the X server with `XOpenDisplay` failed.
|
||||
XOpenDisplayFailed, // TODO: add better message.
|
||||
|
||||
/// We encountered an error while converting the connection to XCB.
|
||||
XcbConversionError(Arc<dyn Error + Send + Sync + 'static>),
|
||||
}
|
||||
|
||||
impl From<ffi::OpenError> for XNotSupported {
|
||||
#[inline]
|
||||
fn from(err: ffi::OpenError) -> XNotSupported {
|
||||
XNotSupported::LibraryOpenError(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl XNotSupported {
|
||||
fn description(&self) -> &'static str {
|
||||
match self {
|
||||
XNotSupported::LibraryOpenError(_) => "Failed to load one of xlib's shared libraries",
|
||||
XNotSupported::XOpenDisplayFailed => "Failed to open connection to X server",
|
||||
XNotSupported::XcbConversionError(_) => "Failed to convert Xlib connection to XCB",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for XNotSupported {
|
||||
#[inline]
|
||||
fn source(&self) -> Option<&(dyn Error + 'static)> {
|
||||
match *self {
|
||||
XNotSupported::LibraryOpenError(ref err) => Some(err),
|
||||
XNotSupported::XcbConversionError(ref err) => Some(&**err),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for XNotSupported {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
|
||||
formatter.write_str(self.description())
|
||||
}
|
||||
}
|
||||
|
||||
/// A newtype wrapper around a `ConnectError` that can't be accessed by downstream libraries.
|
||||
///
|
||||
/// Without this, `x11rb` would become a public dependency.
|
||||
#[derive(Debug)]
|
||||
struct WrapConnectError(x11rb::rust_connection::ConnectError);
|
||||
|
||||
impl fmt::Display for WrapConnectError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
fmt::Display::fmt(&self.0, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for WrapConnectError {
|
||||
// We can't implement `source()` here or otherwise risk exposing `x11rb`.
|
||||
}
|
||||
326
winit-x11/src/xsettings.rs
Normal file
326
winit-x11/src/xsettings.rs
Normal file
|
|
@ -0,0 +1,326 @@
|
|||
//! Parser for the xsettings data format.
|
||||
//!
|
||||
//! Some of this code is referenced from [here].
|
||||
//!
|
||||
//! [here]: https://github.com/derat/xsettingsd
|
||||
|
||||
use std::iter;
|
||||
use std::num::NonZeroUsize;
|
||||
|
||||
use x11rb::protocol::xproto::{self, ConnectionExt};
|
||||
|
||||
use super::atoms::*;
|
||||
use crate::event_loop::X11Error;
|
||||
use crate::xdisplay::XConnection;
|
||||
|
||||
type Result<T> = core::result::Result<T, ParserError>;
|
||||
|
||||
const DPI_NAME: &[u8] = b"Xft/DPI";
|
||||
const DPI_MULTIPLIER: f64 = 1024.0;
|
||||
const LITTLE_ENDIAN: u8 = b'l';
|
||||
const BIG_ENDIAN: u8 = b'B';
|
||||
|
||||
impl XConnection {
|
||||
/// Get the DPI from XSettings.
|
||||
pub(crate) fn xsettings_dpi(
|
||||
&self,
|
||||
xsettings_screen: xproto::Atom,
|
||||
) -> core::result::Result<Option<f64>, X11Error> {
|
||||
let atoms = self.atoms();
|
||||
|
||||
// Get the current owner of the screen's settings.
|
||||
let owner = self.xcb_connection().get_selection_owner(xsettings_screen)?.reply()?;
|
||||
|
||||
// Read the _XSETTINGS_SETTINGS property.
|
||||
let data: Vec<u8> =
|
||||
self.get_property(owner.owner, atoms[_XSETTINGS_SETTINGS], atoms[_XSETTINGS_SETTINGS])?;
|
||||
|
||||
// Parse the property.
|
||||
let dpi_setting = read_settings(&data)?
|
||||
.find(|res| res.as_ref().map_or(true, |s| s.name == DPI_NAME))
|
||||
.transpose()?;
|
||||
if let Some(dpi_setting) = dpi_setting {
|
||||
let base_dpi = match dpi_setting.data {
|
||||
SettingData::Integer(dpi) => dpi as f64,
|
||||
SettingData::String(_) => {
|
||||
return Err(ParserError::BadType(SettingType::String).into())
|
||||
},
|
||||
SettingData::Color(_) => {
|
||||
return Err(ParserError::BadType(SettingType::Color).into())
|
||||
},
|
||||
};
|
||||
|
||||
Ok(Some(base_dpi / DPI_MULTIPLIER))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Read over the settings in the block of data.
|
||||
fn read_settings(data: &[u8]) -> Result<impl Iterator<Item = Result<Setting<'_>>> + '_> {
|
||||
// Create a parser. This automatically parses the first 8 bytes for metadata.
|
||||
let mut parser = Parser::new(data)?;
|
||||
|
||||
// Read the total number of settings.
|
||||
let total_settings = parser.i32()?;
|
||||
|
||||
// Iterate over the settings.
|
||||
let iter = iter::repeat_with(move || Setting::parse(&mut parser)).take(total_settings as usize);
|
||||
Ok(iter)
|
||||
}
|
||||
|
||||
/// A setting in the settings list.
|
||||
struct Setting<'a> {
|
||||
/// The name of the setting.
|
||||
name: &'a [u8],
|
||||
|
||||
/// The data contained in the setting.
|
||||
data: SettingData<'a>,
|
||||
}
|
||||
|
||||
/// The data contained in a setting.
|
||||
enum SettingData<'a> {
|
||||
Integer(i32),
|
||||
String(#[allow(dead_code)] &'a [u8]),
|
||||
Color(#[allow(dead_code)] [i16; 4]),
|
||||
}
|
||||
|
||||
impl<'a> Setting<'a> {
|
||||
/// Parse a new `SettingData`.
|
||||
fn parse(parser: &mut Parser<'a>) -> Result<Self> {
|
||||
// Read the type.
|
||||
let ty: SettingType = parser.i8()?.try_into()?;
|
||||
|
||||
// Read another byte of padding.
|
||||
parser.advance(1)?;
|
||||
|
||||
// Read the name of the setting.
|
||||
let name_len = parser.i16()?;
|
||||
let name = parser.advance(name_len as usize)?;
|
||||
parser.pad(name.len(), 4)?;
|
||||
|
||||
// Ignore the serial number.
|
||||
parser.advance(4)?;
|
||||
|
||||
let data = match ty {
|
||||
SettingType::Integer => {
|
||||
// Read a 32-bit integer.
|
||||
SettingData::Integer(parser.i32()?)
|
||||
},
|
||||
|
||||
SettingType::String => {
|
||||
// Read the data.
|
||||
let data_len = parser.i32()?;
|
||||
let data = parser.advance(data_len as usize)?;
|
||||
parser.pad(data.len(), 4)?;
|
||||
|
||||
SettingData::String(data)
|
||||
},
|
||||
|
||||
SettingType::Color => {
|
||||
// Read i16's of color.
|
||||
let (red, blue, green, alpha) =
|
||||
(parser.i16()?, parser.i16()?, parser.i16()?, parser.i16()?);
|
||||
|
||||
SettingData::Color([red, blue, green, alpha])
|
||||
},
|
||||
};
|
||||
|
||||
Ok(Setting { name, data })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum SettingType {
|
||||
Integer = 0,
|
||||
String = 1,
|
||||
Color = 2,
|
||||
}
|
||||
|
||||
impl TryFrom<i8> for SettingType {
|
||||
type Error = ParserError;
|
||||
|
||||
fn try_from(value: i8) -> Result<Self> {
|
||||
Ok(match value {
|
||||
0 => Self::Integer,
|
||||
1 => Self::String,
|
||||
2 => Self::Color,
|
||||
x => return Err(ParserError::InvalidType(x)),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Parser for the incoming byte stream.
|
||||
struct Parser<'a> {
|
||||
bytes: &'a [u8],
|
||||
endianness: Endianness,
|
||||
}
|
||||
|
||||
impl<'a> Parser<'a> {
|
||||
/// Create a new parser.
|
||||
fn new(bytes: &'a [u8]) -> Result<Self> {
|
||||
let (endianness, bytes) = bytes.split_first().ok_or_else(|| ParserError::ran_out(1, 0))?;
|
||||
let endianness = match *endianness {
|
||||
BIG_ENDIAN => Endianness::Big,
|
||||
LITTLE_ENDIAN => Endianness::Little,
|
||||
_ => Endianness::native(),
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
// Ignore three bytes of padding and the four-byte serial.
|
||||
bytes: bytes.get(7..).ok_or_else(|| ParserError::ran_out(7, bytes.len()))?,
|
||||
endianness,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get a slice of bytes.
|
||||
fn advance(&mut self, n: usize) -> Result<&'a [u8]> {
|
||||
if n == 0 {
|
||||
return Ok(&[]);
|
||||
}
|
||||
|
||||
if n > self.bytes.len() {
|
||||
Err(ParserError::ran_out(n, self.bytes.len()))
|
||||
} else {
|
||||
let (part, rem) = self.bytes.split_at(n);
|
||||
self.bytes = rem;
|
||||
Ok(part)
|
||||
}
|
||||
}
|
||||
|
||||
/// Skip some padding.
|
||||
fn pad(&mut self, size: usize, pad: usize) -> Result<()> {
|
||||
let advance = (pad - (size % pad)) % pad;
|
||||
self.advance(advance)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get a single byte.
|
||||
fn i8(&mut self) -> Result<i8> {
|
||||
self.advance(1).map(|s| s[0] as i8)
|
||||
}
|
||||
|
||||
/// Get two bytes.
|
||||
fn i16(&mut self) -> Result<i16> {
|
||||
self.advance(2).map(|s| {
|
||||
let bytes: &[u8; 2] = s.try_into().unwrap();
|
||||
match self.endianness {
|
||||
Endianness::Big => i16::from_be_bytes(*bytes),
|
||||
Endianness::Little => i16::from_le_bytes(*bytes),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Get four bytes.
|
||||
fn i32(&mut self) -> Result<i32> {
|
||||
self.advance(4).map(|s| {
|
||||
let bytes: &[u8; 4] = s.try_into().unwrap();
|
||||
match self.endianness {
|
||||
Endianness::Big => i32::from_be_bytes(*bytes),
|
||||
Endianness::Little => i32::from_le_bytes(*bytes),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Endianness of the incoming data.
|
||||
enum Endianness {
|
||||
Little,
|
||||
Big,
|
||||
}
|
||||
|
||||
impl Endianness {
|
||||
#[cfg(target_endian = "little")]
|
||||
fn native() -> Self {
|
||||
Endianness::Little
|
||||
}
|
||||
|
||||
#[cfg(target_endian = "big")]
|
||||
fn native() -> Self {
|
||||
Endianness::Big
|
||||
}
|
||||
}
|
||||
|
||||
/// Parser errors.
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub enum ParserError {
|
||||
/// Ran out of bytes.
|
||||
NoMoreBytes { expected: NonZeroUsize, found: usize },
|
||||
|
||||
/// Invalid type.
|
||||
InvalidType(i8),
|
||||
|
||||
/// Bad setting type.
|
||||
BadType(SettingType),
|
||||
}
|
||||
|
||||
impl ParserError {
|
||||
fn ran_out(expected: usize, found: usize) -> ParserError {
|
||||
let expected = NonZeroUsize::new(expected).unwrap();
|
||||
Self::NoMoreBytes { expected, found }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
/// Tests for the XSETTINGS parser.
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const XSETTINGS: &str = include_str!("tests/xsettings.dat");
|
||||
|
||||
#[test]
|
||||
fn empty() {
|
||||
let err = match read_settings(&[]) {
|
||||
Ok(_) => panic!(),
|
||||
Err(err) => err,
|
||||
};
|
||||
match err {
|
||||
ParserError::NoMoreBytes { expected, found } => {
|
||||
assert_eq!(expected.get(), 1);
|
||||
assert_eq!(found, 0);
|
||||
},
|
||||
|
||||
_ => panic!(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_xsettings() {
|
||||
let data = XSETTINGS
|
||||
.trim()
|
||||
.split(',')
|
||||
.map(|tok| {
|
||||
let val = tok.strip_prefix("0x").unwrap();
|
||||
u8::from_str_radix(val, 16).unwrap()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let settings = read_settings(&data).unwrap().collect::<Result<Vec<_>>>().unwrap();
|
||||
|
||||
let dpi = settings.iter().find(|s| s.name == b"Xft/DPI").unwrap();
|
||||
assert_int(&dpi.data, 96 * 1024);
|
||||
let hinting = settings.iter().find(|s| s.name == b"Xft/Hinting").unwrap();
|
||||
assert_int(&hinting.data, 1);
|
||||
|
||||
let rgba = settings.iter().find(|s| s.name == b"Xft/RGBA").unwrap();
|
||||
assert_string(&rgba.data, "rgb");
|
||||
let lcd = settings.iter().find(|s| s.name == b"Xft/Lcdfilter").unwrap();
|
||||
assert_string(&lcd.data, "lcddefault");
|
||||
}
|
||||
|
||||
fn assert_string(dat: &SettingData<'_>, s: &str) {
|
||||
match dat {
|
||||
SettingData::String(left) => assert_eq!(*left, s.as_bytes()),
|
||||
_ => panic!("invalid data type"),
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_int(dat: &SettingData<'_>, i: i32) {
|
||||
match dat {
|
||||
SettingData::Integer(left) => assert_eq!(*left, i),
|
||||
_ => panic!("invalid data type"),
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue