summaryrefslogtreecommitdiff
path: root/src/clients/mod.rs
diff options
context:
space:
mode:
authortcmal <me@aria.rip>2024-06-21 19:13:12 +0100
committertcmal <me@aria.rip>2024-06-21 19:13:12 +0100
commit475253b7bcfd03a932c4b7efd969b3d2bf155035 (patch)
tree44789ce271d14c89cbff777e75f1e9ab6e1a5e64 /src/clients/mod.rs
parentc3e98e34ed7d42ef4271339de88f7131e7647442 (diff)
refactor connection-related stuff out, make things a bit cleaner
Diffstat (limited to 'src/clients/mod.rs')
-rw-r--r--src/clients/mod.rs425
1 files changed, 425 insertions, 0 deletions
diff --git a/src/clients/mod.rs b/src/clients/mod.rs
new file mode 100644
index 0000000..d07de74
--- /dev/null
+++ b/src/clients/mod.rs
@@ -0,0 +1,425 @@
+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, SetInputFocus, UnmapNotifyEvent, Window,
+ },
+ xinerama::{self},
+ BaseEvent, Extension,
+};
+
+pub use client::Client;
+pub use monitors::*;
+
+mod client;
+mod hints;
+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);
+ self.conn.flush()?;
+ } 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)?;
+ self.conn.flush()?;
+ }
+ Ok(())
+ }
+
+ /// Removing destroyed windows from the client list and rearrange.
+ pub(crate) fn handle_destroy_notify(&mut self, e: &DestroyNotifyEvent) -> Result<()> {
+ self.clients.unmanage(&self.conn, e.window(), true);
+ self.conn.flush()?;
+
+ Ok(())
+ }
+
+ /// 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());
+
+ self.conn.flush()?;
+
+ Ok(())
+ }
+
+ /// When a window is unmapped, either stop managing it or update its state.
+ pub(crate) fn handle_unmap_notify(&mut self, e: &UnmapNotifyEvent) -> Result<()> {
+ if let Some(c) = self.clients.find_client_mut(e.window()) {
+ if e.is_from_send_event() {
+ c.set_withdrawn(&self.conn, true);
+ } else {
+ self.clients.unmanage(&self.conn, e.window(), false);
+ }
+ self.conn.flush()?;
+ }
+
+ Ok(())
+ }
+}
+
+/// 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.
+ /// The connection will be flushed after it is called.
+ arrange: &'static dyn Fn(&mut MonitorInfo, &Connection<'_>),
+
+ /// A client list for each monitor.
+ mons: Vec<MonitorInfo>,
+
+ /// Co-ordinates to the currently focused window.
+ focused: (usize, usize),
+}
+
+impl ClientState {
+ /// Update the recorded monitors and monitor sizes, retiling if necessary.
+ /// This function sends multiple requests without checking them, so `conn.flush()` should be called after.
+ 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.
+ /// This function sends multiple requests without checking them, so `conn.flush()` should be called after.
+ pub fn manage(&mut self, conn: &Connection<'_>, window: Window) {
+ // TODO: inherit from parent if window is transient
+ let mon = self.focused_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));
+
+ // TODO: Clamp window size to monitor
+ let c = &mut self.mons[mon].clients[0];
+ c.set_geom(
+ conn,
+ geom.x(),
+ geom.y(),
+ geom.width(),
+ geom.height(),
+ BORDER_WIDTH,
+ );
+ c.set_border(conn, conn.colours.border_normal());
+ c.ensure_mapped(conn);
+
+ // TODO: updatewindowtype
+ // TODO: updatesizehints
+ c.sync_properties(conn, true);
+
+ 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);
+
+ // TODO: XMoveResizeWindow(dpy, c->win, c->x + 2 * sw, c->y, c->w, c->h); /* some windows require this */
+ self.refocus(conn, mon, 0);
+ self.rearrange_monitor(conn, mon);
+ }
+
+ /// Stop managing the given window, and also unset attributes unless `already_destroyed` is true.
+ /// This function sends multiple requests without checking them, so `conn.flush()` should be called after.
+ 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::empty());
+ // TODO: Ungrab button
+ c.set_withdrawn(conn, true);
+ }
+ 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.
+ /// This function sends multiple requests without checking them, so `conn.flush()` should be called after.
+ 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.set_urgent(false);
+ if !new.never_focus() {
+ // XSetInputFocus(dpy, c->win, RevertToPointerRoot, CurrentTime);
+ conn.send_request(&SetInputFocus {
+ revert_to: InputFocus::PointerRoot,
+ focus: new.window(),
+ time: x::CURRENT_TIME,
+ });
+ // XChangeProperty(dpy, root, netatom[NetActiveWindow],
+ // XA_WINDOW, 32, PropModeReplace,
+ // (unsigned char *) &(c->win), 1);
+ // TODO: sendevent(c, wmatom[WMTakeFocus]);
+ }
+ } 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.
+ /// This function sends multiple requests without checking them, so `conn.flush()` should be called after.
+ pub fn unfocus(&mut self, conn: &Connection<'_>) {
+ if let Some(old) = self.focused_mut() {
+ old.set_border(conn, conn.colours.border_normal());
+ // TODO: clear some properties
+ }
+ }
+
+ /// 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
+ 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);
+ }
+
+ /// 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 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.
+ /// This function sends multiple requests without checking them, so `conn.flush()` should be called after.
+ pub fn rearrange(&mut self, conn: &Connection<'_>) {
+ for mon in 0..self.monitor_count() {
+ self.rearrange_monitor(conn, mon);
+ }
+ }
+
+ /// Rearrange a specific monitor
+ /// This function sends multiple requests without checking them, so `conn.flush()` should be called after.
+ 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
+ }
+}
+
+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()
+ }
+}