//! Tracking and managing windows. use std::cmp::min; use crate::{ config::BORDER_WIDTH, conn_info::Connection, error::{Error, Result}, WM, }; use xcb::{ x::{ self, ChangeProperty, ConfigWindow, ConfigureNotifyEvent, ConfigureRequestEvent, ConfigureWindow, DeleteProperty, DestroyNotifyEvent, Drawable, EventMask, GetGeometry, GetWindowAttributes, InputFocus, MapRequestEvent, PropMode, SetInputFocus, UnmapNotifyEvent, Window, }, xinerama::{self}, BaseEvent, Extension, }; pub use client::*; pub use hints::*; pub use monitors::*; #[doc(hidden)] mod client; #[doc(hidden)] mod hints; #[doc(hidden)] mod monitors; mod tile; 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) -> Result<()> { if let Some(c) = self.clients.find_client_mut(e.window()) { // TODO: Allow changing some properties: // - Border width // - Size and position if floating c.configure_notify(&self.conn); } else { // Configure it as requested, and sort the rest when we actually map the window self.conn.send_and_check_request(&ConfigureWindow { window: e.window(), value_list: &[ ConfigWindow::X(e.x().into()), ConfigWindow::Y(e.y().into()), ConfigWindow::Width(e.width().into()), ConfigWindow::Height(e.height().into()), ConfigWindow::BorderWidth(e.border_width().into()), ConfigWindow::StackMode(e.stack_mode()), ], })?; } Ok(()) } /// 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) { self.clients.unmanage(&self.conn, e.window(), true); } /// 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, either stop managing it or update its state. pub(crate) fn handle_unmap_notify(&mut self, e: &UnmapNotifyEvent) { if let Some(c) = self.clients.find_client_mut(e.window()) { if e.is_from_send_event() { 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); } else { self.clients.unmanage(&self.conn, e.window(), false); } } } } /// 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(&mut MonitorInfo, &Connection<'_>), /// A client list for each monitor. mons: Vec, /// Co-ordinates to the currently focused window. focused: (usize, usize), } impl ClientState { /// 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.monitor_count() { 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.monitor_count() > 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(()) } /// 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 mon = self.focused_mon(); let mut floating = false; if let Some(parent) = hints::transient_for(conn, window) { floating = true; if let Some((parent_mon, _)) = self.find_client_pos(parent) { mon = parent_mon; } } 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 }; // We're about to invalidate focus position self.unfocus(conn); // TODO: inserting at index 0 is why dwm uses linked lists, maybe this can be improved self.mons[mon].clients.insert(0, Client::new(window)); let mon_geom @ MonitorGeometry { width: mon_width, height: mon_height, .. } = self.mons[mon].screen_info; let c = &mut self.mons[mon].clients[0]; #[allow(clippy::cast_sign_loss)] c.set_geom( conn, geom.x(), geom.y(), min(geom.width(), mon_width.saturating_sub(geom.x() as u16)), min(geom.height(), mon_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.ensure_mapped(conn); c.set_event_mask( conn, EventMask::ENTER_WINDOW | EventMask::FOCUS_CHANGE | EventMask::PROPERTY_CHANGE | EventMask::STRUCTURE_NOTIFY, ); // TODO: grabbuttons // 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], }); c.set_withdrawn(conn, false); self.refocus(conn, mon, 0); self.rearrange_monitor(conn, mon); } /// Stop managing the given window, and also unset attributes unless `already_destroyed` is true. pub fn unmanage(&mut self, conn: &Connection<'_>, window: Window, already_destroyed: bool) { let Some((mon, i)) = self.find_client_pos(window) else { return; }; let c = self.mons[mon].clients.remove(i); if !already_destroyed { c.set_event_mask(conn, EventMask::NO_EVENT); // TODO: Ungrab button let cookie = c.set_withdrawn(conn, true); // If any of the above requests fail, it's just a race condition and the window is already destroyed, so discard the error here. let _ = conn.check_request(cookie); } 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<'_>, mon: usize, i: usize) { self.unfocus(conn); if let Some(new) = self.set_focused(mon, i) { new.set_border(conn, conn.colours.border_focused()); new.sync_hints(conn, 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()); } } /// 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.focused.0 >= self.mons.len() { return; } let mon = self.focused.0; if self.mons[mon].clients.is_empty() { return; } self.refocus( conn, mon, if increase { (self.focused.1 + 1) % self.mons[mon].clients.len() } else if self.focused.1 > 0 { self.focused.1 - 1 } else { self.mons[mon].clients.len() - 1 }, ); } /// Toggle whether the client with the given position is floating pub fn toggle_floating(&mut self, conn: &Connection<'_>, (mon, pos): (usize, usize)) { let c = &mut self.mons[mon].clients[pos]; if c.tiled() { c.set_floating(conn); } else { c.set_tiled(conn); } self.rearrange_monitor(conn, mon); } /// Toggle whether the client with the given position is fullscreen pub fn toggle_fullscreen(&mut self, conn: &Connection<'_>, (mon, pos): (usize, usize)) { let mon_info = &mut self.mons[mon]; let c = &mut mon_info.clients[pos]; if c.tiled() { c.set_fullscreen(conn, &mon_info.screen_info); } else { c.set_tiled(conn); } self.rearrange_monitor(conn, mon); } /// Get the amount of monitors this state is currently aware of pub fn monitor_count(&self) -> usize { self.mons.len() } /// Set the new amount of screens, without unmanaging any clients. fn truncate_screens(&mut self, new_size: usize) { // hack: working around double borrow stuff if new_size < self.mons.len() { let mut moved_clients = vec![]; for old in self.mons.drain(new_size - self.mons.len()..self.mons.len()) { moved_clients.extend(old.clients.into_iter()); } self.mons[0].clients.extend(moved_clients); } else if new_size > self.mons.len() { for _ in 0..new_size - self.mons.len() { self.mons.push(MonitorInfo::default()); } } } /// 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: usize, 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 (mon, i) = self.find_client_pos(window)?; Some(&mut self.mons[mon].clients[i]) } /// Find the position of the client with the given window, returning (monitor, index) pub fn find_client_pos(&mut self, window: Window) -> Option<(usize, usize)> { for (pos_mon, mon) in self.mons.iter_mut().enumerate() { if let Some(pos) = mon.clients.iter().position(|c| c.window() == window) { return Some((pos_mon, pos)); } } None } /// Get a reference to the currently focused client, if it exists. pub fn focused(&self) -> Option<&Client> { self.client(self.focused.0, self.focused.1) } /// Get the position of the currently focused client, if it exists pub fn focused_pos(&self) -> Option<(usize, usize)> { let (mon, pos) = self.focused; if mon < self.mons.len() && pos < self.mons[mon].clients.len() { Some((mon, pos)) } else { None } } /// Get a mutable reference to the currently focused client, if it exists. pub fn focused_mut(&mut self) -> Option<&mut Client> { self.client_mut(self.focused.0, self.focused.1) } /// Get the position of the currently focused client. This position may be invalid. 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 it if the co-ordinates are valid. pub fn set_focused(&mut self, mut mon: usize, mut i: usize) -> Option<&mut Client> { if self.mons.is_empty() { return None; } if mon >= self.mons.len() { mon = self.mons.len() - 1; } if self.mons[mon].clients.is_empty() { return None; } if i >= self.mons[mon].clients.len() { i = self.mons[mon].clients.len() - 1; } self.focused = (mon, i); Some(&mut self.mons[mon].clients[i]) } /// Get a mutable reference to the client at the given co-ordinates, if they are valid. pub fn client(&self, mon: usize, i: usize) -> Option<&Client> { if mon < self.mons.len() && i < self.mons[mon].clients.len() { Some(&self.mons[mon].clients[i]) } else { None } } /// Get a mutable reference to the client at the given co-ordinates, if they are valid. pub fn client_mut(&mut self, mon: usize, i: usize) -> Option<&mut Client> { if mon < self.mons.len() && i < self.mons[mon].clients.len() { Some(&mut self.mons[mon].clients[i]) } else { None } } /// Rearrange all clients, reconfiguring them as needed. pub fn rearrange(&mut self, conn: &Connection<'_>) { for mon in 0..self.monitor_count() { self.rearrange_monitor(conn, mon); } } /// Rearrange a specific monitor fn rearrange_monitor(&mut self, conn: &Connection<'_>, mon: usize) { (self.arrange)(&mut self.mons[mon], conn); } /// Get the currently focused monitor const fn focused_mon(&self) -> usize { self.focused.0 } /// Get the geometry of the monitor with the given index pub fn mon_geometry(&self, mon: usize) -> MonitorGeometry { self.mons[mon].screen_info } } impl Default for ClientState { fn default() -> Self { Self { arrange: &tile::tile, focused: (0, 0), mons: vec![], } } } impl std::fmt::Debug for ClientState { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("ClientState") .field("focused", &self.focused) .field("mons", &self.mons) .finish_non_exhaustive() } }