Add mouse window controls and finer display scaling (#63)
Add configurable mouse and wheel bindings, window move and resize, tab drops, cross-output dragging, and device settings. Adjust display scaling in 0.05 increments. Preserve opposite-edge positioning during scrolling resizes and show rows for edge drops into tabbed columns.
This commit is contained in:
1 parent
ab11269246
commit
ad00e55fe4
35 files changed
+3454
-512
No files matched your search
@@ -7,9 +7,11 @@ use super::{
|
||||
|
||||
mod index;
|
||||
mod modes;
|
||||
mod pointer;
|
||||
pub(crate) use index::BindingSet;
|
||||
pub(super) use modes::BindingModeConfig;
|
||||
pub(crate) use modes::BindingModes;
|
||||
pub(crate) use pointer::{PointerTrigger, ScrollDirection};
|
||||
|
||||
impl Config {
|
||||
fn binding_commands(&self) -> HashMap<String, Vec<String>> {
|
||||
@@ -120,6 +122,7 @@ pub(super) fn parse_binding(
|
||||
let trigger = match identity.match_mode {
|
||||
BindingMatch::Keysym => BindingTrigger::Keysym(identity.key),
|
||||
BindingMatch::Physical => BindingTrigger::Physical(Keycode::new(identity.key)),
|
||||
BindingMatch::Pointer => BindingTrigger::Pointer(PointerTrigger::from_key(identity.key)),
|
||||
BindingMatch::Swipe => BindingTrigger::Swipe {
|
||||
fingers: identity.key / 4,
|
||||
direction: [
|
||||
@@ -131,6 +134,15 @@ pub(super) fn parse_binding(
|
||||
},
|
||||
};
|
||||
|
||||
if matches!(
|
||||
action,
|
||||
BindingAction::PointerMove | BindingAction::PointerResize
|
||||
) && !matches!(trigger, BindingTrigger::Pointer(PointerTrigger::Button(_)))
|
||||
{
|
||||
return Err(ConfigError::Binding(
|
||||
"pointer-move and pointer-resize require a mouse button".into(),
|
||||
));
|
||||
}
|
||||
Ok(Binding {
|
||||
modifiers: identity.modifiers,
|
||||
trigger,
|
||||
@@ -143,6 +155,18 @@ pub(super) fn binding_identity(
|
||||
keymap: &xkb::Keymap,
|
||||
) -> Result<BindingIdentity, ConfigError> {
|
||||
let (modifiers, key) = parse_chord(&configured.keys)?;
|
||||
if let Some(trigger) = PointerTrigger::parse(&key)? {
|
||||
if configured.match_mode != BindingMatch::Keysym {
|
||||
return Err(ConfigError::Binding(
|
||||
"mouse bindings cannot use physical key matching".into(),
|
||||
));
|
||||
}
|
||||
return Ok(BindingIdentity {
|
||||
modifiers,
|
||||
match_mode: BindingMatch::Pointer,
|
||||
key: trigger.key(),
|
||||
});
|
||||
}
|
||||
if let Some((fingers, direction)) = parse_swipe(&key) {
|
||||
if configured.match_mode == BindingMatch::Physical
|
||||
|| modifiers != BindingModifiers::default()
|
||||
@@ -168,7 +192,7 @@ pub(super) fn binding_identity(
|
||||
configured.keys
|
||||
))
|
||||
})?,
|
||||
BindingMatch::Swipe => {
|
||||
BindingMatch::Swipe | BindingMatch::Pointer => {
|
||||
return Err(ConfigError::Binding(
|
||||
"gesture keys must use Swipe3Left/Right/Up/Down (3–5 fingers)".into(),
|
||||
));
|
||||
@@ -186,6 +210,7 @@ pub(super) fn binding_identity_for_runtime(binding: &Binding) -> BindingIdentity
|
||||
let (match_mode, key) = match binding.trigger {
|
||||
BindingTrigger::Keysym(key) => (BindingMatch::Keysym, key),
|
||||
BindingTrigger::Physical(key) => (BindingMatch::Physical, key.raw()),
|
||||
BindingTrigger::Pointer(trigger) => (BindingMatch::Pointer, trigger.key()),
|
||||
BindingTrigger::Swipe { fingers, direction } => {
|
||||
(BindingMatch::Swipe, fingers * 4 + direction as u32)
|
||||
}
|
||||
@@ -292,6 +317,14 @@ pub(super) fn parse_action(
|
||||
};
|
||||
|
||||
match action {
|
||||
"pointer-move" | "pointer-resize" => {
|
||||
no_argument()?;
|
||||
Ok(if action == "pointer-move" {
|
||||
BindingAction::PointerMove
|
||||
} else {
|
||||
BindingAction::PointerResize
|
||||
})
|
||||
}
|
||||
"binding-mode" => Ok(BindingAction::BindingMode(required_argument()?.to_owned())),
|
||||
"none" => {
|
||||
no_argument()?;
|
||||
@@ -447,6 +480,8 @@ pub(super) fn parse_workspace(argument: &str) -> Result<u8, ConfigError> {
|
||||
|
||||
pub(super) fn default_bindings() -> Vec<BindingConfig> {
|
||||
let mut bindings = vec![
|
||||
binding("Super+MouseLeft", "pointer-move", None),
|
||||
binding("Super+MouseRight", "pointer-resize", None),
|
||||
binding("Super+Space", "toggle-launcher", None),
|
||||
binding("Super+Enter", "spawn", Some("terminal")),
|
||||
binding("Super+Shift+S", "spawn", Some("screenshot")),
|
||||
@@ -538,6 +573,7 @@ pub struct Binding {
|
||||
pub(super) enum BindingTrigger {
|
||||
Keysym(u32),
|
||||
Physical(Keycode),
|
||||
Pointer(PointerTrigger),
|
||||
Swipe {
|
||||
fingers: u32,
|
||||
direction: crate::gestures::SwipeDirection,
|
||||
@@ -563,6 +599,8 @@ pub enum BindingAction {
|
||||
FocusLastWindow,
|
||||
FocusFloating,
|
||||
FocusMru(bool),
|
||||
PointerMove,
|
||||
PointerResize,
|
||||
Move(ferese_layout::Direction),
|
||||
Resize(ferese_layout::Direction),
|
||||
SwitchWorkspace(u8),
|
||||
@@ -664,7 +702,7 @@ impl Binding {
|
||||
&& match self.trigger {
|
||||
BindingTrigger::Keysym(expected) => keysyms.contains(&expected),
|
||||
BindingTrigger::Physical(expected) => expected == keycode,
|
||||
BindingTrigger::Swipe { .. } => false,
|
||||
BindingTrigger::Swipe { .. } | BindingTrigger::Pointer(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -704,6 +742,8 @@ impl Binding {
|
||||
"Next window in focus history"
|
||||
}
|
||||
.into(),
|
||||
BindingAction::PointerMove => "Drag window".into(),
|
||||
BindingAction::PointerResize => "Resize window with pointer".into(),
|
||||
BindingAction::Move(direction) => format!("Move window {direction:?}"),
|
||||
BindingAction::Resize(direction) => format!("Resize window {direction:?}"),
|
||||
BindingAction::SwitchWorkspace(index) => format!("Go to workspace {index}"),
|
||||
@@ -757,6 +797,7 @@ impl Binding {
|
||||
}
|
||||
}
|
||||
parts.push(match self.trigger {
|
||||
BindingTrigger::Pointer(trigger) => trigger.label(),
|
||||
BindingTrigger::Keysym(symbol) => {
|
||||
let name = xkb::keysym_get_name(xkb::Keysym::new(symbol));
|
||||
match name.as_str() {
|
||||
@@ -798,6 +839,8 @@ pub(super) enum BindingMatch {
|
||||
Keysym,
|
||||
Physical,
|
||||
Swipe,
|
||||
#[serde(skip)]
|
||||
Pointer,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
|
||||
@@ -10,27 +10,30 @@ use super::{
|
||||
binding_identity_for_runtime,
|
||||
};
|
||||
|
||||
/// Immutable bindings and their keyboard index are replaced together on reload.
|
||||
/// Immutable bindings and their lookup index are replaced together on reload.
|
||||
/// The ordered slice remains available for gestures and the shortcut guide.
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct BindingSet {
|
||||
ordered: Vec<Binding>,
|
||||
keyboard: HashMap<BindingIdentity, usize>,
|
||||
index: HashMap<BindingIdentity, usize>,
|
||||
}
|
||||
|
||||
impl From<Vec<Binding>> for BindingSet {
|
||||
fn from(ordered: Vec<Binding>) -> Self {
|
||||
let mut keyboard = HashMap::with_capacity(ordered.len());
|
||||
let mut index = HashMap::with_capacity(ordered.len());
|
||||
|
||||
for (index, binding) in ordered.iter().enumerate() {
|
||||
for (position, binding) in ordered.iter().enumerate() {
|
||||
let identity = binding_identity_for_runtime(binding);
|
||||
|
||||
if identity.match_mode != BindingMatch::Swipe {
|
||||
keyboard.entry(identity).or_insert(index);
|
||||
if matches!(
|
||||
identity.match_mode,
|
||||
BindingMatch::Keysym | BindingMatch::Physical | BindingMatch::Pointer
|
||||
) {
|
||||
index.entry(identity).or_insert(position);
|
||||
}
|
||||
}
|
||||
|
||||
Self { ordered, keyboard }
|
||||
Self { ordered, index }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +46,26 @@ impl std::ops::Deref for BindingSet {
|
||||
}
|
||||
|
||||
impl BindingSet {
|
||||
pub(crate) fn binding_for_pointer(
|
||||
&self,
|
||||
trigger: super::PointerTrigger,
|
||||
modifiers: smithay::input::keyboard::ModifiersState,
|
||||
) -> Option<&Binding> {
|
||||
let modifiers = BindingModifiers {
|
||||
logo: modifiers.logo,
|
||||
ctrl: modifiers.ctrl,
|
||||
alt: modifiers.alt,
|
||||
shift: modifiers.shift,
|
||||
};
|
||||
self.index
|
||||
.get(&BindingIdentity {
|
||||
modifiers,
|
||||
match_mode: BindingMatch::Pointer,
|
||||
key: trigger.key(),
|
||||
})
|
||||
.map(|index| &self.ordered[*index])
|
||||
}
|
||||
|
||||
pub(crate) fn binding_for_key(
|
||||
&self,
|
||||
keycode: Keycode,
|
||||
@@ -58,13 +81,13 @@ impl BindingSet {
|
||||
alt,
|
||||
shift,
|
||||
};
|
||||
let physical = self.keyboard.get(&BindingIdentity {
|
||||
let physical = self.index.get(&BindingIdentity {
|
||||
modifiers,
|
||||
match_mode: BindingMatch::Physical,
|
||||
key: keycode.raw(),
|
||||
});
|
||||
let symbolic = symbols.iter().filter_map(|&key| {
|
||||
self.keyboard.get(&BindingIdentity {
|
||||
self.index.get(&BindingIdentity {
|
||||
modifiers,
|
||||
match_mode: BindingMatch::Keysym,
|
||||
key,
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
use super::ConfigError;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[repr(u32)]
|
||||
pub(crate) enum ScrollDirection {
|
||||
Left,
|
||||
Right,
|
||||
Up,
|
||||
Down,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum PointerTrigger {
|
||||
Button(u32),
|
||||
Wheel(ScrollDirection),
|
||||
Touchpad(ScrollDirection),
|
||||
}
|
||||
|
||||
impl PointerTrigger {
|
||||
pub(super) fn parse(name: &str) -> Result<Option<Self>, ConfigError> {
|
||||
let name = name.to_ascii_lowercase();
|
||||
let button = match name.as_str() {
|
||||
"mouseleft" => Some(272),
|
||||
"mouseright" => Some(273),
|
||||
"mousemiddle" => Some(274),
|
||||
"mouseside" | "mouseback" => Some(275),
|
||||
"mouseextra" | "mouseforward" => Some(276),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(button) = button {
|
||||
return Ok(Some(Self::Button(button)));
|
||||
}
|
||||
if let Some(code) = name.strip_prefix("mouse:") {
|
||||
let code = code
|
||||
.parse::<u32>()
|
||||
.ok()
|
||||
.filter(|code| (0x110..=0x11f).contains(code))
|
||||
.ok_or_else(|| {
|
||||
ConfigError::Binding(
|
||||
"mouse button must be a Linux mouse button code (272–287)".into(),
|
||||
)
|
||||
})?;
|
||||
return Ok(Some(Self::Button(code)));
|
||||
}
|
||||
for (prefix, touchpad) in [
|
||||
("wheelscroll", false),
|
||||
("wheel", false),
|
||||
("touchpadscroll", true),
|
||||
] {
|
||||
if let Some(direction) = name.strip_prefix(prefix) {
|
||||
let direction = match direction {
|
||||
"left" => ScrollDirection::Left,
|
||||
"right" => ScrollDirection::Right,
|
||||
"up" => ScrollDirection::Up,
|
||||
"down" => ScrollDirection::Down,
|
||||
_ => {
|
||||
return Err(ConfigError::Binding(format!(
|
||||
"invalid scroll direction {direction:?}"
|
||||
)));
|
||||
}
|
||||
};
|
||||
return Ok(Some(if touchpad {
|
||||
Self::Touchpad(direction)
|
||||
} else {
|
||||
Self::Wheel(direction)
|
||||
}));
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
pub(super) fn key(self) -> u32 {
|
||||
match self {
|
||||
Self::Button(code) => code,
|
||||
Self::Wheel(direction) => 1024 + direction as u32,
|
||||
Self::Touchpad(direction) => 1028 + direction as u32,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn from_key(key: u32) -> Self {
|
||||
let directions = [
|
||||
ScrollDirection::Left,
|
||||
ScrollDirection::Right,
|
||||
ScrollDirection::Up,
|
||||
ScrollDirection::Down,
|
||||
];
|
||||
match key {
|
||||
1024..=1027 => Self::Wheel(directions[(key - 1024) as usize]),
|
||||
1028..=1031 => Self::Touchpad(directions[(key - 1028) as usize]),
|
||||
code => Self::Button(code),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn label(self) -> String {
|
||||
match self {
|
||||
Self::Button(272) => "Mouse Left".into(),
|
||||
Self::Button(273) => "Mouse Right".into(),
|
||||
Self::Button(274) => "Mouse Middle".into(),
|
||||
Self::Button(275) => "Mouse Back".into(),
|
||||
Self::Button(276) => "Mouse Forward".into(),
|
||||
Self::Button(code) => format!("Mouse {code}"),
|
||||
Self::Wheel(direction) => format!("Wheel {direction:?}"),
|
||||
Self::Touchpad(direction) => format!("Touchpad scroll {direction:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::config::{BindingAction, Config};
|
||||
|
||||
#[test]
|
||||
fn pointer_names_round_trip_without_colliding_with_keyboard_keys() {
|
||||
for name in [
|
||||
"MouseLeft",
|
||||
"MouseBack",
|
||||
"MouseForward",
|
||||
"mouse:287",
|
||||
"WheelUp",
|
||||
"WheelScrollLeft",
|
||||
"TouchpadScrollDown",
|
||||
] {
|
||||
let trigger = PointerTrigger::parse(name).unwrap().unwrap();
|
||||
assert_eq!(PointerTrigger::from_key(trigger.key()), trigger);
|
||||
}
|
||||
for name in ["mouse:12", "mouse:288", "mouse:foo", "WheelSideways"] {
|
||||
assert!(PointerTrigger::parse(name).is_err(), "{name}");
|
||||
}
|
||||
assert_eq!(PointerTrigger::parse("Return").unwrap(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pointer_defaults_can_be_overridden_and_disabled() {
|
||||
let config = Config::parse_source(
|
||||
r#"
|
||||
binding "Super+MouseLeft" action="close"
|
||||
binding "Super+MouseRight" disabled=#true
|
||||
binding "Alt+MouseMiddle" action="pointer-resize"
|
||||
binding "Super+WheelDown" action="workspace-next"
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let bindings = config.bindings(&config.input_settings().unwrap()).unwrap();
|
||||
assert!(
|
||||
!bindings
|
||||
.iter()
|
||||
.any(|binding| binding.action == BindingAction::PointerMove)
|
||||
);
|
||||
assert_eq!(
|
||||
bindings
|
||||
.iter()
|
||||
.filter(|binding| binding.action == BindingAction::PointerResize)
|
||||
.count(),
|
||||
1
|
||||
);
|
||||
let config =
|
||||
Config::parse_source(r#"binding "Super+WheelUp" action="pointer-move""#).unwrap();
|
||||
assert!(config.bindings(&config.input_settings().unwrap()).is_err());
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,44 @@ impl Config {
|
||||
value: self.input.touchpad.swipe_threshold.to_string(),
|
||||
});
|
||||
}
|
||||
let mouse = self.input.mouse;
|
||||
for (field, value, valid) in [
|
||||
(
|
||||
"touchpad.accel-speed",
|
||||
self.input.touchpad.accel_speed,
|
||||
(-1.0..=1.0).contains(&self.input.touchpad.accel_speed),
|
||||
),
|
||||
(
|
||||
"mouse.accel-speed",
|
||||
mouse.accel_speed,
|
||||
(-1.0..=1.0).contains(&mouse.accel_speed),
|
||||
),
|
||||
(
|
||||
"mouse.scroll-factor",
|
||||
mouse.scroll_factor,
|
||||
(0.01..=100.0).contains(&mouse.scroll_factor),
|
||||
),
|
||||
(
|
||||
"mouse.drag-threshold",
|
||||
mouse.drag_threshold,
|
||||
(0.0..=100.0).contains(&mouse.drag_threshold),
|
||||
),
|
||||
] {
|
||||
if !value.is_finite() || !valid {
|
||||
return Err(ConfigError::InputValue {
|
||||
field,
|
||||
value: value.to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
if mouse.scroll_cooldown_ms > 5000 {
|
||||
return Err(ConfigError::InputValue {
|
||||
field: "mouse.scroll-cooldown-ms",
|
||||
value: mouse.scroll_cooldown_ms.to_string(),
|
||||
});
|
||||
}
|
||||
Ok(InputSettings {
|
||||
mouse,
|
||||
focus_follows_mouse: self.input.focus_follows_mouse,
|
||||
xkb_layout: self.input.xkb_layout.clone(),
|
||||
xkb_variant: self.input.xkb_variant.clone(),
|
||||
@@ -39,6 +76,8 @@ impl Config {
|
||||
repeat_delay_ms: self.input.repeat_delay_ms,
|
||||
touchpad: TouchpadSettings {
|
||||
tap: self.input.touchpad.tap,
|
||||
accel_speed: self.input.touchpad.accel_speed,
|
||||
disable_while_mouse_connected: self.input.touchpad.disable_while_mouse_connected,
|
||||
natural_scroll: self.input.touchpad.natural_scroll,
|
||||
disable_while_typing: self.input.touchpad.disable_while_typing,
|
||||
swipe_threshold: self.input.touchpad.swipe_threshold,
|
||||
@@ -85,11 +124,14 @@ pub struct InputSettings {
|
||||
pub repeat_rate: i32,
|
||||
pub repeat_delay_ms: i32,
|
||||
pub touchpad: TouchpadSettings,
|
||||
pub mouse: MouseSettings,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct TouchpadSettings {
|
||||
pub tap: bool,
|
||||
pub accel_speed: f64,
|
||||
pub disable_while_mouse_connected: bool,
|
||||
pub natural_scroll: bool,
|
||||
pub disable_while_typing: bool,
|
||||
pub swipe_threshold: u16,
|
||||
@@ -115,6 +157,8 @@ pub(super) struct InputConfig {
|
||||
pub(super) repeat_delay_ms: i32,
|
||||
#[serde(default)]
|
||||
pub(super) touchpad: TouchpadConfig,
|
||||
#[serde(default)]
|
||||
pub(super) mouse: MouseSettings,
|
||||
}
|
||||
|
||||
impl Default for InputConfig {
|
||||
@@ -127,14 +171,19 @@ impl Default for InputConfig {
|
||||
repeat_rate: default_repeat_rate(),
|
||||
repeat_delay_ms: default_repeat_delay(),
|
||||
touchpad: TouchpadConfig::default(),
|
||||
mouse: MouseSettings::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(super) struct TouchpadConfig {
|
||||
#[serde(default)]
|
||||
pub(super) accel_speed: f64,
|
||||
#[serde(default = "enabled_by_default")]
|
||||
pub(super) tap: bool,
|
||||
#[serde(default)]
|
||||
pub(super) disable_while_mouse_connected: bool,
|
||||
#[serde(default = "enabled_by_default")]
|
||||
pub(super) natural_scroll: bool,
|
||||
#[serde(default = "enabled_by_default")]
|
||||
@@ -147,6 +196,8 @@ impl Default for TouchpadConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
tap: true,
|
||||
accel_speed: 0.0,
|
||||
disable_while_mouse_connected: false,
|
||||
natural_scroll: true,
|
||||
disable_while_typing: true,
|
||||
swipe_threshold: default_swipe_threshold(),
|
||||
@@ -160,6 +211,42 @@ pub(super) type AnimationsConfig = ferese_animation::MotionSettings;
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn mouse_settings_are_separate_from_touchpad_and_validate_ranges() {
|
||||
let defaults = Config::parse_source("").unwrap().input_settings().unwrap();
|
||||
assert!(!defaults.mouse.natural_scroll);
|
||||
assert!(defaults.touchpad.natural_scroll);
|
||||
assert!(!defaults.touchpad.disable_while_mouse_connected);
|
||||
let config = Config::parse_source(r#"
|
||||
input {
|
||||
mouse { accel-speed -0.4; accel-profile "flat"; scroll-factor 0.5; left-handed #true; }
|
||||
touchpad { disable-while-mouse-connected #true; accel-speed 0.35; }
|
||||
}
|
||||
"#).unwrap().input_settings().unwrap();
|
||||
assert!(config.touchpad.disable_while_mouse_connected);
|
||||
assert_eq!(config.touchpad.accel_speed, 0.35);
|
||||
assert_eq!(config.mouse.accel_profile, Some(AccelerationProfile::Flat));
|
||||
assert_eq!(config.mouse.accel_speed, -0.4);
|
||||
assert_eq!(config.mouse.scroll_factor, 0.5);
|
||||
assert!(config.mouse.left_handed);
|
||||
for speed in [-1.01, 1.01] {
|
||||
let config =
|
||||
Config::parse_source(&format!("input {{ touchpad {{ accel-speed {speed}; }} }}"))
|
||||
.unwrap();
|
||||
assert!(config.input_settings().is_err());
|
||||
}
|
||||
for (key, value) in [
|
||||
("accel-speed", "1.1"),
|
||||
("scroll-factor", "0"),
|
||||
("drag-threshold", "-1"),
|
||||
("scroll-cooldown-ms", "5001"),
|
||||
] {
|
||||
let config =
|
||||
Config::parse_source(&format!("input {{ mouse {{ {key} {value}; }} }}")).unwrap();
|
||||
assert!(config.input_settings().is_err(), "{key}={value}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compositor_speed_uses_shared_motion_bounds() {
|
||||
for speed in [1e-100, 0.009, 101.0, 1e100] {
|
||||
@@ -169,3 +256,38 @@ mod tests {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum AccelerationProfile {
|
||||
Adaptive,
|
||||
Flat,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, PartialEq)]
|
||||
#[serde(default, deny_unknown_fields)]
|
||||
pub struct MouseSettings {
|
||||
pub natural_scroll: bool,
|
||||
pub accel_speed: f64,
|
||||
pub accel_profile: Option<AccelerationProfile>,
|
||||
pub left_handed: bool,
|
||||
pub middle_emulation: bool,
|
||||
pub scroll_factor: f64,
|
||||
pub drag_threshold: f64,
|
||||
pub scroll_cooldown_ms: u64,
|
||||
}
|
||||
|
||||
impl Default for MouseSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
natural_scroll: false,
|
||||
accel_speed: 0.0,
|
||||
accel_profile: None,
|
||||
left_handed: false,
|
||||
middle_emulation: false,
|
||||
scroll_factor: 1.0,
|
||||
drag_threshold: 4.0,
|
||||
scroll_cooldown_ms: 150,
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user