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