use crate::{
LogicalSize, PhysicalPoint, PhysicalRect, PopupDesc, PopupInput, ShellError, WindowId,
};
use super::{X11Shell, ffi};
const WHOLE_DESKTOP: PhysicalRect = PhysicalRect::new(i32::MIN, i32::MIN, u32::MAX, u32::MAX);
#[expect(
clippy::cast_possible_truncation,
reason = "rounded and clamped into i32's range before the cast"
)]
fn pixels(logical: f64, scale: f64) -> i32 {
(logical * scale)
.round()
.clamp(f64::from(i32::MIN), f64::from(i32::MAX)) as i32
}
impl X11Shell {
pub(super) fn popup_rect_for(
&self,
popup: &PopupDesc,
size: LogicalSize,
) -> Result<PhysicalRect, ShellError> {
let parent = self.window(popup.parent)?;
let (x, y) = self.origin_now(parent.id).ok_or_else(|| {
ShellError::Backend("the X server could not say where the parent is".to_string())
})?;
let scale = self.scale_factor;
let anchor = PhysicalRect::new(
x.saturating_add(pixels(popup.anchor.origin.x, scale)),
y.saturating_add(pixels(popup.anchor.origin.y, scale)),
pixels(popup.anchor.size.width, scale).max(1).unsigned_abs(),
pixels(popup.anchor.size.height, scale)
.max(1)
.unsigned_abs(),
);
let contains = |rect: PhysicalRect| {
let (right, bottom) = (
i64::from(rect.x) + i64::from(rect.width),
i64::from(rect.y) + i64::from(rect.height),
);
anchor.x >= rect.x
&& anchor.y >= rect.y
&& i64::from(anchor.x) < right
&& i64::from(anchor.y) < bottom
};
let area = self
.monitors
.iter()
.find(|monitor| contains(monitor.bounds))
.or_else(|| self.monitors.iter().find(|monitor| monitor.is_primary))
.or_else(|| self.monitors.first())
.map_or(WHOLE_DESKTOP, |monitor| monitor.work_area);
Ok(crate::role::popup_rect(
anchor,
size.to_physical(scale),
popup.placement,
area,
))
}
pub(super) fn write_popup_properties(&self, xid: u32, popup: &PopupDesc) {
let Ok(parent) = self.window(popup.parent) else {
return;
};
let kind = if popup.input == PopupInput::Passthrough {
self.conn.atoms.net_wm_window_type_tooltip
} else {
self.conn.atoms.net_wm_window_type_popup_menu
};
unsafe {
(self.conn.lib.change_property)(
self.conn.raw(),
ffi::value::PROP_MODE_REPLACE,
xid,
ffi::value::ATOM_WM_TRANSIENT_FOR,
ffi::value::ATOM_WINDOW,
32,
1,
(&raw const parent.id).cast(),
);
(self.conn.lib.change_property)(
self.conn.raw(),
ffi::value::PROP_MODE_REPLACE,
xid,
self.conn.atoms.net_wm_window_type,
ffi::value::ATOM_ATOM,
32,
1,
(&raw const kind).cast(),
);
}
}
pub(super) fn grab_for_popup(&mut self, window: WindowId) -> Result<(), ShellError> {
let xid = self.window(window)?.id;
self.grab_pointer(xid, false, ffi::value::NONE, ffi::value::NONE)?;
self.forget_captures_except(window);
let reply = unsafe {
let cookie = (self.conn.lib.grab_keyboard)(
self.conn.raw(),
0,
xid,
ffi::value::CURRENT_TIME,
ffi::value::GRAB_MODE_ASYNC,
ffi::value::GRAB_MODE_ASYNC,
);
(self.conn.lib.grab_keyboard_reply)(self.conn.raw(), cookie, core::ptr::null_mut())
};
let status = if reply.is_null() {
None
} else {
unsafe {
let status = (*reply).status;
ffi::free_reply(reply);
Some(status)
}
};
if status != Some(ffi::value::GRAB_SUCCESS) {
self.release_popup_grab();
return Err(ShellError::Backend(format!(
"another client holds the keyboard (GrabKeyboard status {status:?})"
)));
}
Ok(())
}
pub(super) fn release_popup_grab(&self) {
unsafe {
(self.conn.lib.ungrab_pointer)(self.conn.raw(), ffi::value::CURRENT_TIME);
(self.conn.lib.ungrab_keyboard)(self.conn.raw(), ffi::value::CURRENT_TIME);
}
}
pub(super) fn outside_grabbing_popup(
&self,
window: WindowId,
position: Option<PhysicalPoint>,
) -> bool {
let Ok(state) = self.window(window) else {
return false;
};
if state.popup.map(|popup| popup.input) != Some(PopupInput::Grab) {
return false;
}
let size = state
.configuration
.or(state.pending)
.map(|config| config.size);
let (Some(size), Some(position)) = (size, position) else {
return false;
};
position.x < 0.0
|| position.y < 0.0
|| position.x >= f64::from(size.width)
|| position.y >= f64::from(size.height)
}
pub(super) fn move_popup(
&mut self,
window: WindowId,
anchor: crate::LogicalRect,
size: LogicalSize,
) -> Result<(), ShellError> {
let state = self.window(window)?;
let xid = state.id;
let mut popup = state
.popup
.ok_or_else(|| crate::role::not_a_popup(window))?;
popup.anchor = anchor;
let rect = self.popup_rect_for(&popup, size)?;
let values = [
rect.x.cast_unsigned(),
rect.y.cast_unsigned(),
rect.width.max(1),
rect.height.max(1),
];
unsafe {
(self.conn.lib.configure_window)(
self.conn.raw(),
xid,
ffi::config_window::X
| ffi::config_window::Y
| ffi::config_window::WIDTH
| ffi::config_window::HEIGHT,
values.as_ptr().cast(),
);
}
self.conn.flush();
let state = self.window_mut(window)?;
state.popup = Some(popup);
state.requested_size = size;
Ok(())
}
pub(super) fn popups_of(&self, window: WindowId) -> Vec<WindowId> {
self.windows
.iter()
.filter(|(_, state)| state.popup.is_some_and(|popup| popup.parent == window))
.map(|(handle, _)| handle.cast())
.collect()
}
}
#[cfg(test)]
mod tests {
use super::pixels;
#[test]
fn an_anchor_rounds_to_whole_pixels_at_the_scale() {
assert_eq!(pixels(10.0, 1.0), 10);
assert_eq!(pixels(10.0, 1.5), 15);
assert_eq!(pixels(10.3, 1.0), 10);
assert_eq!(pixels(-4.0, 2.0), -8, "an anchor left of the parent");
}
}