use smithay_client_toolkit::globals::{GlobalData, ProvidesBoundGlobal};
use smithay_client_toolkit::reexports::client::globals::{BindError, GlobalList};
use smithay_client_toolkit::reexports::client::{Connection, Proxy, QueueHandle};
use smithay_client_toolkit::reexports::protocols::xdg::shell::client::{
xdg_positioner, xdg_wm_base::XdgWmBase,
};
use smithay_client_toolkit::shell::xdg::{
XdgPositioner, XdgShell,
popup::{Popup, PopupConfigure, PopupHandler},
};
use super::wayland::{SurfaceRole, WaylandState, WaylandSurfaceState};
use crate::surface::SurfaceId;
pub(crate) struct WmBase(XdgWmBase);
impl WmBase {
pub(super) fn bind(
globals: &GlobalList,
qh: &QueueHandle<WaylandState>,
) -> Result<Self, BindError> {
globals
.bind(qh, 1..=XdgShell::API_VERSION_MAX, GlobalData)
.map(Self)
}
}
impl ProvidesBoundGlobal<XdgWmBase, 5> for WmBase {
fn bound_global(&self) -> Result<XdgWmBase, smithay_client_toolkit::error::GlobalError> {
Ok(self.0.clone())
}
}
impl ProvidesBoundGlobal<XdgWmBase, { XdgShell::API_VERSION_MAX }> for WmBase {
fn bound_global(&self) -> Result<XdgWmBase, smithay_client_toolkit::error::GlobalError> {
Ok(self.0.clone())
}
}
pub struct Popups {
pub(super) wm_base: Option<WmBase>,
pub(super) reposition_token: u32,
}
impl Popups {
pub(super) fn new(wm_base: Option<WmBase>) -> Self {
Self {
wm_base,
reposition_token: 0,
}
}
}
impl WaylandState {
fn build_popup_positioner(
wm_base: &WmBase,
config: &crate::surface::PopupConfig,
size: (u32, u32),
) -> Option<XdgPositioner> {
let positioner = match XdgPositioner::new(wm_base) {
Ok(p) => p,
Err(e) => {
log::error!("Failed to create xdg_positioner: {e}");
return None;
}
};
positioner.set_size(size.0.max(1) as i32, size.1.max(1) as i32);
let r = config.anchor_rect;
positioner.set_anchor_rect(
r.x.floor() as i32,
r.y.floor() as i32,
(r.width.ceil() as i32).max(1),
(r.height.ceil() as i32).max(1),
);
positioner.set_anchor(popup_point_to_anchor(config.anchor));
positioner.set_gravity(popup_point_to_gravity(config.gravity));
positioner.set_offset(config.offset.0, config.offset.1);
positioner.set_constraint_adjustment(
xdg_positioner::ConstraintAdjustment::FlipX
| xdg_positioner::ConstraintAdjustment::FlipY
| xdg_positioner::ConstraintAdjustment::SlideX
| xdg_positioner::ConstraintAdjustment::SlideY,
);
Some(positioner)
}
pub fn create_popup_surface_with_id(
&mut self,
id: SurfaceId,
parent: SurfaceId,
config: &crate::surface::PopupConfig,
size: (u32, u32),
) -> bool {
let qh = &self.qh;
let Some(ref wm_base) = self.popups.wm_base else {
log::error!("Cannot create popup: compositor lacks xdg_wm_base");
return false;
};
let Some(parent_state) = self.surfaces.get(&parent) else {
log::warn!("Cannot create popup: parent surface {:?} is gone", parent);
return false;
};
let Some(positioner) = Self::build_popup_positioner(wm_base, config, size) else {
return false;
};
let wl_surface = self.compositor_state.create_surface(qh);
let popup = match &parent_state.role {
SurfaceRole::Layer(layer_surface) => {
let popup =
match Popup::from_surface(None, &positioner, qh, wl_surface.clone(), wm_base) {
Ok(popup) => popup,
Err(e) => {
log::error!("Failed to create xdg popup: {e}");
return false;
}
};
layer_surface.get_popup(popup.xdg_popup());
popup
}
SurfaceRole::Popup {
popup: parent_popup,
..
} => {
let parent_xdg = parent_popup.xdg_surface().clone();
match Popup::from_surface(
Some(&parent_xdg),
&positioner,
qh,
wl_surface.clone(),
wm_base,
) {
Ok(popup) => popup,
Err(e) => {
log::error!("Failed to create nested xdg popup: {e}");
return false;
}
}
}
SurfaceRole::Lock(_) => {
log::warn!("Cannot anchor a popup to a session-lock surface");
return false;
}
};
if config.grab
&& let Some(seat) = self.seat_state.seats().next()
{
popup
.xdg_popup()
.grab(&seat, self.input.latest_input_serial);
}
let scaling = self.create_surface_scaling(id, &wl_surface);
wl_surface.commit();
self.surface_lookup.insert(wl_surface.id(), id);
let surface_state = WaylandSurfaceState::new(
SurfaceRole::Popup {
popup,
config: config.clone(),
parent,
},
wl_surface,
size.0,
size.1,
scaling,
);
self.surfaces.insert(id, surface_state);
log::info!(
"Created popup {:?} on parent {:?} ({}x{}, grab: {})",
id,
parent,
size.0,
size.1,
config.grab
);
true
}
pub(crate) fn popup_descendants_bottom_up(&self, root: SurfaceId) -> Vec<SurfaceId> {
crate::descendants_bottom_up(
root,
self.surfaces
.iter()
.filter_map(|(id, state)| match &state.role {
SurfaceRole::Popup { parent, .. } => Some((*id, *parent)),
_ => None,
}),
)
}
pub(crate) fn conflicting_grab_popups(&self, new_parent: SurfaceId) -> Vec<SurfaceId> {
crate::conflicting_grabs(
new_parent,
self.surfaces
.iter()
.filter_map(|(id, state)| match &state.role {
SurfaceRole::Popup { parent, config, .. } => Some((*id, *parent, config.grab)),
_ => None,
}),
)
}
pub(crate) fn popup_auto_width(&self, id: SurfaceId) -> Option<u32> {
match &self.surfaces.get(&id)?.role {
SurfaceRole::Popup { config, .. } if config.height.is_none() => Some(config.width),
_ => None,
}
}
pub(crate) fn reposition_popup_if_changed(&mut self, id: SurfaceId, new_height: u32) {
let Some(ref wm_base) = self.popups.wm_base else {
return;
};
let Some(surface_state) = self.surfaces.get_mut(&id) else {
return;
};
if surface_state.height == new_height
|| surface_state.pending_popup_height == Some(new_height)
{
return;
}
let SurfaceRole::Popup { popup, config, .. } = &surface_state.role else {
return;
};
if popup.xdg_popup().version() < 3 {
return;
}
let Some(positioner) =
Self::build_popup_positioner(wm_base, config, (config.width, new_height))
else {
return;
};
self.popups.reposition_token = self.popups.reposition_token.wrapping_add(1);
popup.reposition(&positioner, self.popups.reposition_token);
surface_state.pending_popup_height = Some(new_height);
log::debug!("Popup {:?} repositioning to height {}", id, new_height);
}
}
fn popup_point_to_anchor(point: crate::surface::PopupAnchor) -> xdg_positioner::Anchor {
use crate::surface::PopupAnchor as P;
match point {
P::None => xdg_positioner::Anchor::None,
P::Top => xdg_positioner::Anchor::Top,
P::Bottom => xdg_positioner::Anchor::Bottom,
P::Left => xdg_positioner::Anchor::Left,
P::Right => xdg_positioner::Anchor::Right,
P::TopLeft => xdg_positioner::Anchor::TopLeft,
P::BottomLeft => xdg_positioner::Anchor::BottomLeft,
P::TopRight => xdg_positioner::Anchor::TopRight,
P::BottomRight => xdg_positioner::Anchor::BottomRight,
}
}
fn popup_point_to_gravity(point: crate::surface::PopupAnchor) -> xdg_positioner::Gravity {
use crate::surface::PopupAnchor as P;
match point {
P::None => xdg_positioner::Gravity::None,
P::Top => xdg_positioner::Gravity::Top,
P::Bottom => xdg_positioner::Gravity::Bottom,
P::Left => xdg_positioner::Gravity::Left,
P::Right => xdg_positioner::Gravity::Right,
P::TopLeft => xdg_positioner::Gravity::TopLeft,
P::BottomLeft => xdg_positioner::Gravity::BottomLeft,
P::TopRight => xdg_positioner::Gravity::TopRight,
P::BottomRight => xdg_positioner::Gravity::BottomRight,
}
}
impl PopupHandler for WaylandState {
fn configure(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
popup: &Popup,
config: PopupConfigure,
) {
let surface_id = self.surface_lookup.get(&popup.wl_surface().id()).copied();
if let Some(id) = surface_id
&& let Some(surface_state) = self.surfaces.get_mut(&id)
{
log::info!(
"Popup {:?} configure: {}x{} at {:?}",
id,
config.width,
config.height,
config.position
);
if config.width > 0 {
surface_state.width = config.width as u32;
}
if config.height > 0 {
surface_state.height = config.height as u32;
}
surface_state.pending_popup_height = None;
surface_state.configured = true;
crate::jobs::wake_loop();
}
}
fn done(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, popup: &Popup) {
if let Some(id) = self.surface_lookup.get(&popup.wl_surface().id()).copied() {
log::info!("Popup {:?} dismissed by compositor", id);
crate::surface::mark_popup_dismissed(id);
crate::surface::push_surface_command(crate::surface::SurfaceCommand::Close(id));
}
}
}