//! Tracking and managing windows. use std::{ cmp::{max, min, Ordering}, mem, }; use crate::{ buttons, config::BORDER_WIDTH, conn_info::Connection, debug, error::{Error, Result}, WM, }; use xcb::{ x::{ self, ChangeProperty, CloseDown, ConfigWindow, ConfigWindowMask, ConfigureNotifyEvent, ConfigureRequestEvent, ConfigureWindow, DeleteProperty, DestroyNotifyEvent, Drawable, EventMask, GetGeometry, GetWindowAttributes, InputFocus, KillClient, MapRequestEvent, PropMode, SetCloseDownMode, SetInputFocus, UnmapNotifyEvent, Window, }, xinerama, BaseEvent, Extension, Xid, }; pub use client::*; pub use hints::*; pub use monitors::*; #[doc(hidden)] mod client; #[doc(hidden)] mod hints; #[doc(hidden)] mod monitors; mod tile; /// The tag a client has, similar to a workspace in most WMs. pub type Tag = u8; pub type MonitorIdx = usize; pub type ClientIdx = usize; impl WM<'_> { /// Perform configure requests if we're happy with them, or they're for an unmanaged window. pub(crate) fn handle_configure_request(&mut self, e: &ConfigureRequestEvent) { if let Some(c) = self.clients.find_client_mut(e.window()) { // Always allow setting border width if c.floating() { c.set_geom_from(&self.conn, e); } else if e.value_mask().contains(ConfigWindowMask::BORDER_WIDTH) { c.set_geom( &self.conn, c.x(), c.y(), c.height(), c.width(), e.border_width(), ); } c.configure_notify(&self.conn); } else { // Configure it as requested, and sort the rest when we actually map the window // According to spec, we should check the property mask and only set properties that are specified, setting defaults // for other ones. // Unfortunately, some clients (such as Audacity) will not set the mask correctly. // So instead we just use all the properties, and the ones that can be invalid (width, height > 0) we clamp. self.conn.send_request(&ConfigureWindow { window: e.window(), value_list: &[ ConfigWindow::X(e.x().into()), ConfigWindow::Y(e.y().into()), ConfigWindow::Width(max(e.width().into(), 1)), ConfigWindow::Height(max(e.height().into(), 1)), ConfigWindow::BorderWidth(e.border_width().into()), ConfigWindow::StackMode(e.stack_mode()), ], }); } } /// Update our monitor geometry if the root window is reconfigured pub(crate) fn handle_configure_notify(&mut self, e: &ConfigureNotifyEvent) -> Result<()> { if e.window() == self.conn.root() { self.clients.update_geometry(&self.conn)?; } Ok(()) } /// Removing destroyed windows from the client list and rearrange. pub(crate) fn handle_destroy_notify(&mut self, e: &DestroyNotifyEvent) { // destroynotify gets fired twice: one with event = e.window(), and one with event = root // so only listen to the second one if e.event() == self.conn.root() { self.clients.unmanage_destroyed(&self.conn, e.window()); } } /// Map a window, starting to manage it if needed. pub(crate) fn handle_map_request(&mut self, e: &MapRequestEvent) -> Result<()> { // Ignore already managed windows if self.clients.find_client_mut(e.window()).is_some() { return Ok(()); } let attrs = self.conn.wait_for_reply( self.conn .send_request(&GetWindowAttributes { window: e.window() }), )?; if attrs.override_redirect() { // Something special, or us doing actual mapping. Don't manage it just let it do its thing. return Ok(()); } // Start managing, and map window self.clients.manage(&self.conn, e.window()); Ok(()) } /// When a window is unmapped, update its state. pub(crate) fn handle_unmap_notify(&mut self, e: &UnmapNotifyEvent) { if !e.is_from_send_event() { if let Some(c) = self.clients.find_client_mut(e.window()) { let cookie = c.set_withdrawn(&self.conn, true); // The above may fail if the window has already been destroyed - just discard the error here. let _ = self.conn.check_request(cookie); } if self .clients .focused() .is_some_and(|c| c.window() == e.window()) { self.clients.unfocus_destroyed(); } } } } /// Holds state related to the window manager's clients /// This contains a list of clients per monitor, alongside info on that monitor's screen size. pub struct ClientState { /// The current arranging function. /// This function is expected to ensure that all clients are the correct size, reconfigure them if needed, and map/unmap as needed. arrange: &'static dyn Fn(&Connection<'_>, &mut [Client], &MonitorInfo), /// Clients attached to that monitor pub clients: Vec, /// Information for each monitor. mons: Vec, /// Index of the currently focused client focused_client: Option, } impl ClientState { /// Start managing the given window, adding it to the client list and ensuring its configuration is valid. pub fn manage(&mut self, conn: &Connection<'_>, window: Window) { let mut tag = self.focused_mon().focused_tag.create_tag(); let mut floating = false; if let Some(parent) = hints::transient_for(conn, window) { floating = true; if let Some(c) = self.find_client_mut(parent) { tag = c.tag; } } let is_visible = self.focused_mon().focused_tag.matches(tag); let new_idx = self.clients.len(); self.clients.push(Client::new(window, tag)); let Ok(geom) = conn.wait_for_reply(conn.send_request(&GetGeometry { drawable: Drawable::Window(window), })) else { return; // window stopped existing, so we can't manage it }; let mon_geom = self.focused_mon().screen_info; let c = &mut self.clients[new_idx]; #[allow(clippy::cast_sign_loss)] c.set_geom( conn, geom.x(), geom.y(), min(geom.width(), mon_geom.width.saturating_sub(geom.x() as u16)), min( geom.height(), mon_geom.height.saturating_sub(geom.y() as u16), ), BORDER_WIDTH, ); c.set_border(conn, conn.colours.border_normal()); if floating { c.set_floating(conn); } c.sync_hints(conn, true); c.apply_geometry_hints(conn, &mon_geom); c.set_event_mask( conn, EventMask::ENTER_WINDOW | EventMask::FOCUS_CHANGE | EventMask::PROPERTY_CHANGE | EventMask::STRUCTURE_NOTIFY | EventMask::BUTTON_PRESS | EventMask::KEY_PRESS, ); // Add to net_client_list conn.send_request(&ChangeProperty { mode: xcb::x::PropMode::Append, window: conn.root(), property: conn.atoms.net_client_list, r#type: x::ATOM_WINDOW, data: &[window], }); if is_visible { c.ensure_mapped(conn); self.rearrange(conn); self.refocus(conn, self.clients.len() - 1); } } /// Stop managing the given destroyed window pub fn unmanage_destroyed(&mut self, conn: &Connection<'_>, window: Window) { let Some(i) = self.find_client_pos(window) else { return; }; let focused_mon = self.focused_mon().screen_info; self.clients.remove(i); self.focused_client = self.focused_client.and_then(|f| match f.cmp(&i) { Ordering::Less => Some(f), Ordering::Greater => Some(f - 1), Ordering::Equal => None, }); if self.focused_client.is_none() { if let Some(i) = self .clients .iter() .position(|c| focused_mon.contains(c.x(), c.y())) { self.refocus(conn, i); } } self.rearrange(conn); } /// Refocus on the client with the given co-ordinates, setting X11 properties as required. /// If the given index is invalid, focus on the root instead. pub fn refocus(&mut self, conn: &Connection<'_>, i: ClientIdx) { self.unfocus(conn); if self.set_focused(i).is_some() { let mon_idx = self.client_mon_idx(i); let tag = self.client(i).tag; if !self.mons[mon_idx].focused_tag.matches(tag) { self.set_mon_tag_focus(conn, mon_idx, TagFocus::Tag(tag)); } let new = self.client_mut(i); new.set_border(conn, conn.colours.border_focused()); new.sync_hints(conn, true); buttons::grab(conn, new.window(), true); if !new.never_focus() { conn.send_request(&SetInputFocus { revert_to: InputFocus::PointerRoot, focus: new.window(), time: x::CURRENT_TIME, }); conn.send_request(&ChangeProperty { window: conn.root(), mode: PropMode::Replace, property: conn.atoms.net_active_window, r#type: x::ATOM_WINDOW, data: &[new.window()], }); conn.send_event(new.window(), conn.atoms.wm_take_focus); } } else { conn.send_request(&SetInputFocus { revert_to: InputFocus::PointerRoot, focus: conn.root(), time: x::CURRENT_TIME, }); conn.send_request(&DeleteProperty { window: conn.root(), property: conn.atoms.net_active_window, }); } } /// Unfocus the currently focused window, if it exists. pub fn unfocus(&mut self, conn: &Connection<'_>) { if let Some(old) = self.focused_mut() { old.set_border(conn, conn.colours.border_normal()); buttons::grab(conn, old.window(), false); } self.focused_client = None; } /// Unfocus the currently focused window, if it exists, without doing any of the X11 parts. /// This is used when a focused window is destroyed or unmapped pub fn unfocus_destroyed(&mut self) { self.focused_client = None; } /// Go to the next or previous window in the current monitor, looping around if needed pub fn change_focus(&mut self, conn: &Connection<'_>, increase: bool) { if self.clients.is_empty() { return; } let tag_focus = self.focused_mon().focused_tag; let curr_focused = self.focused_client.unwrap_or(0); let look_through = self .clients .iter() .enumerate() .filter(|(_, c)| tag_focus.matches(c.tag)) .filter(|(_, c)| self.focused_mon().screen_info.contains(c.x(), c.y())) .map(|(i, _)| i); let new_idx = if increase { look_through .cycle() .skip_while(|i| *i != curr_focused) .nth(1) .unwrap_or(0) } else { look_through .rev() .cycle() .skip_while(|i| *i != curr_focused) .nth(1) .unwrap_or(0) }; self.refocus(conn, new_idx); } /// Shift the focused client up (increase = false) or down (increase = true) the client list pub fn shift_focused_client(&mut self, conn: &Connection<'_>, increase: bool) { // We always want to place it after/before the next/previous one on the focused tag let Some(target) = self.focused_client else { return; }; let mon_idx = self.client_mon_idx(target); let tag_focus = self.mons[mon_idx].focused_tag; let look_through = self .clients .iter() .enumerate() .filter(|(_, c)| tag_focus.matches(c.tag)) .filter(|(_, c)| c.tiled()) .map(|(i, _)| i); let new_idx = if increase { look_through .cycle() .skip_while(|i| *i != target) .nth(1) .unwrap_or(0) } else { look_through .rev() .cycle() .skip_while(|i| *i != target) .nth(1) .unwrap_or(0) }; // Need to do split it so that the borrow checker is happy match new_idx.cmp(&target) { Ordering::Less => { let (before, after) = self.clients.split_at_mut(target); mem::swap(&mut before[new_idx], &mut after[0]); } Ordering::Greater => { let (before, after) = self.clients.split_at_mut(new_idx); mem::swap(&mut before[target], &mut after[0]); } Ordering::Equal => return, } self.focused_client = Some(new_idx); // Doesn't need any actual X operations done self.rearrange_mon(conn, mon_idx); } /// Shift the focused client to the top of the stack pub fn shift_focused_to_top(&mut self, conn: &Connection<'_>) { if self.clients.len() == 1 { return; // Already at the top } let Some(target) = self.focused_client else { return; }; // Need to do split it so that the borrow checker is happy let (before, after) = self.clients.split_at_mut(1); mem::swap(&mut before[0], &mut after[target - 1]); self.focused_client = Some(0); // Doesn't need any actual X operations done self.rearrange_mon(conn, self.focused_mon_idx()); } /// Toggle whether the client with the given position is floating pub fn toggle_floating(&mut self, conn: &Connection<'_>, pos: ClientIdx) { let c = &mut self.clients[pos]; if c.tiled() { c.set_floating(conn); } else { c.set_tiled(conn); } self.rearrange_mon(conn, self.client_mon_idx(pos)); } /// Toggle whether the client with the given position is fullscreen pub fn toggle_fullscreen(&mut self, conn: &Connection<'_>, pos: ClientIdx) { let mon = self.client_mon_idx(pos); let mon_info = &self.mons[mon]; let c = &mut self.clients[pos]; if c.tiled() { c.set_fullscreen(conn, &mon_info.screen_info); } else { c.set_tiled(conn); } self.rearrange_mon(conn, mon); } /// Set the focused tag for the given monitor pub fn set_mon_tag_focus( &mut self, conn: &Connection<'_>, mon: MonitorIdx, tag_focus: TagFocus, ) { // Hide windows from currently focused tag filter let mon_info = &self.mons[mon]; let curr_focus = mon_info.focused_tag; self.clients .iter_mut() .filter(|c| curr_focus.matches(c.tag)) .filter(|c| mon_info.screen_info.contains(c.x(), c.y())) .for_each(|c| c.ensure_unmapped(conn)); debug!("setting tag focus to {:?} on mon {}", tag_focus, mon); self.mons[mon].last_focused_tag = curr_focus; self.mons[mon].focused_tag = tag_focus; self.rearrange_mon(conn, mon); self.unfocus(conn); self.focused_client = self.clients.iter().position(|c| tag_focus.matches(c.tag)); } /// Set the given monitor's focused tag to its previous value pub fn mon_prev_tag_focus(&mut self, conn: &Connection<'_>, mon: MonitorIdx) { self.set_mon_tag_focus(conn, mon, self.mons[mon].last_focused_tag); } /// Set the tag for the given client pub fn set_client_tag(&mut self, conn: &Connection<'_>, pos: ClientIdx, tag: Tag) { let c = self.client_mut(pos); if c.tag == tag { return; } debug!("moving client with window {:?} to tag {}", c.window(), tag); c.tag = tag; c.ensure_unmapped(conn); self.unfocus(conn); self.rearrange_mon(conn, self.client_mon_idx(pos)); } /// Update the recorded monitors and monitor sizes, retiling if necessary. pub fn update_geometry(&mut self, conn: &Connection<'_>) -> Result<()> { let mut dirty = false; if conn.active_extensions().any(|e| e == Extension::Xinerama) { let reply = conn.wait_for_reply(conn.send_request(&xinerama::QueryScreens {}))?; // Monitor removed, move its clients away if reply.screen_info().len() > self.mons.len() { dirty = true; self.truncate_screens(reply.screen_info().len()); } // Update screen info & add new client lists if needed for (i, monitor) in reply.screen_info().iter().enumerate() { dirty |= self.set_monitor_geometry(i, (*monitor).into()); } } else { // Only one screen if self.mons.len() > 1 { dirty = true; self.truncate_screens(1); } // TODO: it looks like this won't actually update when the screen size changes? let setup = conn.get_setup(); let screen = setup .roots() .nth(conn.screen_num()) .ok_or(Error::NoSuchScreen)?; dirty |= self.set_monitor_geometry( 0, MonitorGeometry { x_org: 0, y_org: 0, width: screen.width_in_pixels(), height: screen.height_in_pixels(), }, ); } if dirty { self.rearrange(conn); } Ok(()) } /// Set the new amount of screens fn truncate_screens(&mut self, new_size: usize) { match new_size.cmp(&self.mons.len()) { Ordering::Greater => { for _ in 0..new_size - self.mons.len() { self.mons.push(MonitorInfo::default()); } } Ordering::Less => { self.mons.drain(new_size - self.mons.len()..self.mons.len()); } Ordering::Equal => (), } } /// Set the given screen's geometry, resizing the monitor list if necessary. /// Returns true if the new info is different from the old one. fn set_monitor_geometry(&mut self, i: MonitorIdx, info: MonitorGeometry) -> bool { while i >= self.mons.len() { self.mons.push(MonitorInfo::default()); } let dirty = self.mons[i].screen_info != info; self.mons[i].screen_info = info; dirty } /// Find the [`Client`] corresponding to the given window pub fn find_client_mut(&mut self, window: Window) -> Option<&mut Client> { let i = self.find_client_pos(window)?; Some(&mut self.clients[i]) } /// Find the position of the client with the given window, returning an index pub fn find_client_pos(&self, window: Window) -> Option { self.clients.iter().position(|c| c.window() == window) } /// Get the index of the currently focused client, if it exists pub const fn focused_pos(&self) -> Option { self.focused_client } /// Get a reference to the currently focused client, if it exists. pub fn focused(&self) -> Option<&Client> { self.focused_client.map(|i| self.client(i)) } /// Get a mutable reference to the currently focused client, if it exists. pub fn focused_mut(&mut self) -> Option<&mut Client> { self.focused_client.map(|i| self.client_mut(i)) } pub fn is_focused(&self, e: Window) -> bool { self.focused().is_some_and(|c| c.window() == e) } /// Set the currently focused client, returning a mutable reference to the client. pub fn set_focused(&mut self, mut i: ClientIdx) -> Option<&mut Client> { if self.clients.is_empty() { return None; } if i >= self.clients.len() { i = self.clients.len() - 1; } self.focused_client = Some(i); Some(&mut self.clients[i]) } /// Get a reference to the client at the given index pub fn client(&self, i: ClientIdx) -> &Client { &self.clients[i] } /// Get a mutable reference to the client at the given index pub fn client_mut(&mut self, i: ClientIdx) -> &mut Client { &mut self.clients[i] } /// Rearrange all clients, reconfiguring them as needed. pub fn rearrange(&mut self, conn: &Connection<'_>) { for mon in 0..self.mons.len() { self.rearrange_mon(conn, mon); } } /// Rearrange a specific monitor pub fn rearrange_mon(&mut self, conn: &Connection<'_>, mon: MonitorIdx) { (self.arrange)(conn, &mut self.clients, &self.mons[mon]); } /// Get the index of the currently focused monitor pub fn focused_mon_idx(&self) -> MonitorIdx { self.focused_client.map_or(0, |i| self.client_mon_idx(i)) } /// Get the info for the currently focused monitor pub fn focused_mon(&self) -> &MonitorInfo { &self.mons[self.focused_mon_idx()] } /// Get the monitor info for the client with the given index pub fn client_mon_idx(&self, pos: ClientIdx) -> MonitorIdx { let c = self.client(pos); self.mons .iter() .position(|m| m.screen_info.contains(c.x(), c.y())) .unwrap_or_else(|| self.mons.len() - 1) } /// Get the monitor info for the client with the given index pub fn client_mon(&self, pos: ClientIdx) -> &MonitorInfo { &self.mons[self.client_mon_idx(pos)] } pub fn kill_client(&self, conn: &Connection, pos: usize) { let c = self.client(pos); // Modern clients respond to the WM_DELETE event if !conn.send_event(c.window(), conn.atoms.wm_delete) { // Fallback to the old fashioned way // Using checked requests so we can ignore errors here without waiting for them to go to // the event loop let cookie1 = conn.send_request_checked(&SetCloseDownMode { mode: CloseDown::DestroyAll, }); let cookie2 = conn.send_request_checked(&KillClient { resource: c.window().resource_id(), }); let _ = conn.check_request(cookie1); let _ = conn.check_request(cookie2); } } } impl Default for ClientState { fn default() -> Self { Self { arrange: &tile::tile, mons: vec![], clients: vec![], focused_client: None, } } } impl std::fmt::Debug for ClientState { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("ClientState") .field("focused_client", &self.focused_client) .field("mons", &self.mons) .field("clients", &self.clients) .finish_non_exhaustive() } }