mirror of
https://github.com/YaLTeR/niri.git
synced 2024-09-11 20:37:25 +03:00
Compare commits
10 Commits
db49deb7fd
...
54e6a01284
Author | SHA1 | Date | |
---|---|---|---|
|
54e6a01284 | ||
|
7721e3fc44 | ||
|
0d2fdb49ef | ||
|
b06e51da60 | ||
|
6c08ba307a | ||
|
4b2fdd0776 | ||
|
969519b5d8 | ||
|
a0c8c39b06 | ||
|
977f1487c2 | ||
|
fbe021fbdf |
@ -5,6 +5,7 @@ use std::collections::HashSet;
|
||||
use std::ffi::OsStr;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
|
||||
use bitflags::bitflags;
|
||||
use knuffel::errors::DecodeError;
|
||||
@ -716,18 +717,28 @@ impl PartialEq for Match {
|
||||
#[derive(Debug, Default, PartialEq)]
|
||||
pub struct Binds(pub Vec<Bind>);
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Bind {
|
||||
pub key: Key,
|
||||
pub action: Action,
|
||||
pub cooldown: Option<Duration>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
|
||||
pub struct Key {
|
||||
pub keysym: Keysym,
|
||||
pub trigger: Trigger,
|
||||
pub modifiers: Modifiers,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
|
||||
pub enum Trigger {
|
||||
Keysym(Keysym),
|
||||
WheelDown,
|
||||
WheelUp,
|
||||
WheelLeft,
|
||||
WheelRight,
|
||||
}
|
||||
|
||||
bitflags! {
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct Modifiers : u8 {
|
||||
@ -1396,13 +1407,45 @@ where
|
||||
node: &knuffel::ast::SpannedNode<S>,
|
||||
ctx: &mut knuffel::decode::Context<S>,
|
||||
) -> Result<Self, DecodeError<S>> {
|
||||
expect_only_children(node, ctx);
|
||||
if let Some(type_name) = &node.type_name {
|
||||
ctx.emit_error(DecodeError::unexpected(
|
||||
type_name,
|
||||
"type name",
|
||||
"no type name expected for this node",
|
||||
));
|
||||
}
|
||||
|
||||
for val in node.arguments.iter() {
|
||||
ctx.emit_error(DecodeError::unexpected(
|
||||
&val.literal,
|
||||
"argument",
|
||||
"no arguments expected for this node",
|
||||
))
|
||||
}
|
||||
|
||||
let key = node
|
||||
.node_name
|
||||
.parse::<Key>()
|
||||
.map_err(|e| DecodeError::conversion(&node.node_name, e.wrap_err("invalid keybind")))?;
|
||||
|
||||
let mut cooldown = None;
|
||||
for (name, val) in &node.properties {
|
||||
match &***name {
|
||||
"cooldown-ms" => {
|
||||
cooldown = Some(Duration::from_millis(
|
||||
knuffel::traits::DecodeScalar::decode(val, ctx)?,
|
||||
));
|
||||
}
|
||||
name_str => {
|
||||
ctx.emit_error(DecodeError::unexpected(
|
||||
name,
|
||||
"property",
|
||||
format!("unexpected property `{}`", name_str.escape_default()),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut children = node.children();
|
||||
|
||||
// If the action is invalid but the key is fine, we still want to return something.
|
||||
@ -1411,6 +1454,7 @@ where
|
||||
let dummy = Self {
|
||||
key,
|
||||
action: Action::Spawn(vec![]),
|
||||
cooldown: None,
|
||||
};
|
||||
|
||||
if let Some(child) = children.next() {
|
||||
@ -1422,7 +1466,11 @@ where
|
||||
));
|
||||
}
|
||||
match Action::decode_node(child, ctx) {
|
||||
Ok(action) => Ok(Self { key, action }),
|
||||
Ok(action) => Ok(Self {
|
||||
key,
|
||||
action,
|
||||
cooldown,
|
||||
}),
|
||||
Err(e) => {
|
||||
ctx.emit_error(e);
|
||||
Ok(dummy)
|
||||
@ -1499,12 +1547,23 @@ impl FromStr for Key {
|
||||
}
|
||||
}
|
||||
|
||||
let keysym = keysym_from_name(key, KEYSYM_CASE_INSENSITIVE);
|
||||
if keysym.raw() == KEY_NoSymbol {
|
||||
return Err(miette!("invalid key: {key}"));
|
||||
}
|
||||
let trigger = if key.eq_ignore_ascii_case("WheelDown") {
|
||||
Trigger::WheelDown
|
||||
} else if key.eq_ignore_ascii_case("WheelUp") {
|
||||
Trigger::WheelUp
|
||||
} else if key.eq_ignore_ascii_case("WheelLeft") {
|
||||
Trigger::WheelLeft
|
||||
} else if key.eq_ignore_ascii_case("WheelRight") {
|
||||
Trigger::WheelRight
|
||||
} else {
|
||||
let keysym = keysym_from_name(key, KEYSYM_CASE_INSENSITIVE);
|
||||
if keysym.raw() == KEY_NoSymbol {
|
||||
return Err(miette!("invalid key: {key}"));
|
||||
}
|
||||
Trigger::Keysym(keysym)
|
||||
};
|
||||
|
||||
Ok(Key { keysym, modifiers })
|
||||
Ok(Key { trigger, modifiers })
|
||||
}
|
||||
}
|
||||
|
||||
@ -1712,6 +1771,7 @@ mod tests {
|
||||
Mod+Comma { consume-window-into-column; }
|
||||
Mod+1 { focus-workspace 1; }
|
||||
Mod+Shift+E { quit skip-confirmation=true; }
|
||||
Mod+WheelDown cooldown-ms=150 { focus-workspace-down; }
|
||||
}
|
||||
|
||||
debug {
|
||||
@ -1895,52 +1955,67 @@ mod tests {
|
||||
binds: Binds(vec![
|
||||
Bind {
|
||||
key: Key {
|
||||
keysym: Keysym::t,
|
||||
trigger: Trigger::Keysym(Keysym::t),
|
||||
modifiers: Modifiers::COMPOSITOR,
|
||||
},
|
||||
action: Action::Spawn(vec!["alacritty".to_owned()]),
|
||||
cooldown: None,
|
||||
},
|
||||
Bind {
|
||||
key: Key {
|
||||
keysym: Keysym::q,
|
||||
trigger: Trigger::Keysym(Keysym::q),
|
||||
modifiers: Modifiers::COMPOSITOR,
|
||||
},
|
||||
action: Action::CloseWindow,
|
||||
cooldown: None,
|
||||
},
|
||||
Bind {
|
||||
key: Key {
|
||||
keysym: Keysym::h,
|
||||
trigger: Trigger::Keysym(Keysym::h),
|
||||
modifiers: Modifiers::COMPOSITOR | Modifiers::SHIFT,
|
||||
},
|
||||
action: Action::FocusMonitorLeft,
|
||||
cooldown: None,
|
||||
},
|
||||
Bind {
|
||||
key: Key {
|
||||
keysym: Keysym::l,
|
||||
trigger: Trigger::Keysym(Keysym::l),
|
||||
modifiers: Modifiers::COMPOSITOR | Modifiers::SHIFT | Modifiers::CTRL,
|
||||
},
|
||||
action: Action::MoveWindowToMonitorRight,
|
||||
cooldown: None,
|
||||
},
|
||||
Bind {
|
||||
key: Key {
|
||||
keysym: Keysym::comma,
|
||||
trigger: Trigger::Keysym(Keysym::comma),
|
||||
modifiers: Modifiers::COMPOSITOR,
|
||||
},
|
||||
action: Action::ConsumeWindowIntoColumn,
|
||||
cooldown: None,
|
||||
},
|
||||
Bind {
|
||||
key: Key {
|
||||
keysym: Keysym::_1,
|
||||
trigger: Trigger::Keysym(Keysym::_1),
|
||||
modifiers: Modifiers::COMPOSITOR,
|
||||
},
|
||||
action: Action::FocusWorkspace(1),
|
||||
cooldown: None,
|
||||
},
|
||||
Bind {
|
||||
key: Key {
|
||||
keysym: Keysym::e,
|
||||
trigger: Trigger::Keysym(Keysym::e),
|
||||
modifiers: Modifiers::COMPOSITOR | Modifiers::SHIFT,
|
||||
},
|
||||
action: Action::Quit(true),
|
||||
cooldown: None,
|
||||
},
|
||||
Bind {
|
||||
key: Key {
|
||||
trigger: Trigger::WheelDown,
|
||||
modifiers: Modifiers::COMPOSITOR,
|
||||
},
|
||||
action: Action::FocusWorkspaceDown,
|
||||
cooldown: Some(Duration::from_millis(150)),
|
||||
},
|
||||
]),
|
||||
debug: DebugConfig {
|
||||
|
@ -519,6 +519,27 @@ binds {
|
||||
Mod+Shift+U { move-workspace-down; }
|
||||
Mod+Shift+I { move-workspace-up; }
|
||||
|
||||
// You can bind mouse wheel scroll ticks using the following syntax.
|
||||
// To avoid scrolling through workspaces really fast, you can use
|
||||
// the cooldown-ms property. The bind will be rate-limited to this value.
|
||||
// You can set a cooldown on any bind, but it's most useful for the wheel.
|
||||
Mod+WheelDown cooldown-ms=150 { focus-workspace-down; }
|
||||
Mod+WheelUp cooldown-ms=150 { focus-workspace-up; }
|
||||
Mod+Ctrl+WheelDown cooldown-ms=150 { move-column-to-workspace-down; }
|
||||
Mod+Ctrl+WheelUp cooldown-ms=150 { move-column-to-workspace-up; }
|
||||
|
||||
Mod+WheelRight { focus-column-right; }
|
||||
Mod+WheelLeft { focus-column-left; }
|
||||
Mod+Ctrl+WheelRight { move-column-right; }
|
||||
Mod+Ctrl+WheelLeft { move-column-left; }
|
||||
|
||||
// Usually scrolling up and down with Shift in applications results in
|
||||
// horizontal scrolling; these binds replicate that.
|
||||
Mod+Shift+WheelDown { focus-column-right; }
|
||||
Mod+Shift+WheelUp { focus-column-left; }
|
||||
Mod+Ctrl+Shift+WheelDown { move-column-right; }
|
||||
Mod+Ctrl+Shift+WheelUp { move-column-left; }
|
||||
|
||||
// You can refer to workspaces by index. However, keep in mind that
|
||||
// niri is a dynamic workspace system, so these commands are kind of
|
||||
// "best effort". Trying to refer to a workspace index bigger than
|
||||
|
292
src/input.rs
292
src/input.rs
@ -1,9 +1,11 @@
|
||||
use std::any::Any;
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::collections::HashSet;
|
||||
use std::time::Duration;
|
||||
|
||||
use calloop::timer::{TimeoutAction, Timer};
|
||||
use input::event::gesture::GestureEventCoordinates as _;
|
||||
use niri_config::{Action, Binds, Modifiers};
|
||||
use niri_config::{Action, Bind, Binds, Key, Modifiers, Trigger};
|
||||
use niri_ipc::LayoutSwitchTarget;
|
||||
use smithay::backend::input::{
|
||||
AbsolutePositionEvent, Axis, AxisSource, ButtonState, Device, DeviceCapability, Event,
|
||||
@ -248,7 +250,7 @@ impl State {
|
||||
let time = Event::time_msec(&event);
|
||||
let pressed = event.state() == KeyState::Pressed;
|
||||
|
||||
let Some(Some(action)) = self.niri.seat.get_keyboard().unwrap().input(
|
||||
let Some(Some(bind)) = self.niri.seat.get_keyboard().unwrap().input(
|
||||
self,
|
||||
event.key_code(),
|
||||
event.state(),
|
||||
@ -289,7 +291,40 @@ impl State {
|
||||
return;
|
||||
}
|
||||
|
||||
self.do_action(action);
|
||||
self.handle_bind(bind);
|
||||
}
|
||||
|
||||
pub fn handle_bind(&mut self, bind: Bind) {
|
||||
let Some(cooldown) = bind.cooldown else {
|
||||
self.do_action(bind.action);
|
||||
return;
|
||||
};
|
||||
|
||||
// Check this first so that it doesn't trigger the cooldown.
|
||||
if self.niri.is_locked() && !allowed_when_locked(&bind.action) {
|
||||
return;
|
||||
}
|
||||
|
||||
match self.niri.bind_cooldown_timers.entry(bind.key) {
|
||||
// The bind is on cooldown.
|
||||
Entry::Occupied(_) => (),
|
||||
Entry::Vacant(entry) => {
|
||||
let timer = Timer::from_duration(cooldown);
|
||||
let token = self
|
||||
.niri
|
||||
.event_loop
|
||||
.insert_source(timer, move |_, _, state| {
|
||||
if state.niri.bind_cooldown_timers.remove(&bind.key).is_none() {
|
||||
error!("bind cooldown timer entry disappeared");
|
||||
}
|
||||
TimeoutAction::Drop
|
||||
})
|
||||
.unwrap();
|
||||
entry.insert(token);
|
||||
|
||||
self.do_action(bind.action);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn do_action(&mut self, action: Action) {
|
||||
@ -1078,30 +1113,92 @@ impl State {
|
||||
fn on_pointer_axis<I: InputBackend>(&mut self, event: I::PointerAxisEvent) {
|
||||
let source = event.source();
|
||||
|
||||
let horizontal_amount = event
|
||||
.amount(Axis::Horizontal)
|
||||
.unwrap_or_else(|| event.amount_v120(Axis::Horizontal).unwrap_or(0.0) * 3.0 / 120.);
|
||||
let vertical_amount = event
|
||||
.amount(Axis::Vertical)
|
||||
.unwrap_or_else(|| event.amount_v120(Axis::Vertical).unwrap_or(0.0) * 3.0 / 120.);
|
||||
let horizontal_amount_discrete = event.amount_v120(Axis::Horizontal);
|
||||
let vertical_amount_discrete = event.amount_v120(Axis::Vertical);
|
||||
let horizontal_amount_v120 = event.amount_v120(Axis::Horizontal);
|
||||
let vertical_amount_v120 = event.amount_v120(Axis::Vertical);
|
||||
|
||||
// Handle wheel bindings.
|
||||
if source == AxisSource::Wheel {
|
||||
let comp_mod = self.backend.mod_key();
|
||||
let mods = self.niri.seat.get_keyboard().unwrap().modifier_state();
|
||||
|
||||
if let Some(v120) = horizontal_amount_v120 {
|
||||
let config = self.niri.config.borrow();
|
||||
let bindings = &config.binds;
|
||||
let bind_left = find_configured_bind(bindings, comp_mod, Trigger::WheelLeft, mods);
|
||||
let bind_right =
|
||||
find_configured_bind(bindings, comp_mod, Trigger::WheelRight, mods);
|
||||
drop(config);
|
||||
|
||||
// If we have a bind with current modifiers along the scroll direction, then
|
||||
// accumulate and don't pass to Wayland. If there's no bind, reset the accumulator.
|
||||
if bind_left.is_some() || bind_right.is_some() {
|
||||
let ticks = self.niri.horizontal_wheel_tracker.accumulate(v120);
|
||||
if let Some(right) = bind_right {
|
||||
for _ in 0..ticks {
|
||||
self.handle_bind(right.clone());
|
||||
}
|
||||
}
|
||||
if let Some(left) = bind_left {
|
||||
for _ in ticks..0 {
|
||||
self.handle_bind(left.clone());
|
||||
}
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
self.niri.horizontal_wheel_tracker.reset();
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(v120) = vertical_amount_v120 {
|
||||
let config = self.niri.config.borrow();
|
||||
let bindings = &config.binds;
|
||||
let bind_up = find_configured_bind(bindings, comp_mod, Trigger::WheelUp, mods);
|
||||
let bind_down = find_configured_bind(bindings, comp_mod, Trigger::WheelDown, mods);
|
||||
drop(config);
|
||||
|
||||
if bind_up.is_some() || bind_down.is_some() {
|
||||
let ticks = self.niri.vertical_wheel_tracker.accumulate(v120);
|
||||
if let Some(down) = bind_down {
|
||||
for _ in 0..ticks {
|
||||
self.handle_bind(down.clone());
|
||||
}
|
||||
}
|
||||
if let Some(up) = bind_up {
|
||||
for _ in ticks..0 {
|
||||
self.handle_bind(up.clone());
|
||||
}
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
self.niri.vertical_wheel_tracker.reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let horizontal_amount = event.amount(Axis::Horizontal).unwrap_or_else(|| {
|
||||
// Winit backend, discrete scrolling.
|
||||
horizontal_amount_v120.unwrap_or(0.0) / 120. * 15.
|
||||
});
|
||||
let vertical_amount = event.amount(Axis::Vertical).unwrap_or_else(|| {
|
||||
// Winit backend, discrete scrolling.
|
||||
vertical_amount_v120.unwrap_or(0.0) / 120. * 15.
|
||||
});
|
||||
|
||||
let mut frame = AxisFrame::new(event.time_msec()).source(source);
|
||||
if horizontal_amount != 0.0 {
|
||||
frame = frame
|
||||
.relative_direction(Axis::Horizontal, event.relative_direction(Axis::Horizontal));
|
||||
frame = frame.value(Axis::Horizontal, horizontal_amount);
|
||||
if let Some(discrete) = horizontal_amount_discrete {
|
||||
frame = frame.v120(Axis::Horizontal, discrete as i32);
|
||||
if let Some(v120) = horizontal_amount_v120 {
|
||||
frame = frame.v120(Axis::Horizontal, v120 as i32);
|
||||
}
|
||||
}
|
||||
if vertical_amount != 0.0 {
|
||||
frame =
|
||||
frame.relative_direction(Axis::Vertical, event.relative_direction(Axis::Vertical));
|
||||
frame = frame.value(Axis::Vertical, vertical_amount);
|
||||
if let Some(discrete) = vertical_amount_discrete {
|
||||
frame = frame.v120(Axis::Vertical, discrete as i32);
|
||||
if let Some(v120) = vertical_amount_v120 {
|
||||
frame = frame.v120(Axis::Vertical, v120 as i32);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1626,7 +1723,7 @@ fn should_intercept_key(
|
||||
mods: ModifiersState,
|
||||
screenshot_ui: &ScreenshotUi,
|
||||
disable_power_key_handling: bool,
|
||||
) -> FilterResult<Option<Action>> {
|
||||
) -> FilterResult<Option<Bind>> {
|
||||
// Actions are only triggered on presses, release of the key
|
||||
// shouldn't try to intercept anything unless we have marked
|
||||
// the key to suppress.
|
||||
@ -1634,7 +1731,7 @@ fn should_intercept_key(
|
||||
return FilterResult::Forward;
|
||||
}
|
||||
|
||||
let mut final_action = action(
|
||||
let mut final_bind = find_bind(
|
||||
bindings,
|
||||
comp_mod,
|
||||
modified,
|
||||
@ -1648,21 +1745,31 @@ fn should_intercept_key(
|
||||
if screenshot_ui.is_open() {
|
||||
let mut use_screenshot_ui_action = true;
|
||||
|
||||
if let Some(action) = &final_action {
|
||||
if allowed_during_screenshot(action) {
|
||||
if let Some(bind) = &final_bind {
|
||||
if allowed_during_screenshot(&bind.action) {
|
||||
use_screenshot_ui_action = false;
|
||||
}
|
||||
}
|
||||
|
||||
if use_screenshot_ui_action {
|
||||
final_action = screenshot_ui.action(raw, mods);
|
||||
if let Some(raw) = raw {
|
||||
final_bind = screenshot_ui.action(raw, mods).map(|action| Bind {
|
||||
key: Key {
|
||||
trigger: Trigger::Keysym(raw),
|
||||
// Not entirely correct but it doesn't matter in how we currently use it.
|
||||
modifiers: Modifiers::empty(),
|
||||
},
|
||||
action,
|
||||
cooldown: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match (final_action, pressed) {
|
||||
(Some(action), true) => {
|
||||
match (final_bind, pressed) {
|
||||
(Some(bind), true) => {
|
||||
suppressed_keys.insert(key_code);
|
||||
FilterResult::Intercept(Some(action))
|
||||
FilterResult::Intercept(Some(bind))
|
||||
}
|
||||
(_, false) => {
|
||||
suppressed_keys.remove(&key_code);
|
||||
@ -1672,36 +1779,49 @@ fn should_intercept_key(
|
||||
}
|
||||
}
|
||||
|
||||
fn action(
|
||||
fn find_bind(
|
||||
bindings: &Binds,
|
||||
comp_mod: CompositorMod,
|
||||
modified: Keysym,
|
||||
raw: Option<Keysym>,
|
||||
mods: ModifiersState,
|
||||
disable_power_key_handling: bool,
|
||||
) -> Option<Action> {
|
||||
) -> Option<Bind> {
|
||||
use keysyms::*;
|
||||
|
||||
// Handle hardcoded binds.
|
||||
#[allow(non_upper_case_globals)] // wat
|
||||
match modified.raw() {
|
||||
let hardcoded_action = match modified.raw() {
|
||||
modified @ KEY_XF86Switch_VT_1..=KEY_XF86Switch_VT_12 => {
|
||||
let vt = (modified - KEY_XF86Switch_VT_1 + 1) as i32;
|
||||
return Some(Action::ChangeVt(vt));
|
||||
Some(Action::ChangeVt(vt))
|
||||
}
|
||||
KEY_XF86PowerOff if !disable_power_key_handling => return Some(Action::Suspend),
|
||||
_ => (),
|
||||
KEY_XF86PowerOff if !disable_power_key_handling => Some(Action::Suspend),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if let Some(action) = hardcoded_action {
|
||||
return Some(Bind {
|
||||
key: Key {
|
||||
// Not entirely correct but it doesn't matter in how we currently use it.
|
||||
trigger: Trigger::Keysym(modified),
|
||||
modifiers: Modifiers::empty(),
|
||||
},
|
||||
action,
|
||||
cooldown: None,
|
||||
});
|
||||
}
|
||||
|
||||
bound_action(bindings, comp_mod, raw, mods)
|
||||
let trigger = Trigger::Keysym(raw?);
|
||||
find_configured_bind(bindings, comp_mod, trigger, mods)
|
||||
}
|
||||
|
||||
fn bound_action(
|
||||
fn find_configured_bind(
|
||||
bindings: &Binds,
|
||||
comp_mod: CompositorMod,
|
||||
raw: Option<Keysym>,
|
||||
trigger: Trigger,
|
||||
mods: ModifiersState,
|
||||
) -> Option<Action> {
|
||||
) -> Option<Bind> {
|
||||
// Handle configured binds.
|
||||
let mut modifiers = Modifiers::empty();
|
||||
if mods.ctrl {
|
||||
@ -1725,10 +1845,8 @@ fn bound_action(
|
||||
modifiers |= Modifiers::COMPOSITOR;
|
||||
}
|
||||
|
||||
let raw = raw?;
|
||||
|
||||
for bind in &bindings.0 {
|
||||
if bind.key.keysym != raw {
|
||||
if bind.key.trigger != trigger {
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -1740,7 +1858,7 @@ fn bound_action(
|
||||
}
|
||||
|
||||
if bind_modifiers == modifiers {
|
||||
return Some(bind.action.clone());
|
||||
return Some(bind.clone());
|
||||
}
|
||||
}
|
||||
|
||||
@ -1899,8 +2017,6 @@ pub fn apply_libinput_settings(config: &niri_config::Input, device: &mut input::
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use niri_config::{Bind, Key};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
@ -1908,10 +2024,11 @@ mod tests {
|
||||
let close_keysym = Keysym::q;
|
||||
let bindings = Binds(vec![Bind {
|
||||
key: Key {
|
||||
keysym: close_keysym,
|
||||
trigger: Trigger::Keysym(close_keysym),
|
||||
modifiers: Modifiers::COMPOSITOR | Modifiers::CTRL,
|
||||
},
|
||||
action: Action::CloseWindow,
|
||||
cooldown: None,
|
||||
}]);
|
||||
|
||||
let comp_mod = CompositorMod::Super;
|
||||
@ -1966,7 +2083,10 @@ mod tests {
|
||||
let filter = close_key_event(&mut suppressed_keys, mods, true);
|
||||
assert!(matches!(
|
||||
filter,
|
||||
FilterResult::Intercept(Some(Action::CloseWindow))
|
||||
FilterResult::Intercept(Some(Bind {
|
||||
action: Action::CloseWindow,
|
||||
..
|
||||
}))
|
||||
));
|
||||
assert!(suppressed_keys.contains(&close_key_code));
|
||||
|
||||
@ -1997,7 +2117,10 @@ mod tests {
|
||||
let filter = close_key_event(&mut suppressed_keys, mods, true);
|
||||
assert!(matches!(
|
||||
filter,
|
||||
FilterResult::Intercept(Some(Action::CloseWindow))
|
||||
FilterResult::Intercept(Some(Bind {
|
||||
action: Action::CloseWindow,
|
||||
..
|
||||
}))
|
||||
));
|
||||
|
||||
let filter = none_key_event(&mut suppressed_keys, mods, true);
|
||||
@ -2014,7 +2137,10 @@ mod tests {
|
||||
let filter = close_key_event(&mut suppressed_keys, mods, true);
|
||||
assert!(matches!(
|
||||
filter,
|
||||
FilterResult::Intercept(Some(Action::CloseWindow))
|
||||
FilterResult::Intercept(Some(Bind {
|
||||
action: Action::CloseWindow,
|
||||
..
|
||||
}))
|
||||
));
|
||||
|
||||
mods = Default::default();
|
||||
@ -2030,90 +2156,97 @@ mod tests {
|
||||
let bindings = Binds(vec![
|
||||
Bind {
|
||||
key: Key {
|
||||
keysym: Keysym::q,
|
||||
trigger: Trigger::Keysym(Keysym::q),
|
||||
modifiers: Modifiers::COMPOSITOR,
|
||||
},
|
||||
action: Action::CloseWindow,
|
||||
cooldown: None,
|
||||
},
|
||||
Bind {
|
||||
key: Key {
|
||||
keysym: Keysym::h,
|
||||
trigger: Trigger::Keysym(Keysym::h),
|
||||
modifiers: Modifiers::SUPER,
|
||||
},
|
||||
action: Action::FocusColumnLeft,
|
||||
cooldown: None,
|
||||
},
|
||||
Bind {
|
||||
key: Key {
|
||||
keysym: Keysym::j,
|
||||
trigger: Trigger::Keysym(Keysym::j),
|
||||
modifiers: Modifiers::empty(),
|
||||
},
|
||||
action: Action::FocusWindowDown,
|
||||
cooldown: None,
|
||||
},
|
||||
Bind {
|
||||
key: Key {
|
||||
keysym: Keysym::k,
|
||||
trigger: Trigger::Keysym(Keysym::k),
|
||||
modifiers: Modifiers::COMPOSITOR | Modifiers::SUPER,
|
||||
},
|
||||
action: Action::FocusWindowUp,
|
||||
cooldown: None,
|
||||
},
|
||||
Bind {
|
||||
key: Key {
|
||||
keysym: Keysym::l,
|
||||
trigger: Trigger::Keysym(Keysym::l),
|
||||
modifiers: Modifiers::SUPER | Modifiers::ALT,
|
||||
},
|
||||
action: Action::FocusColumnRight,
|
||||
cooldown: None,
|
||||
},
|
||||
]);
|
||||
|
||||
assert_eq!(
|
||||
bound_action(
|
||||
find_configured_bind(
|
||||
&bindings,
|
||||
CompositorMod::Super,
|
||||
Some(Keysym::q),
|
||||
Trigger::Keysym(Keysym::q),
|
||||
ModifiersState {
|
||||
logo: true,
|
||||
..Default::default()
|
||||
}
|
||||
),
|
||||
Some(Action::CloseWindow)
|
||||
)
|
||||
.as_ref(),
|
||||
Some(&bindings.0[0])
|
||||
);
|
||||
assert_eq!(
|
||||
bound_action(
|
||||
find_configured_bind(
|
||||
&bindings,
|
||||
CompositorMod::Super,
|
||||
Some(Keysym::q),
|
||||
Trigger::Keysym(Keysym::q),
|
||||
ModifiersState::default(),
|
||||
),
|
||||
None,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
bound_action(
|
||||
find_configured_bind(
|
||||
&bindings,
|
||||
CompositorMod::Super,
|
||||
Some(Keysym::h),
|
||||
Trigger::Keysym(Keysym::h),
|
||||
ModifiersState {
|
||||
logo: true,
|
||||
..Default::default()
|
||||
}
|
||||
),
|
||||
Some(Action::FocusColumnLeft)
|
||||
)
|
||||
.as_ref(),
|
||||
Some(&bindings.0[1])
|
||||
);
|
||||
assert_eq!(
|
||||
bound_action(
|
||||
find_configured_bind(
|
||||
&bindings,
|
||||
CompositorMod::Super,
|
||||
Some(Keysym::h),
|
||||
Trigger::Keysym(Keysym::h),
|
||||
ModifiersState::default(),
|
||||
),
|
||||
None,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
bound_action(
|
||||
find_configured_bind(
|
||||
&bindings,
|
||||
CompositorMod::Super,
|
||||
Some(Keysym::j),
|
||||
Trigger::Keysym(Keysym::j),
|
||||
ModifiersState {
|
||||
logo: true,
|
||||
..Default::default()
|
||||
@ -2122,55 +2255,58 @@ mod tests {
|
||||
None,
|
||||
);
|
||||
assert_eq!(
|
||||
bound_action(
|
||||
find_configured_bind(
|
||||
&bindings,
|
||||
CompositorMod::Super,
|
||||
Some(Keysym::j),
|
||||
Trigger::Keysym(Keysym::j),
|
||||
ModifiersState::default(),
|
||||
),
|
||||
Some(Action::FocusWindowDown)
|
||||
)
|
||||
.as_ref(),
|
||||
Some(&bindings.0[2])
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
bound_action(
|
||||
find_configured_bind(
|
||||
&bindings,
|
||||
CompositorMod::Super,
|
||||
Some(Keysym::k),
|
||||
Trigger::Keysym(Keysym::k),
|
||||
ModifiersState {
|
||||
logo: true,
|
||||
..Default::default()
|
||||
}
|
||||
),
|
||||
Some(Action::FocusWindowUp)
|
||||
)
|
||||
.as_ref(),
|
||||
Some(&bindings.0[3])
|
||||
);
|
||||
assert_eq!(
|
||||
bound_action(
|
||||
find_configured_bind(
|
||||
&bindings,
|
||||
CompositorMod::Super,
|
||||
Some(Keysym::k),
|
||||
Trigger::Keysym(Keysym::k),
|
||||
ModifiersState::default(),
|
||||
),
|
||||
None,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
bound_action(
|
||||
find_configured_bind(
|
||||
&bindings,
|
||||
CompositorMod::Super,
|
||||
Some(Keysym::l),
|
||||
Trigger::Keysym(Keysym::l),
|
||||
ModifiersState {
|
||||
logo: true,
|
||||
alt: true,
|
||||
..Default::default()
|
||||
}
|
||||
),
|
||||
Some(Action::FocusColumnRight)
|
||||
)
|
||||
.as_ref(),
|
||||
Some(&bindings.0[4])
|
||||
);
|
||||
assert_eq!(
|
||||
bound_action(
|
||||
find_configured_bind(
|
||||
&bindings,
|
||||
CompositorMod::Super,
|
||||
Some(Keysym::l),
|
||||
Trigger::Keysym(Keysym::l),
|
||||
ModifiersState {
|
||||
logo: true,
|
||||
..Default::default()
|
||||
|
@ -19,6 +19,7 @@ pub mod rubber_band;
|
||||
pub mod swipe_tracker;
|
||||
pub mod ui;
|
||||
pub mod utils;
|
||||
pub mod wheel_tracker;
|
||||
pub mod window;
|
||||
|
||||
#[cfg(not(feature = "xdp-gnome-screencast"))]
|
||||
|
@ -11,7 +11,7 @@ use std::{env, mem, thread};
|
||||
use _server_decoration::server::org_kde_kwin_server_decoration_manager::Mode as KdeDecorationsMode;
|
||||
use anyhow::{ensure, Context};
|
||||
use calloop::futures::Scheduler;
|
||||
use niri_config::{Config, TrackLayout};
|
||||
use niri_config::{Config, Key, TrackLayout};
|
||||
use smithay::backend::allocator::Fourcc;
|
||||
use smithay::backend::renderer::element::memory::MemoryRenderBufferRenderElement;
|
||||
use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement};
|
||||
@ -114,6 +114,7 @@ use crate::utils::spawning::CHILD_ENV;
|
||||
use crate::utils::{
|
||||
center, center_f64, get_monotonic_time, make_screenshot_path, output_size, write_png_rgba8,
|
||||
};
|
||||
use crate::wheel_tracker::WheelTracker;
|
||||
use crate::window::{InitialConfigureState, Mapped, ResolvedWindowRules, Unmapped};
|
||||
use crate::{animation, niri_render_elements};
|
||||
|
||||
@ -193,6 +194,7 @@ pub struct Niri {
|
||||
pub seat: Seat<State>,
|
||||
/// Scancodes of the keys to suppress.
|
||||
pub suppressed_keys: HashSet<u32>,
|
||||
pub bind_cooldown_timers: HashMap<Key, RegistrationToken>,
|
||||
pub keyboard_focus: KeyboardFocus,
|
||||
pub idle_inhibiting_surfaces: HashSet<WlSurface>,
|
||||
pub is_fdo_idle_inhibited: Arc<AtomicBool>,
|
||||
@ -204,6 +206,8 @@ pub struct Niri {
|
||||
pub pointer_focus: PointerFocus,
|
||||
pub tablet_cursor_location: Option<Point<f64, Logical>>,
|
||||
pub gesture_swipe_3f_cumulative: Option<(f64, f64)>,
|
||||
pub vertical_wheel_tracker: WheelTracker,
|
||||
pub horizontal_wheel_tracker: WheelTracker,
|
||||
|
||||
pub lock_state: LockState,
|
||||
|
||||
@ -1248,6 +1252,7 @@ impl Niri {
|
||||
popups: PopupManager::default(),
|
||||
popup_grab: None,
|
||||
suppressed_keys: HashSet::new(),
|
||||
bind_cooldown_timers: HashMap::new(),
|
||||
presentation_state,
|
||||
security_context_state,
|
||||
gamma_control_manager_state,
|
||||
@ -1263,6 +1268,8 @@ impl Niri {
|
||||
pointer_focus: PointerFocus::default(),
|
||||
tablet_cursor_location: None,
|
||||
gesture_swipe_3f_cumulative: None,
|
||||
vertical_wheel_tracker: WheelTracker::new(),
|
||||
horizontal_wheel_tracker: WheelTracker::new(),
|
||||
|
||||
lock_state: LockState::Unlocked,
|
||||
|
||||
|
@ -177,21 +177,24 @@ where
|
||||
}
|
||||
|
||||
// Verify that there's no more data.
|
||||
match file.read(&mut [0]) {
|
||||
Ok(0) => (),
|
||||
Ok(_) => {
|
||||
warn!("gamma data is too large");
|
||||
resource.failed();
|
||||
gamma_controls.remove(&output);
|
||||
let _ = state.set_gamma(&output, None);
|
||||
return;
|
||||
}
|
||||
Err(err) => {
|
||||
warn!("error reading gamma data: {err:?}");
|
||||
resource.failed();
|
||||
gamma_controls.remove(&output);
|
||||
let _ = state.set_gamma(&output, None);
|
||||
return;
|
||||
#[allow(clippy::unused_io_amount)] // False positive on 1.77.0
|
||||
{
|
||||
match file.read(&mut [0]) {
|
||||
Ok(0) => (),
|
||||
Ok(_) => {
|
||||
warn!("gamma data is too large");
|
||||
resource.failed();
|
||||
gamma_controls.remove(&output);
|
||||
let _ = state.set_gamma(&output, None);
|
||||
return;
|
||||
}
|
||||
Err(err) => {
|
||||
warn!("error reading gamma data: {err:?}");
|
||||
resource.failed();
|
||||
gamma_controls.remove(&output);
|
||||
let _ = state.set_gamma(&output, None);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ use std::collections::HashMap;
|
||||
use std::iter::zip;
|
||||
use std::rc::Rc;
|
||||
|
||||
use niri_config::{Action, Config, Key, Modifiers};
|
||||
use niri_config::{Action, Config, Key, Modifiers, Trigger};
|
||||
use pangocairo::cairo::{self, ImageSurface};
|
||||
use pangocairo::pango::{AttrColor, AttrInt, AttrList, AttrString, FontDescription, Weight};
|
||||
use smithay::backend::renderer::element::memory::{
|
||||
@ -414,7 +414,15 @@ fn key_name(comp_mod: CompositorMod, key: &Key) -> String {
|
||||
if key.modifiers.contains(Modifiers::CTRL) {
|
||||
name.push_str("Ctrl + ");
|
||||
}
|
||||
name.push_str(&prettify_keysym_name(&keysym_get_name(key.keysym)));
|
||||
|
||||
let pretty = match key.trigger {
|
||||
Trigger::Keysym(keysym) => prettify_keysym_name(&keysym_get_name(keysym)),
|
||||
Trigger::WheelDown => String::from("Wheel Down"),
|
||||
Trigger::WheelUp => String::from("Wheel Up"),
|
||||
Trigger::WheelLeft => String::from("Wheel Left"),
|
||||
Trigger::WheelRight => String::from("Wheel Right"),
|
||||
};
|
||||
name.push_str(&pretty);
|
||||
|
||||
name
|
||||
}
|
||||
|
@ -316,12 +316,12 @@ impl ScreenshotUi {
|
||||
Ok((rect.size, copy.to_vec()))
|
||||
}
|
||||
|
||||
pub fn action(&self, raw: Option<Keysym>, mods: ModifiersState) -> Option<Action> {
|
||||
pub fn action(&self, raw: Keysym, mods: ModifiersState) -> Option<Action> {
|
||||
if !matches!(self, Self::Open { .. }) {
|
||||
return None;
|
||||
}
|
||||
|
||||
action(raw?, mods)
|
||||
action(raw, mods)
|
||||
}
|
||||
|
||||
pub fn selection_output(&self) -> Option<&Output> {
|
||||
|
36
src/wheel_tracker.rs
Normal file
36
src/wheel_tracker.rs
Normal file
@ -0,0 +1,36 @@
|
||||
pub struct WheelTracker {
|
||||
last: f64,
|
||||
acc: f64,
|
||||
}
|
||||
|
||||
impl WheelTracker {
|
||||
#[allow(clippy::new_without_default)]
|
||||
pub fn new() -> Self {
|
||||
Self { last: 0., acc: 0. }
|
||||
}
|
||||
|
||||
pub fn accumulate(&mut self, amount_v120: f64) -> i8 {
|
||||
let changed_direction =
|
||||
(self.last > 0. && amount_v120 < 0.) || (self.last < 0. && amount_v120 > 0.);
|
||||
if changed_direction {
|
||||
self.acc = 0.
|
||||
}
|
||||
|
||||
self.last = amount_v120;
|
||||
self.acc += amount_v120;
|
||||
|
||||
let mut ticks = 0;
|
||||
if self.acc.abs() >= 120. {
|
||||
let clamped = self.acc.clamp(-127. * 120., 127. * 120.);
|
||||
ticks = (clamped as i16 / 120) as i8;
|
||||
self.acc %= 120.;
|
||||
}
|
||||
|
||||
ticks
|
||||
}
|
||||
|
||||
pub fn reset(&mut self) {
|
||||
self.last = 0.;
|
||||
self.acc = 0.;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user