x11: Improve error handling (#32913)

Continuing this work from a while back in #21079, now greatly aided by
agent + sonnet 4. With this change, there are now only a few spots that
explicitly panic, though errors during initialization will panic.

Motivation was this recent user panic in `handle_event`, figured fixing
all this use of unwrap was a great use of the agent.

> called `Result::unwrap()` on an `Err` value: X11 GetProperty for
_NET_WM_STATE failed.

Release Notes:

- N/A
This commit is contained in:
Michael Sloan 2025-06-17 19:40:17 -06:00 committed by GitHub
parent 90aa99bb14
commit aa1b2d74ee
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 407 additions and 304 deletions

View file

@ -93,6 +93,9 @@ pub(crate) fn current_platform(headless: bool) -> Rc<dyn Platform> {
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
pub(crate) fn current_platform(headless: bool) -> Rc<dyn Platform> {
#[cfg(feature = "x11")]
use anyhow::Context as _;
if headless {
return Rc::new(HeadlessClient::new());
}
@ -102,7 +105,11 @@ pub(crate) fn current_platform(headless: bool) -> Rc<dyn Platform> {
"Wayland" => Rc::new(WaylandClient::new()),
#[cfg(feature = "x11")]
"X11" => Rc::new(X11Client::new()),
"X11" => Rc::new(
X11Client::new()
.context("Failed to initialize X11 client.")
.unwrap(),
),
"Headless" => Rc::new(HeadlessClient::new()),
_ => unreachable!(),

View file

@ -17,6 +17,7 @@ use calloop::{
use collections::HashMap;
use futures::channel::oneshot;
use http_client::Url;
use log::Level;
use smallvec::SmallVec;
use util::ResultExt;
@ -41,12 +42,12 @@ use xkbc::x11::ffi::{XKB_X11_MIN_MAJOR_XKB_VERSION, XKB_X11_MIN_MINOR_XKB_VERSIO
use xkbcommon::xkb::{self as xkbc, LayoutIndex, ModMask, STATE_LAYOUT_EFFECTIVE};
use super::{
ButtonOrScroll, ScrollDirection, button_or_scroll_from_event_detail,
ButtonOrScroll, ScrollDirection, X11Display, X11WindowStatePtr, XcbAtoms, XimCallbackEvent,
XimHandler, button_or_scroll_from_event_detail, check_reply,
clipboard::{self, Clipboard},
get_valuator_axis_index, modifiers_from_state, pressed_button_from_mask,
get_reply, get_valuator_axis_index, handle_connection_error, modifiers_from_state,
pressed_button_from_mask,
};
use super::{X11Display, X11WindowStatePtr, XcbAtoms};
use super::{XimCallbackEvent, XimHandler};
use crate::platform::{
LinuxCommon, PlatformWindow,
@ -230,12 +231,14 @@ pub struct X11ClientState {
pub struct X11ClientStatePtr(pub Weak<RefCell<X11ClientState>>);
impl X11ClientStatePtr {
fn get_client(&self) -> X11Client {
X11Client(self.0.upgrade().expect("client already dropped"))
fn get_client(&self) -> Option<X11Client> {
self.0.upgrade().map(X11Client)
}
pub fn drop_window(&self, x_window: u32) {
let client = self.get_client();
let Some(client) = self.get_client() else {
return;
};
let mut state = client.0.borrow_mut();
if let Some(window_ref) = state.windows.remove(&x_window) {
@ -262,14 +265,23 @@ impl X11ClientStatePtr {
}
pub fn update_ime_position(&self, bounds: Bounds<ScaledPixels>) {
let client = self.get_client();
let Some(client) = self.get_client() else {
return;
};
let mut state = client.0.borrow_mut();
if state.composing || state.ximc.is_none() {
return;
}
let mut ximc = state.ximc.take().unwrap();
let xim_handler = state.xim_handler.take().unwrap();
let Some(mut ximc) = state.ximc.take() else {
log::error!("bug: xim connection not set");
return;
};
let Some(xim_handler) = state.xim_handler.take() else {
log::error!("bug: xim handler not set");
state.ximc = Some(ximc);
return;
};
let ic_attributes = ximc
.build_ic_attributes()
.push(
@ -300,8 +312,8 @@ impl X11ClientStatePtr {
pub(crate) struct X11Client(Rc<RefCell<X11ClientState>>);
impl X11Client {
pub(crate) fn new() -> Self {
let event_loop = EventLoop::try_new().unwrap();
pub(crate) fn new() -> anyhow::Result<Self> {
let event_loop = EventLoop::try_new()?;
let (common, main_receiver) = LinuxCommon::new(event_loop.get_signal());
@ -321,39 +333,34 @@ impl X11Client {
}
}
})
.unwrap();
.map_err(|err| {
anyhow!("Failed to initialize event loop handling of foreground tasks: {err:?}")
})?;
let (xcb_connection, x_root_index) = XCBConnection::connect(None).unwrap();
xcb_connection
.prefetch_extension_information(xkb::X11_EXTENSION_NAME)
.unwrap();
xcb_connection
.prefetch_extension_information(randr::X11_EXTENSION_NAME)
.unwrap();
xcb_connection
.prefetch_extension_information(render::X11_EXTENSION_NAME)
.unwrap();
xcb_connection
.prefetch_extension_information(xinput::X11_EXTENSION_NAME)
.unwrap();
let (xcb_connection, x_root_index) = XCBConnection::connect(None)?;
xcb_connection.prefetch_extension_information(xkb::X11_EXTENSION_NAME)?;
xcb_connection.prefetch_extension_information(randr::X11_EXTENSION_NAME)?;
xcb_connection.prefetch_extension_information(render::X11_EXTENSION_NAME)?;
xcb_connection.prefetch_extension_information(xinput::X11_EXTENSION_NAME)?;
// Announce to X server that XInput up to 2.1 is supported. To increase this to 2.2 and
// beyond, support for touch events would need to be added.
let xinput_version = xcb_connection
.xinput_xi_query_version(2, 1)
.unwrap()
.reply()
.unwrap();
// XInput 1.x is not supported.
let xinput_version = get_reply(
|| "XInput XiQueryVersion failed",
xcb_connection.xinput_xi_query_version(2, 1),
)?;
assert!(
xinput_version.major_version >= 2,
"XInput version >= 2 required."
);
let pointer_device_states =
get_new_pointer_device_states(&xcb_connection, &BTreeMap::new());
current_pointer_device_states(&xcb_connection, &BTreeMap::new()).unwrap_or_default();
let atoms = XcbAtoms::new(&xcb_connection).unwrap().reply().unwrap();
let atoms = XcbAtoms::new(&xcb_connection)
.context("Failed to get XCB atoms")?
.reply()
.context("Failed to get XCB atoms")?;
let root = xcb_connection.setup().roots[0].root;
let compositor_present = check_compositor_present(&xcb_connection, root);
@ -366,11 +373,11 @@ impl X11Client {
gtk_frame_extents_supported
);
let xkb = xcb_connection
.xkb_use_extension(XKB_X11_MIN_MAJOR_XKB_VERSION, XKB_X11_MIN_MINOR_XKB_VERSION)
.unwrap()
.reply()
.unwrap();
let xkb = get_reply(
|| "Failed to initialize XKB extension",
xcb_connection
.xkb_use_extension(XKB_X11_MIN_MAJOR_XKB_VERSION, XKB_X11_MIN_MINOR_XKB_VERSION),
)?;
let events = xkb::EventType::STATE_NOTIFY
| xkb::EventType::MAP_NOTIFY
@ -383,16 +390,17 @@ impl X11Client {
| xkb::MapPart::KEY_BEHAVIORS
| xkb::MapPart::VIRTUAL_MODS
| xkb::MapPart::VIRTUAL_MOD_MAP;
xcb_connection
.xkb_select_events(
check_reply(
|| "Failed to select XKB events",
xcb_connection.xkb_select_events(
xkb::ID::USE_CORE_KBD.into(),
0u8.into(),
events,
map_notify_parts,
map_notify_parts,
&xkb::SelectEventsAux::new(),
)
.unwrap();
),
)?;
assert!(xkb.supported);
let xkb_context = xkbc::Context::new(xkbc::CONTEXT_NO_FLAGS);
@ -414,9 +422,10 @@ impl X11Client {
.to_string();
let keyboard_layout = LinuxKeyboardLayout::new(layout_name.into());
let gpu_context = BladeContext::new().expect("Unable to init GPU context");
let gpu_context = BladeContext::new().context("Unable to init GPU context")?;
let resource_database = x11rb::resource_manager::new_from_default(&xcb_connection).unwrap();
let resource_database = x11rb::resource_manager::new_from_default(&xcb_connection)
.context("Failed to create resource database")?;
let scale_factor = resource_database
.get_value("Xft.dpi", "Xft.dpi")
.ok()
@ -424,11 +433,11 @@ impl X11Client {
.map(|dpi: f32| dpi / 96.0)
.unwrap_or(1.0);
let cursor_handle = cursor::Handle::new(&xcb_connection, x_root_index, &resource_database)
.unwrap()
.context("Failed to initialize cursor theme handler")?
.reply()
.unwrap();
.context("Failed to initialize cursor theme handler")?;
let clipboard = Clipboard::new().unwrap();
let clipboard = Clipboard::new().context("Failed to initialize clipboard")?;
let xcb_connection = Rc::new(xcb_connection);
@ -457,7 +466,7 @@ impl X11Client {
}
},
)
.expect("Failed to initialize x11 event source");
.map_err(|err| anyhow!("Failed to initialize X11 event source: {err:?}"))?;
handle
.insert_source(XDPEventSource::new(&common.background_executor), {
@ -473,9 +482,9 @@ impl X11Client {
}
}
})
.unwrap();
.map_err(|err| anyhow!("Failed to initialize XDP event source: {err:?}"))?;
X11Client(Rc::new(RefCell::new(X11ClientState {
Ok(X11Client(Rc::new(RefCell::new(X11ClientState {
modifiers: Modifiers::default(),
capslock: Capslock::default(),
last_modifiers_changed_event: Modifiers::default(),
@ -520,7 +529,7 @@ impl X11Client {
clipboard,
clipboard_item: None,
xdnd_state: Xdnd::default(),
})))
}))))
}
pub fn process_x11_events(
@ -606,8 +615,9 @@ impl X11Client {
Ok(None) => {
break;
}
Err(e) => {
log::warn!("error polling for X11 events: {e:?}");
Err(err) => {
let err = handle_connection_error(err);
log::warn!("error while polling for X11 events: {err:?}");
break;
}
}
@ -633,14 +643,15 @@ impl X11Client {
for event in events.into_iter() {
let mut state = self.0.borrow_mut();
if state.ximc.is_none() || state.xim_handler.is_none() {
if !state.has_xim() {
drop(state);
self.handle_event(event);
continue;
}
let mut ximc = state.ximc.take().unwrap();
let mut xim_handler = state.xim_handler.take().unwrap();
let Some((mut ximc, mut xim_handler)) = state.take_xim() else {
continue;
};
let xim_connected = xim_handler.connected;
drop(state);
@ -654,8 +665,7 @@ impl X11Client {
let xim_callback_event = xim_handler.last_callback_event.take();
let mut state = self.0.borrow_mut();
state.ximc = Some(ximc);
state.xim_handler = Some(xim_handler);
state.restore_xim(ximc, xim_handler);
drop(state);
if let Some(event) = xim_callback_event {
@ -678,12 +688,13 @@ impl X11Client {
pub fn enable_ime(&self) {
let mut state = self.0.borrow_mut();
if state.ximc.is_none() {
if !state.has_xim() {
return;
}
let mut ximc = state.ximc.take().unwrap();
let mut xim_handler = state.xim_handler.take().unwrap();
let Some((mut ximc, mut xim_handler)) = state.take_xim() else {
return;
};
let mut ic_attributes = ximc
.build_ic_attributes()
.push(AttributeName::InputStyle, InputStyle::PREEDIT_CALLBACKS)
@ -693,7 +704,13 @@ impl X11Client {
let window_id = state.keyboard_focused_window;
drop(state);
if let Some(window_id) = window_id {
let window = self.get_window(window_id).unwrap();
let Some(window) = self.get_window(window_id) else {
log::error!("Failed to get window for IME positioning");
let mut state = self.0.borrow_mut();
state.ximc = Some(ximc);
state.xim_handler = Some(xim_handler);
return;
};
if let Some(area) = window.get_ime_area() {
ic_attributes =
ic_attributes.nested_list(xim::AttributeName::PreeditAttributes, |b| {
@ -709,17 +726,19 @@ impl X11Client {
}
ximc.create_ic(xim_handler.im_id, ic_attributes.build())
.ok();
state = self.0.borrow_mut();
state.xim_handler = Some(xim_handler);
state.ximc = Some(ximc);
let mut state = self.0.borrow_mut();
state.restore_xim(ximc, xim_handler);
}
pub fn reset_ime(&self) {
let mut state = self.0.borrow_mut();
state.composing = false;
if let Some(mut ximc) = state.ximc.take() {
let xim_handler = state.xim_handler.as_ref().unwrap();
ximc.reset_ic(xim_handler.im_id, xim_handler.ic_id).ok();
if let Some(xim_handler) = state.xim_handler.as_ref() {
ximc.reset_ic(xim_handler.im_id, xim_handler.ic_id).ok();
} else {
log::error!("bug: xim handler not set in reset_ime");
}
state.ximc = Some(ximc);
}
}
@ -799,26 +818,25 @@ impl X11Client {
window.handle_input(PlatformInput::FileDrop(FileDropEvent::Exited {}));
self.0.borrow_mut().xdnd_state = Xdnd::default();
} else if event.type_ == state.atoms.XdndPosition {
if let Ok(pos) = state
.xcb_connection
.query_pointer(event.window)
.unwrap()
.reply()
{
if let Ok(pos) = get_reply(
|| "Failed to query pointer position",
state.xcb_connection.query_pointer(event.window),
) {
state.xdnd_state.position =
Point::new(Pixels(pos.win_x as f32), Pixels(pos.win_y as f32));
}
if !state.xdnd_state.retrieved {
state
.xcb_connection
.convert_selection(
check_reply(
|| "Failed to convert selection for drag and drop",
state.xcb_connection.convert_selection(
event.window,
state.atoms.XdndSelection,
state.xdnd_state.drag_type,
state.atoms.XDND_DATA,
arg3,
)
.unwrap();
),
)
.log_err();
}
xdnd_send_status(
&state.xcb_connection,
@ -848,35 +866,37 @@ impl X11Client {
Event::SelectionNotify(event) => {
let window = self.get_window(event.requestor)?;
let mut state = self.0.borrow_mut();
let property = state.xcb_connection.get_property(
false,
event.requestor,
state.atoms.XDND_DATA,
AtomEnum::ANY,
0,
1024,
);
if property.as_ref().log_err().is_none() {
let reply = get_reply(
|| "Failed to get XDND_DATA",
state.xcb_connection.get_property(
false,
event.requestor,
state.atoms.XDND_DATA,
AtomEnum::ANY,
0,
1024,
),
)
.log_err();
let Some(reply) = reply else {
return Some(());
}
if let Ok(reply) = property.unwrap().reply() {
match str::from_utf8(&reply.value) {
Ok(file_list) => {
let paths: SmallVec<[_; 2]> = file_list
.lines()
.filter_map(|path| Url::parse(path).log_err())
.filter_map(|url| url.to_file_path().log_err())
.collect();
let input = PlatformInput::FileDrop(FileDropEvent::Entered {
position: state.xdnd_state.position,
paths: crate::ExternalPaths(paths),
});
drop(state);
window.handle_input(input);
self.0.borrow_mut().xdnd_state.retrieved = true;
}
Err(_) => {}
};
match str::from_utf8(&reply.value) {
Ok(file_list) => {
let paths: SmallVec<[_; 2]> = file_list
.lines()
.filter_map(|path| Url::parse(path).log_err())
.filter_map(|url| url.to_file_path().log_err())
.collect();
let input = PlatformInput::FileDrop(FileDropEvent::Entered {
position: state.xdnd_state.position,
paths: crate::ExternalPaths(paths),
});
drop(state);
window.handle_input(input);
self.0.borrow_mut().xdnd_state.retrieved = true;
}
Err(_) => {}
}
}
Event::ConfigureNotify(event) => {
@ -891,11 +911,17 @@ impl X11Client {
},
};
let window = self.get_window(event.window)?;
window.configure(bounds).unwrap();
window
.set_bounds(bounds)
.context("X11: Failed to set window bounds")
.log_err();
}
Event::PropertyNotify(event) => {
let window = self.get_window(event.window)?;
window.property_notify(event).unwrap();
window
.property_notify(event)
.context("X11: Failed to handle property notify")
.log_err();
}
Event::FocusIn(event) => {
let window = self.get_window(event.event)?;
@ -1262,10 +1288,12 @@ impl X11Client {
state.pointer_device_states.remove(&info.deviceid);
}
}
state.pointer_device_states = get_new_pointer_device_states(
if let Some(pointer_device_states) = current_pointer_device_states(
&state.xcb_connection,
&state.pointer_device_states,
);
) {
state.pointer_device_states = pointer_device_states;
}
}
Event::XinputDeviceChanged(event) => {
let mut state = self.0.borrow_mut();
@ -1302,8 +1330,7 @@ impl X11Client {
state.modifiers,
event.detail.into(),
));
let mut ximc = state.ximc.take().unwrap();
let mut xim_handler = state.xim_handler.take().unwrap();
let (mut ximc, mut xim_handler) = state.take_xim()?;
drop(state);
xim_handler.window = event.event;
ximc.forward_event(
@ -1312,10 +1339,10 @@ impl X11Client {
xim::ForwardEventFlag::empty(),
&event,
)
.unwrap();
.context("X11: Failed to forward XIM event")
.log_err();
let mut state = self.0.borrow_mut();
state.ximc = Some(ximc);
state.xim_handler = Some(xim_handler);
state.restore_xim(ximc, xim_handler);
drop(state);
}
event => {
@ -1326,7 +1353,10 @@ impl X11Client {
}
fn xim_handle_commit(&self, window: xproto::Window, text: String) -> Option<()> {
let window = self.get_window(window).unwrap();
let Some(window) = self.get_window(window) else {
log::error!("bug: Failed to get window for XIM commit");
return None;
};
let mut state = self.0.borrow_mut();
let keystroke = state.pre_key_char_down.take();
state.composing = false;
@ -1343,11 +1373,13 @@ impl X11Client {
}
fn xim_handle_preedit(&self, window: xproto::Window, text: String) -> Option<()> {
let window = self.get_window(window).unwrap();
let Some(window) = self.get_window(window) else {
log::error!("bug: Failed to get window for XIM preedit");
return None;
};
let mut state = self.0.borrow_mut();
let mut ximc = state.ximc.take().unwrap();
let mut xim_handler = state.xim_handler.take().unwrap();
let (mut ximc, mut xim_handler) = state.take_xim()?;
state.composing = !text.is_empty();
drop(state);
window.handle_ime_preedit(text);
@ -1375,8 +1407,7 @@ impl X11Client {
.ok();
}
let mut state = self.0.borrow_mut();
state.ximc = Some(ximc);
state.xim_handler = Some(xim_handler);
state.restore_xim(ximc, xim_handler);
drop(state);
Some(())
}
@ -1429,15 +1460,13 @@ impl LinuxClient for X11Client {
fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>> {
let state = self.0.borrow();
Some(Rc::new(
X11Display::new(
&state.xcb_connection,
state.scale_factor,
state.x_root_index,
)
.expect("There should always be a root index"),
))
X11Display::new(
&state.xcb_connection,
state.scale_factor,
state.x_root_index,
)
.log_err()
.map(|display| Rc::new(display) as Rc<dyn PlatformDisplay>)
}
fn display(&self, id: DisplayId) -> Option<Rc<dyn PlatformDisplay>> {
@ -1464,7 +1493,10 @@ impl LinuxClient for X11Client {
params: WindowParams,
) -> anyhow::Result<Box<dyn PlatformWindow>> {
let mut state = self.0.borrow_mut();
let x_window = state.xcb_connection.generate_id().unwrap();
let x_window = state
.xcb_connection
.generate_id()
.context("X11: Failed to generate window ID")?;
let window = X11Window::new(
handle,
@ -1480,16 +1512,17 @@ impl LinuxClient for X11Client {
state.scale_factor,
state.common.appearance,
)?;
state
.xcb_connection
.change_property32(
check_reply(
|| "Failed to set XdndAware property",
state.xcb_connection.change_property32(
xproto::PropMode::REPLACE,
x_window,
state.atoms.XdndAware,
state.atoms.XA_ATOM,
&[5],
)
.unwrap();
),
)
.log_err();
let window_ref = WindowRef {
window: window.0.clone(),
@ -1532,7 +1565,7 @@ impl LinuxClient for X11Client {
)
.anyhow()
.and_then(|cookie| cookie.check().anyhow())
.context("setting cursor style")
.context("X11: Failed to set cursor style")
.log_err();
}
@ -1555,7 +1588,7 @@ impl LinuxClient for X11Client {
clipboard::ClipboardKind::Primary,
clipboard::WaitConfig::None,
)
.context("Failed to write to clipboard (primary)")
.context("X11 Failed to write to clipboard (primary)")
.log_with_level(log::Level::Debug);
}
@ -1568,7 +1601,7 @@ impl LinuxClient for X11Client {
clipboard::ClipboardKind::Clipboard,
clipboard::WaitConfig::None,
)
.context("Failed to write to clipboard (clipboard)")
.context("X11: Failed to write to clipboard (clipboard)")
.log_with_level(log::Level::Debug);
state.clipboard_item.replace(item);
}
@ -1578,7 +1611,7 @@ impl LinuxClient for X11Client {
return state
.clipboard
.get_any(clipboard::ClipboardKind::Primary)
.context("Failed to read from clipboard (primary)")
.context("X11: Failed to read from clipboard (primary)")
.log_with_level(log::Level::Debug);
}
@ -1595,17 +1628,21 @@ impl LinuxClient for X11Client {
return state
.clipboard
.get_any(clipboard::ClipboardKind::Clipboard)
.context("Failed to read from clipboard (clipboard)")
.context("X11: Failed to read from clipboard (clipboard)")
.log_with_level(log::Level::Debug);
}
fn run(&self) {
let mut event_loop = self
let Some(mut event_loop) = self
.0
.borrow_mut()
.event_loop
.take()
.expect("App is already running");
.context("X11Client::run called but it's already running")
.log_err()
else {
return;
};
event_loop.run(None, &mut self.clone(), |_| {}).log_err();
}
@ -1641,7 +1678,7 @@ impl LinuxClient for X11Client {
let window_ids = reply
.value
.chunks_exact(4)
.map(|chunk| u32::from_ne_bytes(chunk.try_into().unwrap()))
.filter_map(|chunk| chunk.try_into().ok().map(u32::from_ne_bytes))
.collect::<Vec<xproto::Window>>();
let mut handles = Vec::new();
@ -1664,6 +1701,30 @@ impl LinuxClient for X11Client {
}
impl X11ClientState {
fn has_xim(&self) -> bool {
self.ximc.is_some() && self.xim_handler.is_some()
}
fn take_xim(&mut self) -> Option<(X11rbClient<Rc<XCBConnection>>, XimHandler)> {
let ximc = self
.ximc
.take()
.ok_or(anyhow!("bug: XIM connection not set"))
.log_err()?;
if let Some(xim_handler) = self.xim_handler.take() {
Some((ximc, xim_handler))
} else {
self.ximc = Some(ximc);
log::error!("bug: XIM handler not set");
None
}
}
fn restore_xim(&mut self, ximc: X11rbClient<Rc<XCBConnection>>, xim_handler: XimHandler) {
self.ximc = Some(ximc);
self.xim_handler = Some(xim_handler);
}
fn update_refresh_loop(&mut self, x_window: xproto::Window) {
let Some(window_ref) = self.windows.get_mut(&x_window) else {
return;
@ -1697,35 +1758,41 @@ impl X11ClientState {
});
}
(true, None) => {
let screen_resources = self
.xcb_connection
.randr_get_screen_resources_current(x_window)
.unwrap()
.reply()
.expect("Could not find available screens");
let Some(screen_resources) = get_reply(
|| "Failed to get screen resources",
self.xcb_connection
.randr_get_screen_resources_current(x_window),
)
.log_err() else {
return;
};
// Ideally this would be re-queried when the window changes screens, but there
// doesn't seem to be an efficient / straightforward way to do this. Should also be
// updated when screen configurations change.
let refresh_rate = mode_refresh_rate(
screen_resources
.crtcs
.iter()
.find_map(|crtc| {
let crtc_info = self
.xcb_connection
.randr_get_crtc_info(*crtc, x11rb::CURRENT_TIME)
.ok()?
.reply()
.ok()?;
let mode_info = screen_resources.crtcs.iter().find_map(|crtc| {
let crtc_info = self
.xcb_connection
.randr_get_crtc_info(*crtc, x11rb::CURRENT_TIME)
.ok()?
.reply()
.ok()?;
screen_resources
.modes
.iter()
.find(|m| m.id == crtc_info.mode)
})
.expect("Unable to find screen refresh rate"),
);
screen_resources
.modes
.iter()
.find(|m| m.id == crtc_info.mode)
});
let refresh_rate = match mode_info {
Some(mode_info) => mode_refresh_rate(mode_info),
None => {
log::error!(
"Failed to get screen mode info from xrandr, \
defaulting to 60hz refresh rate."
);
Duration::from_micros(1_000_000 / 60)
}
};
let event_loop_token = self.start_refresh_loop(x_window, refresh_rate);
let Some(window_ref) = self.windows.get_mut(&x_window) else {
@ -1772,7 +1839,7 @@ impl X11ClientState {
calloop::timer::TimeoutAction::ToInstant(instant)
}
})
.expect("Failed to initialize refresh timer")
.expect("Failed to initialize window refresh timer")
}
fn get_cursor_icon(&mut self, style: CursorStyle) -> Option<xproto::Cursor> {
@ -1784,7 +1851,7 @@ impl X11ClientState {
match style {
CursorStyle::None => match create_invisible_cursor(&self.xcb_connection) {
Ok(loaded_cursor) => result = Ok(loaded_cursor),
Err(err) => result = Err(err.context("error while creating invisible cursor")),
Err(err) => result = Err(err.context("X11: error while creating invisible cursor")),
},
_ => 'outer: {
let mut errors = String::new();
@ -1827,14 +1894,14 @@ impl X11ClientState {
{
Ok(default) => {
log_cursor_icon_warning(err.context(format!(
"x11: error loading cursor icon, falling back on default icon '{}'",
"X11: error loading cursor icon, falling back on default icon '{}'",
DEFAULT_CURSOR_ICON_NAME
)));
Some(default)
}
Err(default_err) => {
log_cursor_icon_warning(err.context(default_err).context(format!(
"x11: error loading default cursor fallback '{}'",
"X11: error loading default cursor fallback '{}'",
DEFAULT_CURSOR_ICON_NAME
)));
None
@ -1857,7 +1924,7 @@ pub fn mode_refresh_rate(mode: &randr::ModeInfo) -> Duration {
let millihertz = mode.dot_clock as u64 * 1_000 / (mode.htotal as u64 * mode.vtotal as u64);
let micros = 1_000_000_000 / millihertz;
log::info!("Refreshing at {} micros", micros);
log::info!("Refreshing every {}ms", micros / 1_000);
Duration::from_micros(micros)
}
@ -1868,66 +1935,63 @@ fn fp3232_to_f32(value: xinput::Fp3232) -> f32 {
fn check_compositor_present(xcb_connection: &XCBConnection, root: u32) -> bool {
// Method 1: Check for _NET_WM_CM_S{root}
let atom_name = format!("_NET_WM_CM_S{}", root);
let atom = xcb_connection
.intern_atom(false, atom_name.as_bytes())
.unwrap()
.reply()
.map(|reply| reply.atom)
.unwrap_or(0);
let method1 = if atom != 0 {
xcb_connection
.get_selection_owner(atom)
.unwrap()
.reply()
let atom1 = get_reply(
|| format!("Failed to intern {atom_name}"),
xcb_connection.intern_atom(false, atom_name.as_bytes()),
);
let method1 = match atom1.log_with_level(Level::Debug) {
Some(reply) if reply.atom != x11rb::NONE => {
let atom = reply.atom;
get_reply(
|| format!("Failed to get {atom_name} owner"),
xcb_connection.get_selection_owner(atom),
)
.map(|reply| reply.owner != 0)
.log_with_level(Level::Debug)
.unwrap_or(false)
} else {
false
}
_ => false,
};
// Method 2: Check for _NET_WM_CM_OWNER
let atom_name = "_NET_WM_CM_OWNER";
let atom = xcb_connection
.intern_atom(false, atom_name.as_bytes())
.unwrap()
.reply()
.map(|reply| reply.atom)
.unwrap_or(0);
let method2 = if atom != 0 {
xcb_connection
.get_property(false, root, atom, xproto::AtomEnum::WINDOW, 0, 1)
.unwrap()
.reply()
let atom2 = get_reply(
|| format!("Failed to intern {atom_name}"),
xcb_connection.intern_atom(false, atom_name.as_bytes()),
);
let method2 = match atom2.log_with_level(Level::Debug) {
Some(reply) if reply.atom != x11rb::NONE => {
let atom = reply.atom;
get_reply(
|| format!("Failed to get {atom_name}"),
xcb_connection.get_property(false, root, atom, xproto::AtomEnum::WINDOW, 0, 1),
)
.map(|reply| reply.value_len > 0)
.unwrap_or(false)
} else {
false
}
_ => return false,
};
// Method 3: Check for _NET_SUPPORTING_WM_CHECK
let atom_name = "_NET_SUPPORTING_WM_CHECK";
let atom = xcb_connection
.intern_atom(false, atom_name.as_bytes())
.unwrap()
.reply()
.map(|reply| reply.atom)
.unwrap_or(0);
let method3 = if atom != 0 {
xcb_connection
.get_property(false, root, atom, xproto::AtomEnum::WINDOW, 0, 1)
.unwrap()
.reply()
let atom3 = get_reply(
|| format!("Failed to intern {atom_name}"),
xcb_connection.intern_atom(false, atom_name.as_bytes()),
);
let method3 = match atom3.log_with_level(Level::Debug) {
Some(reply) if reply.atom != x11rb::NONE => {
let atom = reply.atom;
get_reply(
|| format!("Failed to get {atom_name}"),
xcb_connection.get_property(false, root, atom, xproto::AtomEnum::WINDOW, 0, 1),
)
.map(|reply| reply.value_len > 0)
.unwrap_or(false)
} else {
false
}
_ => return false,
};
// TODO: Remove this
log::info!(
log::debug!(
"Compositor detection: _NET_WM_CM_S?={}, _NET_WM_CM_OWNER={}, _NET_SUPPORTING_WM_CHECK={}",
method1,
method2,
@ -1942,28 +2006,28 @@ fn check_gtk_frame_extents_supported(
atoms: &XcbAtoms,
root: xproto::Window,
) -> bool {
let supported_atoms = xcb_connection
.get_property(
let Some(supported_atoms) = get_reply(
|| "Failed to get _NET_SUPPORTED",
xcb_connection.get_property(
false,
root,
atoms._NET_SUPPORTED,
xproto::AtomEnum::ATOM,
0,
1024,
)
.unwrap()
.reply()
.map(|reply| {
// Convert Vec<u8> to Vec<u32>
reply
.value
.chunks_exact(4)
.map(|chunk| u32::from_ne_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]))
.collect::<Vec<u32>>()
})
.unwrap_or_default();
),
)
.log_with_level(Level::Debug) else {
return false;
};
supported_atoms.contains(&atoms._GTK_FRAME_EXTENTS)
let supported_atom_ids: Vec<u32> = supported_atoms
.value
.chunks_exact(4)
.filter_map(|chunk| chunk.try_into().ok().map(u32::from_ne_bytes))
.collect();
supported_atom_ids.contains(&atoms._GTK_FRAME_EXTENTS)
}
fn xdnd_is_atom_supported(atom: u32, atoms: &XcbAtoms) -> bool {
@ -1980,17 +2044,19 @@ fn xdnd_get_supported_atom(
supported_atoms: &XcbAtoms,
target: xproto::Window,
) -> u32 {
let property = xcb_connection
.get_property(
if let Some(reply) = get_reply(
|| "Failed to get XDnD supported atoms",
xcb_connection.get_property(
false,
target,
supported_atoms.XdndTypeList,
AtomEnum::ANY,
0,
1024,
)
.unwrap();
if let Ok(reply) = property.reply() {
),
)
.log_with_level(Level::Warn)
{
if let Some(atoms) = reply.value32() {
for atom in atoms {
if xdnd_is_atom_supported(atom, &supported_atoms) {
@ -2016,9 +2082,11 @@ fn xdnd_send_finished(
sequence: 0,
response_type: xproto::CLIENT_MESSAGE_EVENT,
};
xcb_connection
.send_event(false, target, EventMask::default(), message)
.unwrap();
check_reply(
|| "Failed to send XDnD finished event",
xcb_connection.send_event(false, target, EventMask::default(), message),
)
.log_err();
}
fn xdnd_send_status(
@ -2036,22 +2104,24 @@ fn xdnd_send_status(
sequence: 0,
response_type: xproto::CLIENT_MESSAGE_EVENT,
};
xcb_connection
.send_event(false, target, EventMask::default(), message)
.unwrap();
check_reply(
|| "Failed to send XDnD status event",
xcb_connection.send_event(false, target, EventMask::default(), message),
)
.log_err();
}
/// Recomputes `pointer_device_states` by querying all pointer devices.
/// When a device is present in `scroll_values_to_preserve`, its value for `ScrollAxisState.scroll_value` is used.
fn get_new_pointer_device_states(
fn current_pointer_device_states(
xcb_connection: &XCBConnection,
scroll_values_to_preserve: &BTreeMap<xinput::DeviceId, PointerDeviceState>,
) -> BTreeMap<xinput::DeviceId, PointerDeviceState> {
let devices_query_result = xcb_connection
.xinput_xi_query_device(XINPUT_ALL_DEVICES)
.unwrap()
.reply()
.unwrap();
) -> Option<BTreeMap<xinput::DeviceId, PointerDeviceState>> {
let devices_query_result = get_reply(
|| "Failed to query XInput devices",
xcb_connection.xinput_xi_query_device(XINPUT_ALL_DEVICES),
)
.log_err()?;
let mut pointer_device_states = BTreeMap::new();
pointer_device_states.extend(
@ -2094,7 +2164,7 @@ fn get_new_pointer_device_states(
if pointer_device_states.is_empty() {
log::error!("Found no xinput mouse pointers.");
}
return pointer_device_states;
return Some(pointer_device_states);
}
/// Returns true if the device is a pointer device. Does not include pointer device groups.

View file

@ -1,4 +1,5 @@
use anyhow::{Context as _, anyhow};
use x11rb::connection::RequestConnection;
use crate::platform::blade::{BladeContext, BladeRenderer, BladeSurfaceConfig};
use crate::{
@ -32,6 +33,7 @@ use std::{
};
use super::{X11Display, XINPUT_ALL_DEVICE_GROUPS, XINPUT_ALL_DEVICES};
x11rb::atom_manager! {
pub XcbAtoms: AtomsCookie {
XA_ATOM,
@ -318,43 +320,57 @@ impl rwh::HasDisplayHandle for X11Window {
}
}
fn check_reply<C, F>(
pub(crate) fn check_reply<E, F, C>(
failure_context: F,
result: Result<VoidCookie<'_, Rc<XCBConnection>>, ConnectionError>,
result: Result<VoidCookie<'_, C>, ConnectionError>,
) -> anyhow::Result<()>
where
C: Display + Send + Sync + 'static,
F: FnOnce() -> C,
E: Display + Send + Sync + 'static,
F: FnOnce() -> E,
C: RequestConnection,
{
result
.map_err(|connection_error| anyhow!(connection_error))
.and_then(|response| {
response
.check()
.map_err(|error_response| anyhow!(error_response))
})
.map_err(handle_connection_error)
.and_then(|response| response.check().map_err(|reply_error| anyhow!(reply_error)))
.with_context(failure_context)
}
fn get_reply<C, F, O>(
pub(crate) fn get_reply<E, F, C, O>(
failure_context: F,
result: Result<Cookie<'_, Rc<XCBConnection>, O>, ConnectionError>,
result: Result<Cookie<'_, C, O>, ConnectionError>,
) -> anyhow::Result<O>
where
C: Display + Send + Sync + 'static,
F: FnOnce() -> C,
E: Display + Send + Sync + 'static,
F: FnOnce() -> E,
C: RequestConnection,
O: x11rb::x11_utils::TryParse,
{
result
.map_err(|connection_error| anyhow!(connection_error))
.and_then(|response| {
response
.reply()
.map_err(|error_response| anyhow!(error_response))
})
.map_err(handle_connection_error)
.and_then(|response| response.reply().map_err(|reply_error| anyhow!(reply_error)))
.with_context(failure_context)
}
/// Convert X11 connection errors to `anyhow::Error` and panic for unrecoverable errors.
pub(crate) fn handle_connection_error(err: ConnectionError) -> anyhow::Error {
match err {
ConnectionError::UnknownError => anyhow!("X11 connection: Unknown error"),
ConnectionError::UnsupportedExtension => anyhow!("X11 connection: Unsupported extension"),
ConnectionError::MaximumRequestLengthExceeded => {
anyhow!("X11 connection: Maximum request length exceeded")
}
ConnectionError::FdPassingFailed => {
panic!("X11 connection: File descriptor passing failed")
}
ConnectionError::ParseError(parse_error) => {
anyhow!(parse_error).context("Parse error in X11 response")
}
ConnectionError::InsufficientMemory => panic!("X11 connection: Insufficient memory"),
ConnectionError::IoError(err) => anyhow!(err).context("X11 connection: IOError"),
_ => anyhow!(err),
}
}
impl X11WindowState {
pub fn new(
handle: AnyWindowHandle,
@ -581,7 +597,7 @@ impl X11WindowState {
),
)?;
xcb.flush().with_context(|| "X11 Flush failed.")?;
xcb.flush()?;
let renderer = {
let raw_window = RawWindow {
@ -641,8 +657,7 @@ impl X11WindowState {
|| "X11 DestroyWindow failed while cleaning it up after setup failure.",
xcb.destroy_window(x_window),
)?;
xcb.flush()
.with_context(|| "X11 Flush failed while cleaning it up after setup failure.")?;
xcb.flush()?;
}
setup_result
@ -670,9 +685,7 @@ impl Drop for X11WindowHandle {
|| "X11 DestroyWindow failed while dropping X11WindowHandle.",
self.xcb.destroy_window(self.id),
)?;
self.xcb
.flush()
.with_context(|| "X11 Flush failed while dropping X11WindowHandle.")?;
self.xcb.flush()?;
anyhow::Ok(())
})
.log_err();
@ -691,10 +704,7 @@ impl Drop for X11Window {
|| "X11 DestroyWindow failure.",
self.0.xcb.destroy_window(self.0.x_window),
)?;
self.0
.xcb
.flush()
.with_context(|| "X11 Flush failed after calling DestroyWindow.")?;
self.0.xcb.flush()?;
anyhow::Ok(())
})
@ -812,7 +822,7 @@ impl X11Window {
let state = self.0.state.borrow();
check_reply(
|| "X11 UngrabPointer before move/resize of window ailed.",
|| "X11 UngrabPointer before move/resize of window failed.",
self.0.xcb.ungrab_pointer(x11rb::CURRENT_TIME),
)?;
@ -846,7 +856,11 @@ impl X11Window {
}
fn flush(&self) -> anyhow::Result<()> {
self.0.xcb.flush().with_context(|| "X11 Flush failed.")
self.0
.xcb
.flush()
.map_err(handle_connection_error)
.context("X11 flush failed")
}
}
@ -889,9 +903,13 @@ impl X11WindowStatePtr {
)?;
if reply.value_len != 0 {
let atom = u32::from_ne_bytes(reply.value[0..4].try_into().unwrap());
let edge_constraints = EdgeConstraints::from_atom(atom);
state.edge_constraints.replace(edge_constraints);
if let Ok(bytes) = reply.value[0..4].try_into() {
let atom = u32::from_ne_bytes(bytes);
let edge_constraints = EdgeConstraints::from_atom(atom);
state.edge_constraints.replace(edge_constraints);
} else {
log::error!("Failed to parse GTK_EDGE_CONSTRAINTS");
}
}
Ok(())
@ -1030,7 +1048,7 @@ impl X11WindowStatePtr {
bounds
}
pub fn configure(&self, bounds: Bounds<i32>) -> anyhow::Result<()> {
pub fn set_bounds(&self, bounds: Bounds<i32>) -> anyhow::Result<()> {
let mut resize_args = None;
let is_resize;
{
@ -1196,12 +1214,14 @@ impl PlatformWindow for X11Window {
}
fn mouse_position(&self) -> Point<Pixels> {
let reply = get_reply(
get_reply(
|| "X11 QueryPointer failed.",
self.0.xcb.query_pointer(self.0.x_window),
)
.unwrap();
Point::new((reply.root_x as u32).into(), (reply.root_y as u32).into())
.log_err()
.map_or(Point::new(Pixels(0.0), Pixels(0.0)), |reply| {
Point::new((reply.root_x as u32).into(), (reply.root_y as u32).into())
})
}
fn modifiers(&self) -> Modifiers {
@ -1269,7 +1289,7 @@ impl PlatformWindow for X11Window {
xproto::Time::CURRENT_TIME,
)
.log_err();
self.flush().unwrap();
self.flush().log_err();
}
fn is_active(&self) -> bool {
@ -1323,7 +1343,7 @@ impl PlatformWindow for X11Window {
&data,
),
)
.unwrap();
.log_err();
}
fn map_window(&mut self) -> anyhow::Result<()> {
@ -1359,7 +1379,7 @@ impl PlatformWindow for X11Window {
message,
),
)
.unwrap();
.log_err();
}
fn zoom(&self) {
@ -1370,7 +1390,7 @@ impl PlatformWindow for X11Window {
state.atoms._NET_WM_STATE_MAXIMIZED_VERT,
state.atoms._NET_WM_STATE_MAXIMIZED_HORZ,
)
.unwrap();
.log_err();
}
fn toggle_fullscreen(&self) {
@ -1381,7 +1401,7 @@ impl PlatformWindow for X11Window {
state.atoms._NET_WM_STATE_FULLSCREEN,
xproto::AtomEnum::NONE.into(),
)
.unwrap();
.log_err();
}
fn is_fullscreen(&self) -> bool {
@ -1444,9 +1464,11 @@ impl PlatformWindow for X11Window {
|| "X11 UngrabPointer failed.",
self.0.xcb.ungrab_pointer(x11rb::CURRENT_TIME),
)
.unwrap();
.log_err();
let coords = self.get_root_position(position).unwrap();
let Some(coords) = self.get_root_position(position).log_err() else {
return;
};
let message = ClientMessageEvent::new(
32,
self.0.x_window,
@ -1468,16 +1490,16 @@ impl PlatformWindow for X11Window {
message,
),
)
.unwrap();
.log_err();
}
fn start_window_move(&self) {
const MOVERESIZE_MOVE: u32 = 8;
self.send_moveresize(MOVERESIZE_MOVE).unwrap();
self.send_moveresize(MOVERESIZE_MOVE).log_err();
}
fn start_window_resize(&self, edge: ResizeEdge) {
self.send_moveresize(edge.to_moveresize()).unwrap();
self.send_moveresize(edge.to_moveresize()).log_err();
}
fn window_decorations(&self) -> crate::Decorations {
@ -1552,7 +1574,7 @@ impl PlatformWindow for X11Window {
bytemuck::cast_slice::<u32, u8>(&insets),
),
)
.unwrap();
.log_err();
}
}
@ -1574,7 +1596,7 @@ impl PlatformWindow for X11Window {
WindowDecorations::Client => [1 << 1, 0, 0, 0, 0],
};
check_reply(
let success = check_reply(
|| "X11 ChangeProperty for _MOTIF_WM_HINTS failed.",
self.0.xcb.change_property(
xproto::PropMode::REPLACE,
@ -1586,7 +1608,11 @@ impl PlatformWindow for X11Window {
bytemuck::cast_slice::<u32, u8>(&hints_data),
),
)
.unwrap();
.log_err();
let Some(()) = success else {
return;
};
match decorations {
WindowDecorations::Server => {