diff --git a/Cargo.toml b/Cargo.toml index 0c428250..c12dddec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "penrose" -version = "0.3.5" +version = "0.3.6" edition = "2021" authors = ["sminez "] license = "MIT" @@ -30,7 +30,7 @@ x11rb-xcb = ["x11rb", "x11rb/allow-unsafe-code"] anymap = "0.12" bitflags = { version = "2.5", features = ["serde"] } nix = { version = "0.29", default-features = false, features = ["signal"] } -penrose_keysyms = { version = "0.3.4", path = "crates/penrose_keysyms", optional = true } +penrose_keysyms = { version = "0.3.6", path = "crates/penrose_keysyms", optional = true } serde = { version = "1.0", features = ["derive"], optional = true } strum = { version = "0.26", features = ["derive"] } thiserror = "1.0" diff --git a/crates/penrose_keysyms/Cargo.toml b/crates/penrose_keysyms/Cargo.toml index 7153c1e2..bb44a23d 100644 --- a/crates/penrose_keysyms/Cargo.toml +++ b/crates/penrose_keysyms/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "penrose_keysyms" -version = "0.3.5" +version = "0.3.6" authors = ["IDAM "] edition = "2018" license = "MIT" diff --git a/crates/penrose_ui/Cargo.toml b/crates/penrose_ui/Cargo.toml index 571b4a60..ecf870af 100644 --- a/crates/penrose_ui/Cargo.toml +++ b/crates/penrose_ui/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "penrose_ui" -version = "0.3.5" +version = "0.3.6" edition = "2021" authors = ["sminez "] license = "MIT" @@ -12,7 +12,7 @@ description = "UI elements for the penrose window manager library" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -penrose = { version = "0.3.5", path = "../../" } +penrose = { version = "0.3.6", path = "../../" } tracing = { version = "0.1", features = ["attributes"] } thiserror = "1.0" yeslogic-fontconfig-sys = "5.0" diff --git a/examples/minimal/main.rs b/examples/minimal/main.rs index b938312c..3c497079 100644 --- a/examples/minimal/main.rs +++ b/examples/minimal/main.rs @@ -4,11 +4,17 @@ //! has multiple workspaces and simple client / workspace movement. use penrose::{ builtin::{ - actions::{exit, modify_with, send_layout_message, spawn}, + actions::{ + exit, + floating::{sink_clicked, MouseDragHandler, MouseResizeHandler}, + modify_with, send_layout_message, spawn, + }, layout::messages::{ExpandMain, IncMain, ShrinkMain}, }, core::{ - bindings::{parse_keybindings_with_xmodmap, KeyEventHandler}, + bindings::{ + parse_keybindings_with_xmodmap, KeyEventHandler, MouseEventHandler, MouseState, + }, Config, WindowManager, }, map, @@ -57,6 +63,21 @@ fn raw_key_bindings() -> HashMap>> { raw_bindings } +fn mouse_bindings() -> HashMap>> { + use penrose::core::bindings::{ + ModifierKey::{Meta, Shift}, + MouseButton::{Left, Middle, Right}, + }; + + map! { + map_keys: |(button, modifiers)| MouseState { button, modifiers }; + + (Left, vec![Shift, Meta]) => MouseDragHandler::boxed_default(), + (Right, vec![Shift, Meta]) => MouseResizeHandler::boxed_default(), + (Middle, vec![Shift, Meta]) => sink_clicked(), + } +} + fn main() -> Result<()> { tracing_subscriber::fmt() .with_env_filter("info") @@ -65,7 +86,7 @@ fn main() -> Result<()> { let conn = RustConn::new()?; let key_bindings = parse_keybindings_with_xmodmap(raw_key_bindings())?; - let wm = WindowManager::new(Config::default(), key_bindings, HashMap::new(), conn)?; + let wm = WindowManager::new(Config::default(), key_bindings, mouse_bindings(), conn)?; wm.run() } diff --git a/src/builtin/actions/floating.rs b/src/builtin/actions/floating.rs index 0697e4f4..adca8adc 100644 --- a/src/builtin/actions/floating.rs +++ b/src/builtin/actions/floating.rs @@ -1,8 +1,16 @@ //! Actions for manipulating floating windows. use crate::{ - builtin::actions::{key_handler, modify_with}, - core::bindings::KeyEventHandler, + builtin::actions::{key_handler, modify_with, mouse_modify_with}, + core::{ + bindings::{ + KeyEventHandler, MotionNotifyEvent, MouseEvent, MouseEventHandler, MouseEventKind, + }, + State, + }, + custom_error, + pure::geometry::{Point, Rect}, x::{XConn, XConnExt}, + Result, Xid, }; use tracing::error; @@ -93,3 +101,152 @@ pub fn float_all() -> Box> { pub fn sink_all() -> Box> { modify_with(|cs| cs.floating.clear()) } + +#[derive(Debug, Default, Clone, Copy)] +struct ClickData { + x_initial: i32, + y_initial: i32, + r_initial: Rect, +} + +impl ClickData { + fn on_motion( + &self, + f: impl Fn(&mut Rect, i32, i32), + id: Xid, + rpt: Point, + state: &mut State, + x: &X, + ) -> Result<()> { + let (dx, dy) = (rpt.x as i32 - self.x_initial, rpt.y as i32 - self.y_initial); + + let mut r = self.r_initial; + (f)(&mut r, dx, dy); + + state.client_set.float(id, r)?; + x.position_client(id, r)?; + + Ok(()) + } +} + +trait ClickWrapper { + fn data(&mut self) -> &mut Option; + + fn motion_fn(&self) -> impl Fn(&mut Rect, i32, i32); + + fn on_mouse_event( + &mut self, + evt: &MouseEvent, + state: &mut State, + x: &X, + ) -> Result<()> { + let id = evt.data.id; + + match evt.kind { + MouseEventKind::Press => { + let r_client = x.client_geometry(id)?; + state.client_set.float(id, r_client)?; + *self.data() = Some(ClickData { + x_initial: evt.data.rpt.x as i32, + y_initial: evt.data.rpt.y as i32, + r_initial: r_client, + }); + } + + MouseEventKind::Release => *self.data() = None, + } + + Ok(()) + } + + fn on_motion( + &mut self, + evt: &MotionNotifyEvent, + state: &mut State, + x: &X, + ) -> Result<()> { + match *self.data() { + Some(data) => data.on_motion(self.motion_fn(), evt.data.id, evt.data.rpt, state, x), + None => Err(custom_error!("mouse motion without held state")), + } + } +} + +/// A simple mouse event handler for dragging a window +#[derive(Debug, Default, Clone)] +pub struct MouseDragHandler { + data: Option, +} + +impl MouseDragHandler { + /// Construct a boxed [MouseEventHandler] trait object ready to be added to your bindings + pub fn boxed_default() -> Box> { + Box::::default() + } +} + +impl ClickWrapper for MouseDragHandler { + fn data(&mut self) -> &mut Option { + &mut self.data + } + + fn motion_fn(&self) -> impl Fn(&mut Rect, i32, i32) { + |r, dx, dy| r.reposition(dx, dy) + } +} + +impl MouseEventHandler for MouseDragHandler { + fn on_mouse_event(&mut self, evt: &MouseEvent, state: &mut State, x: &X) -> Result<()> { + ClickWrapper::on_mouse_event(self, evt, state, x) + } + + fn on_motion(&mut self, evt: &MotionNotifyEvent, state: &mut State, x: &X) -> Result<()> { + ClickWrapper::on_motion(self, evt, state, x) + } +} + +/// A simple mouse event handler for resizing a window +#[derive(Debug, Default, Clone)] +pub struct MouseResizeHandler { + data: Option, +} + +impl MouseResizeHandler { + /// Construct a boxed [MouseEventHandler] trait object ready to be added to your bindings + pub fn boxed_default() -> Box> { + Box::::default() + } +} + +impl ClickWrapper for MouseResizeHandler { + fn data(&mut self) -> &mut Option { + &mut self.data + } + + fn motion_fn(&self) -> impl Fn(&mut Rect, i32, i32) { + |r, dw, dh| r.resize(dw, dh) + } +} + +impl MouseEventHandler for MouseResizeHandler { + fn on_mouse_event(&mut self, evt: &MouseEvent, state: &mut State, x: &X) -> Result<()> { + ClickWrapper::on_mouse_event(self, evt, state, x) + } + + fn on_motion(&mut self, evt: &MotionNotifyEvent, state: &mut State, x: &X) -> Result<()> { + ClickWrapper::on_motion(self, evt, state, x) + } +} + +/// Sink the current window back into tiling mode if it was floating +pub fn sink_clicked() -> Box> { + mouse_modify_with(|cs| { + let id = match cs.current_client() { + Some(&id) => id, + None => return, + }; + + cs.sink(&id); + }) +} diff --git a/src/builtin/actions/mod.rs b/src/builtin/actions/mod.rs index 272e2860..b36f5afb 100644 --- a/src/builtin/actions/mod.rs +++ b/src/builtin/actions/mod.rs @@ -1,6 +1,10 @@ //! Helpers and pre-defined actions for use in user defined key bindings use crate::{ - core::{bindings::KeyEventHandler, layout::IntoMessage, ClientSet, State}, + core::{ + bindings::{KeyEventHandler, MouseEventHandler}, + layout::IntoMessage, + ClientSet, State, + }, util, x::{XConn, XConnExt}, Result, @@ -102,3 +106,27 @@ pub fn remove_and_unmap_focused_client() -> Box } }) } + +// NOTE: this is here to force the correct lifetime requirements on closures being +// used as handlers. The generic impl in crate::bindings for functions of the +// right signature isn't sufficient on its own. + +/// Construct a [MouseEventHandler] from a closure or free function. +/// +/// The resulting handler will run on button press events. +pub fn mouse_handler(f: F) -> Box> +where + F: FnMut(&mut State, &X) -> Result<()> + 'static, + X: XConn, +{ + Box::new(f) +} + +/// Mutate the [ClientSet] and refresh the on screen state +pub fn mouse_modify_with(f: F) -> Box> +where + F: FnMut(&mut ClientSet) + Clone + 'static, + X: XConn, +{ + Box::new(move |s: &mut State, x: &X| x.modify_and_refresh(s, f.clone())) +} diff --git a/src/core/bindings.rs b/src/core/bindings.rs index 72245971..4b44401d 100644 --- a/src/core/bindings.rs +++ b/src/core/bindings.rs @@ -116,28 +116,37 @@ pub trait MouseEventHandler where X: XConn, { - /// Call this handler with the current window manager state and mouse state - fn call(&mut self, evt: &MouseEvent, state: &mut State, x: &X) -> Result<()>; + /// Called when the [MouseState] associated with this handler is seen with a button press or + /// release. + fn on_mouse_event(&mut self, evt: &MouseEvent, state: &mut State, x: &X) -> Result<()>; + + /// Called when the [ModifierKey]s associated with this handler are seen when the mouse is + /// moving. + fn on_motion(&mut self, evt: &MotionNotifyEvent, state: &mut State, x: &X) -> Result<()>; } impl fmt::Debug for Box> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("KeyEventHandler").finish() + f.debug_struct("MouseEventHandler").finish() } } impl MouseEventHandler for F where - F: FnMut(&MouseEvent, &mut State, &X) -> Result<()>, + F: FnMut(&mut State, &X) -> Result<()>, X: XConn, { - fn call(&mut self, evt: &MouseEvent, state: &mut State, x: &X) -> Result<()> { - (self)(evt, state, x) + fn on_mouse_event(&mut self, _: &MouseEvent, state: &mut State, x: &X) -> Result<()> { + (self)(state, x) + } + + fn on_motion(&mut self, _: &MotionNotifyEvent, _: &mut State, _: &X) -> Result<()> { + Ok(()) } } /// User defined mouse bindings -pub type MouseBindings = HashMap<(MouseEventKind, MouseState), Box>>; +pub type MouseBindings = HashMap>>; /// Abstraction layer for working with key presses #[derive(Debug, Clone, PartialEq, Eq)] @@ -217,10 +226,11 @@ impl KeyCode { } /// Known mouse buttons for binding actions -#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)] +#[derive(Debug, Default, PartialEq, Eq, Hash, Clone, Copy)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum MouseButton { /// 1 + #[default] Left, /// 2 Middle, @@ -350,23 +360,29 @@ pub enum MouseEventKind { Press, /// A button was released Release, - /// The mouse was moved while a button was held - Motion, } -/// A mouse movement or button event +/// Data from a button press or motion-notify event #[derive(Debug, Clone, PartialEq, Eq, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub struct MouseEvent { +pub struct MouseEventData { /// The ID of the window that was contained the click pub id: Xid, /// Absolute coordinate of the event pub rpt: Point, /// Coordinate of the event relative to top-left of the window itself pub wpt: Point, +} + +/// A mouse movement or button event +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct MouseEvent { + /// The details of which window the event applies to and where the event occurred + pub data: MouseEventData, /// The modifier and button code that was received pub state: MouseState, - /// Was this press, release or motion? + /// Was this press or release pub kind: MouseEventKind, } @@ -382,11 +398,37 @@ impl MouseEvent { kind: MouseEventKind, ) -> Self { MouseEvent { - id, - rpt: Point::new(rx as u32, ry as u32), - wpt: Point::new(ex as u32, ey as u32), + data: MouseEventData { + id, + rpt: Point::new(rx as u32, ry as u32), + wpt: Point::new(ex as u32, ey as u32), + }, state, kind, } } } + +/// Mouse motion with a held button and optional modifiers +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct MotionNotifyEvent { + /// The details of which window the event applies to and where the event occurred + pub data: MouseEventData, + /// All [ModifierKey]s being held + pub modifiers: Vec, +} + +impl MotionNotifyEvent { + /// Construct a new [MotionNotifyEvent] from raw data + pub fn new(id: Xid, rx: i16, ry: i16, ex: i16, ey: i16, modifiers: Vec) -> Self { + MotionNotifyEvent { + data: MouseEventData { + id, + rpt: Point::new(rx as u32, ry as u32), + wpt: Point::new(ex as u32, ey as u32), + }, + modifiers, + } + } +} diff --git a/src/core/handle.rs b/src/core/handle.rs index c268109a..88b2f5c0 100644 --- a/src/core/handle.rs +++ b/src/core/handle.rs @@ -1,7 +1,9 @@ //! XEvent handlers for use in the main event loop; use crate::{ core::{ - bindings::{KeyBindings, KeyCode, MouseBindings, MouseEvent}, + bindings::{ + KeyBindings, KeyCode, MotionNotifyEvent, MouseBindings, MouseEvent, MouseEventKind, + }, State, Xid, }, pure::geometry::Point, @@ -13,7 +15,7 @@ use crate::{ }, Result, }; -use tracing::{error, info, trace}; +use tracing::{error, info, trace, warn}; // Currently no client messages are handled by default (see the ewmh extension for some examples of messages // that are handled when that is enabled) @@ -31,10 +33,7 @@ pub(crate) fn mapping_notify( ) -> Result<()> { trace!("grabbing key and mouse bindings"); let key_codes: Vec<_> = key_bindings.keys().copied().collect(); - let mouse_states: Vec<_> = mouse_bindings - .keys() - .map(|(_, state)| state.clone()) - .collect(); + let mouse_states: Vec<_> = mouse_bindings.keys().cloned().collect(); x.grab(&key_codes, &mouse_states) } @@ -62,8 +61,37 @@ pub(crate) fn mouse_event( state: &mut State, x: &X, ) -> Result<()> { - if let Some(action) = bindings.get_mut(&(e.kind, e.state.clone())) { - if let Err(error) = action.call(&e, state, x) { + if let Some(action) = bindings.get_mut(&e.state) { + if let Err(error) = action.on_mouse_event(&e, state, x) { + error!(%error, ?e, "error running user mouse binding"); + return Err(error); + } + + match e.kind { + MouseEventKind::Press => state.held_mouse_state = Some(e.state), + MouseEventKind::Release => state.held_mouse_state = None, + } + } + + Ok(()) +} + +pub(crate) fn motion_event( + e: MotionNotifyEvent, + bindings: &mut MouseBindings, + state: &mut State, + x: &X, +) -> Result<()> { + let held_state = match state.held_mouse_state.as_ref() { + Some(state) => state, + None => { + warn!("got motion notify event without known held state"); + return Ok(()); + } + }; + + if let Some(action) = bindings.get_mut(held_state) { + if let Err(error) = action.on_motion(&e, state, x) { error!(%error, ?e, "error running user mouse binding"); return Err(error); } diff --git a/src/core/mod.rs b/src/core/mod.rs index 1af314f9..f0b584ca 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -27,7 +27,7 @@ pub(crate) mod handle; pub mod hooks; pub mod layout; -use bindings::{KeyBindings, MouseBindings}; +use bindings::{KeyBindings, MouseBindings, MouseState}; use hooks::{EventHook, LayoutHook, ManageHook, StateHook}; use layout::{Layout, LayoutStack}; @@ -85,8 +85,7 @@ where pub(crate) current_event: Option, pub(crate) diff: Diff, pub(crate) running: bool, - // pub(crate) mouse_focused: bool, - // pub(crate) mouse_position: Option<(Point, Point)>, + pub(crate) held_mouse_state: Option, } impl State @@ -113,6 +112,7 @@ where current_event: None, diff, running: false, + held_mouse_state: None, }) } @@ -545,6 +545,7 @@ where MappingNotify => handle::mapping_notify(key_bindings, mouse_bindings, x)?, MapRequest(xid) => handle::map_request(*xid, state, x)?, MouseEvent(e) => handle::mouse_event(e.clone(), mouse_bindings, state, x)?, + MotionNotify(e) => handle::motion_event(e.clone(), mouse_bindings, state, x)?, PropertyNotify(_) => (), // Not currently handled RandrNotify => handle::detect_screens(state, x)?, ScreenChange => handle::screen_change(state, x)?, diff --git a/src/pure/stack_set.rs b/src/pure/stack_set.rs index f509896e..6f9c2a75 100644 --- a/src/pure/stack_set.rs +++ b/src/pure/stack_set.rs @@ -761,6 +761,7 @@ impl StackSet { current_event: None, diff: Default::default(), running: false, + held_mouse_state: None, }; s.visible_client_positions(&crate::x::StubXConn) diff --git a/src/x/event.rs b/src/x/event.rs index 0c33b1eb..8a742b7d 100644 --- a/src/x/event.rs +++ b/src/x/event.rs @@ -1,6 +1,6 @@ //! Data types for working with X events use crate::{ - core::bindings::{KeyCode, MouseEvent}, + core::bindings::{KeyCode, MotionNotifyEvent, MouseEvent}, pure::geometry::{Point, Rect}, x::{Atom, XConn}, Result, Xid, @@ -40,8 +40,10 @@ pub enum XEvent { MappingNotify, /// A client window is requesting to be positioned and rendered on the screen. MapRequest(Xid), - /// The mouse has moved or a mouse button has been pressed + /// A mouse button has been pressed or released MouseEvent(MouseEvent), + /// The mouse has moved or a mouse button has been pressed + MotionNotify(MotionNotifyEvent), /// A client property has changed in some way PropertyNotify(PropertyEvent), /// A randr action has occured (new outputs, resolution change etc) @@ -71,6 +73,7 @@ impl std::fmt::Display for XEvent { MappingNotify => write!(f, "MappingNotify"), MapRequest(_) => write!(f, "MapRequest"), MouseEvent(_) => write!(f, "MouseEvent"), + MotionNotify(_) => write!(f, "MotionNotify"), PropertyNotify(_) => write!(f, "PropertyNotify"), RandrNotify => write!(f, "RandrNotify"), ResizeRequest(_) => write!(f, "ResizeRequest"), diff --git a/src/x11rb/conversions.rs b/src/x11rb/conversions.rs index fd65cf6e..3e75a841 100644 --- a/src/x11rb/conversions.rs +++ b/src/x11rb/conversions.rs @@ -1,6 +1,9 @@ //! Conversions to Penrose types from X11rb types use crate::{ - core::bindings::{KeyCode, ModifierKey, MouseButton, MouseEvent, MouseEventKind, MouseState}, + core::bindings::{ + KeyCode, ModifierKey, MotionNotifyEvent, MouseButton, MouseEvent, MouseEventKind, + MouseState, + }, pure::geometry::{Point, Rect}, x::{ event::{ @@ -31,7 +34,7 @@ pub(crate) fn convert_event(conn: &Conn, event: Event) -> Resu Event::ButtonPress(event) => Ok(to_mouse_state(event.detail, event.state).map(|state| { XEvent::MouseEvent(MouseEvent::new( - Xid(event.event), + Xid(event.child), event.root_x, event.root_y, event.event_x, @@ -43,7 +46,7 @@ pub(crate) fn convert_event(conn: &Conn, event: Event) -> Resu Event::ButtonRelease(event) => Ok(to_mouse_state(event.detail, event.state).map(|state| { XEvent::MouseEvent(MouseEvent::new( - Xid(event.event), + Xid(event.child), event.root_x, event.root_y, event.event_x, @@ -53,16 +56,15 @@ pub(crate) fn convert_event(conn: &Conn, event: Event) -> Resu )) })), - // FIXME: The 5 is due to https://github.com/sminez/penrose/issues/113 - Event::MotionNotify(event) => Ok(to_mouse_state(5, event.state).map(|state| { - XEvent::MouseEvent(MouseEvent::new( - Xid(event.event), + // NOTE: the '1' here is not actually used + Event::MotionNotify(event) => Ok(to_mouse_state(1, event.state).map(|state| { + XEvent::MotionNotify(MotionNotifyEvent::new( + Xid(event.child), event.root_x, event.root_y, event.event_x, event.event_y, - state, - MouseEventKind::Motion, + state.modifiers, )) })),