use xcb::{ x::{ self, ChangeProperty, ChangeWindowAttributes, ConfigWindow, ConfigWindowMask, ConfigureNotifyEvent, ConfigureWindow, Cw, EventMask, MapWindow, PropMode, SendEvent, SendEventDest, StackMode, UnmapWindow, Window, }, VoidCookieChecked, Xid, }; use crate::{config::BORDER_WIDTH, conn_info::Connection}; use super::{hints, MonitorGeometry, Tag}; /// Information about a single client / window #[derive(Debug)] #[allow(clippy::struct_excessive_bools)] pub struct Client { /// The corresponding X11 window window: Window, x: i16, y: i16, width: u16, height: u16, border_width: u16, mapped: bool, urgent: bool, never_focus: bool, layout_mode: LayoutMode, pub tag: Tag, } /// How this window's geometry is determined, #[derive(Debug)] pub enum LayoutMode { /// By the tiling algorithm Tiled, /// By the user Floating, /// By the monitor its on Fullscreen, } impl Client { pub const fn new(window: Window, tag: Tag) -> Self { Self { window, x: 0, y: 0, width: 0, height: 0, border_width: BORDER_WIDTH, mapped: false, urgent: false, never_focus: false, layout_mode: LayoutMode::Tiled, tag, } } /// Send a configure configure notify event with the current geometry. pub fn configure_notify(&self, conn: &Connection<'_>) { conn.send_request(&SendEvent { destination: SendEventDest::Window(self.window), event_mask: EventMask::STRUCTURE_NOTIFY, event: &ConfigureNotifyEvent::new( self.window, self.window, Window::none(), self.x, self.y, self.width, self.height, self.border_width, false, ), propagate: false, }); } /// Set this client's geometry, also updating the X11 window if needed. pub fn set_geom( &mut self, conn: &Connection<'_>, x: i16, y: i16, width: u16, height: u16, border_width: u16, ) { if (x, y, width, height, border_width) == (self.x, self.y, self.width, self.height, self.border_width) { return; } self.x = x; self.y = y; self.width = width; self.height = height; self.border_width = border_width; conn.send_request(&ConfigureWindow { window: self.window, value_list: &[ ConfigWindow::X(self.x.into()), ConfigWindow::Y(self.y.into()), ConfigWindow::Width(self.width.into()), ConfigWindow::Height(self.height.into()), ConfigWindow::BorderWidth(self.border_width.into()), ], }); } /// Set the client's geometry as requested by the given event. /// Properties not specified in `e` are not changed. pub fn set_geom_from(&mut self, conn: &Connection, e: &x::ConfigureRequestEvent) { let Self { mut x, mut y, mut width, mut height, mut border_width, .. } = self; if e.value_mask().contains(ConfigWindowMask::X) { x = e.x(); } if e.value_mask().contains(ConfigWindowMask::Y) { y = e.y(); } if e.value_mask().contains(ConfigWindowMask::HEIGHT) { height = e.height(); } if e.value_mask().contains(ConfigWindowMask::WIDTH) { width = e.width(); } if e.value_mask().contains(ConfigWindowMask::BORDER_WIDTH) { border_width = e.border_width(); } self.set_geom(conn, x, y, height, width, border_width); } /// Set the border colour of the X11 window to the given value (see `crate::colours::Colours`) pub fn set_border(&self, conn: &Connection<'_>, colour: u32) { conn.send_request(&ChangeWindowAttributes { window: self.window(), value_list: &[Cw::BorderPixel(colour)], }); } /// Whether the client is tiled pub const fn tiled(&self) -> bool { matches!(self.layout_mode, LayoutMode::Tiled) } /// Whether the client is fullscreen pub const fn fullscreen(&self) -> bool { matches!(self.layout_mode, LayoutMode::Fullscreen) } /// Whether the client is floating pub const fn floating(&self) -> bool { matches!(self.layout_mode, LayoutMode::Floating) } /// Set the window as tiled pub fn set_tiled(&mut self, conn: &Connection<'_>) { let dirty = !matches!(self.layout_mode, LayoutMode::Tiled); self.layout_mode = LayoutMode::Tiled; if dirty { self.apply_net_wm_state(conn); } } /// Set the window as floating pub fn set_floating(&mut self, conn: &Connection<'_>) { let dirty = !matches!(self.layout_mode, LayoutMode::Floating); self.layout_mode = LayoutMode::Floating; if dirty { conn.send_request(&ConfigureWindow { window: self.window, value_list: &[ConfigWindow::StackMode(StackMode::Above)], }); self.apply_net_wm_state(conn); } } /// Set the window as fullscreen pub fn set_fullscreen(&mut self, conn: &Connection<'_>, mon_geom: &MonitorGeometry) { let dirty = !matches!(self.layout_mode, LayoutMode::Fullscreen); self.layout_mode = LayoutMode::Fullscreen; if dirty { conn.send_request(&ConfigureWindow { window: self.window, value_list: &[ConfigWindow::StackMode(StackMode::Above)], }); self.set_geom( conn, mon_geom.x_org, mon_geom.y_org, mon_geom.width, mon_geom.height, self.border_width, ); self.apply_net_wm_state(conn); } } fn apply_net_wm_state(&self, conn: &Connection<'_>) { conn.send_request(&ChangeProperty { mode: PropMode::Replace, window: self.window, property: conn.atoms.net_wm_state, r#type: x::ATOM_ATOM, data: &[match self.layout_mode { LayoutMode::Tiled | LayoutMode::Floating => 0_u32, LayoutMode::Fullscreen => conn.atoms.net_wm_fullscreen.resource_id(), }], }); } /// Ensure this client is currently unmapped pub fn ensure_unmapped(&mut self, conn: &Connection<'_>) { if self.mapped { conn.send_request(&UnmapWindow { window: self.window, }); self.mapped = false; } } /// Ensure this client is currently mapped / visible pub fn ensure_mapped(&mut self, conn: &Connection<'_>) { if !self.mapped { conn.send_request(&MapWindow { window: self.window, }); self.mapped = true; } } /// Get the associated window pub const fn window(&self) -> Window { self.window } /// Set the event mask for this window pub fn set_event_mask( &self, conn: &Connection<'_>, event_mask: EventMask, ) -> VoidCookieChecked { conn.send_request_checked(&ChangeWindowAttributes { window: self.window(), value_list: &[Cw::EventMask(event_mask)], }) } /// Sync the non-geometry related hints with EWMH hints pub fn sync_hints(&mut self, conn: &Connection<'_>, focused: bool) { let Some(mut hints) = hints::Ewm::get(conn, self.window) else { return; }; if focused && hints.is_urgent() { hints.set_urgent(false); hints.apply(conn, self.window); } else { self.urgent = hints.is_urgent(); } self.never_focus = hints.input().is_some_and(|i| !i); } /// Apply any geometry-related EWM hints on the window pub fn apply_geometry_hints(&mut self, conn: &Connection<'_>, mon_geom: &MonitorGeometry) { if hints::is_fullscreen(conn, self.window) { self.set_fullscreen(conn, mon_geom); } else if hints::is_dialog(conn, self.window) { self.set_floating(conn); } } /// Set the given window as withdrawn / not withdrawn. pub fn set_withdrawn(&self, conn: &Connection<'_>, withdrawn: bool) -> VoidCookieChecked { conn.send_request_checked(&ChangeProperty { mode: xcb::x::PropMode::Replace, window: self.window, property: conn.atoms.wm_state, r#type: conn.atoms.wm_state, data: &[u32::from(!withdrawn), 0_u32], }) } pub fn set_urgent(&mut self, urgent: bool) { self.urgent = urgent; } pub const fn x(&self) -> i16 { self.x } pub const fn y(&self) -> i16 { self.y } pub const fn height(&self) -> u16 { self.height } pub const fn width(&self) -> u16 { self.width } pub const fn border_width(&self) -> u16 { self.border_width } pub const fn never_focus(&self) -> bool { self.never_focus } }