From 66e3423828892a72e5e525f2dc8d5ad91e634445 Mon Sep 17 00:00:00 2001 From: tcmal Date: Thu, 6 Jun 2024 21:39:23 +0100 Subject: track focus and draw borders --- src/clients.rs | 178 ++++++++++++++++++++++++++++++++++++++++++++++----------- src/colours.rs | 44 ++++++++++++++ src/focus.rs | 66 ++++++++++++++++++--- src/main.rs | 13 +++-- 4 files changed, 254 insertions(+), 47 deletions(-) create mode 100644 src/colours.rs diff --git a/src/clients.rs b/src/clients.rs index d5bd8a9..75ed38a 100644 --- a/src/clients.rs +++ b/src/clients.rs @@ -2,15 +2,17 @@ use std::cmp::min; use xcb::{ x::{ - ConfigWindow, ConfigureNotifyEvent, ConfigureRequestEvent, ConfigureWindow, - DestroyNotifyEvent, EventMask, GetWindowAttributes, MapRequestEvent, MapWindow, SendEvent, - SendEventDest, UnmapNotifyEvent, UnmapWindow, Window, + ChangeWindowAttributes, ConfigWindow, ConfigureNotifyEvent, ConfigureRequestEvent, + ConfigureWindow, Cw, DestroyNotifyEvent, Drawable, EventMask, GetGeometry, + GetWindowAttributes, MapRequestEvent, MapWindow, SendEvent, SendEventDest, + UnmapNotifyEvent, UnmapWindow, Window, }, xinerama::{self, ScreenInfo}, BaseEvent, Connection, Extension, Xid, }; use crate::{error::*, WM}; +const BORDER_WIDTH: u16 = 3; impl WM<'_> { /// Update the client state's recorded monitors and monitor sizes @@ -126,8 +128,7 @@ impl WM<'_> { } // Start managing, and map window - self.clients.manage(self.conn, e.window()); - // TODO: clear focus + self.manage(self.conn, e.window()); self.conn.flush()?; @@ -148,6 +149,70 @@ impl WM<'_> { 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. + fn manage(&mut self, conn: &Connection, window: Window) { + // TODO: inherit from parent if window is transient + let mon = self.clients.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 + }; + + self.unfocus(); + + // TODO: inserting at index 0 is why dwm uses linked lists, maybe this can be improved + self.clients.mons[mon].clients.insert( + 0, + Client { + window, + x: 0, + y: 0, + width: 0, + height: 0, + border_width: BORDER_WIDTH, + mapped: false, + }, + ); + + // TODO: Clamp window size to monitor + + let c = &mut self.clients.mons[mon].clients[0]; + + c.set_geom( + conn, + geom.x(), + geom.y(), + geom.width(), + geom.height(), + BORDER_WIDTH, + ); + c.set_border(conn, self.colours.border_normal()); + c.ensure_mapped(conn); + + // TODO: updatewindowtype + // TODO: updatesizehints + // TODO: updatewmhints + + // XSelectInput(dpy, w, EnterWindowMask|FocusChangeMask|PropertyChangeMask|StructureNotifyMask); + c.select_input( + conn, + EventMask::ENTER_WINDOW + | EventMask::FOCUS_CHANGE + | EventMask::PROPERTY_CHANGE + | EventMask::STRUCTURE_NOTIFY, + ); + // TODO: grabbuttons + // TODO: add to NetClientList + // TODO: XMoveResizeWindow(dpy, c->win, c->x + 2 * sw, c->y, c->w, c->h); /* some windows require this */ + // TODO: setclientstate(c, NormalState); + + self.refocus(mon, 0); + self.clients.rearrange_monitor(conn, mon); + } } /// Holds state related to the window manager's clients @@ -156,15 +221,16 @@ 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), - selected_monitor: usize, mons: Vec, + + focused: (usize, usize), } impl Default for ClientState { fn default() -> Self { Self { arrange: &tile, - selected_monitor: 0, + focused: (0, 0), mons: vec![], } } @@ -173,7 +239,7 @@ impl Default for ClientState { impl std::fmt::Debug for ClientState { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("ClientState") - .field("selected_monitor", &self.selected_monitor) + .field("focused", &self.focused) .field("mons", &self.mons) .finish() } @@ -251,38 +317,57 @@ impl ClientState { .find(|c| c.window == window) } - /// Remove the client associated with the given window. - /// This doesn't perform any of the associated X11 stuff - pub fn remove_client(&mut self, window: Window) -> Option { - for mon in self.mons.iter_mut() { + /// Find the position of the client with the given window, returning (monitor, index) + pub(crate) 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(mon.clients.remove(pos)); + return Some((pos_mon, pos)); } } None } - /// 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. - fn manage(&mut self, conn: &Connection, window: Window) { - // TODO: inherit from parent if window is transient - let mon = self.selected_monitor; + /// 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) + } - // TODO: inserting at index 0 is why dwm uses linked lists, maybe this can be improved - self.mons[mon].clients.insert( - 0, - Client { - window, - x: 0, - y: 0, - width: 0, - height: 0, - border_width: 0, - mapped: false, - }, - ); + /// 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; + } - self.rearrange_monitor(conn, mon) + 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_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 + } + } + + /// Remove the client associated with the given window. + /// This doesn't perform any of the associated X11 stuff + pub fn remove_client(&mut self, window: Window) -> Option { + let (mon, i) = self.find_client_pos(window)?; + Some(self.mons[mon].clients.remove(i)) } /// Rearrange all clients, reconfiguring them as needed. @@ -298,6 +383,10 @@ impl ClientState { fn rearrange_monitor(&mut self, conn: &Connection, mon: usize) { (self.arrange)(&mut self.mons[mon], conn); } + + fn focused_mon(&self) -> usize { + self.focused.0 + } } /// Information about a single client / window @@ -369,8 +458,17 @@ impl Client { }); } + /// Set the border of the X11 window to the given value. + /// This sends a request but doesn't wait for the response. + pub fn set_border(&self, conn: &Connection, colour: u32) { + conn.send_request(&ChangeWindowAttributes { + window: self.window(), + value_list: &[Cw::BorderPixel(colour)], + }); + } + /// Ensure this client is currently mapped / visible - fn ensure_mapped(&mut self, conn: &Connection) { + pub fn ensure_mapped(&mut self, conn: &Connection) { if !self.mapped { conn.send_request(&MapWindow { window: self.window, @@ -380,7 +478,7 @@ impl Client { } /// Ensure this client is currently unmapped / invisible - fn ensure_unmapped(&mut self, conn: &Connection) { + pub fn ensure_unmapped(&mut self, conn: &Connection) { if self.mapped { conn.send_request(&UnmapWindow { window: self.window, @@ -388,6 +486,18 @@ impl Client { self.mapped = false; } } + + /// Get the associated window + pub fn window(&self) -> Window { + self.window + } + + fn select_input(&self, conn: &Connection, event_mask: EventMask) { + conn.send_request(&ChangeWindowAttributes { + window: self.window(), + value_list: &[Cw::EventMask(event_mask)], + }); + } } impl Default for MonitorInfo { diff --git a/src/colours.rs b/src/colours.rs new file mode 100644 index 0000000..6f862c3 --- /dev/null +++ b/src/colours.rs @@ -0,0 +1,44 @@ +use crate::error::*; +use xcb::{ + x::{AllocColor, Colormap}, + Connection, +}; + +pub struct Colours { + cmap: Colormap, + border_normal: u32, + border_focused: u32, +} + +impl Colours { + pub fn new_with(conn: &Connection, cmap: Colormap) -> Result { + let (border_normal, border_focused) = ( + conn.wait_for_reply(conn.send_request(&AllocColor { + cmap, + red: 0, + green: 0, + blue: 0, + }))?, + conn.wait_for_reply(conn.send_request(&AllocColor { + cmap, + red: u16::MAX, + green: 0, + blue: u16::MAX, + }))?, + ); + + Ok(Self { + cmap, + border_normal: border_normal.pixel(), + border_focused: border_focused.pixel(), + }) + } + + pub fn border_normal(&self) -> u32 { + self.border_normal + } + + pub fn border_focused(&self) -> u32 { + self.border_focused + } +} diff --git a/src/focus.rs b/src/focus.rs index ea4f021..a237e24 100644 --- a/src/focus.rs +++ b/src/focus.rs @@ -1,20 +1,68 @@ -use xcb::x::{EnterNotifyEvent, FocusInEvent, MotionNotifyEvent}; +use xcb::x::{ + self, ChangeWindowAttributes, Cw, EnterNotifyEvent, FocusInEvent, InputFocus, NotifyDetail, + NotifyMode, SetInputFocus, Window, +}; -use crate::{error::*, WM}; +use crate::{clients::Client, error::*, WM}; impl WM<'_> { - pub(crate) fn handle_enter_notify(&self, _e: EnterNotifyEvent) -> Result<()> { - // todo!() + pub(crate) fn handle_enter_notify(&mut self, e: EnterNotifyEvent) -> Result<()> { + if (e.mode() != NotifyMode::Normal || e.detail() == NotifyDetail::Inferior) + && e.event() != self.root + { + return Ok(()); + } + + self.focus_window(e.event()); + self.conn.flush()?; + Ok(()) } - pub(crate) fn handle_focus_in(&self, _e: FocusInEvent) -> Result<()> { - // todo!() + pub(crate) fn handle_focus_in(&mut self, e: FocusInEvent) -> Result<()> { + if self + .clients + .focused_mut() + .map(|c| c.window() != e.event()) + .unwrap_or(true) + { + self.focus_window(e.event()); + } Ok(()) } - pub(crate) fn handle_motion_notify(&self, _e: MotionNotifyEvent) -> Result<()> { - // todo!() - Ok(()) + /// Attempt to focus the given window, falling back to the root if the window isn't our client. + /// This function sends multiple requests without checking them, so conn.flush() should be called after. + pub fn focus_window(&mut self, window: Window) { + if let Some((mon, i)) = self.clients.find_client_pos(window) { + self.refocus(mon, i); + } else { + self.conn.send_request(&SetInputFocus { + revert_to: InputFocus::PointerRoot, + focus: window, + time: x::CURRENT_TIME, + }); + // TODO: XDeleteProperty(dpy, root, netatom[NetActiveWindow]); + } + } + + /// Refocus on the client with the given co-ordinates, setting the X11 properties as required. + /// This function sends multiple requests without checking them, so conn.flush() should be called after. + pub fn refocus(&mut self, mon: usize, i: usize) { + self.unfocus(); + if let Some(new) = self.clients.set_focused(mon, i) { + new.set_border(self.conn, self.colours.border_focused()); + // TODO: reset urgent flag + // TODO: something to do with grabbuttons + // TODO: set input focus + // TODO: set active window + // TODO: send wmtakefocus event + } + } + + pub fn unfocus(&mut self) { + if let Some(old) = self.clients.focused_mut() { + old.set_border(self.conn, self.colours.border_normal()); + } } } diff --git a/src/main.rs b/src/main.rs index 6559ad7..e5ec003 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,6 @@ use atoms::InternedAtoms; use clients::ClientState; +use colours::Colours; use cursors::Cursors; use error::*; use keys::KeyboardInfo; @@ -10,6 +11,7 @@ use xcb::{ mod atoms; mod clients; +mod colours; mod cursors; mod error; mod focus; @@ -42,6 +44,9 @@ struct WM<'a> { /// WM client state clients: ClientState, + /// Cached colours, + colours: Colours, + /// Cached cursors cursors: Cursors, @@ -74,6 +79,7 @@ impl WM<'_> { .map_err(|_| Error::OtherWMRunning)?; Ok(WM { + colours: Colours::new_with(conn, screen.default_colormap())?, atoms: InternedAtoms::new_with(conn)?, cursors: Cursors::new_with(conn)?, keyboard_state: KeyboardInfo::new_with(conn)?, @@ -95,9 +101,8 @@ impl WM<'_> { x::EventMask::SUBSTRUCTURE_REDIRECT | x::EventMask::SUBSTRUCTURE_NOTIFY | x::EventMask::BUTTON_PRESS - | x::EventMask::POINTER_MOTION | x::EventMask::ENTER_WINDOW - | x::EventMask::LEAVE_WINDOW + | x::EventMask::FOCUS_CHANGE | x::EventMask::STRUCTURE_NOTIFY | x::EventMask::PROPERTY_CHANGE, ), @@ -133,7 +138,6 @@ impl WM<'_> { // See focus.rs Event::X(x::Event::EnterNotify(e)) => self.handle_enter_notify(e)?, Event::X(x::Event::FocusIn(e)) => self.handle_focus_in(e)?, - Event::X(x::Event::MotionNotify(e)) => self.handle_motion_notify(e)?, // See below Event::X(x::Event::PropertyNotify(e)) => self.handle_property_notify(e)?, @@ -144,7 +148,8 @@ impl WM<'_> { /// Handle a property notify event, by doing *todo* fn handle_property_notify(&self, _e: PropertyNotifyEvent) -> Result<()> { - todo!() + println!("TODO: handle_property_notify"); + Ok(()) } } -- cgit v1.2.3