diff --git a/niri-config/src/lib.rs b/niri-config/src/lib.rs index 1de7b1592..ef9ef5b26 100644 --- a/niri-config/src/lib.rs +++ b/niri-config/src/lib.rs @@ -1124,6 +1124,11 @@ pub struct Key { #[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)] pub enum Trigger { Keysym(Keysym), + MouseLeft, + MouseRight, + MouseMiddle, + MouseBack, + MouseForward, WheelScrollDown, WheelScrollUp, WheelScrollLeft, @@ -2924,7 +2929,17 @@ impl FromStr for Key { } } - let trigger = if key.eq_ignore_ascii_case("WheelScrollDown") { + let trigger = if key.eq_ignore_ascii_case("MouseLeft") { + Trigger::MouseLeft + } else if key.eq_ignore_ascii_case("MouseRight") { + Trigger::MouseRight + } else if key.eq_ignore_ascii_case("MouseMiddle") { + Trigger::MouseMiddle + } else if key.eq_ignore_ascii_case("MouseBack") { + Trigger::MouseBack + } else if key.eq_ignore_ascii_case("MouseForward") { + Trigger::MouseForward + } else if key.eq_ignore_ascii_case("WheelScrollDown") { Trigger::WheelScrollDown } else if key.eq_ignore_ascii_case("WheelScrollUp") { Trigger::WheelScrollUp diff --git a/src/input/mod.rs b/src/input/mod.rs index 8ce00712b..c86e1467a 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -1656,11 +1656,43 @@ impl State { let serial = SERIAL_COUNTER.next_serial(); - let button = event.button_code(); + let button = event.button(); + + let button_code = event.button_code(); let button_state = event.state(); + // Ignore release events for mouse clicks that triggered a bind. + if self.niri.suppressed_buttons.remove(&button_code) { + return; + } + if ButtonState::Pressed == button_state { + let mods = self.niri.seat.get_keyboard().unwrap().modifier_state(); + let modifiers = modifiers_from_state(mods); + + if self.niri.mods_with_mouse_binds.contains(&modifiers) { + let comp_mod = self.backend.mod_key(); + + if let Some(bind) = match button { + Some(MouseButton::Left) => Some(Trigger::MouseLeft), + Some(MouseButton::Right) => Some(Trigger::MouseRight), + Some(MouseButton::Middle) => Some(Trigger::MouseMiddle), + Some(MouseButton::Back) => Some(Trigger::MouseBack), + Some(MouseButton::Forward) => Some(Trigger::MouseForward), + _ => None, + } + .and_then(|trigger| { + let config = self.niri.config.borrow(); + let bindings = &config.binds; + find_configured_bind(bindings, comp_mod, trigger, mods) + }) { + self.niri.suppressed_buttons.insert(button_code); + self.handle_bind(bind.clone()); + return; + }; + } + // We received an event for the regular pointer, so show it now. self.niri.pointer_hidden = false; self.niri.tablet_cursor_location = None; @@ -1669,8 +1701,7 @@ impl State { let window = mapped.window.clone(); // Check if we need to start an interactive move. - if event.button() == Some(MouseButton::Left) && !pointer.is_grabbed() { - let mods = self.niri.seat.get_keyboard().unwrap().modifier_state(); + if button == Some(MouseButton::Left) && !pointer.is_grabbed() { let mod_down = match self.backend.mod_key() { CompositorMod::Super => mods.logo, CompositorMod::Alt => mods.alt, @@ -1689,7 +1720,7 @@ impl State { ) { let start_data = PointerGrabStartData { focus: None, - button: event.button_code(), + button: button_code, location, }; let grab = MoveGrab::new(start_data, window.clone()); @@ -1701,8 +1732,7 @@ impl State { } } // Check if we need to start an interactive resize. - else if event.button() == Some(MouseButton::Right) && !pointer.is_grabbed() { - let mods = self.niri.seat.get_keyboard().unwrap().modifier_state(); + else if button == Some(MouseButton::Right) && !pointer.is_grabbed() { let mod_down = match self.backend.mod_key() { CompositorMod::Super => mods.logo, CompositorMod::Alt => mods.alt, @@ -1762,7 +1792,7 @@ impl State { { let start_data = PointerGrabStartData { focus: None, - button: event.button_code(), + button: button_code, location, }; let grab = ResizeGrab::new(start_data, window.clone()); @@ -1786,8 +1816,7 @@ impl State { self.niri.queue_redraw_all(); } - if event.button() == Some(MouseButton::Middle) && !pointer.is_grabbed() { - let mods = self.niri.seat.get_keyboard().unwrap().modifier_state(); + if button == Some(MouseButton::Middle) && !pointer.is_grabbed() { let mod_down = match self.backend.mod_key() { CompositorMod::Super => mods.logo, CompositorMod::Alt => mods.alt, @@ -1797,7 +1826,7 @@ impl State { let location = pointer.current_location(); let start_data = PointerGrabStartData { focus: None, - button: event.button_code(), + button: button_code, location, }; let grab = SpatialMovementGrab::new(start_data, output); @@ -1817,7 +1846,7 @@ impl State { self.niri.focus_layer_surface_if_on_demand(layer_under); } - if let Some(button) = event.button() { + if let Some(button) = button { let pos = pointer.current_location(); if let Some((output, _)) = self.niri.output_under(pos) { let output = output.clone(); @@ -1845,7 +1874,7 @@ impl State { pointer.button( self, &ButtonEvent { - button, + button: button_code, state: button_state, serial, time: event.time_msec(), @@ -3110,6 +3139,20 @@ pub fn mods_with_binds( rv } +pub fn mods_with_mouse_binds(comp_mod: CompositorMod, binds: &Binds) -> HashSet { + mods_with_binds( + comp_mod, + binds, + &[ + Trigger::MouseLeft, + Trigger::MouseRight, + Trigger::MouseMiddle, + Trigger::MouseBack, + Trigger::MouseForward, + ], + ) +} + pub fn mods_with_wheel_binds(comp_mod: CompositorMod, binds: &Binds) -> HashSet { mods_with_binds( comp_mod, diff --git a/src/niri.rs b/src/niri.rs index 7d81bbc68..ada6039e9 100644 --- a/src/niri.rs +++ b/src/niri.rs @@ -116,7 +116,8 @@ use crate::frame_clock::FrameClock; use crate::handlers::{configure_lock_surface, XDG_ACTIVATION_TOKEN_TIMEOUT}; use crate::input::scroll_tracker::ScrollTracker; use crate::input::{ - apply_libinput_settings, mods_with_finger_scroll_binds, mods_with_wheel_binds, TabletData, + apply_libinput_settings, mods_with_finger_scroll_binds, mods_with_mouse_binds, + mods_with_wheel_binds, TabletData, }; use crate::ipc::server::IpcServer; use crate::layer::mapped::LayerSurfaceRenderElement; @@ -277,6 +278,8 @@ pub struct Niri { pub seat: Seat, /// Scancodes of the keys to suppress. pub suppressed_keys: HashSet, + /// Button codes of the mouse buttons to suppress. + pub suppressed_buttons: HashSet, pub bind_cooldown_timers: HashMap, pub bind_repeat_timer: Option, pub keyboard_focus: KeyboardFocus, @@ -313,6 +316,7 @@ pub struct Niri { pub gesture_swipe_3f_cumulative: Option<(f64, f64)>, pub vertical_wheel_tracker: ScrollTracker, pub horizontal_wheel_tracker: ScrollTracker, + pub mods_with_mouse_binds: HashSet, pub mods_with_wheel_binds: HashSet, pub vertical_finger_scroll_tracker: ScrollTracker, pub horizontal_finger_scroll_tracker: ScrollTracker, @@ -1142,6 +1146,8 @@ impl State { if config.binds != old_config.binds { self.niri.hotkey_overlay.on_hotkey_config_updated(); + self.niri.mods_with_mouse_binds = + mods_with_mouse_binds(self.backend.mod_key(), &config.binds); self.niri.mods_with_wheel_binds = mods_with_wheel_binds(self.backend.mod_key(), &config.binds); self.niri.mods_with_finger_scroll_binds = @@ -1846,6 +1852,7 @@ impl Niri { let cursor_manager = CursorManager::new(&config_.cursor.xcursor_theme, config_.cursor.xcursor_size); + let mods_with_mouse_binds = mods_with_mouse_binds(backend.mod_key(), &config_.binds); let mods_with_wheel_binds = mods_with_wheel_binds(backend.mod_key(), &config_.binds); let mods_with_finger_scroll_binds = mods_with_finger_scroll_binds(backend.mod_key(), &config_.binds); @@ -1998,6 +2005,7 @@ impl Niri { popups: PopupManager::default(), popup_grab: None, suppressed_keys: HashSet::new(), + suppressed_buttons: HashSet::new(), bind_cooldown_timers: HashMap::new(), bind_repeat_timer: Option::default(), presentation_state, @@ -2025,6 +2033,7 @@ impl Niri { gesture_swipe_3f_cumulative: None, vertical_wheel_tracker: ScrollTracker::new(120), horizontal_wheel_tracker: ScrollTracker::new(120), + mods_with_mouse_binds, mods_with_wheel_binds, // 10 is copied from Clutter: DISCRETE_SCROLL_STEP. diff --git a/src/ui/hotkey_overlay.rs b/src/ui/hotkey_overlay.rs index 209e7deef..4bdfddc60 100644 --- a/src/ui/hotkey_overlay.rs +++ b/src/ui/hotkey_overlay.rs @@ -415,6 +415,11 @@ fn key_name(comp_mod: CompositorMod, key: &Key) -> String { let pretty = match key.trigger { Trigger::Keysym(keysym) => prettify_keysym_name(&keysym_get_name(keysym)), + Trigger::MouseLeft => String::from("Mouse Left"), + Trigger::MouseRight => String::from("Mouse Right"), + Trigger::MouseMiddle => String::from("Mouse Middle"), + Trigger::MouseBack => String::from("Mouse Back"), + Trigger::MouseForward => String::from("Mouse Forward"), Trigger::WheelScrollDown => String::from("Wheel Scroll Down"), Trigger::WheelScrollUp => String::from("Wheel Scroll Up"), Trigger::WheelScrollLeft => String::from("Wheel Scroll Left"), diff --git a/wiki/Configuration:-Key-Bindings.md b/wiki/Configuration:-Key-Bindings.md index 32448f5f1..42208bd51 100644 --- a/wiki/Configuration:-Key-Bindings.md +++ b/wiki/Configuration:-Key-Bindings.md @@ -99,6 +99,26 @@ binds { Both mouse wheel and touchpad scroll binds will prevent applications from receiving any scroll events when their modifiers are held down. For example, if you have a `Mod+WheelScrollDown` bind, then while holding `Mod`, all mouse wheel scrolling will be consumed by niri. +### Mouse Click Bindings + +Since: next release + +You can bind mouse clicks using the following syntax. + +```kdl +binds { + Mod+MouseLeft { close-window; } + Mod+MouseRight { close-window; } + Mod+MouseMiddle { close-window; } + Mod+MouseForward { close-window; } + Mod+MouseBack { close-window; } +} +``` + +Mouse clicks operate on the window that was focused at the time of the click, not the window you're clicking. + +Note that binding `Mod+MouseLeft` or `Mod+MouseRight` will override the corresponding gesture (moving or resizing the window). + ### Actions Every action that you can bind is also available for programmatic invocation via `niri msg action`.