use xcb::{ x::{ ChangeProperty, ChangeWindowAttributes, ConfigWindow, ConfigureNotifyEvent, ConfigureWindow, Cw, EventMask, MapWindow, SendEvent, SendEventDest, StackMode, Window, }, VoidCookieChecked, Xid, }; use crate::{config::BORDER_WIDTH, conn_info::Connection}; use super::{hints, MonitorGeometry}; /// 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, } /// 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) -> 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, } } /// 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 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) } /// Set the window as tiled pub fn set_tiled(&mut self) { self.layout_mode = LayoutMode::Tiled; } /// Set the window as floating pub fn set_floating(&mut self, conn: &Connection<'_>) { if !matches!(self.layout_mode, LayoutMode::Floating) { conn.send_request(&ConfigureWindow { window: self.window, value_list: &[ConfigWindow::StackMode(StackMode::Above)], }); } self.layout_mode = LayoutMode::Floating; } /// Set the window as fullscreen pub fn set_fullscreen(&mut self, conn: &Connection<'_>, mon_geom: &MonitorGeometry) { if !matches!(self.layout_mode, LayoutMode::Fullscreen) { 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.layout_mode = LayoutMode::Fullscreen; } /// 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 properties with EWMH hints pub fn sync_properties(&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); } /// 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 update_window_type(&mut self, conn: &Connection<'_>) { // TODO: Fullscreen from net_wm_state if hints::is_dialog(conn, self.window) { self.set_floating(conn); } } pub fn set_urgent(&mut self, urgent: bool) { self.urgent = urgent; } pub const fn border_width(&self) -> u16 { self.border_width } pub const fn never_focus(&self) -> bool { self.never_focus } }