Commit 9944e8618a62
tarball8 files changed+422-2
@@ -544,6 +544,7 @@ name = "monolith"
544544
version = "0.0.0"545545
dependencies = [546546
"calloop-wayland-source",547
+ "input",547548
"libc",548549
"monolith-config",549550
"monolith-ipc",@@ -54,6 +54,33 @@ layout = ""
5454
repeat_delay = 2005555
repeat_rate = 255656
57
+# libinput settings, bare metal only. Anything left out keeps the device's58
+# own default, and options a device lacks are skipped. Touchpads are devices59
+# that can tap, every other pointing device takes [input.pointer].60
+#61
+# accel_profile flat, adaptive or custom62
+# accel_speed -1 to 163
+# accel_points the custom profile's curve, pointer speed at 0, accel_step,64
+# accel_step 2 * accel_step and so on, in device units per millisecond65
+# natural_scroll, left_handed, tap, tap_drag, tap_drag_lock, dwt (disable66
+# while typing), middle_emulation true or false67
+# scroll_method none, two-finger, edge or on-button-down68
+# scroll_button the evdev code of the on-button-down button, 274 for middle69
+# click_method button-areas or clickfinger70
+# scroll_factor multiplies scroll distance, applied by monolith71
+[input.pointer]72
+73
+[input.touchpad]74
+# tap = true75
+# natural_scroll = true76
+77
+# Per device, by the name `libinput list-devices` shows, over its kind's settings.78
+# Later entries win.79
+# [[input.device]]80
+# name = "Logitech G502"81
+# accel_profile = "flat"82
+# accel_speed = -0.383
+5784
# Each value is a command in word form, or an argv-style list.5885
[binds]5986
"Mod+Return" = "spawn beer"@@ -16,6 +16,7 @@ pub struct Config {
1616
pub decoration: Decoration,1717
pub animation: Animation,1818
pub keyboard: Keyboard,19
+ pub input: Input,1920
pub binds: Vec<Bind>,2021
pub rules: Vec<Rule>,2122
pub startup: Vec<Vec<String>>,@@ -260,6 +261,122 @@ impl Default for Keyboard {
260261
}261262
}262263
264
+/// libinput settings for pointing devices. Unset options keep libinput's265
+/// default for the device.266
+#[derive(Debug, Clone, PartialEq, Default)]267
+pub struct Input {268
+ pub pointer: DeviceSettings,269
+ /// Devices that can tap, which libinput only offers on touchpads.270
+ pub touchpad: DeviceSettings,271
+ /// Overrides for devices by name, later entries winning.272
+ pub device: Vec<NamedDevice>,273
+}274
+275
+#[derive(Debug, Clone, PartialEq)]276
+pub struct NamedDevice {277
+ pub name: String,278
+ pub settings: DeviceSettings,279
+}280
+281
+#[derive(Deserialize, Debug, Clone, PartialEq, Default)]282
+#[serde(default, deny_unknown_fields)]283
+pub struct DeviceSettings {284
+ pub accel_profile: Option<AccelProfile>,285
+ pub accel_speed: Option<f64>,286
+ /// The custom profile's curve, pointer speed sampled every `accel_step`287
+ /// device units per millisecond.288
+ pub accel_points: Option<Vec<f64>>,289
+ pub accel_step: Option<f64>,290
+ pub natural_scroll: Option<bool>,291
+ pub left_handed: Option<bool>,292
+ pub tap: Option<bool>,293
+ pub tap_drag: Option<bool>,294
+ pub tap_drag_lock: Option<bool>,295
+ pub dwt: Option<bool>,296
+ pub middle_emulation: Option<bool>,297
+ pub scroll_method: Option<ScrollMethod>,298
+ pub scroll_button: Option<u32>,299
+ pub click_method: Option<ClickMethod>,300
+ /// Applied by the compositor, libinput has no such setting.301
+ pub scroll_factor: Option<f64>,302
+}303
+304
+impl DeviceSettings {305
+ /// `self` with every option `over` sets replaced.306
+ pub fn overlay(&self, over: &Self) -> Self {307
+ Self {308
+ accel_profile: over.accel_profile.or(self.accel_profile),309
+ accel_speed: over.accel_speed.or(self.accel_speed),310
+ accel_points: over311
+ .accel_points312
+ .clone()313
+ .or_else(|| self.accel_points.clone()),314
+ accel_step: over.accel_step.or(self.accel_step),315
+ natural_scroll: over.natural_scroll.or(self.natural_scroll),316
+ left_handed: over.left_handed.or(self.left_handed),317
+ tap: over.tap.or(self.tap),318
+ tap_drag: over.tap_drag.or(self.tap_drag),319
+ tap_drag_lock: over.tap_drag_lock.or(self.tap_drag_lock),320
+ dwt: over.dwt.or(self.dwt),321
+ middle_emulation: over.middle_emulation.or(self.middle_emulation),322
+ scroll_method: over.scroll_method.or(self.scroll_method),323
+ scroll_button: over.scroll_button.or(self.scroll_button),324
+ click_method: over.click_method.or(self.click_method),325
+ scroll_factor: over.scroll_factor.or(self.scroll_factor),326
+ }327
+ }328
+329
+ fn validate(&mut self, name: &str, w: &mut Vec<String>) {330
+ if let Some(s) = self.accel_speed331
+ && !(-1.0..=1.0).contains(&s)332
+ {333
+ w.push(format!(334
+ "{name}.accel_speed: {s} is outside -1 to 1, ignored"335
+ ));336
+ self.accel_speed = None;337
+ }338
+ if let Some(f) = self.scroll_factor339
+ && !(f.is_finite() && f >= 0.0)340
+ {341
+ w.push(format!(342
+ "{name}.scroll_factor: {f} must be zero or more, ignored"343
+ ));344
+ self.scroll_factor = None;345
+ }346
+ if self.accel_points.is_some() != self.accel_step.is_some() {347
+ w.push(format!(348
+ "{name}: accel_points needs accel_step and the other way round, both ignored"349
+ ));350
+ self.accel_points = None;351
+ self.accel_step = None;352
+ }353
+ }354
+}355
+356
+#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq)]357
+#[serde(rename_all = "lowercase")]358
+pub enum AccelProfile {359
+ Flat,360
+ Adaptive,361
+ Custom,362
+}363
+364
+#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq)]365
+#[serde(rename_all = "kebab-case")]366
+pub enum ScrollMethod {367
+ None,368
+ TwoFinger,369
+ Edge,370
+ OnButtonDown,371
+}372
+373
+#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq)]374
+#[serde(rename_all = "kebab-case")]375
+pub enum ClickMethod {376
+ ButtonAreas,377
+ Clickfinger,378
+}379
+263380
#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq)]264381
#[serde(rename_all = "lowercase")]265382
pub enum Modifier {@@ -394,6 +511,7 @@ impl Config {
394511
let w = &mut warnings;395512
let general: General = section("general", table.remove("general"), w);396513
let keyboard = section("keyboard", table.remove("keyboard"), w);514
+ let input = input(table.remove("input"), w);397515
let animation = section("animation", table.remove("animation"), w);398516
let decoration = section("decoration", table.remove("decoration"), w);399517
let output: Outputs = section("output", table.remove("output"), w);@@ -452,6 +570,7 @@ impl Config {
452570
decoration,453571
output,454572
keyboard,573
+ input,455574
binds,456575
rules,457576
startup,@@ -518,6 +637,45 @@ fn entry<T: DeserializeOwned>(name: &str, value: toml::Value, w: &mut Vec<String
518637
.ok()519638
}520639
640
+fn input(value: Option<toml::Value>, w: &mut Vec<String>) -> Input {641
+ let mut table = match value {642
+ None => toml::Table::new(),643
+ Some(toml::Value::Table(t)) => t,644
+ Some(_) => {645
+ w.push("[input] is not a table, using the defaults".into());646
+ toml::Table::new()647
+ }648
+ };649
+ let mut input = Input {650
+ pointer: section("input.pointer", table.remove("pointer"), w),651
+ touchpad: section("input.touchpad", table.remove("touchpad"), w),652
+ device: Vec::new(),653
+ };654
+ for (i, mut entry) in items::<toml::Table>("input.device", table.remove("device"), w)655
+ .into_iter()656
+ .enumerate()657
+ {658
+ let name = format!("input.device[{i}]");659
+ let Some(toml::Value::String(device)) = entry.remove("name") else {660
+ w.push(format!("{name}: no name, left out"));661
+ continue;662
+ };663
+ input.device.push(NamedDevice {664
+ name: device,665
+ settings: section(&name, Some(toml::Value::Table(entry)), w),666
+ });667
+ }668
+ for key in table.keys() {669
+ w.push(format!("input.{key}: unknown option, ignored"));670
+ }671
+ input.pointer.validate("input.pointer", w);672
+ input.touchpad.validate("input.touchpad", w);673
+ for (i, d) in input.device.iter_mut().enumerate() {674
+ d.settings.validate(&format!("input.device[{i}]"), w);675
+ }676
+ input677
+}678
+521679
fn connector_like(name: &str) -> bool {522680
name.rsplit_once('-').is_some_and(|(kind, n)| {523681
!kind.is_empty() && !n.is_empty() && n.bytes().all(|b| b.is_ascii_digit())@@ -600,6 +758,7 @@ mod tests {
600758
assert_eq!(c.decoration, Decoration::default());601759
assert_eq!(c.animation, Animation::default());602760
assert_eq!(c.keyboard, Keyboard::default());761
+ assert_eq!(c.input, Input::default());603762
assert!(c.binds.len() > 40);604763
}605764
@@ -641,4 +800,37 @@ mod tests {
641800
assert!(!c.output.named.contains_key("DP-2"));642801
assert_eq!(loaded.warnings.len(), 7, "{:#?}", loaded.warnings);643802
}803
+804
+ #[test]805
+ fn a_bad_device_option_costs_only_itself() {806
+ let loaded = Config::parse(807
+ r#"808
+ [input.pointer]809
+ accel_speed = 2.0810
+ scroll_method = "two-finger"811
+ [[input.device]]812
+ name = "Mouse"813
+ accel_profile = "flat"814
+ [[input.device]]815
+ name = "Typo"816
+ tapp = true817
+ [[input.device]]818
+ name = "Half"819
+ accel_points = [0.0, 1.0]820
+ "#,821
+ )822
+ .unwrap();823
+ let input = loaded.config.input;824
+ assert_eq!(input.pointer.accel_speed, None);825
+ assert_eq!(input.pointer.scroll_method, Some(ScrollMethod::TwoFinger));826
+ let names: Vec<_> = input.device.iter().map(|d| d.name.as_str()).collect();827
+ assert_eq!(names, ["Mouse", "Typo", "Half"]);828
+ assert_eq!(829
+ input.device[0].settings.accel_profile,830
+ Some(AccelProfile::Flat)831
+ );832
+ assert_eq!(input.device[1].settings, DeviceSettings::default());833
+ assert_eq!(input.device[2].settings.accel_points, None);834
+ assert_eq!(loaded.warnings.len(), 3, "{:#?}", loaded.warnings);835
+ }644836
}@@ -22,6 +22,9 @@ monolith-planner.workspace = true
2222
monolith-protocols.workspace = true2323
monolith-render-vk.workspace = true2424
libc = "0.2"25
+# Raises the libinput bindings smithay uses. 1.23 brings custom acceleration,26
+# and below 1.27 reading a sticky drag lock default panics.27
+input = { version = "0.10", default-features = false, features = ["libinput_1_27"] }2528
pound = "0.5"2629
profiling = "1"2730
serde_json.workspace = true@@ -217,6 +217,7 @@ impl State {
217217
let loaded = Config::load(self.settings.path.as_deref()).map_err(|e| e.to_string())?;218218
let config = warn_config(loaded);219219
let keyboard_changed = config.keyboard != self.settings.config.keyboard;220
+ let input_changed = config.input != self.settings.config.input;220221
self.settings.binds = Binds::new(&config);221222
self.wm.layout.gaps = crate::state::gaps(&config);222223
self.wm.anim.settings = config.animation.clone();@@ -224,6 +225,9 @@ impl State {
224225
if keyboard_changed {225226
self.reconfigure_keyboard();226227
}228
+ if input_changed && let Some(drm) = self.backend.drm_mut() {229
+ drm.devices.reconfigure(&self.settings.config.input);230
+ }227231
crate::drm::reconfigure_outputs(self);228232
tracing::info!("config reloaded");229233
Ok(())@@ -0,0 +1,173 @@
1
+//! libinput device settings from `[input]`, on bare metal only. A nested2
+//! compositor's devices belong to its host.3
+4
+use monolith_config::{AccelProfile, ClickMethod, DeviceSettings, Input, ScrollMethod};5
+use smithay::reexports::input::{6
+ self, AccelConfig, AccelType, Device, DeviceCapability, DeviceConfigResult, DragLockState,7
+};8
+9
+#[derive(Default)]10
+pub struct Devices(Vec<(Device, f64)>);11
+12
+impl Devices {13
+ pub fn add(&mut self, mut device: Device, config: &Input) {14
+ let factor = configure(&mut device, config);15
+ self.0.push((device, factor));16
+ }17
+18
+ pub fn remove(&mut self, device: &Device) {19
+ self.0.retain(|(d, _)| d != device);20
+ }21
+22
+ pub fn reconfigure(&mut self, config: &Input) {23
+ for (device, factor) in &mut self.0 {24
+ *factor = configure(device, config);25
+ }26
+ }27
+28
+ pub fn scroll_factor(&self, device: &Device) -> f64 {29
+ self.030
+ .iter()31
+ .find(|(d, _)| d == device)32
+ .map_or(1.0, |&(_, f)| f)33
+ }34
+}35
+36
+fn settings(device: &Device, config: &Input) -> DeviceSettings {37
+ let base = if device.config_tap_finger_count() > 0 {38
+ config.touchpad.clone()39
+ } else if device.has_capability(DeviceCapability::Pointer) {40
+ config.pointer.clone()41
+ } else {42
+ DeviceSettings::default()43
+ };44
+ let name = device.name();45
+ config46
+ .device47
+ .iter()48
+ .filter(|d| d.name == name)49
+ .fold(base, |s, d| s.overlay(&d.settings))50
+}51
+52
+/// Unset options go back to the device's default rather than being left53
+/// alone, so removing one from the config undoes it on reload.54
+fn configure(device: &mut Device, config: &Input) -> f64 {55
+ let s = settings(device, config);56
+ let name = device.name().into_owned();57
+ let check = |what: &str, result: DeviceConfigResult| {58
+ if let Err(e) = result {59
+ tracing::warn!("input {name:?}: libinput refused {what}: {e:?}");60
+ }61
+ };62
+63
+ if device.config_accel_is_available() {64
+ let profile = s.accel_profile.map(|p| match p {65
+ AccelProfile::Flat => input::AccelProfile::Flat,66
+ AccelProfile::Adaptive => input::AccelProfile::Adaptive,67
+ AccelProfile::Custom => input::AccelProfile::Custom,68
+ });69
+ let curve = s.accel_points.as_deref().zip(s.accel_step);70
+ match (profile.or(device.config_accel_default_profile()), curve) {71
+ (Some(input::AccelProfile::Custom), Some((points, step))) => {72
+ if let Some(accel) = AccelConfig::new(input::AccelProfile::Custom) {73
+ check(74
+ "accel_points",75
+ accel.set_points(AccelType::Motion, step, points),76
+ );77
+ check("accel_profile", device.config_accel_apply(accel));78
+ }79
+ }80
+ (Some(p), _) if device.config_accel_profiles().contains(&p) => {81
+ check("accel_profile", device.config_accel_set_profile(p));82
+ }83
+ (Some(p), _) => tracing::warn!("input {name:?}: no {p:?} accel profile"),84
+ (None, _) => {}85
+ }86
+ let speed = s87
+ .accel_speed88
+ .unwrap_or_else(|| device.config_accel_default_speed());89
+ check("accel_speed", device.config_accel_set_speed(speed));90
+ }91
+ if device.config_scroll_has_natural_scroll() {92
+ let on = s93
+ .natural_scroll94
+ .unwrap_or_else(|| device.config_scroll_default_natural_scroll_enabled());95
+ check(96
+ "natural_scroll",97
+ device.config_scroll_set_natural_scroll_enabled(on),98
+ );99
+ }100
+ if device.config_left_handed_is_available() {101
+ let on = s102
+ .left_handed103
+ .unwrap_or_else(|| device.config_left_handed_default());104
+ check("left_handed", device.config_left_handed_set(on));105
+ }106
+ if device.config_tap_finger_count() > 0 {107
+ let on = s.tap.unwrap_or_else(|| device.config_tap_default_enabled());108
+ check("tap", device.config_tap_set_enabled(on));109
+ let on = s110
+ .tap_drag111
+ .unwrap_or_else(|| device.config_tap_default_drag_enabled());112
+ check("tap_drag", device.config_tap_set_drag_enabled(on));113
+ let lock = match s.tap_drag_lock {114
+ Some(true) => DragLockState::EnabledTimeout,115
+ Some(false) => DragLockState::Disabled,116
+ None => device.config_tap_default_drag_lock_enabled(),117
+ };118
+ check(119
+ "tap_drag_lock",120
+ device.config_tap_set_drag_lock_enabled(lock),121
+ );122
+ }123
+ if device.config_dwt_is_available() {124
+ let on = s.dwt.unwrap_or_else(|| device.config_dwt_default_enabled());125
+ check("dwt", device.config_dwt_set_enabled(on));126
+ }127
+ if device.config_middle_emulation_is_available() {128
+ let on = s129
+ .middle_emulation130
+ .unwrap_or_else(|| device.config_middle_emulation_default_enabled());131
+ check(132
+ "middle_emulation",133
+ device.config_middle_emulation_set_enabled(on),134
+ );135
+ }136
+ let methods = device.config_scroll_methods();137
+ if !methods.is_empty() {138
+ let method = s.scroll_method.map(|m| match m {139
+ ScrollMethod::None => input::ScrollMethod::NoScroll,140
+ ScrollMethod::TwoFinger => input::ScrollMethod::TwoFinger,141
+ ScrollMethod::Edge => input::ScrollMethod::Edge,142
+ ScrollMethod::OnButtonDown => input::ScrollMethod::OnButtonDown,143
+ });144
+ match method.or(device.config_scroll_default_method()) {145
+ Some(m) if m == input::ScrollMethod::NoScroll || methods.contains(&m) => {146
+ check("scroll_method", device.config_scroll_set_method(m));147
+ }148
+ Some(m) => tracing::warn!("input {name:?}: no {m:?} scroll method"),149
+ None => {}150
+ }151
+ }152
+ if methods.contains(&input::ScrollMethod::OnButtonDown) {153
+ let button = s154
+ .scroll_button155
+ .unwrap_or_else(|| device.config_scroll_default_button());156
+ check("scroll_button", device.config_scroll_set_button(button));157
+ }158
+ let methods = device.config_click_methods();159
+ if !methods.is_empty() {160
+ let method = s.click_method.map(|m| match m {161
+ ClickMethod::ButtonAreas => input::ClickMethod::ButtonAreas,162
+ ClickMethod::Clickfinger => input::ClickMethod::Clickfinger,163
+ });164
+ match method.or(device.config_click_default_method()) {165
+ Some(m) if methods.contains(&m) => {166
+ check("click_method", device.config_click_set_method(m));167
+ }168
+ Some(m) => tracing::warn!("input {name:?}: no {m:?} click method"),169
+ None => {}170
+ }171
+ }172
+ s.scroll_factor.unwrap_or(1.0)173
+}@@ -3,6 +3,7 @@
33
//! each rendered when its own vblank lands.44
55
use crate::backend::Backend;6
+use crate::devices::Devices;67
use crate::scene::Draw;78
use crate::state::Services;89
use crate::state::{State, now_ns};@@ -63,6 +64,7 @@ pub struct Drm {
6364
nvidia: bool,6465
frame_flags: FrameFlags,6566
surfaces: HashMap<crtc::Handle, Surface>,67
+ pub devices: Devices,6668
}6769
6870
struct Surface {@@ -227,6 +229,7 @@ pub fn init(
227229
FrameFlags::DEFAULT228230
},229231
surfaces: HashMap::new(),232
+ devices: Devices::default(),230233
}));231234
scan(state);232235
Ok(())@@ -883,15 +886,21 @@ fn on_input(state: &mut State, event: InputEvent<LibinputInputBackend>) {
883886
state.pointer_frame();884887
}885888
InputEvent::PointerAxis { event } => {889
+ let factor = state890
+ .backend891
+ .drm()892
+ .map_or(1.0, |d| d.devices.scroll_factor(&event.device()));886893
let source = event.source();887894
let mut frame = AxisFrame::new(event.time()).source(source);888895
for axis in [Axis::Horizontal, Axis::Vertical] {889
- if let Some(v120) = event.amount_v120(axis) {896
+ let v120 = event.amount_v120(axis).map(|v| v * factor);897
+ if let Some(v120) = v120 {890898
frame = frame.v120(axis, v120 as i32);891899
}892900
let amount = event893901
.amount(axis)894
- .or_else(|| event.amount_v120(axis).map(|v| v * 15.0 / 120.0));902
+ .map(|a| a * factor)903
+ .or_else(|| v120.map(|v| v * 15.0 / 120.0));895904
match amount {896905
Some(a) if a != 0.0 => frame = frame.value(axis, a),897906
Some(_) if source == AxisSource::Finger => frame = frame.stop(axis),@@ -901,6 +910,16 @@ fn on_input(state: &mut State, event: InputEvent<LibinputInputBackend>) {
901910
state.pointer_axis(frame);902911
state.pointer_frame();903912
}913
+ InputEvent::DeviceAdded { device } => {914
+ if let Some(drm) = state.backend.drm_mut() {915
+ drm.devices.add(device, &state.settings.config.input);916
+ }917
+ }918
+ InputEvent::DeviceRemoved { device } => {919
+ if let Some(drm) = state.backend.drm_mut() {920
+ drm.devices.remove(&device);921
+ }922
+ }904923
_ => {}905924
}906925
}@@ -5,6 +5,7 @@ mod capture;
55
mod commands;66
mod commit;77
mod cursor;8
+mod devices;89
mod drm;910
mod focus;1011
mod frame;