| 1 | use std::ffi::CString; |
| 2 | use std::mem::replace; |
| 3 | use std::os::raw::*; |
| 4 | use std::path::Path; |
| 5 | use std::sync::{Arc, Mutex, MutexGuard}; |
| 6 | use std::{cmp, env}; |
| 7 | |
| 8 | use tracing::{debug, info, warn}; |
| 9 | use x11rb::connection::Connection; |
| 10 | use x11rb::properties::{WmHints, WmSizeHints, WmSizeHintsSpecification}; |
| 11 | use x11rb::protocol::shape::SK; |
| 12 | use x11rb::protocol::xfixes::{ConnectionExt, RegionWrapper}; |
| 13 | use x11rb::protocol::xproto::{self, ConnectionExt as _, Rectangle}; |
| 14 | use x11rb::protocol::{randr, xinput}; |
| 15 | |
| 16 | use crate::cursor::{Cursor, CustomCursor as RootCustomCursor}; |
| 17 | use crate::dpi::{PhysicalPosition, PhysicalSize, Position, Size}; |
| 18 | use crate::error::{ExternalError, NotSupportedError, OsError as RootOsError}; |
| 19 | use crate::event::{Event, InnerSizeWriter, WindowEvent}; |
| 20 | use crate::event_loop::AsyncRequestSerial; |
| 21 | use crate::platform::x11::WindowType; |
| 22 | use crate::platform_impl::x11::atoms::*; |
| 23 | use crate::platform_impl::x11::{ |
| 24 | xinput_fp1616_to_float, MonitorHandle as X11MonitorHandle, WakeSender, X11Error, |
| 25 | }; |
| 26 | use crate::platform_impl::{ |
| 27 | Fullscreen, MonitorHandle as PlatformMonitorHandle, OsError, PlatformCustomCursor, |
| 28 | PlatformIcon, VideoModeHandle as PlatformVideoModeHandle, |
| 29 | }; |
| 30 | use crate::window::{ |
| 31 | CursorGrabMode, ImePurpose, ResizeDirection, Theme, UserAttentionType, WindowAttributes, |
| 32 | WindowButtons, WindowLevel, |
| 33 | }; |
| 34 | |
| 35 | use super::util::{self, SelectedCursor}; |
| 36 | use super::{ |
| 37 | ffi, ActiveEventLoop, CookieResultExt, ImeRequest, ImeSender, VoidCookie, WindowId, XConnection, |
| 38 | }; |
| 39 | |
| 40 | #[derive (Debug)] |
| 41 | pub struct SharedState { |
| 42 | pub cursor_pos: Option<(f64, f64)>, |
| 43 | pub size: Option<(u32, u32)>, |
| 44 | pub position: Option<(i32, i32)>, |
| 45 | pub inner_position: Option<(i32, i32)>, |
| 46 | pub inner_position_rel_parent: Option<(i32, i32)>, |
| 47 | pub is_resizable: bool, |
| 48 | pub is_decorated: bool, |
| 49 | pub last_monitor: X11MonitorHandle, |
| 50 | pub dpi_adjusted: Option<(u32, u32)>, |
| 51 | pub(crate) fullscreen: Option<Fullscreen>, |
| 52 | // Set when application calls `set_fullscreen` when window is not visible |
| 53 | pub(crate) desired_fullscreen: Option<Option<Fullscreen>>, |
| 54 | // Used to restore position after exiting fullscreen |
| 55 | pub restore_position: Option<(i32, i32)>, |
| 56 | // Used to restore video mode after exiting fullscreen |
| 57 | pub desktop_video_mode: Option<(randr::Crtc, randr::Mode)>, |
| 58 | pub frame_extents: Option<util::FrameExtentsHeuristic>, |
| 59 | pub min_inner_size: Option<Size>, |
| 60 | pub max_inner_size: Option<Size>, |
| 61 | pub resize_increments: Option<Size>, |
| 62 | pub base_size: Option<Size>, |
| 63 | pub visibility: Visibility, |
| 64 | pub has_focus: bool, |
| 65 | // Use `Option` to not apply hittest logic when it was never requested. |
| 66 | pub cursor_hittest: Option<bool>, |
| 67 | } |
| 68 | |
| 69 | #[derive (Copy, Clone, Debug, Eq, PartialEq)] |
| 70 | pub enum Visibility { |
| 71 | No, |
| 72 | Yes, |
| 73 | // Waiting for VisibilityNotify |
| 74 | YesWait, |
| 75 | } |
| 76 | |
| 77 | impl SharedState { |
| 78 | fn new(last_monitor: X11MonitorHandle, window_attributes: &WindowAttributes) -> Mutex<Self> { |
| 79 | let visibility = |
| 80 | if window_attributes.visible { Visibility::YesWait } else { Visibility::No }; |
| 81 | |
| 82 | Mutex::new(SharedState { |
| 83 | last_monitor, |
| 84 | visibility, |
| 85 | |
| 86 | is_resizable: window_attributes.resizable, |
| 87 | is_decorated: window_attributes.decorations, |
| 88 | cursor_pos: None, |
| 89 | size: None, |
| 90 | position: None, |
| 91 | inner_position: None, |
| 92 | inner_position_rel_parent: None, |
| 93 | dpi_adjusted: None, |
| 94 | fullscreen: None, |
| 95 | desired_fullscreen: None, |
| 96 | restore_position: None, |
| 97 | desktop_video_mode: None, |
| 98 | frame_extents: None, |
| 99 | min_inner_size: None, |
| 100 | max_inner_size: None, |
| 101 | resize_increments: None, |
| 102 | base_size: None, |
| 103 | has_focus: false, |
| 104 | cursor_hittest: None, |
| 105 | }) |
| 106 | } |
| 107 | } |
| 108 | |
| 109 | unsafe impl Send for UnownedWindow {} |
| 110 | unsafe impl Sync for UnownedWindow {} |
| 111 | |
| 112 | pub struct UnownedWindow { |
| 113 | pub(crate) xconn: Arc<XConnection>, // never changes |
| 114 | xwindow: xproto::Window, // never changes |
| 115 | #[allow (dead_code)] |
| 116 | visual: u32, // never changes |
| 117 | root: xproto::Window, // never changes |
| 118 | #[allow (dead_code)] |
| 119 | screen_id: i32, // never changes |
| 120 | selected_cursor: Mutex<SelectedCursor>, |
| 121 | cursor_grabbed_mode: Mutex<CursorGrabMode>, |
| 122 | #[allow (clippy::mutex_atomic)] |
| 123 | cursor_visible: Mutex<bool>, |
| 124 | ime_sender: Mutex<ImeSender>, |
| 125 | pub shared_state: Mutex<SharedState>, |
| 126 | redraw_sender: WakeSender<WindowId>, |
| 127 | activation_sender: WakeSender<super::ActivationToken>, |
| 128 | } |
| 129 | |
| 130 | macro_rules! leap { |
| 131 | ($e:expr) => { |
| 132 | match $e { |
| 133 | Ok(x) => x, |
| 134 | Err(err) => return Err(os_error!(OsError::XError(X11Error::from(err).into()))), |
| 135 | } |
| 136 | }; |
| 137 | } |
| 138 | |
| 139 | impl UnownedWindow { |
| 140 | #[allow (clippy::unnecessary_cast)] |
| 141 | pub(crate) fn new( |
| 142 | event_loop: &ActiveEventLoop, |
| 143 | window_attrs: WindowAttributes, |
| 144 | ) -> Result<UnownedWindow, RootOsError> { |
| 145 | let xconn = &event_loop.xconn; |
| 146 | let atoms = xconn.atoms(); |
| 147 | |
| 148 | let screen_id = match window_attrs.platform_specific.x11.screen_id { |
| 149 | Some(id) => id, |
| 150 | None => xconn.default_screen_index() as c_int, |
| 151 | }; |
| 152 | |
| 153 | let screen = { |
| 154 | let screen_id_usize = usize::try_from(screen_id) |
| 155 | .map_err(|_| os_error!(OsError::Misc("screen id must be non-negative" )))?; |
| 156 | xconn.xcb_connection().setup().roots.get(screen_id_usize).ok_or(os_error!( |
| 157 | OsError::Misc("requested screen id not present in server's response" ) |
| 158 | ))? |
| 159 | }; |
| 160 | |
| 161 | #[cfg (feature = "rwh_06" )] |
| 162 | let root = match window_attrs.parent_window.as_ref().map(|handle| handle.0) { |
| 163 | Some(rwh_06::RawWindowHandle::Xlib(handle)) => handle.window as xproto::Window, |
| 164 | Some(rwh_06::RawWindowHandle::Xcb(handle)) => handle.window.get(), |
| 165 | Some(raw) => unreachable!("Invalid raw window handle {raw:?} on X11" ), |
| 166 | None => screen.root, |
| 167 | }; |
| 168 | #[cfg (not(feature = "rwh_06" ))] |
| 169 | let root = event_loop.root; |
| 170 | |
| 171 | let mut monitors = leap!(xconn.available_monitors()); |
| 172 | let guessed_monitor = if monitors.is_empty() { |
| 173 | X11MonitorHandle::dummy() |
| 174 | } else { |
| 175 | xconn |
| 176 | .query_pointer(root, util::VIRTUAL_CORE_POINTER) |
| 177 | .ok() |
| 178 | .and_then(|pointer_state| { |
| 179 | let (x, y) = (pointer_state.root_x as i64, pointer_state.root_y as i64); |
| 180 | |
| 181 | for i in 0..monitors.len() { |
| 182 | if monitors[i].rect.contains_point(x, y) { |
| 183 | return Some(monitors.swap_remove(i)); |
| 184 | } |
| 185 | } |
| 186 | |
| 187 | None |
| 188 | }) |
| 189 | .unwrap_or_else(|| monitors.swap_remove(0)) |
| 190 | }; |
| 191 | let scale_factor = guessed_monitor.scale_factor(); |
| 192 | |
| 193 | info!("Guessed window scale factor: {}" , scale_factor); |
| 194 | |
| 195 | let max_inner_size: Option<(u32, u32)> = |
| 196 | window_attrs.max_inner_size.map(|size| size.to_physical::<u32>(scale_factor).into()); |
| 197 | let min_inner_size: Option<(u32, u32)> = |
| 198 | window_attrs.min_inner_size.map(|size| size.to_physical::<u32>(scale_factor).into()); |
| 199 | |
| 200 | let position = |
| 201 | window_attrs.position.map(|position| position.to_physical::<i32>(scale_factor)); |
| 202 | |
| 203 | let dimensions = { |
| 204 | // x11 only applies constraints when the window is actively resized |
| 205 | // by the user, so we have to manually apply the initial constraints |
| 206 | let mut dimensions: (u32, u32) = window_attrs |
| 207 | .inner_size |
| 208 | .map(|size| size.to_physical::<u32>(scale_factor)) |
| 209 | .or_else(|| Some((800, 600).into())) |
| 210 | .map(Into::into) |
| 211 | .unwrap(); |
| 212 | if let Some(max) = max_inner_size { |
| 213 | dimensions.0 = cmp::min(dimensions.0, max.0); |
| 214 | dimensions.1 = cmp::min(dimensions.1, max.1); |
| 215 | } |
| 216 | if let Some(min) = min_inner_size { |
| 217 | dimensions.0 = cmp::max(dimensions.0, min.0); |
| 218 | dimensions.1 = cmp::max(dimensions.1, min.1); |
| 219 | } |
| 220 | debug!("Calculated physical dimensions: {}x {}" , dimensions.0, dimensions.1); |
| 221 | dimensions |
| 222 | }; |
| 223 | |
| 224 | // An iterator over the visuals matching screen id combined with their depths. |
| 225 | let mut all_visuals = screen |
| 226 | .allowed_depths |
| 227 | .iter() |
| 228 | .flat_map(|depth| depth.visuals.iter().map(move |visual| (visual, depth.depth))); |
| 229 | |
| 230 | // creating |
| 231 | let (visualtype, depth, require_colormap) = |
| 232 | match window_attrs.platform_specific.x11.visual_id { |
| 233 | Some(vi) => { |
| 234 | // Find this specific visual. |
| 235 | let (visualtype, depth) = |
| 236 | all_visuals.find(|(visual, _)| visual.visual_id == vi).ok_or_else( |
| 237 | || os_error!(OsError::XError(X11Error::NoSuchVisual(vi).into())), |
| 238 | )?; |
| 239 | |
| 240 | (Some(visualtype), depth, true) |
| 241 | }, |
| 242 | None if window_attrs.transparent => { |
| 243 | // Find a suitable visual, true color with 32 bits of depth. |
| 244 | all_visuals |
| 245 | .find_map(|(visual, depth)| { |
| 246 | (depth == 32 && visual.class == xproto::VisualClass::TRUE_COLOR) |
| 247 | .then_some((Some(visual), depth, true)) |
| 248 | }) |
| 249 | .unwrap_or_else(|| { |
| 250 | debug!( |
| 251 | "Could not set transparency, because XMatchVisualInfo returned \ |
| 252 | zero for the required parameters" |
| 253 | ); |
| 254 | (None as _, x11rb::COPY_FROM_PARENT as _, false) |
| 255 | }) |
| 256 | }, |
| 257 | _ => (None, x11rb::COPY_FROM_PARENT as _, false), |
| 258 | }; |
| 259 | let mut visual = visualtype.map_or(x11rb::COPY_FROM_PARENT, |v| v.visual_id); |
| 260 | |
| 261 | let window_attributes = { |
| 262 | use xproto::EventMask; |
| 263 | |
| 264 | let mut aux = xproto::CreateWindowAux::new(); |
| 265 | let event_mask = EventMask::EXPOSURE |
| 266 | | EventMask::STRUCTURE_NOTIFY |
| 267 | | EventMask::VISIBILITY_CHANGE |
| 268 | | EventMask::KEY_PRESS |
| 269 | | EventMask::KEY_RELEASE |
| 270 | | EventMask::KEYMAP_STATE |
| 271 | | EventMask::BUTTON_PRESS |
| 272 | | EventMask::BUTTON_RELEASE |
| 273 | | EventMask::POINTER_MOTION |
| 274 | | EventMask::PROPERTY_CHANGE; |
| 275 | |
| 276 | aux = aux.event_mask(event_mask).border_pixel(0); |
| 277 | |
| 278 | if window_attrs.platform_specific.x11.override_redirect { |
| 279 | aux = aux.override_redirect(true as u32); |
| 280 | } |
| 281 | |
| 282 | // Add a colormap if needed. |
| 283 | let colormap_visual = match window_attrs.platform_specific.x11.visual_id { |
| 284 | Some(vi) => Some(vi), |
| 285 | None if require_colormap => Some(visual), |
| 286 | _ => None, |
| 287 | }; |
| 288 | |
| 289 | if let Some(visual) = colormap_visual { |
| 290 | let colormap = leap!(xconn.xcb_connection().generate_id()); |
| 291 | leap!(xconn.xcb_connection().create_colormap( |
| 292 | xproto::ColormapAlloc::NONE, |
| 293 | colormap, |
| 294 | root, |
| 295 | visual, |
| 296 | )); |
| 297 | aux = aux.colormap(colormap); |
| 298 | } else { |
| 299 | aux = aux.colormap(0); |
| 300 | } |
| 301 | |
| 302 | aux |
| 303 | }; |
| 304 | |
| 305 | // Figure out the window's parent. |
| 306 | let parent = window_attrs.platform_specific.x11.embed_window.unwrap_or(root); |
| 307 | |
| 308 | // finally creating the window |
| 309 | let xwindow = { |
| 310 | let (x, y) = position.map_or((0, 0), Into::into); |
| 311 | let wid = leap!(xconn.xcb_connection().generate_id()); |
| 312 | let result = xconn.xcb_connection().create_window( |
| 313 | depth, |
| 314 | wid, |
| 315 | parent, |
| 316 | x, |
| 317 | y, |
| 318 | dimensions.0.try_into().unwrap(), |
| 319 | dimensions.1.try_into().unwrap(), |
| 320 | 0, |
| 321 | xproto::WindowClass::INPUT_OUTPUT, |
| 322 | visual, |
| 323 | &window_attributes, |
| 324 | ); |
| 325 | leap!(leap!(result).check()); |
| 326 | |
| 327 | wid |
| 328 | }; |
| 329 | |
| 330 | // The COPY_FROM_PARENT is a special value for the visual used to copy |
| 331 | // the visual from the parent window, thus we have to query the visual |
| 332 | // we've got when we built the window above. |
| 333 | if visual == x11rb::COPY_FROM_PARENT { |
| 334 | visual = leap!(leap!(xconn |
| 335 | .xcb_connection() |
| 336 | .get_window_attributes(xwindow as xproto::Window)) |
| 337 | .reply()) |
| 338 | .visual; |
| 339 | } |
| 340 | |
| 341 | #[allow (clippy::mutex_atomic)] |
| 342 | let mut window = UnownedWindow { |
| 343 | xconn: Arc::clone(xconn), |
| 344 | xwindow: xwindow as xproto::Window, |
| 345 | visual, |
| 346 | root, |
| 347 | screen_id, |
| 348 | selected_cursor: Default::default(), |
| 349 | cursor_grabbed_mode: Mutex::new(CursorGrabMode::None), |
| 350 | cursor_visible: Mutex::new(true), |
| 351 | ime_sender: Mutex::new(event_loop.ime_sender.clone()), |
| 352 | shared_state: SharedState::new(guessed_monitor, &window_attrs), |
| 353 | redraw_sender: event_loop.redraw_sender.clone(), |
| 354 | activation_sender: event_loop.activation_sender.clone(), |
| 355 | }; |
| 356 | |
| 357 | // Title must be set before mapping. Some tiling window managers (i.e. i3) use the window |
| 358 | // title to determine placement/etc., so doing this after mapping would cause the WM to |
| 359 | // act on the wrong title state. |
| 360 | leap!(window.set_title_inner(&window_attrs.title)).ignore_error(); |
| 361 | leap!(window.set_decorations_inner(window_attrs.decorations)).ignore_error(); |
| 362 | |
| 363 | if let Some(theme) = window_attrs.preferred_theme { |
| 364 | leap!(window.set_theme_inner(Some(theme))).ignore_error(); |
| 365 | } |
| 366 | |
| 367 | // Embed the window if needed. |
| 368 | if window_attrs.platform_specific.x11.embed_window.is_some() { |
| 369 | window.embed_window()?; |
| 370 | } |
| 371 | |
| 372 | { |
| 373 | // Enable drag and drop (TODO: extend API to make this toggleable) |
| 374 | { |
| 375 | let dnd_aware_atom = atoms[XdndAware]; |
| 376 | let version = &[5u32]; // Latest version; hasn't changed since 2002 |
| 377 | leap!(xconn.change_property( |
| 378 | window.xwindow, |
| 379 | dnd_aware_atom, |
| 380 | u32::from(xproto::AtomEnum::ATOM), |
| 381 | xproto::PropMode::REPLACE, |
| 382 | version, |
| 383 | )) |
| 384 | .ignore_error(); |
| 385 | } |
| 386 | |
| 387 | // WM_CLASS must be set *before* mapping the window, as per ICCCM! |
| 388 | { |
| 389 | let (instance, class) = if let Some(name) = window_attrs.platform_specific.name { |
| 390 | (name.instance, name.general) |
| 391 | } else { |
| 392 | let class = env::args_os() |
| 393 | .next() |
| 394 | .as_ref() |
| 395 | // Default to the name of the binary (via argv[0]) |
| 396 | .and_then(|path| Path::new(path).file_name()) |
| 397 | .and_then(|bin_name| bin_name.to_str()) |
| 398 | .map(|bin_name| bin_name.to_owned()) |
| 399 | .unwrap_or_else(|| window_attrs.title.clone()); |
| 400 | // This environment variable is extraordinarily unlikely to actually be used... |
| 401 | let instance = env::var("RESOURCE_NAME" ).ok().unwrap_or_else(|| class.clone()); |
| 402 | (instance, class) |
| 403 | }; |
| 404 | |
| 405 | let class = format!(" {instance}\0{class}\0" ); |
| 406 | leap!(xconn.change_property( |
| 407 | window.xwindow, |
| 408 | xproto::Atom::from(xproto::AtomEnum::WM_CLASS), |
| 409 | xproto::Atom::from(xproto::AtomEnum::STRING), |
| 410 | xproto::PropMode::REPLACE, |
| 411 | class.as_bytes(), |
| 412 | )) |
| 413 | .ignore_error(); |
| 414 | } |
| 415 | |
| 416 | if let Some(flusher) = leap!(window.set_pid()) { |
| 417 | flusher.ignore_error() |
| 418 | } |
| 419 | |
| 420 | leap!(window.set_window_types(window_attrs.platform_specific.x11.x11_window_types)) |
| 421 | .ignore_error(); |
| 422 | |
| 423 | // Set size hints. |
| 424 | let mut min_inner_size = |
| 425 | window_attrs.min_inner_size.map(|size| size.to_physical::<u32>(scale_factor)); |
| 426 | let mut max_inner_size = |
| 427 | window_attrs.max_inner_size.map(|size| size.to_physical::<u32>(scale_factor)); |
| 428 | |
| 429 | if !window_attrs.resizable { |
| 430 | if util::wm_name_is_one_of(&["Xfwm4" ]) { |
| 431 | warn!("To avoid a WM bug, disabling resizing has no effect on Xfwm4" ); |
| 432 | } else { |
| 433 | max_inner_size = Some(dimensions.into()); |
| 434 | min_inner_size = Some(dimensions.into()); |
| 435 | } |
| 436 | } |
| 437 | |
| 438 | let shared_state = window.shared_state.get_mut().unwrap(); |
| 439 | shared_state.min_inner_size = min_inner_size.map(Into::into); |
| 440 | shared_state.max_inner_size = max_inner_size.map(Into::into); |
| 441 | shared_state.resize_increments = window_attrs.resize_increments; |
| 442 | shared_state.base_size = window_attrs.platform_specific.x11.base_size; |
| 443 | |
| 444 | let normal_hints = WmSizeHints { |
| 445 | position: position.map(|PhysicalPosition { x, y }| { |
| 446 | (WmSizeHintsSpecification::UserSpecified, x, y) |
| 447 | }), |
| 448 | size: Some(( |
| 449 | WmSizeHintsSpecification::UserSpecified, |
| 450 | cast_dimension_to_hint(dimensions.0), |
| 451 | cast_dimension_to_hint(dimensions.1), |
| 452 | )), |
| 453 | max_size: max_inner_size.map(cast_physical_size_to_hint), |
| 454 | min_size: min_inner_size.map(cast_physical_size_to_hint), |
| 455 | size_increment: window_attrs |
| 456 | .resize_increments |
| 457 | .map(|size| cast_size_to_hint(size, scale_factor)), |
| 458 | base_size: window_attrs |
| 459 | .platform_specific |
| 460 | .x11 |
| 461 | .base_size |
| 462 | .map(|size| cast_size_to_hint(size, scale_factor)), |
| 463 | aspect: None, |
| 464 | win_gravity: None, |
| 465 | }; |
| 466 | leap!(leap!(normal_hints.set( |
| 467 | xconn.xcb_connection(), |
| 468 | window.xwindow as xproto::Window, |
| 469 | xproto::AtomEnum::WM_NORMAL_HINTS, |
| 470 | )) |
| 471 | .check()); |
| 472 | |
| 473 | // Set window icons |
| 474 | if let Some(icon) = window_attrs.window_icon { |
| 475 | leap!(window.set_icon_inner(icon.inner)).ignore_error(); |
| 476 | } |
| 477 | |
| 478 | // Opt into handling window close |
| 479 | let result = xconn.xcb_connection().change_property( |
| 480 | xproto::PropMode::REPLACE, |
| 481 | window.xwindow, |
| 482 | atoms[WM_PROTOCOLS], |
| 483 | xproto::AtomEnum::ATOM, |
| 484 | 32, |
| 485 | 2, |
| 486 | bytemuck::cast_slice::<xproto::Atom, u8>(&[ |
| 487 | atoms[WM_DELETE_WINDOW], |
| 488 | atoms[_NET_WM_PING], |
| 489 | ]), |
| 490 | ); |
| 491 | leap!(result).ignore_error(); |
| 492 | |
| 493 | // Select XInput2 events |
| 494 | let mask = xinput::XIEventMask::MOTION |
| 495 | | xinput::XIEventMask::BUTTON_PRESS |
| 496 | | xinput::XIEventMask::BUTTON_RELEASE |
| 497 | | xinput::XIEventMask::ENTER |
| 498 | | xinput::XIEventMask::LEAVE |
| 499 | | xinput::XIEventMask::FOCUS_IN |
| 500 | | xinput::XIEventMask::FOCUS_OUT |
| 501 | | xinput::XIEventMask::TOUCH_BEGIN |
| 502 | | xinput::XIEventMask::TOUCH_UPDATE |
| 503 | | xinput::XIEventMask::TOUCH_END; |
| 504 | leap!(xconn.select_xinput_events(window.xwindow, super::ALL_MASTER_DEVICES, mask)) |
| 505 | .ignore_error(); |
| 506 | |
| 507 | // Set visibility (map window) |
| 508 | if window_attrs.visible { |
| 509 | leap!(xconn.xcb_connection().map_window(window.xwindow)).ignore_error(); |
| 510 | leap!(xconn.xcb_connection().configure_window( |
| 511 | xwindow, |
| 512 | &xproto::ConfigureWindowAux::new().stack_mode(xproto::StackMode::ABOVE) |
| 513 | )) |
| 514 | .ignore_error(); |
| 515 | } |
| 516 | |
| 517 | // Attempt to make keyboard input repeat detectable |
| 518 | unsafe { |
| 519 | let mut supported_ptr = ffi::False; |
| 520 | (xconn.xlib.XkbSetDetectableAutoRepeat)( |
| 521 | xconn.display, |
| 522 | ffi::True, |
| 523 | &mut supported_ptr, |
| 524 | ); |
| 525 | if supported_ptr == ffi::False { |
| 526 | return Err(os_error!(OsError::Misc("`XkbSetDetectableAutoRepeat` failed" ))); |
| 527 | } |
| 528 | } |
| 529 | |
| 530 | // Try to create input context for the window. |
| 531 | if let Some(ime) = event_loop.ime.as_ref() { |
| 532 | let result = ime.borrow_mut().create_context(window.xwindow as ffi::Window, false); |
| 533 | leap!(result); |
| 534 | } |
| 535 | |
| 536 | // These properties must be set after mapping |
| 537 | if window_attrs.maximized { |
| 538 | leap!(window.set_maximized_inner(window_attrs.maximized)).ignore_error(); |
| 539 | } |
| 540 | if window_attrs.fullscreen.is_some() { |
| 541 | if let Some(flusher) = |
| 542 | leap!(window |
| 543 | .set_fullscreen_inner(window_attrs.fullscreen.clone().map(Into::into))) |
| 544 | { |
| 545 | flusher.ignore_error() |
| 546 | } |
| 547 | |
| 548 | if let Some(PhysicalPosition { x, y }) = position { |
| 549 | let shared_state = window.shared_state.get_mut().unwrap(); |
| 550 | |
| 551 | shared_state.restore_position = Some((x, y)); |
| 552 | } |
| 553 | } |
| 554 | |
| 555 | leap!(window.set_window_level_inner(window_attrs.window_level)).ignore_error(); |
| 556 | } |
| 557 | |
| 558 | window.set_cursor(window_attrs.cursor); |
| 559 | |
| 560 | // Remove the startup notification if we have one. |
| 561 | if let Some(startup) = window_attrs.platform_specific.activation_token.as_ref() { |
| 562 | leap!(xconn.remove_activation_token(xwindow, &startup.token)); |
| 563 | } |
| 564 | |
| 565 | // We never want to give the user a broken window, since by then, it's too late to handle. |
| 566 | let window = leap!(xconn.sync_with_server().map(|_| window)); |
| 567 | |
| 568 | Ok(window) |
| 569 | } |
| 570 | |
| 571 | /// Embed this window into a parent window. |
| 572 | pub(super) fn embed_window(&self) -> Result<(), RootOsError> { |
| 573 | let atoms = self.xconn.atoms(); |
| 574 | leap!(leap!(self.xconn.change_property( |
| 575 | self.xwindow, |
| 576 | atoms[_XEMBED], |
| 577 | atoms[_XEMBED], |
| 578 | xproto::PropMode::REPLACE, |
| 579 | &[0u32, 1u32], |
| 580 | )) |
| 581 | .check()); |
| 582 | |
| 583 | Ok(()) |
| 584 | } |
| 585 | |
| 586 | pub(super) fn shared_state_lock(&self) -> MutexGuard<'_, SharedState> { |
| 587 | self.shared_state.lock().unwrap() |
| 588 | } |
| 589 | |
| 590 | fn set_pid(&self) -> Result<Option<VoidCookie<'_>>, X11Error> { |
| 591 | let atoms = self.xconn.atoms(); |
| 592 | let pid_atom = atoms[_NET_WM_PID]; |
| 593 | let client_machine_atom = atoms[WM_CLIENT_MACHINE]; |
| 594 | |
| 595 | // Get the hostname and the PID. |
| 596 | let uname = rustix::system::uname(); |
| 597 | let pid = rustix::process::getpid(); |
| 598 | |
| 599 | self.xconn |
| 600 | .change_property( |
| 601 | self.xwindow, |
| 602 | pid_atom, |
| 603 | xproto::Atom::from(xproto::AtomEnum::CARDINAL), |
| 604 | xproto::PropMode::REPLACE, |
| 605 | &[pid.as_raw_nonzero().get() as util::Cardinal], |
| 606 | )? |
| 607 | .ignore_error(); |
| 608 | let flusher = self.xconn.change_property( |
| 609 | self.xwindow, |
| 610 | client_machine_atom, |
| 611 | xproto::Atom::from(xproto::AtomEnum::STRING), |
| 612 | xproto::PropMode::REPLACE, |
| 613 | uname.nodename().to_bytes(), |
| 614 | ); |
| 615 | flusher.map(Some) |
| 616 | } |
| 617 | |
| 618 | fn set_window_types(&self, window_types: Vec<WindowType>) -> Result<VoidCookie<'_>, X11Error> { |
| 619 | let atoms = self.xconn.atoms(); |
| 620 | let hint_atom = atoms[_NET_WM_WINDOW_TYPE]; |
| 621 | let atoms: Vec<_> = window_types.iter().map(|t| t.as_atom(&self.xconn)).collect(); |
| 622 | |
| 623 | self.xconn.change_property( |
| 624 | self.xwindow, |
| 625 | hint_atom, |
| 626 | xproto::Atom::from(xproto::AtomEnum::ATOM), |
| 627 | xproto::PropMode::REPLACE, |
| 628 | &atoms, |
| 629 | ) |
| 630 | } |
| 631 | |
| 632 | pub fn set_theme_inner(&self, theme: Option<Theme>) -> Result<VoidCookie<'_>, X11Error> { |
| 633 | let atoms = self.xconn.atoms(); |
| 634 | let hint_atom = atoms[_GTK_THEME_VARIANT]; |
| 635 | let utf8_atom = atoms[UTF8_STRING]; |
| 636 | let variant = match theme { |
| 637 | Some(Theme::Dark) => "dark" , |
| 638 | Some(Theme::Light) => "light" , |
| 639 | None => "dark" , |
| 640 | }; |
| 641 | let variant = CString::new(variant).expect("`_GTK_THEME_VARIANT` contained null byte" ); |
| 642 | self.xconn.change_property( |
| 643 | self.xwindow, |
| 644 | hint_atom, |
| 645 | utf8_atom, |
| 646 | xproto::PropMode::REPLACE, |
| 647 | variant.as_bytes(), |
| 648 | ) |
| 649 | } |
| 650 | |
| 651 | #[inline ] |
| 652 | pub fn set_theme(&self, theme: Option<Theme>) { |
| 653 | self.set_theme_inner(theme).expect("Failed to change window theme" ).ignore_error(); |
| 654 | |
| 655 | self.xconn.flush_requests().expect("Failed to change window theme" ); |
| 656 | } |
| 657 | |
| 658 | fn set_netwm( |
| 659 | &self, |
| 660 | operation: util::StateOperation, |
| 661 | properties: (u32, u32, u32, u32), |
| 662 | ) -> Result<VoidCookie<'_>, X11Error> { |
| 663 | let atoms = self.xconn.atoms(); |
| 664 | let state_atom = atoms[_NET_WM_STATE]; |
| 665 | self.xconn.send_client_msg( |
| 666 | self.xwindow, |
| 667 | self.root, |
| 668 | state_atom, |
| 669 | Some(xproto::EventMask::SUBSTRUCTURE_REDIRECT | xproto::EventMask::SUBSTRUCTURE_NOTIFY), |
| 670 | [operation as u32, properties.0, properties.1, properties.2, properties.3], |
| 671 | ) |
| 672 | } |
| 673 | |
| 674 | fn set_fullscreen_hint(&self, fullscreen: bool) -> Result<VoidCookie<'_>, X11Error> { |
| 675 | let atoms = self.xconn.atoms(); |
| 676 | let fullscreen_atom = atoms[_NET_WM_STATE_FULLSCREEN]; |
| 677 | let flusher = self.set_netwm(fullscreen.into(), (fullscreen_atom, 0, 0, 0)); |
| 678 | |
| 679 | if fullscreen { |
| 680 | // Ensure that the fullscreen window receives input focus to prevent |
| 681 | // locking up the user's display. |
| 682 | self.xconn |
| 683 | .xcb_connection() |
| 684 | .set_input_focus(xproto::InputFocus::PARENT, self.xwindow, x11rb::CURRENT_TIME)? |
| 685 | .ignore_error(); |
| 686 | } |
| 687 | |
| 688 | flusher |
| 689 | } |
| 690 | |
| 691 | fn set_fullscreen_inner( |
| 692 | &self, |
| 693 | fullscreen: Option<Fullscreen>, |
| 694 | ) -> Result<Option<VoidCookie<'_>>, X11Error> { |
| 695 | let mut shared_state_lock = self.shared_state_lock(); |
| 696 | |
| 697 | match shared_state_lock.visibility { |
| 698 | // Setting fullscreen on a window that is not visible will generate an error. |
| 699 | Visibility::No | Visibility::YesWait => { |
| 700 | shared_state_lock.desired_fullscreen = Some(fullscreen); |
| 701 | return Ok(None); |
| 702 | }, |
| 703 | Visibility::Yes => (), |
| 704 | } |
| 705 | |
| 706 | let old_fullscreen = shared_state_lock.fullscreen.clone(); |
| 707 | if old_fullscreen == fullscreen { |
| 708 | return Ok(None); |
| 709 | } |
| 710 | shared_state_lock.fullscreen.clone_from(&fullscreen); |
| 711 | |
| 712 | match (&old_fullscreen, &fullscreen) { |
| 713 | // Store the desktop video mode before entering exclusive |
| 714 | // fullscreen, so we can restore it upon exit, as XRandR does not |
| 715 | // provide a mechanism to set this per app-session or restore this |
| 716 | // to the desktop video mode as macOS and Windows do |
| 717 | (&None, &Some(Fullscreen::Exclusive(PlatformVideoModeHandle::X(ref video_mode)))) |
| 718 | | ( |
| 719 | &Some(Fullscreen::Borderless(_)), |
| 720 | &Some(Fullscreen::Exclusive(PlatformVideoModeHandle::X(ref video_mode))), |
| 721 | ) => { |
| 722 | let monitor = video_mode.monitor.as_ref().unwrap(); |
| 723 | shared_state_lock.desktop_video_mode = Some(( |
| 724 | monitor.id, |
| 725 | self.xconn.get_crtc_mode(monitor.id).expect("Failed to get desktop video mode" ), |
| 726 | )); |
| 727 | }, |
| 728 | // Restore desktop video mode upon exiting exclusive fullscreen |
| 729 | (&Some(Fullscreen::Exclusive(_)), &None) |
| 730 | | (&Some(Fullscreen::Exclusive(_)), &Some(Fullscreen::Borderless(_))) => { |
| 731 | let (monitor_id, mode_id) = shared_state_lock.desktop_video_mode.take().unwrap(); |
| 732 | self.xconn |
| 733 | .set_crtc_config(monitor_id, mode_id) |
| 734 | .expect("failed to restore desktop video mode" ); |
| 735 | }, |
| 736 | _ => (), |
| 737 | } |
| 738 | |
| 739 | drop(shared_state_lock); |
| 740 | |
| 741 | match fullscreen { |
| 742 | None => { |
| 743 | let flusher = self.set_fullscreen_hint(false); |
| 744 | let mut shared_state_lock = self.shared_state_lock(); |
| 745 | if let Some(position) = shared_state_lock.restore_position.take() { |
| 746 | drop(shared_state_lock); |
| 747 | self.set_position_inner(position.0, position.1) |
| 748 | .expect_then_ignore_error("Failed to restore window position" ); |
| 749 | } |
| 750 | flusher.map(Some) |
| 751 | }, |
| 752 | Some(fullscreen) => { |
| 753 | let (video_mode, monitor) = match fullscreen { |
| 754 | Fullscreen::Exclusive(PlatformVideoModeHandle::X(ref video_mode)) => { |
| 755 | (Some(video_mode), video_mode.monitor.clone().unwrap()) |
| 756 | }, |
| 757 | Fullscreen::Borderless(Some(PlatformMonitorHandle::X(monitor))) => { |
| 758 | (None, monitor) |
| 759 | }, |
| 760 | Fullscreen::Borderless(None) => { |
| 761 | (None, self.shared_state_lock().last_monitor.clone()) |
| 762 | }, |
| 763 | #[cfg (wayland_platform)] |
| 764 | _ => unreachable!(), |
| 765 | }; |
| 766 | |
| 767 | // Don't set fullscreen on an invalid dummy monitor handle |
| 768 | if monitor.is_dummy() { |
| 769 | return Ok(None); |
| 770 | } |
| 771 | |
| 772 | if let Some(video_mode) = video_mode { |
| 773 | // FIXME: this is actually not correct if we're setting the |
| 774 | // video mode to a resolution higher than the current |
| 775 | // desktop resolution, because XRandR does not automatically |
| 776 | // reposition the monitors to the right and below this |
| 777 | // monitor. |
| 778 | // |
| 779 | // What ends up happening is we will get the fullscreen |
| 780 | // window showing up on those monitors as well, because |
| 781 | // their virtual position now overlaps with the monitor that |
| 782 | // we just made larger.. |
| 783 | // |
| 784 | // It'd be quite a bit of work to handle this correctly (and |
| 785 | // nobody else seems to bother doing this correctly either), |
| 786 | // so we're just leaving this broken. Fixing this would |
| 787 | // involve storing all CRTCs upon entering fullscreen, |
| 788 | // restoring them upon exit, and after entering fullscreen, |
| 789 | // repositioning displays to the right and below this |
| 790 | // display. I think there would still be edge cases that are |
| 791 | // difficult or impossible to handle correctly, e.g. what if |
| 792 | // a new monitor was plugged in while in fullscreen? |
| 793 | // |
| 794 | // I think we might just want to disallow setting the video |
| 795 | // mode higher than the current desktop video mode (I'm sure |
| 796 | // this will make someone unhappy, but it's very unusual for |
| 797 | // games to want to do this anyway). |
| 798 | self.xconn |
| 799 | .set_crtc_config(monitor.id, video_mode.native_mode) |
| 800 | .expect("failed to set video mode" ); |
| 801 | } |
| 802 | |
| 803 | let window_position = self.outer_position_physical(); |
| 804 | self.shared_state_lock().restore_position = Some(window_position); |
| 805 | let monitor_origin: (i32, i32) = monitor.position().into(); |
| 806 | self.set_position_inner(monitor_origin.0, monitor_origin.1) |
| 807 | .expect_then_ignore_error("Failed to set window position" ); |
| 808 | self.set_fullscreen_hint(true).map(Some) |
| 809 | }, |
| 810 | } |
| 811 | } |
| 812 | |
| 813 | #[inline ] |
| 814 | pub(crate) fn fullscreen(&self) -> Option<Fullscreen> { |
| 815 | let shared_state = self.shared_state_lock(); |
| 816 | |
| 817 | shared_state.desired_fullscreen.clone().unwrap_or_else(|| shared_state.fullscreen.clone()) |
| 818 | } |
| 819 | |
| 820 | #[inline ] |
| 821 | pub(crate) fn set_fullscreen(&self, fullscreen: Option<Fullscreen>) { |
| 822 | if let Some(flusher) = |
| 823 | self.set_fullscreen_inner(fullscreen).expect("Failed to change window fullscreen state" ) |
| 824 | { |
| 825 | flusher.check().expect("Failed to change window fullscreen state" ); |
| 826 | self.invalidate_cached_frame_extents(); |
| 827 | } |
| 828 | } |
| 829 | |
| 830 | // Called by EventProcessor when a VisibilityNotify event is received |
| 831 | pub(crate) fn visibility_notify(&self) { |
| 832 | let mut shared_state = self.shared_state_lock(); |
| 833 | |
| 834 | match shared_state.visibility { |
| 835 | Visibility::No => self |
| 836 | .xconn |
| 837 | .xcb_connection() |
| 838 | .unmap_window(self.xwindow) |
| 839 | .expect_then_ignore_error("Failed to unmap window" ), |
| 840 | Visibility::Yes => (), |
| 841 | Visibility::YesWait => { |
| 842 | shared_state.visibility = Visibility::Yes; |
| 843 | |
| 844 | if let Some(fullscreen) = shared_state.desired_fullscreen.take() { |
| 845 | drop(shared_state); |
| 846 | self.set_fullscreen(fullscreen); |
| 847 | } |
| 848 | }, |
| 849 | } |
| 850 | } |
| 851 | |
| 852 | pub fn current_monitor(&self) -> Option<X11MonitorHandle> { |
| 853 | Some(self.shared_state_lock().last_monitor.clone()) |
| 854 | } |
| 855 | |
| 856 | pub fn available_monitors(&self) -> Vec<X11MonitorHandle> { |
| 857 | self.xconn.available_monitors().expect("Failed to get available monitors" ) |
| 858 | } |
| 859 | |
| 860 | pub fn primary_monitor(&self) -> Option<X11MonitorHandle> { |
| 861 | Some(self.xconn.primary_monitor().expect("Failed to get primary monitor" )) |
| 862 | } |
| 863 | |
| 864 | #[inline ] |
| 865 | pub fn is_minimized(&self) -> Option<bool> { |
| 866 | let atoms = self.xconn.atoms(); |
| 867 | let state_atom = atoms[_NET_WM_STATE]; |
| 868 | let state = self.xconn.get_property( |
| 869 | self.xwindow, |
| 870 | state_atom, |
| 871 | xproto::Atom::from(xproto::AtomEnum::ATOM), |
| 872 | ); |
| 873 | let hidden_atom = atoms[_NET_WM_STATE_HIDDEN]; |
| 874 | |
| 875 | Some(match state { |
| 876 | Ok(atoms) => { |
| 877 | atoms.iter().any(|atom: &xproto::Atom| *atom as xproto::Atom == hidden_atom) |
| 878 | }, |
| 879 | _ => false, |
| 880 | }) |
| 881 | } |
| 882 | |
| 883 | /// Refresh the API for the given monitor. |
| 884 | #[inline ] |
| 885 | pub(super) fn refresh_dpi_for_monitor<T: 'static>( |
| 886 | &self, |
| 887 | new_monitor: &X11MonitorHandle, |
| 888 | maybe_prev_scale_factor: Option<f64>, |
| 889 | mut callback: impl FnMut(Event<T>), |
| 890 | ) { |
| 891 | // Check if the self is on this monitor |
| 892 | let monitor = self.shared_state_lock().last_monitor.clone(); |
| 893 | if monitor.name == new_monitor.name { |
| 894 | let (width, height) = self.inner_size_physical(); |
| 895 | let (new_width, new_height) = self.adjust_for_dpi( |
| 896 | // If we couldn't determine the previous scale |
| 897 | // factor (e.g., because all monitors were closed |
| 898 | // before), just pick whatever the current monitor |
| 899 | // has set as a baseline. |
| 900 | maybe_prev_scale_factor.unwrap_or(monitor.scale_factor), |
| 901 | new_monitor.scale_factor, |
| 902 | width, |
| 903 | height, |
| 904 | &self.shared_state_lock(), |
| 905 | ); |
| 906 | |
| 907 | let window_id = crate::window::WindowId(self.id()); |
| 908 | let old_inner_size = PhysicalSize::new(width, height); |
| 909 | let inner_size = Arc::new(Mutex::new(PhysicalSize::new(new_width, new_height))); |
| 910 | callback(Event::WindowEvent { |
| 911 | window_id, |
| 912 | event: WindowEvent::ScaleFactorChanged { |
| 913 | scale_factor: new_monitor.scale_factor, |
| 914 | inner_size_writer: InnerSizeWriter::new(Arc::downgrade(&inner_size)), |
| 915 | }, |
| 916 | }); |
| 917 | |
| 918 | let new_inner_size = *inner_size.lock().unwrap(); |
| 919 | drop(inner_size); |
| 920 | |
| 921 | if new_inner_size != old_inner_size { |
| 922 | let (new_width, new_height) = new_inner_size.into(); |
| 923 | self.request_inner_size_physical(new_width, new_height); |
| 924 | } |
| 925 | } |
| 926 | } |
| 927 | |
| 928 | fn set_minimized_inner(&self, minimized: bool) -> Result<VoidCookie<'_>, X11Error> { |
| 929 | let atoms = self.xconn.atoms(); |
| 930 | |
| 931 | if minimized { |
| 932 | let root_window = self.xconn.default_root().root; |
| 933 | |
| 934 | self.xconn.send_client_msg( |
| 935 | self.xwindow, |
| 936 | root_window, |
| 937 | atoms[WM_CHANGE_STATE], |
| 938 | Some( |
| 939 | xproto::EventMask::SUBSTRUCTURE_REDIRECT |
| 940 | | xproto::EventMask::SUBSTRUCTURE_NOTIFY, |
| 941 | ), |
| 942 | [3u32, 0, 0, 0, 0], |
| 943 | ) |
| 944 | } else { |
| 945 | self.xconn.send_client_msg( |
| 946 | self.xwindow, |
| 947 | self.root, |
| 948 | atoms[_NET_ACTIVE_WINDOW], |
| 949 | Some( |
| 950 | xproto::EventMask::SUBSTRUCTURE_REDIRECT |
| 951 | | xproto::EventMask::SUBSTRUCTURE_NOTIFY, |
| 952 | ), |
| 953 | [1, x11rb::CURRENT_TIME, 0, 0, 0], |
| 954 | ) |
| 955 | } |
| 956 | } |
| 957 | |
| 958 | #[inline ] |
| 959 | pub fn set_minimized(&self, minimized: bool) { |
| 960 | self.set_minimized_inner(minimized) |
| 961 | .expect_then_ignore_error("Failed to change window minimization" ); |
| 962 | |
| 963 | self.xconn.flush_requests().expect("Failed to change window minimization" ); |
| 964 | } |
| 965 | |
| 966 | #[inline ] |
| 967 | pub fn is_maximized(&self) -> bool { |
| 968 | let atoms = self.xconn.atoms(); |
| 969 | let state_atom = atoms[_NET_WM_STATE]; |
| 970 | let state = self.xconn.get_property( |
| 971 | self.xwindow, |
| 972 | state_atom, |
| 973 | xproto::Atom::from(xproto::AtomEnum::ATOM), |
| 974 | ); |
| 975 | let horz_atom = atoms[_NET_WM_STATE_MAXIMIZED_HORZ]; |
| 976 | let vert_atom = atoms[_NET_WM_STATE_MAXIMIZED_VERT]; |
| 977 | match state { |
| 978 | Ok(atoms) => { |
| 979 | let horz_maximized = atoms.iter().any(|atom: &xproto::Atom| *atom == horz_atom); |
| 980 | let vert_maximized = atoms.iter().any(|atom: &xproto::Atom| *atom == vert_atom); |
| 981 | horz_maximized && vert_maximized |
| 982 | }, |
| 983 | _ => false, |
| 984 | } |
| 985 | } |
| 986 | |
| 987 | fn set_maximized_inner(&self, maximized: bool) -> Result<VoidCookie<'_>, X11Error> { |
| 988 | let atoms = self.xconn.atoms(); |
| 989 | let horz_atom = atoms[_NET_WM_STATE_MAXIMIZED_HORZ]; |
| 990 | let vert_atom = atoms[_NET_WM_STATE_MAXIMIZED_VERT]; |
| 991 | |
| 992 | self.set_netwm(maximized.into(), (horz_atom, vert_atom, 0, 0)) |
| 993 | } |
| 994 | |
| 995 | #[inline ] |
| 996 | pub fn set_maximized(&self, maximized: bool) { |
| 997 | self.set_maximized_inner(maximized) |
| 998 | .expect_then_ignore_error("Failed to change window maximization" ); |
| 999 | self.xconn.flush_requests().expect("Failed to change window maximization" ); |
| 1000 | self.invalidate_cached_frame_extents(); |
| 1001 | } |
| 1002 | |
| 1003 | fn set_title_inner(&self, title: &str) -> Result<VoidCookie<'_>, X11Error> { |
| 1004 | let atoms = self.xconn.atoms(); |
| 1005 | |
| 1006 | let title = CString::new(title).expect("Window title contained null byte" ); |
| 1007 | self.xconn |
| 1008 | .change_property( |
| 1009 | self.xwindow, |
| 1010 | xproto::Atom::from(xproto::AtomEnum::WM_NAME), |
| 1011 | xproto::Atom::from(xproto::AtomEnum::STRING), |
| 1012 | xproto::PropMode::REPLACE, |
| 1013 | title.as_bytes(), |
| 1014 | )? |
| 1015 | .ignore_error(); |
| 1016 | self.xconn.change_property( |
| 1017 | self.xwindow, |
| 1018 | atoms[_NET_WM_NAME], |
| 1019 | atoms[UTF8_STRING], |
| 1020 | xproto::PropMode::REPLACE, |
| 1021 | title.as_bytes(), |
| 1022 | ) |
| 1023 | } |
| 1024 | |
| 1025 | #[inline ] |
| 1026 | pub fn set_title(&self, title: &str) { |
| 1027 | self.set_title_inner(title).expect_then_ignore_error("Failed to set window title" ); |
| 1028 | |
| 1029 | self.xconn.flush_requests().expect("Failed to set window title" ); |
| 1030 | } |
| 1031 | |
| 1032 | #[inline ] |
| 1033 | pub fn set_transparent(&self, _transparent: bool) {} |
| 1034 | |
| 1035 | #[inline ] |
| 1036 | pub fn set_blur(&self, _blur: bool) {} |
| 1037 | |
| 1038 | fn set_decorations_inner(&self, decorations: bool) -> Result<VoidCookie<'_>, X11Error> { |
| 1039 | self.shared_state_lock().is_decorated = decorations; |
| 1040 | let mut hints = self.xconn.get_motif_hints(self.xwindow); |
| 1041 | |
| 1042 | hints.set_decorations(decorations); |
| 1043 | |
| 1044 | self.xconn.set_motif_hints(self.xwindow, &hints) |
| 1045 | } |
| 1046 | |
| 1047 | #[inline ] |
| 1048 | pub fn set_decorations(&self, decorations: bool) { |
| 1049 | self.set_decorations_inner(decorations) |
| 1050 | .expect_then_ignore_error("Failed to set decoration state" ); |
| 1051 | self.xconn.flush_requests().expect("Failed to set decoration state" ); |
| 1052 | self.invalidate_cached_frame_extents(); |
| 1053 | } |
| 1054 | |
| 1055 | #[inline ] |
| 1056 | pub fn is_decorated(&self) -> bool { |
| 1057 | self.shared_state_lock().is_decorated |
| 1058 | } |
| 1059 | |
| 1060 | fn set_maximizable_inner(&self, maximizable: bool) -> Result<VoidCookie<'_>, X11Error> { |
| 1061 | let mut hints = self.xconn.get_motif_hints(self.xwindow); |
| 1062 | |
| 1063 | hints.set_maximizable(maximizable); |
| 1064 | |
| 1065 | self.xconn.set_motif_hints(self.xwindow, &hints) |
| 1066 | } |
| 1067 | |
| 1068 | fn toggle_atom(&self, atom_name: AtomName, enable: bool) -> Result<VoidCookie<'_>, X11Error> { |
| 1069 | let atoms = self.xconn.atoms(); |
| 1070 | let atom = atoms[atom_name]; |
| 1071 | self.set_netwm(enable.into(), (atom, 0, 0, 0)) |
| 1072 | } |
| 1073 | |
| 1074 | fn set_window_level_inner(&self, level: WindowLevel) -> Result<VoidCookie<'_>, X11Error> { |
| 1075 | self.toggle_atom(_NET_WM_STATE_ABOVE, level == WindowLevel::AlwaysOnTop)?.ignore_error(); |
| 1076 | self.toggle_atom(_NET_WM_STATE_BELOW, level == WindowLevel::AlwaysOnBottom) |
| 1077 | } |
| 1078 | |
| 1079 | #[inline ] |
| 1080 | pub fn set_window_level(&self, level: WindowLevel) { |
| 1081 | self.set_window_level_inner(level) |
| 1082 | .expect_then_ignore_error("Failed to set window-level state" ); |
| 1083 | self.xconn.flush_requests().expect("Failed to set window-level state" ); |
| 1084 | } |
| 1085 | |
| 1086 | fn set_icon_inner(&self, icon: PlatformIcon) -> Result<VoidCookie<'_>, X11Error> { |
| 1087 | let atoms = self.xconn.atoms(); |
| 1088 | let icon_atom = atoms[_NET_WM_ICON]; |
| 1089 | let data = icon.to_cardinals(); |
| 1090 | self.xconn.change_property( |
| 1091 | self.xwindow, |
| 1092 | icon_atom, |
| 1093 | xproto::Atom::from(xproto::AtomEnum::CARDINAL), |
| 1094 | xproto::PropMode::REPLACE, |
| 1095 | data.as_slice(), |
| 1096 | ) |
| 1097 | } |
| 1098 | |
| 1099 | fn unset_icon_inner(&self) -> Result<VoidCookie<'_>, X11Error> { |
| 1100 | let atoms = self.xconn.atoms(); |
| 1101 | let icon_atom = atoms[_NET_WM_ICON]; |
| 1102 | let empty_data: [util::Cardinal; 0] = []; |
| 1103 | self.xconn.change_property( |
| 1104 | self.xwindow, |
| 1105 | icon_atom, |
| 1106 | xproto::Atom::from(xproto::AtomEnum::CARDINAL), |
| 1107 | xproto::PropMode::REPLACE, |
| 1108 | &empty_data, |
| 1109 | ) |
| 1110 | } |
| 1111 | |
| 1112 | #[inline ] |
| 1113 | pub(crate) fn set_window_icon(&self, icon: Option<PlatformIcon>) { |
| 1114 | match icon { |
| 1115 | Some(icon) => self.set_icon_inner(icon), |
| 1116 | None => self.unset_icon_inner(), |
| 1117 | } |
| 1118 | .expect_then_ignore_error("Failed to set icons" ); |
| 1119 | |
| 1120 | self.xconn.flush_requests().expect("Failed to set icons" ); |
| 1121 | } |
| 1122 | |
| 1123 | #[inline ] |
| 1124 | pub fn set_visible(&self, visible: bool) { |
| 1125 | let mut shared_state = self.shared_state_lock(); |
| 1126 | |
| 1127 | match (visible, shared_state.visibility) { |
| 1128 | (true, Visibility::Yes) | (true, Visibility::YesWait) | (false, Visibility::No) => { |
| 1129 | return |
| 1130 | }, |
| 1131 | _ => (), |
| 1132 | } |
| 1133 | |
| 1134 | if visible { |
| 1135 | self.xconn |
| 1136 | .xcb_connection() |
| 1137 | .map_window(self.xwindow) |
| 1138 | .expect_then_ignore_error("Failed to call `xcb_map_window`" ); |
| 1139 | self.xconn |
| 1140 | .xcb_connection() |
| 1141 | .configure_window( |
| 1142 | self.xwindow, |
| 1143 | &xproto::ConfigureWindowAux::new().stack_mode(xproto::StackMode::ABOVE), |
| 1144 | ) |
| 1145 | .expect_then_ignore_error("Failed to call `xcb_configure_window`" ); |
| 1146 | self.xconn.flush_requests().expect("Failed to call XMapRaised" ); |
| 1147 | shared_state.visibility = Visibility::YesWait; |
| 1148 | } else { |
| 1149 | self.xconn |
| 1150 | .xcb_connection() |
| 1151 | .unmap_window(self.xwindow) |
| 1152 | .expect_then_ignore_error("Failed to call `xcb_unmap_window`" ); |
| 1153 | self.xconn.flush_requests().expect("Failed to call XUnmapWindow" ); |
| 1154 | shared_state.visibility = Visibility::No; |
| 1155 | } |
| 1156 | } |
| 1157 | |
| 1158 | #[inline ] |
| 1159 | pub fn is_visible(&self) -> Option<bool> { |
| 1160 | Some(self.shared_state_lock().visibility == Visibility::Yes) |
| 1161 | } |
| 1162 | |
| 1163 | fn update_cached_frame_extents(&self) { |
| 1164 | let extents = self.xconn.get_frame_extents_heuristic(self.xwindow, self.root); |
| 1165 | self.shared_state_lock().frame_extents = Some(extents); |
| 1166 | } |
| 1167 | |
| 1168 | pub(crate) fn invalidate_cached_frame_extents(&self) { |
| 1169 | self.shared_state_lock().frame_extents.take(); |
| 1170 | } |
| 1171 | |
| 1172 | pub(crate) fn outer_position_physical(&self) -> (i32, i32) { |
| 1173 | let extents = self.shared_state_lock().frame_extents.clone(); |
| 1174 | if let Some(extents) = extents { |
| 1175 | let (x, y) = self.inner_position_physical(); |
| 1176 | extents.inner_pos_to_outer(x, y) |
| 1177 | } else { |
| 1178 | self.update_cached_frame_extents(); |
| 1179 | self.outer_position_physical() |
| 1180 | } |
| 1181 | } |
| 1182 | |
| 1183 | #[inline ] |
| 1184 | pub fn outer_position(&self) -> Result<PhysicalPosition<i32>, NotSupportedError> { |
| 1185 | let extents = self.shared_state_lock().frame_extents.clone(); |
| 1186 | if let Some(extents) = extents { |
| 1187 | let (x, y) = self.inner_position_physical(); |
| 1188 | Ok(extents.inner_pos_to_outer(x, y).into()) |
| 1189 | } else { |
| 1190 | self.update_cached_frame_extents(); |
| 1191 | self.outer_position() |
| 1192 | } |
| 1193 | } |
| 1194 | |
| 1195 | pub(crate) fn inner_position_physical(&self) -> (i32, i32) { |
| 1196 | // This should be okay to unwrap since the only error XTranslateCoordinates can return |
| 1197 | // is BadWindow, and if the window handle is bad we have bigger problems. |
| 1198 | self.xconn |
| 1199 | .translate_coords(self.xwindow, self.root) |
| 1200 | .map(|coords| (coords.dst_x.into(), coords.dst_y.into())) |
| 1201 | .unwrap() |
| 1202 | } |
| 1203 | |
| 1204 | #[inline ] |
| 1205 | pub fn inner_position(&self) -> Result<PhysicalPosition<i32>, NotSupportedError> { |
| 1206 | Ok(self.inner_position_physical().into()) |
| 1207 | } |
| 1208 | |
| 1209 | pub(crate) fn set_position_inner( |
| 1210 | &self, |
| 1211 | mut x: i32, |
| 1212 | mut y: i32, |
| 1213 | ) -> Result<VoidCookie<'_>, X11Error> { |
| 1214 | // There are a few WMs that set client area position rather than window position, so |
| 1215 | // we'll translate for consistency. |
| 1216 | if util::wm_name_is_one_of(&["Enlightenment" , "FVWM" ]) { |
| 1217 | let extents = self.shared_state_lock().frame_extents.clone(); |
| 1218 | if let Some(extents) = extents { |
| 1219 | x += cast_dimension_to_hint(extents.frame_extents.left); |
| 1220 | y += cast_dimension_to_hint(extents.frame_extents.top); |
| 1221 | } else { |
| 1222 | self.update_cached_frame_extents(); |
| 1223 | return self.set_position_inner(x, y); |
| 1224 | } |
| 1225 | } |
| 1226 | |
| 1227 | self.xconn |
| 1228 | .xcb_connection() |
| 1229 | .configure_window(self.xwindow, &xproto::ConfigureWindowAux::new().x(x).y(y)) |
| 1230 | .map_err(Into::into) |
| 1231 | } |
| 1232 | |
| 1233 | pub(crate) fn set_position_physical(&self, x: i32, y: i32) { |
| 1234 | self.set_position_inner(x, y).expect_then_ignore_error("Failed to call `XMoveWindow`" ); |
| 1235 | } |
| 1236 | |
| 1237 | #[inline ] |
| 1238 | pub fn set_outer_position(&self, position: Position) { |
| 1239 | let (x, y) = position.to_physical::<i32>(self.scale_factor()).into(); |
| 1240 | self.set_position_physical(x, y); |
| 1241 | } |
| 1242 | |
| 1243 | pub(crate) fn inner_size_physical(&self) -> (u32, u32) { |
| 1244 | // This should be okay to unwrap since the only error XGetGeometry can return |
| 1245 | // is BadWindow, and if the window handle is bad we have bigger problems. |
| 1246 | self.xconn |
| 1247 | .get_geometry(self.xwindow) |
| 1248 | .map(|geo| (geo.width.into(), geo.height.into())) |
| 1249 | .unwrap() |
| 1250 | } |
| 1251 | |
| 1252 | #[inline ] |
| 1253 | pub fn inner_size(&self) -> PhysicalSize<u32> { |
| 1254 | self.inner_size_physical().into() |
| 1255 | } |
| 1256 | |
| 1257 | #[inline ] |
| 1258 | pub fn outer_size(&self) -> PhysicalSize<u32> { |
| 1259 | let extents = self.shared_state_lock().frame_extents.clone(); |
| 1260 | if let Some(extents) = extents { |
| 1261 | let (width, height) = self.inner_size_physical(); |
| 1262 | extents.inner_size_to_outer(width, height).into() |
| 1263 | } else { |
| 1264 | self.update_cached_frame_extents(); |
| 1265 | self.outer_size() |
| 1266 | } |
| 1267 | } |
| 1268 | |
| 1269 | pub(crate) fn request_inner_size_physical(&self, width: u32, height: u32) { |
| 1270 | self.xconn |
| 1271 | .xcb_connection() |
| 1272 | .configure_window( |
| 1273 | self.xwindow, |
| 1274 | &xproto::ConfigureWindowAux::new().width(width).height(height), |
| 1275 | ) |
| 1276 | .expect_then_ignore_error("Failed to call `xcb_configure_window`" ); |
| 1277 | self.xconn.flush_requests().expect("Failed to call XResizeWindow" ); |
| 1278 | // cursor_hittest needs to be reapplied after each window resize. |
| 1279 | if self.shared_state_lock().cursor_hittest.unwrap_or(false) { |
| 1280 | let _ = self.set_cursor_hittest(true); |
| 1281 | } |
| 1282 | } |
| 1283 | |
| 1284 | #[inline ] |
| 1285 | pub fn request_inner_size(&self, size: Size) -> Option<PhysicalSize<u32>> { |
| 1286 | let scale_factor = self.scale_factor(); |
| 1287 | let size = size.to_physical::<u32>(scale_factor).into(); |
| 1288 | if !self.shared_state_lock().is_resizable { |
| 1289 | self.update_normal_hints(|normal_hints| { |
| 1290 | normal_hints.min_size = Some(size); |
| 1291 | normal_hints.max_size = Some(size); |
| 1292 | }) |
| 1293 | .expect("Failed to call `XSetWMNormalHints`" ); |
| 1294 | } |
| 1295 | self.request_inner_size_physical(size.0 as u32, size.1 as u32); |
| 1296 | |
| 1297 | None |
| 1298 | } |
| 1299 | |
| 1300 | fn update_normal_hints<F>(&self, callback: F) -> Result<(), X11Error> |
| 1301 | where |
| 1302 | F: FnOnce(&mut WmSizeHints), |
| 1303 | { |
| 1304 | let mut normal_hints = WmSizeHints::get( |
| 1305 | self.xconn.xcb_connection(), |
| 1306 | self.xwindow as xproto::Window, |
| 1307 | xproto::AtomEnum::WM_NORMAL_HINTS, |
| 1308 | )? |
| 1309 | .reply()? |
| 1310 | .unwrap_or_default(); |
| 1311 | callback(&mut normal_hints); |
| 1312 | normal_hints |
| 1313 | .set( |
| 1314 | self.xconn.xcb_connection(), |
| 1315 | self.xwindow as xproto::Window, |
| 1316 | xproto::AtomEnum::WM_NORMAL_HINTS, |
| 1317 | )? |
| 1318 | .ignore_error(); |
| 1319 | Ok(()) |
| 1320 | } |
| 1321 | |
| 1322 | pub(crate) fn set_min_inner_size_physical(&self, dimensions: Option<(u32, u32)>) { |
| 1323 | self.update_normal_hints(|normal_hints| { |
| 1324 | normal_hints.min_size = |
| 1325 | dimensions.map(|(w, h)| (cast_dimension_to_hint(w), cast_dimension_to_hint(h))) |
| 1326 | }) |
| 1327 | .expect("Failed to call `XSetWMNormalHints`" ); |
| 1328 | } |
| 1329 | |
| 1330 | #[inline ] |
| 1331 | pub fn set_min_inner_size(&self, dimensions: Option<Size>) { |
| 1332 | self.shared_state_lock().min_inner_size = dimensions; |
| 1333 | let physical_dimensions = |
| 1334 | dimensions.map(|dimensions| dimensions.to_physical::<u32>(self.scale_factor()).into()); |
| 1335 | self.set_min_inner_size_physical(physical_dimensions); |
| 1336 | } |
| 1337 | |
| 1338 | pub(crate) fn set_max_inner_size_physical(&self, dimensions: Option<(u32, u32)>) { |
| 1339 | self.update_normal_hints(|normal_hints| { |
| 1340 | normal_hints.max_size = |
| 1341 | dimensions.map(|(w, h)| (cast_dimension_to_hint(w), cast_dimension_to_hint(h))) |
| 1342 | }) |
| 1343 | .expect("Failed to call `XSetWMNormalHints`" ); |
| 1344 | } |
| 1345 | |
| 1346 | #[inline ] |
| 1347 | pub fn set_max_inner_size(&self, dimensions: Option<Size>) { |
| 1348 | self.shared_state_lock().max_inner_size = dimensions; |
| 1349 | let physical_dimensions = |
| 1350 | dimensions.map(|dimensions| dimensions.to_physical::<u32>(self.scale_factor()).into()); |
| 1351 | self.set_max_inner_size_physical(physical_dimensions); |
| 1352 | } |
| 1353 | |
| 1354 | #[inline ] |
| 1355 | pub fn resize_increments(&self) -> Option<PhysicalSize<u32>> { |
| 1356 | WmSizeHints::get( |
| 1357 | self.xconn.xcb_connection(), |
| 1358 | self.xwindow as xproto::Window, |
| 1359 | xproto::AtomEnum::WM_NORMAL_HINTS, |
| 1360 | ) |
| 1361 | .ok() |
| 1362 | .and_then(|cookie| cookie.reply().ok()) |
| 1363 | .flatten() |
| 1364 | .and_then(|hints| hints.size_increment) |
| 1365 | .map(|(width, height)| (width as u32, height as u32).into()) |
| 1366 | } |
| 1367 | |
| 1368 | #[inline ] |
| 1369 | pub fn set_resize_increments(&self, increments: Option<Size>) { |
| 1370 | self.shared_state_lock().resize_increments = increments; |
| 1371 | let physical_increments = |
| 1372 | increments.map(|increments| cast_size_to_hint(increments, self.scale_factor())); |
| 1373 | self.update_normal_hints(|hints| hints.size_increment = physical_increments) |
| 1374 | .expect("Failed to call `XSetWMNormalHints`" ); |
| 1375 | } |
| 1376 | |
| 1377 | pub(crate) fn adjust_for_dpi( |
| 1378 | &self, |
| 1379 | old_scale_factor: f64, |
| 1380 | new_scale_factor: f64, |
| 1381 | width: u32, |
| 1382 | height: u32, |
| 1383 | shared_state: &SharedState, |
| 1384 | ) -> (u32, u32) { |
| 1385 | let scale_factor = new_scale_factor / old_scale_factor; |
| 1386 | self.update_normal_hints(|normal_hints| { |
| 1387 | let dpi_adjuster = |size: Size| -> (i32, i32) { cast_size_to_hint(size, scale_factor) }; |
| 1388 | let max_size = shared_state.max_inner_size.map(dpi_adjuster); |
| 1389 | let min_size = shared_state.min_inner_size.map(dpi_adjuster); |
| 1390 | let resize_increments = shared_state.resize_increments.map(dpi_adjuster); |
| 1391 | let base_size = shared_state.base_size.map(dpi_adjuster); |
| 1392 | |
| 1393 | normal_hints.max_size = max_size; |
| 1394 | normal_hints.min_size = min_size; |
| 1395 | normal_hints.size_increment = resize_increments; |
| 1396 | normal_hints.base_size = base_size; |
| 1397 | }) |
| 1398 | .expect("Failed to update normal hints" ); |
| 1399 | |
| 1400 | let new_width = (width as f64 * scale_factor).round() as u32; |
| 1401 | let new_height = (height as f64 * scale_factor).round() as u32; |
| 1402 | |
| 1403 | (new_width, new_height) |
| 1404 | } |
| 1405 | |
| 1406 | pub fn set_resizable(&self, resizable: bool) { |
| 1407 | if util::wm_name_is_one_of(&["Xfwm4" ]) { |
| 1408 | // Making the window unresizable on Xfwm prevents further changes to `WM_NORMAL_HINTS` |
| 1409 | // from being detected. This makes it impossible for resizing to be |
| 1410 | // re-enabled, and also breaks DPI scaling. As such, we choose the lesser of |
| 1411 | // two evils and do nothing. |
| 1412 | warn!("To avoid a WM bug, disabling resizing has no effect on Xfwm4" ); |
| 1413 | return; |
| 1414 | } |
| 1415 | |
| 1416 | let (min_size, max_size) = if resizable { |
| 1417 | let shared_state_lock = self.shared_state_lock(); |
| 1418 | (shared_state_lock.min_inner_size, shared_state_lock.max_inner_size) |
| 1419 | } else { |
| 1420 | let window_size = Some(Size::from(self.inner_size())); |
| 1421 | (window_size, window_size) |
| 1422 | }; |
| 1423 | self.shared_state_lock().is_resizable = resizable; |
| 1424 | |
| 1425 | self.set_maximizable_inner(resizable) |
| 1426 | .expect_then_ignore_error("Failed to call `XSetWMNormalHints`" ); |
| 1427 | |
| 1428 | let scale_factor = self.scale_factor(); |
| 1429 | let min_inner_size = min_size.map(|size| cast_size_to_hint(size, scale_factor)); |
| 1430 | let max_inner_size = max_size.map(|size| cast_size_to_hint(size, scale_factor)); |
| 1431 | self.update_normal_hints(|normal_hints| { |
| 1432 | normal_hints.min_size = min_inner_size; |
| 1433 | normal_hints.max_size = max_inner_size; |
| 1434 | }) |
| 1435 | .expect("Failed to call `XSetWMNormalHints`" ); |
| 1436 | } |
| 1437 | |
| 1438 | #[inline ] |
| 1439 | pub fn is_resizable(&self) -> bool { |
| 1440 | self.shared_state_lock().is_resizable |
| 1441 | } |
| 1442 | |
| 1443 | #[inline ] |
| 1444 | pub fn set_enabled_buttons(&self, _buttons: WindowButtons) {} |
| 1445 | |
| 1446 | #[inline ] |
| 1447 | pub fn enabled_buttons(&self) -> WindowButtons { |
| 1448 | WindowButtons::all() |
| 1449 | } |
| 1450 | |
| 1451 | #[allow (dead_code)] |
| 1452 | #[inline ] |
| 1453 | pub fn xlib_display(&self) -> *mut c_void { |
| 1454 | self.xconn.display as _ |
| 1455 | } |
| 1456 | |
| 1457 | #[allow (dead_code)] |
| 1458 | #[inline ] |
| 1459 | pub fn xlib_window(&self) -> c_ulong { |
| 1460 | self.xwindow as ffi::Window |
| 1461 | } |
| 1462 | |
| 1463 | #[inline ] |
| 1464 | pub fn set_cursor(&self, cursor: Cursor) { |
| 1465 | match cursor { |
| 1466 | Cursor::Icon(icon) => { |
| 1467 | let old_cursor = replace( |
| 1468 | &mut *self.selected_cursor.lock().unwrap(), |
| 1469 | SelectedCursor::Named(icon), |
| 1470 | ); |
| 1471 | |
| 1472 | #[allow (clippy::mutex_atomic)] |
| 1473 | if SelectedCursor::Named(icon) != old_cursor && *self.cursor_visible.lock().unwrap() |
| 1474 | { |
| 1475 | self.xconn.set_cursor_icon(self.xwindow, Some(icon)); |
| 1476 | } |
| 1477 | }, |
| 1478 | Cursor::Custom(RootCustomCursor { inner: PlatformCustomCursor::X(cursor) }) => { |
| 1479 | #[allow (clippy::mutex_atomic)] |
| 1480 | if *self.cursor_visible.lock().unwrap() { |
| 1481 | self.xconn.set_custom_cursor(self.xwindow, &cursor); |
| 1482 | } |
| 1483 | |
| 1484 | *self.selected_cursor.lock().unwrap() = SelectedCursor::Custom(cursor); |
| 1485 | }, |
| 1486 | #[cfg (wayland_platform)] |
| 1487 | Cursor::Custom(RootCustomCursor { inner: PlatformCustomCursor::Wayland(_) }) => { |
| 1488 | tracing::error!("passed a Wayland cursor to X11 backend" ) |
| 1489 | }, |
| 1490 | } |
| 1491 | } |
| 1492 | |
| 1493 | #[inline ] |
| 1494 | pub fn set_cursor_grab(&self, mode: CursorGrabMode) -> Result<(), ExternalError> { |
| 1495 | // We don't support the locked cursor yet, so ignore it early on. |
| 1496 | if mode == CursorGrabMode::Locked { |
| 1497 | return Err(ExternalError::NotSupported(NotSupportedError::new())); |
| 1498 | } |
| 1499 | |
| 1500 | let mut grabbed_lock = self.cursor_grabbed_mode.lock().unwrap(); |
| 1501 | if mode == *grabbed_lock { |
| 1502 | return Ok(()); |
| 1503 | } |
| 1504 | |
| 1505 | // We ungrab before grabbing to prevent passive grabs from causing `AlreadyGrabbed`. |
| 1506 | // Therefore, this is common to both codepaths. |
| 1507 | self.xconn |
| 1508 | .xcb_connection() |
| 1509 | .ungrab_pointer(x11rb::CURRENT_TIME) |
| 1510 | .expect_then_ignore_error("Failed to call `xcb_ungrab_pointer`" ); |
| 1511 | *grabbed_lock = CursorGrabMode::None; |
| 1512 | |
| 1513 | let result = match mode { |
| 1514 | CursorGrabMode::None => self.xconn.flush_requests().map_err(|err| { |
| 1515 | ExternalError::Os(os_error!(OsError::XError(X11Error::Xlib(err).into()))) |
| 1516 | }), |
| 1517 | CursorGrabMode::Confined => { |
| 1518 | let result = self |
| 1519 | .xconn |
| 1520 | .xcb_connection() |
| 1521 | .grab_pointer( |
| 1522 | true as _, |
| 1523 | self.xwindow, |
| 1524 | xproto::EventMask::BUTTON_PRESS |
| 1525 | | xproto::EventMask::BUTTON_RELEASE |
| 1526 | | xproto::EventMask::ENTER_WINDOW |
| 1527 | | xproto::EventMask::LEAVE_WINDOW |
| 1528 | | xproto::EventMask::POINTER_MOTION |
| 1529 | | xproto::EventMask::POINTER_MOTION_HINT |
| 1530 | | xproto::EventMask::BUTTON1_MOTION |
| 1531 | | xproto::EventMask::BUTTON2_MOTION |
| 1532 | | xproto::EventMask::BUTTON3_MOTION |
| 1533 | | xproto::EventMask::BUTTON4_MOTION |
| 1534 | | xproto::EventMask::BUTTON5_MOTION |
| 1535 | | xproto::EventMask::KEYMAP_STATE, |
| 1536 | xproto::GrabMode::ASYNC, |
| 1537 | xproto::GrabMode::ASYNC, |
| 1538 | self.xwindow, |
| 1539 | 0u32, |
| 1540 | x11rb::CURRENT_TIME, |
| 1541 | ) |
| 1542 | .expect("Failed to call `grab_pointer`" ) |
| 1543 | .reply() |
| 1544 | .expect("Failed to receive reply from `grab_pointer`" ); |
| 1545 | |
| 1546 | match result.status { |
| 1547 | xproto::GrabStatus::SUCCESS => Ok(()), |
| 1548 | xproto::GrabStatus::ALREADY_GRABBED => { |
| 1549 | Err("Cursor could not be confined: already confined by another client" ) |
| 1550 | }, |
| 1551 | xproto::GrabStatus::INVALID_TIME => { |
| 1552 | Err("Cursor could not be confined: invalid time" ) |
| 1553 | }, |
| 1554 | xproto::GrabStatus::NOT_VIEWABLE => { |
| 1555 | Err("Cursor could not be confined: confine location not viewable" ) |
| 1556 | }, |
| 1557 | xproto::GrabStatus::FROZEN => { |
| 1558 | Err("Cursor could not be confined: frozen by another client" ) |
| 1559 | }, |
| 1560 | _ => unreachable!(), |
| 1561 | } |
| 1562 | .map_err(|err| ExternalError::Os(os_error!(OsError::Misc(err)))) |
| 1563 | }, |
| 1564 | CursorGrabMode::Locked => return Ok(()), |
| 1565 | }; |
| 1566 | |
| 1567 | if result.is_ok() { |
| 1568 | *grabbed_lock = mode; |
| 1569 | } |
| 1570 | |
| 1571 | result |
| 1572 | } |
| 1573 | |
| 1574 | #[inline ] |
| 1575 | pub fn set_cursor_visible(&self, visible: bool) { |
| 1576 | #[allow (clippy::mutex_atomic)] |
| 1577 | let mut visible_lock = self.cursor_visible.lock().unwrap(); |
| 1578 | if visible == *visible_lock { |
| 1579 | return; |
| 1580 | } |
| 1581 | let cursor = |
| 1582 | if visible { Some((*self.selected_cursor.lock().unwrap()).clone()) } else { None }; |
| 1583 | *visible_lock = visible; |
| 1584 | drop(visible_lock); |
| 1585 | match cursor { |
| 1586 | Some(SelectedCursor::Custom(cursor)) => { |
| 1587 | self.xconn.set_custom_cursor(self.xwindow, &cursor); |
| 1588 | }, |
| 1589 | Some(SelectedCursor::Named(cursor)) => { |
| 1590 | self.xconn.set_cursor_icon(self.xwindow, Some(cursor)); |
| 1591 | }, |
| 1592 | None => { |
| 1593 | self.xconn.set_cursor_icon(self.xwindow, None); |
| 1594 | }, |
| 1595 | } |
| 1596 | } |
| 1597 | |
| 1598 | #[inline ] |
| 1599 | pub fn scale_factor(&self) -> f64 { |
| 1600 | self.shared_state_lock().last_monitor.scale_factor |
| 1601 | } |
| 1602 | |
| 1603 | pub fn set_cursor_position_physical(&self, x: i32, y: i32) -> Result<(), ExternalError> { |
| 1604 | { |
| 1605 | self.xconn |
| 1606 | .xcb_connection() |
| 1607 | .warp_pointer(x11rb::NONE, self.xwindow, 0, 0, 0, 0, x as _, y as _) |
| 1608 | .map_err(|e| { |
| 1609 | ExternalError::Os(os_error!(OsError::XError(X11Error::from(e).into()))) |
| 1610 | })?; |
| 1611 | self.xconn.flush_requests().map_err(|e| { |
| 1612 | ExternalError::Os(os_error!(OsError::XError(X11Error::Xlib(e).into()))) |
| 1613 | }) |
| 1614 | } |
| 1615 | } |
| 1616 | |
| 1617 | #[inline ] |
| 1618 | pub fn set_cursor_position(&self, position: Position) -> Result<(), ExternalError> { |
| 1619 | let (x, y) = position.to_physical::<i32>(self.scale_factor()).into(); |
| 1620 | self.set_cursor_position_physical(x, y) |
| 1621 | } |
| 1622 | |
| 1623 | #[inline ] |
| 1624 | pub fn set_cursor_hittest(&self, hittest: bool) -> Result<(), ExternalError> { |
| 1625 | let mut rectangles: Vec<Rectangle> = Vec::new(); |
| 1626 | if hittest { |
| 1627 | let size = self.inner_size(); |
| 1628 | rectangles.push(Rectangle { |
| 1629 | x: 0, |
| 1630 | y: 0, |
| 1631 | width: size.width as u16, |
| 1632 | height: size.height as u16, |
| 1633 | }) |
| 1634 | } |
| 1635 | let region = RegionWrapper::create_region(self.xconn.xcb_connection(), &rectangles) |
| 1636 | .map_err(|_e| ExternalError::Ignored)?; |
| 1637 | self.xconn |
| 1638 | .xcb_connection() |
| 1639 | .xfixes_set_window_shape_region(self.xwindow, SK::INPUT, 0, 0, region.region()) |
| 1640 | .map_err(|_e| ExternalError::Ignored)?; |
| 1641 | self.shared_state_lock().cursor_hittest = Some(hittest); |
| 1642 | Ok(()) |
| 1643 | } |
| 1644 | |
| 1645 | /// Moves the window while it is being dragged. |
| 1646 | pub fn drag_window(&self) -> Result<(), ExternalError> { |
| 1647 | self.drag_initiate(util::MOVERESIZE_MOVE) |
| 1648 | } |
| 1649 | |
| 1650 | #[inline ] |
| 1651 | pub fn show_window_menu(&self, _position: Position) {} |
| 1652 | |
| 1653 | /// Resizes the window while it is being dragged. |
| 1654 | pub fn drag_resize_window(&self, direction: ResizeDirection) -> Result<(), ExternalError> { |
| 1655 | self.drag_initiate(match direction { |
| 1656 | ResizeDirection::East => util::MOVERESIZE_RIGHT, |
| 1657 | ResizeDirection::North => util::MOVERESIZE_TOP, |
| 1658 | ResizeDirection::NorthEast => util::MOVERESIZE_TOPRIGHT, |
| 1659 | ResizeDirection::NorthWest => util::MOVERESIZE_TOPLEFT, |
| 1660 | ResizeDirection::South => util::MOVERESIZE_BOTTOM, |
| 1661 | ResizeDirection::SouthEast => util::MOVERESIZE_BOTTOMRIGHT, |
| 1662 | ResizeDirection::SouthWest => util::MOVERESIZE_BOTTOMLEFT, |
| 1663 | ResizeDirection::West => util::MOVERESIZE_LEFT, |
| 1664 | }) |
| 1665 | } |
| 1666 | |
| 1667 | /// Initiates a drag operation while the left mouse button is pressed. |
| 1668 | fn drag_initiate(&self, action: isize) -> Result<(), ExternalError> { |
| 1669 | let pointer = self |
| 1670 | .xconn |
| 1671 | .query_pointer(self.xwindow, util::VIRTUAL_CORE_POINTER) |
| 1672 | .map_err(|err| ExternalError::Os(os_error!(OsError::XError(err.into()))))?; |
| 1673 | |
| 1674 | let window = self.inner_position().map_err(ExternalError::NotSupported)?; |
| 1675 | |
| 1676 | let atoms = self.xconn.atoms(); |
| 1677 | let message = atoms[_NET_WM_MOVERESIZE]; |
| 1678 | |
| 1679 | // we can't use `set_cursor_grab(false)` here because it doesn't run `XUngrabPointer` |
| 1680 | // if the cursor isn't currently grabbed |
| 1681 | let mut grabbed_lock = self.cursor_grabbed_mode.lock().unwrap(); |
| 1682 | self.xconn |
| 1683 | .xcb_connection() |
| 1684 | .ungrab_pointer(x11rb::CURRENT_TIME) |
| 1685 | .map_err(|err| { |
| 1686 | ExternalError::Os(os_error!(OsError::XError(X11Error::from(err).into()))) |
| 1687 | })? |
| 1688 | .ignore_error(); |
| 1689 | self.xconn.flush_requests().map_err(|err| { |
| 1690 | ExternalError::Os(os_error!(OsError::XError(X11Error::Xlib(err).into()))) |
| 1691 | })?; |
| 1692 | *grabbed_lock = CursorGrabMode::None; |
| 1693 | |
| 1694 | // we keep the lock until we are done |
| 1695 | self.xconn |
| 1696 | .send_client_msg( |
| 1697 | self.xwindow, |
| 1698 | self.root, |
| 1699 | message, |
| 1700 | Some( |
| 1701 | xproto::EventMask::SUBSTRUCTURE_REDIRECT |
| 1702 | | xproto::EventMask::SUBSTRUCTURE_NOTIFY, |
| 1703 | ), |
| 1704 | [ |
| 1705 | (window.x + xinput_fp1616_to_float(pointer.win_x) as i32) as u32, |
| 1706 | (window.y + xinput_fp1616_to_float(pointer.win_y) as i32) as u32, |
| 1707 | action.try_into().unwrap(), |
| 1708 | 1, // Button 1 |
| 1709 | 1, |
| 1710 | ], |
| 1711 | ) |
| 1712 | .map_err(|err| ExternalError::Os(os_error!(OsError::XError(err.into()))))?; |
| 1713 | |
| 1714 | self.xconn.flush_requests().map_err(|err| { |
| 1715 | ExternalError::Os(os_error!(OsError::XError(X11Error::Xlib(err).into()))) |
| 1716 | }) |
| 1717 | } |
| 1718 | |
| 1719 | #[inline ] |
| 1720 | pub fn set_ime_cursor_area(&self, spot: Position, _size: Size) { |
| 1721 | let (x, y) = spot.to_physical::<i32>(self.scale_factor()).into(); |
| 1722 | let _ = self.ime_sender.lock().unwrap().send(ImeRequest::Position( |
| 1723 | self.xwindow as ffi::Window, |
| 1724 | x, |
| 1725 | y, |
| 1726 | )); |
| 1727 | } |
| 1728 | |
| 1729 | #[inline ] |
| 1730 | pub fn set_ime_allowed(&self, allowed: bool) { |
| 1731 | let _ = self |
| 1732 | .ime_sender |
| 1733 | .lock() |
| 1734 | .unwrap() |
| 1735 | .send(ImeRequest::Allow(self.xwindow as ffi::Window, allowed)); |
| 1736 | } |
| 1737 | |
| 1738 | #[inline ] |
| 1739 | pub fn set_ime_purpose(&self, _purpose: ImePurpose) {} |
| 1740 | |
| 1741 | #[inline ] |
| 1742 | pub fn focus_window(&self) { |
| 1743 | let atoms = self.xconn.atoms(); |
| 1744 | let state_atom = atoms[WM_STATE]; |
| 1745 | let state_type_atom = atoms[CARD32]; |
| 1746 | let is_minimized = if let Ok(state) = |
| 1747 | self.xconn.get_property::<u32>(self.xwindow, state_atom, state_type_atom) |
| 1748 | { |
| 1749 | state.contains(&super::ICONIC_STATE) |
| 1750 | } else { |
| 1751 | false |
| 1752 | }; |
| 1753 | let is_visible = match self.shared_state_lock().visibility { |
| 1754 | Visibility::Yes => true, |
| 1755 | Visibility::YesWait | Visibility::No => false, |
| 1756 | }; |
| 1757 | |
| 1758 | if is_visible && !is_minimized { |
| 1759 | self.xconn |
| 1760 | .send_client_msg( |
| 1761 | self.xwindow, |
| 1762 | self.root, |
| 1763 | atoms[_NET_ACTIVE_WINDOW], |
| 1764 | Some( |
| 1765 | xproto::EventMask::SUBSTRUCTURE_REDIRECT |
| 1766 | | xproto::EventMask::SUBSTRUCTURE_NOTIFY, |
| 1767 | ), |
| 1768 | [1, x11rb::CURRENT_TIME, 0, 0, 0], |
| 1769 | ) |
| 1770 | .expect_then_ignore_error("Failed to send client message" ); |
| 1771 | if let Err(e) = self.xconn.flush_requests() { |
| 1772 | tracing::error!( |
| 1773 | "`flush` returned an error when focusing the window. Error was: {}" , |
| 1774 | e |
| 1775 | ); |
| 1776 | } |
| 1777 | } |
| 1778 | } |
| 1779 | |
| 1780 | #[inline ] |
| 1781 | pub fn request_user_attention(&self, request_type: Option<UserAttentionType>) { |
| 1782 | let mut wm_hints = |
| 1783 | WmHints::get(self.xconn.xcb_connection(), self.xwindow as xproto::Window) |
| 1784 | .ok() |
| 1785 | .and_then(|cookie| cookie.reply().ok()) |
| 1786 | .flatten() |
| 1787 | .unwrap_or_default(); |
| 1788 | |
| 1789 | wm_hints.urgent = request_type.is_some(); |
| 1790 | wm_hints |
| 1791 | .set(self.xconn.xcb_connection(), self.xwindow as xproto::Window) |
| 1792 | .expect_then_ignore_error("Failed to set WM hints" ); |
| 1793 | } |
| 1794 | |
| 1795 | #[inline ] |
| 1796 | pub(crate) fn generate_activation_token(&self) -> Result<String, X11Error> { |
| 1797 | // Get the title from the WM_NAME property. |
| 1798 | let atoms = self.xconn.atoms(); |
| 1799 | let title = { |
| 1800 | let title_bytes = self |
| 1801 | .xconn |
| 1802 | .get_property(self.xwindow, atoms[_NET_WM_NAME], atoms[UTF8_STRING]) |
| 1803 | .expect("Failed to get title" ); |
| 1804 | |
| 1805 | String::from_utf8(title_bytes).expect("Bad title" ) |
| 1806 | }; |
| 1807 | |
| 1808 | // Get the activation token and then put it in the event queue. |
| 1809 | let token = self.xconn.request_activation_token(&title)?; |
| 1810 | |
| 1811 | Ok(token) |
| 1812 | } |
| 1813 | |
| 1814 | #[inline ] |
| 1815 | pub fn request_activation_token(&self) -> Result<AsyncRequestSerial, NotSupportedError> { |
| 1816 | let serial = AsyncRequestSerial::get(); |
| 1817 | self.activation_sender |
| 1818 | .send((self.id(), serial)) |
| 1819 | .expect("activation token channel should never be closed" ); |
| 1820 | Ok(serial) |
| 1821 | } |
| 1822 | |
| 1823 | #[inline ] |
| 1824 | pub fn id(&self) -> WindowId { |
| 1825 | WindowId(self.xwindow as _) |
| 1826 | } |
| 1827 | |
| 1828 | #[inline ] |
| 1829 | pub fn request_redraw(&self) { |
| 1830 | self.redraw_sender.send(WindowId(self.xwindow as _)).unwrap(); |
| 1831 | } |
| 1832 | |
| 1833 | #[inline ] |
| 1834 | pub fn pre_present_notify(&self) { |
| 1835 | // TODO timer |
| 1836 | } |
| 1837 | |
| 1838 | #[cfg (feature = "rwh_04" )] |
| 1839 | #[inline ] |
| 1840 | pub fn raw_window_handle_rwh_04(&self) -> rwh_04::RawWindowHandle { |
| 1841 | let mut window_handle = rwh_04::XlibHandle::empty(); |
| 1842 | window_handle.display = self.xlib_display(); |
| 1843 | window_handle.window = self.xlib_window(); |
| 1844 | window_handle.visual_id = self.visual as c_ulong; |
| 1845 | rwh_04::RawWindowHandle::Xlib(window_handle) |
| 1846 | } |
| 1847 | |
| 1848 | #[cfg (feature = "rwh_05" )] |
| 1849 | #[inline ] |
| 1850 | pub fn raw_window_handle_rwh_05(&self) -> rwh_05::RawWindowHandle { |
| 1851 | let mut window_handle = rwh_05::XlibWindowHandle::empty(); |
| 1852 | window_handle.window = self.xlib_window(); |
| 1853 | window_handle.visual_id = self.visual as c_ulong; |
| 1854 | window_handle.into() |
| 1855 | } |
| 1856 | |
| 1857 | #[cfg (feature = "rwh_05" )] |
| 1858 | #[inline ] |
| 1859 | pub fn raw_display_handle_rwh_05(&self) -> rwh_05::RawDisplayHandle { |
| 1860 | let mut display_handle = rwh_05::XlibDisplayHandle::empty(); |
| 1861 | display_handle.display = self.xlib_display(); |
| 1862 | display_handle.screen = self.screen_id; |
| 1863 | display_handle.into() |
| 1864 | } |
| 1865 | |
| 1866 | #[cfg (feature = "rwh_06" )] |
| 1867 | #[inline ] |
| 1868 | pub fn raw_window_handle_rwh_06(&self) -> Result<rwh_06::RawWindowHandle, rwh_06::HandleError> { |
| 1869 | let mut window_handle = rwh_06::XlibWindowHandle::new(self.xlib_window()); |
| 1870 | window_handle.visual_id = self.visual as c_ulong; |
| 1871 | Ok(window_handle.into()) |
| 1872 | } |
| 1873 | |
| 1874 | #[cfg (feature = "rwh_06" )] |
| 1875 | #[inline ] |
| 1876 | pub fn raw_display_handle_rwh_06( |
| 1877 | &self, |
| 1878 | ) -> Result<rwh_06::RawDisplayHandle, rwh_06::HandleError> { |
| 1879 | Ok(rwh_06::XlibDisplayHandle::new( |
| 1880 | Some( |
| 1881 | std::ptr::NonNull::new(self.xlib_display()) |
| 1882 | .expect("display pointer should never be null" ), |
| 1883 | ), |
| 1884 | self.screen_id, |
| 1885 | ) |
| 1886 | .into()) |
| 1887 | } |
| 1888 | |
| 1889 | #[inline ] |
| 1890 | pub fn theme(&self) -> Option<Theme> { |
| 1891 | None |
| 1892 | } |
| 1893 | |
| 1894 | pub fn set_content_protected(&self, _protected: bool) {} |
| 1895 | |
| 1896 | #[inline ] |
| 1897 | pub fn has_focus(&self) -> bool { |
| 1898 | self.shared_state_lock().has_focus |
| 1899 | } |
| 1900 | |
| 1901 | pub fn title(&self) -> String { |
| 1902 | String::new() |
| 1903 | } |
| 1904 | } |
| 1905 | |
| 1906 | /// Cast a dimension value into a hinted dimension for `WmSizeHints`, clamping if too large. |
| 1907 | fn cast_dimension_to_hint(val: u32) -> i32 { |
| 1908 | val.try_into().unwrap_or(default:i32::MAX) |
| 1909 | } |
| 1910 | |
| 1911 | /// Use the above strategy to cast a physical size into a hinted size. |
| 1912 | fn cast_physical_size_to_hint(size: PhysicalSize<u32>) -> (i32, i32) { |
| 1913 | let PhysicalSize { width: u32, height: u32 } = size; |
| 1914 | (cast_dimension_to_hint(val:width), cast_dimension_to_hint(val:height)) |
| 1915 | } |
| 1916 | |
| 1917 | /// Use the above strategy to cast a size into a hinted size. |
| 1918 | fn cast_size_to_hint(size: Size, scale_factor: f64) -> (i32, i32) { |
| 1919 | match size { |
| 1920 | Size::Physical(size: PhysicalSize) => cast_physical_size_to_hint(size), |
| 1921 | Size::Logical(size: LogicalSize) => size.to_physical::<i32>(scale_factor).into(), |
| 1922 | } |
| 1923 | } |
| 1924 | |