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:
Samuel Onoja authored and GitHub committed 2026-10-25 14:14:35 +01:00
1 parent ab11269246
commit ad00e55fe4
35 files changed
+3454 -512

No files matched your search

+58 -1
View File
@@ -424,6 +424,7 @@ pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<()
}
InputEvent::DeviceRemoved { device } => {
state.cancel_window_interaction();
if let Some(backend) = state.direct_backend.as_mut() {
backend
.input_devices
@@ -437,6 +438,7 @@ pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<()
.handle()
.insert_source(notifier, move |event, _, state| match event {
SessionEvent::PauseSession => {
state.cancel_window_interaction();
state.set_binding_mode("default");
let handle = state.loop_handle.clone();
if let Some(backend) = state.direct_backend.as_mut() {
@@ -522,6 +524,57 @@ pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<()
fn configure_libinput_device(state: &Ferese, device: &mut LibinputDevice) {
let settings = state.input_settings.touchpad;
let touchpad = device.config_tap_finger_count() > 0 || device.config_dwt_is_available();
let mouse = state.input_settings.mouse;
if touchpad {
if device.config_accel_is_available()
&& let Err(error) = device.config_accel_set_speed(settings.accel_speed)
{
tracing::warn!(device = %device.name(), ?error, "failed to configure touchpad acceleration");
}
use smithay::reexports::input::SendEventsMode;
let mode = if settings.disable_while_mouse_connected {
SendEventsMode::DISABLED_ON_EXTERNAL_MOUSE
} else {
SendEventsMode::ENABLED
};
if device
.config_send_events_modes()
.contains(SendEventsMode::DISABLED_ON_EXTERNAL_MOUSE)
&& let Err(error) = device.config_send_events_set_mode(mode)
{
tracing::warn!(device = %device.name(), ?error, "failed to configure touchpad external-mouse policy");
}
}
if !touchpad && device.has_capability(smithay::reexports::input::DeviceCapability::Pointer) {
if device.config_accel_is_available() {
if let Err(error) = device.config_accel_set_speed(mouse.accel_speed) {
tracing::warn!(device = %device.name(), ?error, "failed to configure mouse acceleration");
}
use smithay::reexports::input::AccelProfile;
let profile = match mouse.accel_profile {
Some(crate::config::AccelerationProfile::Adaptive) => Some(AccelProfile::Adaptive),
Some(crate::config::AccelerationProfile::Flat) => Some(AccelProfile::Flat),
None => device.config_accel_default_profile(),
};
if let Some(profile) =
profile.filter(|profile| device.config_accel_profiles().contains(profile))
&& let Err(error) = device.config_accel_set_profile(profile)
{
tracing::warn!(device = %device.name(), ?error, "failed to configure mouse acceleration profile");
}
}
if device.config_left_handed_is_available()
&& let Err(error) = device.config_left_handed_set(mouse.left_handed)
{
tracing::warn!(device = %device.name(), ?error, "failed to configure mouse handedness");
}
if device.config_middle_emulation_is_available()
&& let Err(error) = device.config_middle_emulation_set_enabled(mouse.middle_emulation)
{
tracing::warn!(device = %device.name(), ?error, "failed to configure middle button emulation");
}
}
if device.config_tap_finger_count() > 0
&& let Err(error) = device.config_tap_set_enabled(settings.tap)
@@ -529,7 +582,11 @@ fn configure_libinput_device(state: &Ferese, device: &mut LibinputDevice) {
tracing::warn!(device = %device.name(), ?error, "failed to configure tap-to-click");
}
if device.config_scroll_has_natural_scroll()
&& let Err(error) = device.config_scroll_set_natural_scroll_enabled(settings.natural_scroll)
&& let Err(error) = device.config_scroll_set_natural_scroll_enabled(if touchpad {
settings.natural_scroll
} else {
mouse.natural_scroll
})
{
tracing::warn!(device = %device.name(), ?error, "failed to configure natural scrolling");
}
+8 -4
View File
@@ -19,10 +19,11 @@ pub use appearance::{BorderGradient, FocusEffectSettings, MaterialStyle, ThemeSe
pub use bindings::{Binding, BindingAction};
use bindings::{BindingConfig, BindingMatch, BindingModeConfig, BindingModifiers};
pub(crate) use bindings::{BindingModes, BindingSet, physical_keymap};
pub(crate) use bindings::{PointerTrigger, ScrollDirection};
use ferese_animation::SpringConfig;
use ferese_core::LayoutMode;
use ferese_layout::{ColumnWidth, Direction, GapConfig, ViewportFocusStrategy};
pub use input_motion::InputSettings;
pub use input_motion::{AccelerationProfile, InputSettings};
use input_motion::{AnimationsConfig, InputConfig};
use outputs::OutputProfileConfig;
pub use outputs::{
@@ -1134,13 +1135,16 @@ mod tests {
config.input_settings().unwrap(),
InputSettings {
focus_follows_mouse: true,
mouse: Default::default(),
xkb_layout: "us,de".to_owned(),
xkb_variant: ",nodeadkeys".to_owned(),
xkb_options: vec!["grp:alt_shift_toggle".to_owned()],
repeat_rate: 30,
repeat_delay_ms: 450,
touchpad: TouchpadSettings {
accel_speed: 0.0,
tap: false,
disable_while_mouse_connected: false,
natural_scroll: false,
disable_while_typing: true,
swipe_threshold: 80,
@@ -1303,7 +1307,7 @@ mod tests {
let input = config.input_settings().unwrap();
let bindings = config.bindings(&input).unwrap();
assert_eq!(bindings.len(), 63);
assert_eq!(bindings.len(), 65);
for (shift, action) in [
(false, BindingAction::ToggleMaximized),
(true, BindingAction::ToggleFullscreen),
@@ -1362,7 +1366,7 @@ mod tests {
let input = replaced.input_settings().unwrap();
let bindings = replaced.bindings(&input).unwrap();
assert_eq!(bindings.len(), 63);
assert_eq!(bindings.len(), 65);
assert!(bindings.iter().any(|binding| {
binding.action
== BindingAction::Spawn(
@@ -1372,7 +1376,7 @@ mod tests {
let input = unbound.input_settings().unwrap();
let bindings = unbound.bindings(&input).unwrap();
assert_eq!(bindings.len(), 62);
assert_eq!(bindings.len(), 64);
assert!(
!bindings
.iter()
+45 -2
View File
@@ -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)]
+32 -9
View File
@@ -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,
+161
View File
@@ -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());
}
}
+122
View File
@@ -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,
}
}
}
+5 -3
View File
@@ -1,5 +1,7 @@
mod move_grab;
mod resize_grab;
pub use move_grab::MoveSurfaceGrab;
pub use resize_grab::{ResizeEdge, ResizeSurfaceGrab};
pub use resize_grab::ResizeEdge;
mod window_grab;
pub(crate) use resize_grab::{constrained_size, resized_rect};
pub(crate) use window_grab::WindowPointerGrab;
+15 -285
View File
@@ -1,17 +1,5 @@
use smithay::desktop::Window;
use smithay::input::pointer::{
AxisFrame, ButtonEvent, GestureHoldBeginEvent, GestureHoldEndEvent, GesturePinchBeginEvent,
GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent, GestureSwipeEndEvent,
GestureSwipeUpdateEvent, GrabStartData, MotionEvent, PointerGrab, PointerInnerHandle,
RelativeMotionEvent,
};
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel;
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use smithay::utils::{Logical, Point, Rectangle, Size};
use smithay::wayland::compositor::with_states;
use smithay::wayland::shell::xdg::SurfaceCachedState;
use crate::Ferese;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ResizeEdge(xdg_toplevel::ResizeEdge);
@@ -23,6 +11,15 @@ impl From<xdg_toplevel::ResizeEdge> for ResizeEdge {
}
impl ResizeEdge {
pub(crate) fn cursor(self) -> smithay::input::pointer::CursorIcon {
use smithay::input::pointer::CursorIcon;
match (self.left(), self.right(), self.top(), self.bottom()) {
(true, _, true, _) | (_, true, _, true) => CursorIcon::NwseResize,
(true, _, _, true) | (_, true, true, _) => CursorIcon::NeswResize,
(true, _, _, _) | (_, true, _, _) => CursorIcon::EwResize,
_ => CursorIcon::NsResize,
}
}
pub fn at(point: Point<f64, Logical>, rect: Rectangle<i32, Logical>) -> Self {
use xdg_toplevel::ResizeEdge::*;
let x = (point.x - rect.loc.x as f64) / rect.size.w.max(1) as f64;
@@ -46,7 +43,7 @@ impl ResizeEdge {
_ => Bottom,
})
}
fn left(self) -> bool {
pub(crate) fn left(self) -> bool {
matches!(
self.0,
xdg_toplevel::ResizeEdge::Left
@@ -55,7 +52,7 @@ impl ResizeEdge {
)
}
fn right(self) -> bool {
pub(crate) fn right(self) -> bool {
matches!(
self.0,
xdg_toplevel::ResizeEdge::Right
@@ -64,7 +61,7 @@ impl ResizeEdge {
)
}
fn top(self) -> bool {
pub(crate) fn top(self) -> bool {
matches!(
self.0,
xdg_toplevel::ResizeEdge::Top
@@ -73,7 +70,7 @@ impl ResizeEdge {
)
}
fn bottom(self) -> bool {
pub(crate) fn bottom(self) -> bool {
matches!(
self.0,
xdg_toplevel::ResizeEdge::Bottom
@@ -83,274 +80,7 @@ impl ResizeEdge {
}
}
pub struct ResizeSurfaceGrab {
start_data: GrabStartData<Ferese>,
window: Window,
edges: ResizeEdge,
initial_rect: Rectangle<i32, Logical>,
last_size: Size<i32, Logical>,
finished: bool,
snap_x: crate::floating::AxisSnap,
snap_y: crate::floating::AxisSnap,
}
impl ResizeSurfaceGrab {
pub fn new(
start_data: GrabStartData<Ferese>,
window: Window,
edges: ResizeEdge,
initial_rect: Rectangle<i32, Logical>,
) -> Self {
Self {
start_data,
window,
edges,
initial_rect,
last_size: initial_rect.size,
finished: false,
snap_x: Default::default(),
snap_y: Default::default(),
}
}
}
impl PointerGrab<Ferese> for ResizeSurfaceGrab {
fn motion(
&mut self,
data: &mut Ferese,
handle: &mut PointerInnerHandle<'_, Ferese>,
_focus: Option<(WlSurface, Point<f64, Logical>)>,
event: &MotionEvent,
) {
handle.motion(data, None, event);
let mut delta = event.location - self.start_data.location;
let initial = self.initial_rect;
let raw_left = initial.loc.x as f64 + if self.edges.left() { delta.x } else { 0. };
let raw_top = initial.loc.y as f64 + if self.edges.top() { delta.y } else { 0. };
let raw_right = initial.loc.x as f64
+ initial.size.w as f64
+ if self.edges.right() { delta.x } else { 0. };
let raw_bottom = initial.loc.y as f64
+ initial.size.h as f64
+ if self.edges.bottom() { delta.y } else { 0. };
let raw = ferese_layout::Rect::new(
raw_left,
raw_top,
(raw_right - raw_left).max(1.),
(raw_bottom - raw_top).max(1.),
);
let (xs, ys) = data.floating_snap_lines(&self.window, raw);
if data.floating_snap_bypassed() {
self.snap_x.clear();
self.snap_y.clear();
} else {
if self.edges.left() || self.edges.right() {
let moving = if self.edges.left() {
raw_left
} else {
raw_right
};
delta.x += self.snap_x.apply(moving, 0., &xs) - moving;
}
if self.edges.top() || self.edges.bottom() {
let moving = if self.edges.top() {
raw_top
} else {
raw_bottom
};
delta.y += self.snap_y.apply(moving, 0., &ys) - moving;
}
}
let surface = self
.window
.toplevel()
.expect("managed window has a toplevel");
let (minimum, maximum) = with_states(surface.wl_surface(), |states| {
let mut cached = states.cached_state.get::<SurfaceCachedState>();
let state = cached.current();
(state.min_size, state.max_size)
});
let (minimum, maximum) = data.effective_size_constraints(
&self.window,
(minimum.w, minimum.h),
(maximum.w, maximum.h),
);
self.last_size = constrained_size(
self.initial_rect.size,
delta,
self.edges,
minimum,
(maximum.0, maximum.1),
);
let rect = resized_rect(self.initial_rect, self.last_size, self.edges);
data.set_floating_window_geometry(&self.window, rect.loc, rect.size);
if let Some(id) = data.windows.ids().get(&self.window).copied() {
data.windows.update(id, |w| {
w.resize_anchor = Some((
self.edges.left(),
self.edges.top(),
ferese_layout::Rect::new(
initial.loc.x as f64,
initial.loc.y as f64,
initial.size.w as f64,
initial.size.h as f64,
),
))
});
}
surface.with_pending_state(|state| {
state.states.set(xdg_toplevel::State::Resizing);
state.size = Some(self.last_size);
});
surface.send_pending_configure();
}
fn relative_motion(
&mut self,
data: &mut Ferese,
handle: &mut PointerInnerHandle<'_, Ferese>,
focus: Option<(WlSurface, Point<f64, Logical>)>,
event: &RelativeMotionEvent,
) {
handle.relative_motion(data, focus, event);
}
fn button(
&mut self,
data: &mut Ferese,
handle: &mut PointerInnerHandle<'_, Ferese>,
event: &ButtonEvent,
) {
handle.button(data, event);
if handle.current_pressed().is_empty() {
self.finished = true;
data.remember_floating(&self.window);
handle.unset_grab(self, data, event.serial, event.time, true);
let surface = self
.window
.toplevel()
.expect("managed window has a toplevel");
surface.with_pending_state(|state| {
state.states.unset(xdg_toplevel::State::Resizing);
state.size = Some(self.last_size);
});
surface.send_pending_configure();
}
}
fn axis(
&mut self,
data: &mut Ferese,
handle: &mut PointerInnerHandle<'_, Ferese>,
frame: AxisFrame,
) {
handle.axis(data, frame);
}
fn frame(&mut self, data: &mut Ferese, handle: &mut PointerInnerHandle<'_, Ferese>) {
handle.frame(data);
}
fn gesture_swipe_begin(
&mut self,
data: &mut Ferese,
handle: &mut PointerInnerHandle<'_, Ferese>,
event: &GestureSwipeBeginEvent,
) {
handle.gesture_swipe_begin(data, event);
}
fn gesture_swipe_update(
&mut self,
data: &mut Ferese,
handle: &mut PointerInnerHandle<'_, Ferese>,
event: &GestureSwipeUpdateEvent,
) {
handle.gesture_swipe_update(data, event);
}
fn gesture_swipe_end(
&mut self,
data: &mut Ferese,
handle: &mut PointerInnerHandle<'_, Ferese>,
event: &GestureSwipeEndEvent,
) {
handle.gesture_swipe_end(data, event);
}
fn gesture_pinch_begin(
&mut self,
data: &mut Ferese,
handle: &mut PointerInnerHandle<'_, Ferese>,
event: &GesturePinchBeginEvent,
) {
handle.gesture_pinch_begin(data, event);
}
fn gesture_pinch_update(
&mut self,
data: &mut Ferese,
handle: &mut PointerInnerHandle<'_, Ferese>,
event: &GesturePinchUpdateEvent,
) {
handle.gesture_pinch_update(data, event);
}
fn gesture_pinch_end(
&mut self,
data: &mut Ferese,
handle: &mut PointerInnerHandle<'_, Ferese>,
event: &GesturePinchEndEvent,
) {
handle.gesture_pinch_end(data, event);
}
fn gesture_hold_begin(
&mut self,
data: &mut Ferese,
handle: &mut PointerInnerHandle<'_, Ferese>,
event: &GestureHoldBeginEvent,
) {
handle.gesture_hold_begin(data, event);
}
fn gesture_hold_end(
&mut self,
data: &mut Ferese,
handle: &mut PointerInnerHandle<'_, Ferese>,
event: &GestureHoldEndEvent,
) {
handle.gesture_hold_end(data, event);
}
fn start_data(&self) -> &GrabStartData<Ferese> {
&self.start_data
}
fn unset(&mut self, data: &mut Ferese) {
if self.finished {
return;
}
let surface = self
.window
.toplevel()
.expect("managed window has a toplevel");
surface.with_pending_state(|state| {
state.states.unset(xdg_toplevel::State::Resizing);
state.size = Some(self.initial_rect.size);
});
surface.send_pending_configure();
data.set_floating_window_geometry(
&self.window,
self.initial_rect.loc,
self.initial_rect.size,
);
}
}
fn constrained_size(
pub(crate) fn constrained_size(
initial: Size<i32, Logical>,
delta: Point<f64, Logical>,
edges: ResizeEdge,
@@ -392,7 +122,7 @@ fn constrained_size(
.into()
}
fn resized_rect(
pub(crate) fn resized_rect(
initial: Rectangle<i32, Logical>,
size: Size<i32, Logical>,
edges: ResizeEdge,
@@ -1,4 +1,3 @@
use smithay::desktop::Window;
use smithay::input::pointer::{
AxisFrame, ButtonEvent, GestureHoldBeginEvent, GestureHoldEndEvent, GesturePinchBeginEvent,
GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent, GestureSwipeEndEvent,
@@ -6,22 +5,16 @@ use smithay::input::pointer::{
RelativeMotionEvent,
};
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use smithay::utils::{Logical, Point, Size};
use smithay::utils::{Logical, Point};
use crate::Ferese;
use crate::floating::AxisSnap;
pub struct MoveSurfaceGrab {
pub(crate) struct WindowPointerGrab {
pub start_data: GrabStartData<Ferese>,
pub window: Window,
pub initial_location: Point<i32, Logical>,
pub initial_size: Size<i32, Logical>,
pub finished: bool,
pub snap_x: AxisSnap,
pub snap_y: AxisSnap,
}
impl PointerGrab<Ferese> for MoveSurfaceGrab {
impl PointerGrab<Ferese> for WindowPointerGrab {
fn motion(
&mut self,
data: &mut Ferese,
@@ -30,36 +23,21 @@ impl PointerGrab<Ferese> for MoveSurfaceGrab {
event: &MotionEvent,
) {
handle.motion(data, None, event);
let location = self.initial_location.to_f64() + (event.location - self.start_data.location);
let raw = ferese_layout::Rect::new(
location.x,
location.y,
self.initial_size.w as f64,
self.initial_size.h as f64,
);
let (xs, ys) = data.floating_snap_lines(&self.window, raw);
let location = if data.floating_snap_bypassed() {
self.snap_x.clear();
self.snap_y.clear();
location
} else {
Point::from((
self.snap_x.apply(raw.x, raw.width, &xs),
self.snap_y.apply(raw.y, raw.height, &ys),
))
if !self.finished
&& let Some(operation) = data.pointer_operation.as_mut()
{
operation.location = event.location;
}
.to_i32_round();
data.set_floating_window_geometry(&self.window, location, self.initial_size);
}
fn relative_motion(
&mut self,
data: &mut Ferese,
handle: &mut PointerInnerHandle<'_, Ferese>,
focus: Option<(WlSurface, Point<f64, Logical>)>,
_focus: Option<(WlSurface, Point<f64, Logical>)>,
event: &RelativeMotionEvent,
) {
handle.relative_motion(data, focus, event);
handle.relative_motion(data, None, event);
}
fn button(
@@ -68,21 +46,46 @@ impl PointerGrab<Ferese> for MoveSurfaceGrab {
handle: &mut PointerInnerHandle<'_, Ferese>,
event: &ButtonEvent,
) {
if let Some(operation) = data.pointer_operation.as_mut() {
if event.state == smithay::backend::input::ButtonState::Pressed {
operation.pressed.insert(event.button);
if event.button == 0x111 && event.button != self.start_data.button {
operation.toggle_drop_mode();
}
} else {
operation.pressed.remove(&event.button);
}
}
handle.button(data, event);
if handle.current_pressed().is_empty() {
if event.button == self.start_data.button
&& event.state == smithay::backend::input::ButtonState::Released
{
self.finished = true;
data.remember_floating(&self.window);
handle.unset_grab(self, data, event.serial, event.time, true);
if let Some(operation) = data.pointer_operation.as_mut() {
operation.finished = true;
}
}
// Extra buttons are drained with focus cleared, so their releases cannot
// reach a client that never received the corresponding presses.
if handle.current_pressed().is_empty() {
handle.unset_grab(self, data, event.serial, event.time, false);
}
}
fn axis(
&mut self,
data: &mut Ferese,
handle: &mut PointerInnerHandle<'_, Ferese>,
_handle: &mut PointerInnerHandle<'_, Ferese>,
frame: AxisFrame,
) {
handle.axis(data, frame);
if let Some(operation) = data.pointer_operation.as_mut() {
let amount = if frame.axis.0 != 0.0 {
frame.axis.0
} else {
frame.axis.1
};
operation.scroll(amount);
}
}
fn frame(&mut self, data: &mut Ferese, handle: &mut PointerInnerHandle<'_, Ferese>) {
@@ -166,10 +169,12 @@ impl PointerGrab<Ferese> for MoveSurfaceGrab {
}
fn unset(&mut self, data: &mut Ferese) {
if self.finished {
return;
// Resolve focus after the layout settles and the pointer mutex is released.
data.window_grab_focus_pending = true;
if !self.finished
&& let Some(operation) = data.pointer_operation.as_mut()
{
operation.cancelled = true;
}
data.set_floating_window_geometry(&self.window, self.initial_location, self.initial_size);
}
}
+10 -32
View File
@@ -19,7 +19,7 @@ use smithay::wayland::shell::xdg::{
};
use crate::Ferese;
use crate::grabs::{MoveSurfaceGrab, ResizeEdge, ResizeSurfaceGrab};
use crate::grabs::ResizeEdge;
impl XdgShellHandler for Ferese {
fn xdg_shell_state(&mut self) -> &mut XdgShellState {
@@ -150,26 +150,7 @@ impl XdgShellHandler for Ferese {
else {
return;
};
let Some(initial_rect) = self.visual_rect_for_window(&window) else {
return;
};
if !self.is_floating_window(&window) {
return;
}
pointer.set_grab(
self,
MoveSurfaceGrab {
start_data,
window,
initial_location: initial_rect.loc,
initial_size: initial_rect.size,
finished: false,
snap_x: Default::default(),
snap_y: Default::default(),
},
serial,
Focus::Clear,
);
self.begin_window_interaction(&pointer, window, start_data, serial, None);
}
fn resize_request(
@@ -203,18 +184,15 @@ impl XdgShellHandler for Ferese {
else {
return;
};
let Some(rect) = self.visual_rect_for_window(&window) else {
return;
};
if !self.is_floating_window(&window) {
return;
if edges != xdg_toplevel::ResizeEdge::None {
self.begin_window_interaction(
&pointer,
window,
start_data,
serial,
Some(ResizeEdge::from(edges)),
);
}
pointer.set_grab(
self,
ResizeSurfaceGrab::new(start_data, window, ResizeEdge::from(edges), rect),
serial,
Focus::Clear,
);
}
fn maximize_request(&mut self, surface: ToplevelSurface) {
+101 -80
View File
@@ -1,6 +1,10 @@
mod window;
pub(crate) use window::WindowInteraction;
mod pointer;
use ferese_layout::Direction;
pub(crate) use pointer::ScrollBindings;
use smithay::backend::input::{
AbsolutePositionEvent, Axis, AxisSource, ButtonState, Event, GestureBeginEvent,
AbsolutePositionEvent, Axis, AxisSource, ButtonState, Device, Event, GestureBeginEvent,
GestureEndEvent, GestureSwipeUpdateEvent as _, InputBackend, InputEvent, KeyState,
KeyboardKeyEvent, PointerAxisEvent, PointerButtonEvent, PointerMotionEvent, Switch,
SwitchState, SwitchToggleEvent, TouchEvent,
@@ -20,11 +24,10 @@ use smithay::wayland::pointer_constraints::{PointerConstraint, with_pointer_cons
use crate::Ferese;
use crate::config::BindingAction;
use crate::gestures::SwipeDirection;
use crate::grabs::{MoveSurfaceGrab, ResizeEdge, ResizeSurfaceGrab};
use crate::grabs::ResizeEdge;
use crate::process::spawn_client;
const BTN_LEFT: u32 = 0x110;
const BTN_RIGHT: u32 = 0x111;
#[cfg(test)]
pub(crate) mod binding_mode_tests;
@@ -130,6 +133,11 @@ impl Ferese {
}
pub fn process_input_event<I: InputBackend>(&mut self, event: InputEvent<I>) {
self.process_input_event_inner(event);
self.poll_window_interaction();
}
fn process_input_event_inner<I: InputBackend>(&mut self, event: InputEvent<I>) {
if self.input_capture.active() {
self.set_binding_mode("default");
}
@@ -372,6 +380,11 @@ impl Ferese {
};
}
let symbol = keysym.modified_sym().raw();
if state == KeyState::Pressed && symbol == keysyms::KEY_Escape && let Some(operation) = data.pointer_operation.as_mut() {
operation.cancelled = true;
data.intercepted_keys.insert(keycode);
return FilterResult::Intercept(());
}
if data.binding_modes.active().is_some() && symbol == keysyms::KEY_Escape {
if state == KeyState::Pressed {
data.intercepted_keys.insert(keycode);
@@ -586,17 +599,21 @@ impl Ferese {
}
InputEvent::PointerButton { event, .. } => {
self.last_pointer_time = event.time();
if self.input_capture.active() {
if self.input_capture.captures(2) {
self.input_capture.push(serde_json::json!({"type":"button", "button":event.button_code(), "pressed":event.state() == ButtonState::Pressed}));
}
return;
if event.state() == ButtonState::Pressed {
// A disconnected device may never deliver its consumed release.
self.consumed_pointer_buttons.remove(&event.button_code());
}
if event.state() == ButtonState::Released
&& self.consumed_pointer_buttons.remove(&event.button_code())
{
return;
}
if self.input_capture.active() {
if self.input_capture.captures(2) {
self.input_capture.push(serde_json::json!({"type":"button", "button":event.button_code(), "pressed":event.state() == ButtonState::Pressed}));
}
return;
}
let pointer = self.seat.get_pointer().expect("seat has a pointer");
let serial = SERIAL_COUNTER.next_serial();
if self.session_lock.active() {
@@ -709,8 +726,11 @@ impl Ferese {
}
if event.state() == ButtonState::Pressed
&& self.start_floating_pointer_grab(&pointer, event.button_code(), serial)
&& self.handle_pointer_binding(&pointer, event.button_code(), serial)
{
if self.consumed_pointer_buttons.contains(&event.button_code()) {
return;
}
pointer.button(
self,
&ButtonEvent {
@@ -764,47 +784,82 @@ impl Ferese {
}
return;
}
let consumed = self.filter_pointer_scroll(
event.device().id(),
source,
[horizontal, vertical],
[
event.amount_v120(Axis::Horizontal),
event.amount_v120(Axis::Vertical),
],
[
source == AxisSource::Finger && event.amount(Axis::Horizontal) == Some(0.0),
source == AxisSource::Finger && event.amount(Axis::Vertical) == Some(0.0),
],
);
let horizontal = if consumed[0] { 0.0 } else { horizontal };
let vertical = if consumed[1] { 0.0 } else { vertical };
if consumed == [true, true] {
return;
}
let pointer = self.seat.get_pointer().expect("seat has a pointer");
if self.scroll_tab_indicator(
pointer.current_location(),
if vertical != 0.0 {
vertical
} else {
horizontal
},
) {
if !pointer.is_grabbed()
&& self.scroll_tab_indicator(
pointer.current_location(),
if vertical != 0.0 {
vertical
} else {
horizontal
},
)
{
return;
}
if self.scroll_overview_strip(
pointer.current_location(),
if horizontal != 0.0 {
horizontal
} else {
vertical
},
) {
if !pointer.is_grabbed()
&& self.scroll_overview_strip(
pointer.current_location(),
if horizontal != 0.0 {
horizontal
} else {
vertical
},
)
{
return;
}
let ([horizontal, vertical], scaled_v120) = self.scale_mouse_scroll(
event.device().id(),
source,
[horizontal, vertical],
[
(!consumed[0])
.then(|| event.amount_v120(Axis::Horizontal))
.flatten(),
(!consumed[1])
.then(|| event.amount_v120(Axis::Vertical))
.flatten(),
],
);
let mut frame = AxisFrame::new(event.time()).source(source);
if horizontal != 0.0 {
frame = frame.value(Axis::Horizontal, horizontal);
if let Some(value) = event.amount_v120(Axis::Horizontal) {
frame = frame.v120(Axis::Horizontal, value as i32);
if let Some(value) = scaled_v120[0] {
frame = frame.v120(Axis::Horizontal, value);
}
}
if vertical != 0.0 {
frame = frame.value(Axis::Vertical, vertical);
if let Some(value) = event.amount_v120(Axis::Vertical) {
frame = frame.v120(Axis::Vertical, value as i32);
if let Some(value) = scaled_v120[1] {
frame = frame.v120(Axis::Vertical, value);
}
}
if source == AxisSource::Finger {
if event.amount(Axis::Horizontal) == Some(0.0) {
if !consumed[0] && event.amount(Axis::Horizontal) == Some(0.0) {
frame = frame.stop(Axis::Horizontal);
}
if event.amount(Axis::Vertical) == Some(0.0) {
if !consumed[1] && event.amount(Axis::Vertical) == Some(0.0) {
frame = frame.stop(Axis::Vertical);
}
}
@@ -925,23 +980,13 @@ impl Ferese {
true
}
fn start_floating_pointer_grab(
fn start_pointer_window_grab(
&mut self,
pointer: &PointerHandle<Self>,
button: u32,
serial: Serial,
resize: bool,
) -> bool {
if self.session_lock.active()
|| pointer.is_grabbed()
|| !matches!(button, BTN_LEFT | BTN_RIGHT)
|| !self
.seat
.get_keyboard()
.is_some_and(|keyboard| keyboard.modifier_state().logo)
{
return false;
}
let location = pointer.current_location();
if self.layer_under(location).is_some() {
return false;
@@ -949,46 +994,21 @@ impl Ferese {
let Some(window) = self.window_under_visual(location) else {
return false;
};
if !self.is_floating_window(&window) {
return false;
}
let Some(rect) = self.visual_rect_for_window(&window) else {
return false;
};
self.focus_window_at(location, serial, true);
let start_data = GrabStartData {
focus: None,
button,
location,
};
if button == BTN_LEFT {
pointer.set_grab(
self,
MoveSurfaceGrab {
start_data,
window,
initial_location: rect.loc,
initial_size: rect.size,
finished: false,
snap_x: Default::default(),
snap_y: Default::default(),
},
serial,
Focus::Clear,
);
} else {
let edge = ResizeEdge::at(location, rect);
pointer.set_grab(
self,
ResizeSurfaceGrab::new(start_data, window, edge, rect),
serial,
Focus::Clear,
);
}
true
self.begin_window_interaction(
pointer,
window,
GrabStartData {
focus: None,
button,
location,
},
serial,
resize.then(|| ResizeEdge::at(location, rect)),
)
}
fn absolute_event_position<I, E>(&self, event: &E) -> Option<Point<f64, Logical>>
@@ -1383,7 +1403,8 @@ impl Ferese {
fn execute_binding(&mut self, action: BindingAction) {
match action {
BindingAction::None => {}
// Interactive pointer actions are dispatched with the originating button.
BindingAction::None | BindingAction::PointerMove | BindingAction::PointerResize => {}
BindingAction::BindingMode(name) => self.set_binding_mode(&name),
BindingAction::Spawn(argv) => {
if let Some(child) = spawn_client(
+552
View File
@@ -0,0 +1,552 @@
use super::{AxisSource, BindingAction, Ferese, PointerHandle, Serial, with_pointer_constraint};
use crate::config::{PointerTrigger, ScrollDirection};
use std::collections::HashMap;
use std::time::{Duration, Instant};
#[derive(Clone, Debug, PartialEq, Eq)]
struct ScrollContext {
modifiers: u8,
mode: Option<String>,
revision: u64,
}
#[derive(Default)]
struct ScrollAxis {
remainder: f64,
context: Option<ScrollContext>,
finger_owner: Option<bool>,
last_action: Option<Instant>,
}
impl ScrollAxis {
fn feed(
&mut self,
delta: Option<f64>,
finger: bool,
context: &ScrollContext,
bound: bool,
now: Instant,
cooldown: Duration,
) -> (bool, i32) {
let owned = if finger {
*self.finger_owner.get_or_insert(bound)
} else {
bound
};
let Some(delta) = delta else {
*self = Self::default();
return (owned, 0);
};
if self.context.as_ref() != Some(context) {
self.remainder = 0.0;
self.last_action = None;
if finger && self.context.is_some() && owned {
// Finish an intercepted finger gesture without delivering its tail
// or changing actions halfway through it.
return (true, 0);
}
self.context = Some(context.clone());
}
if !owned || !bound || !delta.is_finite() {
self.remainder = 0.0;
return (owned, 0);
}
if self.remainder.signum() != delta.signum() {
self.remainder = 0.0;
}
self.remainder += delta;
let threshold = if finger { 15.0 } else { 120.0 };
let ticks = (self.remainder / threshold).trunc().clamp(-32.0, 32.0) as i32;
self.remainder %= threshold;
if ticks == 0
|| self
.last_action
.is_some_and(|last| now.duration_since(last) < cooldown)
{
return (true, 0);
}
self.last_action = Some(now);
(
true,
if cooldown.is_zero() {
ticks
} else {
ticks.signum()
},
)
}
}
#[derive(Default)]
struct DeviceScroll {
axes: [ScrollAxis; 2],
fraction: [f64; 2],
factor: f64,
}
#[derive(Default)]
pub(crate) struct ScrollBindings {
devices: HashMap<String, DeviceScroll>,
}
impl Ferese {
fn pointer_bindings_allowed(&self, pointer: &PointerHandle<Self>) -> bool {
!self.session_lock.active()
&& !self.input_capture.active()
&& !self.overview.is_active()
&& self.active_shortcuts_inhibitor.is_none()
&& !pointer.is_grabbed()
&& !pointer.current_focus().is_some_and(|surface| {
with_pointer_constraint(&surface, pointer, |constraint| {
constraint.is_some_and(|constraint| constraint.is_active())
})
})
}
pub(super) fn handle_pointer_binding(
&mut self,
pointer: &PointerHandle<Self>,
button: u32,
serial: Serial,
) -> bool {
if !self.pointer_bindings_allowed(pointer) {
return false;
}
let Some(keyboard) = self.seat.get_keyboard() else {
return false;
};
let action = self
.binding_modes
.bindings(&self.bindings)
.binding_for_pointer(PointerTrigger::Button(button), keyboard.modifier_state())
.map(|binding| binding.action.clone());
let Some(action) = action else {
return false;
};
if matches!(
action,
BindingAction::PointerMove | BindingAction::PointerResize
) {
return self.start_pointer_window_grab(
pointer,
button,
serial,
action == BindingAction::PointerResize,
);
}
// Bindings use the current keyboard focus, like ordinary shortcuts.
self.consumed_pointer_buttons.insert(button);
self.execute_binding(action);
true
}
}
impl Ferese {
pub(super) fn filter_pointer_scroll(
&mut self,
device: String,
source: AxisSource,
values: [f64; 2],
v120: [Option<f64>; 2],
stops: [bool; 2],
) -> [bool; 2] {
let Some(pointer) = self.seat.get_pointer() else {
return [false; 2];
};
let Some(keyboard) = self.seat.get_keyboard() else {
return [false; 2];
};
let modifiers = keyboard.modifier_state();
let allowed = self.pointer_bindings_allowed(&pointer);
let finger = source == AxisSource::Finger;
let wheel = matches!(source, AxisSource::Wheel | AxisSource::WheelTilt);
if !finger && !wheel {
return [false; 2];
}
let context = ScrollContext {
modifiers: (modifiers.logo as u8)
| ((modifiers.ctrl as u8) << 1)
| ((modifiers.alt as u8) << 2)
| ((modifiers.shift as u8) << 3),
mode: self.binding_modes.active().map(str::to_owned),
revision: self.ipc_events.config_revision,
};
let bindings = self.binding_modes.bindings(&self.bindings);
let actions = [
ScrollDirection::Left,
ScrollDirection::Right,
ScrollDirection::Up,
ScrollDirection::Down,
]
.map(|direction| {
allowed
.then(|| {
bindings.binding_for_pointer(
if finger {
PointerTrigger::Touchpad(direction)
} else {
PointerTrigger::Wheel(direction)
},
modifiers,
)
})
.flatten()
.map(|binding| binding.action.clone())
});
// Bound storage even for synthetic backends that invent device IDs.
if self.pointer_scroll.devices.len() >= 64
&& !self.pointer_scroll.devices.contains_key(&device)
{
self.pointer_scroll.devices.clear();
}
let axes = &mut self.pointer_scroll.devices.entry(device).or_default().axes;
let mut consumed = [false; 2];
let mut steps = [0; 2];
for axis in 0..2 {
if values[axis] == 0.0 && v120[axis].is_none() && !stops[axis] {
continue;
}
let delta = if finger {
values[axis]
} else {
v120[axis].unwrap_or(values[axis] * 8.0)
};
(consumed[axis], steps[axis]) = axes[axis].feed(
(!stops[axis]).then_some(delta),
finger,
&context,
actions[axis * 2].is_some() || actions[axis * 2 + 1].is_some(),
Instant::now(),
Duration::from_millis(self.input_settings.mouse.scroll_cooldown_ms),
);
}
for (axis, steps) in steps.into_iter().enumerate() {
let index = axis * 2 + usize::from(steps > 0);
if let Some(action) = &actions[index] {
for _ in 0..steps.unsigned_abs() {
if self.session_lock.active() || self.input_capture.active() {
break;
}
self.execute_binding(action.clone());
}
}
}
consumed
}
}
#[cfg(test)]
mod tests {
use super::*;
fn context(modifiers: u8) -> ScrollContext {
ScrollContext {
modifiers,
mode: None,
revision: 0,
}
}
#[test]
fn wheel_accumulates_high_resolution_steps_and_discards_reversed_remainder() {
let mut axis = ScrollAxis::default();
let now = Instant::now();
let feed = |axis: &mut ScrollAxis, amount| {
axis.feed(Some(amount), false, &context(1), true, now, Duration::ZERO)
};
assert_eq!(feed(&mut axis, 60.0), (true, 0));
assert_eq!(feed(&mut axis, 60.0), (true, 1));
assert_eq!(feed(&mut axis, 90.0), (true, 0));
assert_eq!(feed(&mut axis, -60.0), (true, 0));
assert_eq!(feed(&mut axis, -60.0), (true, -1));
assert_eq!(feed(&mut axis, f64::NAN), (true, 0));
}
#[test]
fn wheel_remainder_does_not_cross_modifier_or_config_changes() {
let mut axis = ScrollAxis::default();
let now = Instant::now();
axis.feed(Some(60.0), false, &context(1), true, now, Duration::ZERO);
assert_eq!(
axis.feed(Some(60.0), false, &context(2), true, now, Duration::ZERO),
(true, 0)
);
let mut updated = context(2);
updated.revision = 1;
assert_eq!(
axis.feed(Some(60.0), false, &updated, true, now, Duration::ZERO),
(true, 0)
);
}
#[test]
fn finger_gesture_keeps_ownership_until_stop() {
let now = Instant::now();
let mut axis = ScrollAxis::default();
assert_eq!(
axis.feed(Some(15.0), true, &context(1), true, now, Duration::ZERO),
(true, 1)
);
assert_eq!(
axis.feed(Some(15.0), true, &context(0), false, now, Duration::ZERO),
(true, 0)
);
assert_eq!(
axis.feed(None, true, &context(0), false, now, Duration::ZERO),
(true, 0)
);
assert_eq!(
axis.feed(Some(15.0), true, &context(0), false, now, Duration::ZERO),
(false, 0)
);
assert_eq!(
axis.feed(Some(15.0), true, &context(1), true, now, Duration::ZERO),
(false, 0)
);
assert_eq!(
axis.feed(None, true, &context(1), true, now, Duration::ZERO),
(false, 0)
);
}
#[test]
fn cooldown_consumes_ticks_without_replaying_them_later() {
let mut axis = ScrollAxis::default();
let now = Instant::now();
let cooldown = Duration::from_millis(150);
assert_eq!(
axis.feed(Some(120.0), false, &context(1), true, now, cooldown),
(true, 1)
);
assert_eq!(
axis.feed(Some(240.0), false, &context(1), true, now, cooldown),
(true, 0)
);
assert_eq!(
axis.feed(
Some(120.0),
false,
&context(1),
true,
now + cooldown,
cooldown
),
(true, 1)
);
}
}
impl Ferese {
pub(super) fn scale_mouse_scroll(
&mut self,
device: String,
source: AxisSource,
values: [f64; 2],
v120: [Option<f64>; 2],
) -> ([f64; 2], [Option<i32>; 2]) {
if !matches!(source, AxisSource::Wheel | AxisSource::WheelTilt) {
return (values, v120.map(|value| value.map(|value| value as i32)));
}
if self.pointer_scroll.devices.len() >= 64
&& !self.pointer_scroll.devices.contains_key(&device)
{
self.pointer_scroll.devices.clear();
}
let device = self.pointer_scroll.devices.entry(device).or_default();
let factor = self.input_settings.mouse.scroll_factor;
if device.factor != factor {
device.factor = factor;
device.fraction = [0.0; 2];
}
let scaled = std::array::from_fn(|axis| {
v120[axis].filter(|value| value.is_finite()).map(|value| {
let value = value * factor + device.fraction[axis];
device.fraction[axis] = value.fract();
value as i32
})
});
(values.map(|value| value * factor), scaled)
}
}
#[cfg(test)]
mod routing_tests {
use super::*;
use crate::input::{
ButtonEvent, ButtonState, Event, InputBackend, InputEvent, PointerButtonEvent,
SERIAL_COUNTER,
};
use smithay::backend::input::UnusedEvent;
use smithay::backend::winit::WinitVirtualDevice;
#[derive(Debug)]
struct Input;
impl InputBackend for Input {
type Device = WinitVirtualDevice;
type PointerButtonEvent = Button;
type KeyboardKeyEvent = UnusedEvent;
type PointerAxisEvent = UnusedEvent;
type PointerMotionEvent = UnusedEvent;
type PointerMotionAbsoluteEvent = UnusedEvent;
type GestureSwipeBeginEvent = UnusedEvent;
type GestureSwipeUpdateEvent = UnusedEvent;
type GestureSwipeEndEvent = UnusedEvent;
type GesturePinchBeginEvent = UnusedEvent;
type GesturePinchUpdateEvent = UnusedEvent;
type GesturePinchEndEvent = UnusedEvent;
type GestureHoldBeginEvent = UnusedEvent;
type GestureHoldEndEvent = UnusedEvent;
type TouchDownEvent = UnusedEvent;
type TouchUpEvent = UnusedEvent;
type TouchMotionEvent = UnusedEvent;
type TouchCancelEvent = UnusedEvent;
type TouchFrameEvent = UnusedEvent;
type TabletToolAxisEvent = UnusedEvent;
type TabletToolProximityEvent = UnusedEvent;
type TabletToolTipEvent = UnusedEvent;
type TabletToolButtonEvent = UnusedEvent;
type SwitchToggleEvent = UnusedEvent;
type SpecialEvent = UnusedEvent;
}
#[derive(Debug)]
struct Button(bool);
impl Event<Input> for Button {
fn time(&self) -> smithay::backend::input::InputTime {
smithay::backend::input::InputTime::from_millis(1)
}
fn device(&self) -> WinitVirtualDevice {
WinitVirtualDevice
}
}
impl PointerButtonEvent<Input> for Button {
fn button_code(&self) -> u32 {
275
}
fn state(&self) -> ButtonState {
if self.0 {
ButtonState::Pressed
} else {
ButtonState::Released
}
}
}
#[test]
fn pointer_bindings_use_modes_and_consume_releases_after_modifier_changes() {
if !crate::startup_tests::private_runtime(
"input::pointer::routing_tests::pointer_bindings_use_modes_and_consume_releases_after_modifier_changes",
) {
return;
}
let mut events = smithay::reexports::calloop::EventLoop::try_new().unwrap();
let mut state = crate::startup_tests::state(&mut events);
state
.reload_config_source(
r#"
binding "Super+MouseBack" action="binding-mode" argument="browse"
binding-mode "browse" { binding "MouseBack" action="binding-mode" argument="default"; }
"#
.into(),
)
.unwrap();
let pointer = state.seat.get_pointer().unwrap();
super::super::binding_mode_tests::key(&mut state, 133, true);
state.process_input_event::<Input>(InputEvent::PointerButton {
event: Button(true),
});
assert_eq!(state.binding_modes.active(), Some("browse"));
assert!(state.consumed_pointer_buttons.contains(&275));
assert!(!pointer.is_grabbed());
super::super::binding_mode_tests::key(&mut state, 133, false);
state.process_input_event::<Input>(InputEvent::PointerButton {
event: Button(false),
});
assert!(!state.consumed_pointer_buttons.contains(&275));
state.process_input_event::<Input>(InputEvent::PointerButton {
event: Button(true),
});
assert_eq!(state.binding_modes.active(), None);
state.process_input_event::<Input>(InputEvent::PointerButton {
event: Button(false),
});
// A missing release after unplugging must not poison the next click.
state.consumed_pointer_buttons.insert(275);
// With no matching modifiers, the application receives an ordinary click grab.
state.process_input_event::<Input>(InputEvent::PointerButton {
event: Button(true),
});
assert!(
pointer
.with_grab(|_, grab| grab.is::<smithay::input::pointer::ClickGrab<Ferese>>())
.unwrap()
);
assert!(!state.consumed_pointer_buttons.contains(&275));
state.process_input_event::<Input>(InputEvent::PointerButton {
event: Button(false),
});
assert!(!pointer.is_grabbed());
}
#[test]
fn pointer_scroll_routes_axes_independently_and_preserves_fractional_ticks() {
if !crate::startup_tests::private_runtime(
"input::pointer::routing_tests::pointer_scroll_routes_axes_independently_and_preserves_fractional_ticks",
) {
return;
}
let mut events = smithay::reexports::calloop::EventLoop::try_new().unwrap();
let mut state = crate::startup_tests::state(&mut events);
state
.reload_config_source(
r#"
binding "WheelUp" action="none"
binding "Super+TouchpadScrollLeft" action="none"
input { mouse { scroll-factor 0.25; } }
"#
.into(),
)
.unwrap();
let filtered = state.filter_pointer_scroll(
"mouse".into(),
AxisSource::Wheel,
[15.0, -15.0],
[Some(120.0), Some(-120.0)],
[false; 2],
);
assert_eq!(filtered, [false, true]);
for index in 1..=4 {
let (values, ticks) = state.scale_mouse_scroll(
"mouse".into(),
AxisSource::Wheel,
[1.0, 0.0],
[Some(1.0), None],
);
assert_eq!(values, [0.25, 0.0]);
assert_eq!(ticks, [Some(i32::from(index == 4)), None]);
}
let (values, ticks) =
state.scale_mouse_scroll("touchpad".into(), AxisSource::Finger, [1.0, 2.0], [None; 2]);
assert_eq!(values, [1.0, 2.0]);
assert_eq!(ticks, [None; 2]);
// Pointer grabs retain their scroll events even when a binding exists.
let pointer = state.seat.get_pointer().unwrap();
pointer.button(
&mut state,
&ButtonEvent {
button: 272,
state: ButtonState::Pressed,
serial: SERIAL_COUNTER.next_serial(),
time: smithay::backend::input::InputTime::from_millis(1),
},
);
assert_eq!(
state.filter_pointer_scroll(
"mouse".into(),
AxisSource::Wheel,
[0.0, -15.0],
[None, Some(-120.0)],
[false; 2]
),
[false; 2]
);
}
}
+728
View File
@@ -0,0 +1,728 @@
use super::{
ButtonEvent, ButtonState, Direction, Ferese, Focus, GrabStartData, Logical, MotionEvent, Point,
PointerHandle, ResizeEdge, SERIAL_COUNTER, Serial, with_pointer_constraint,
};
use ferese_core::{WindowPlacement, WorkspaceId, WorkspaceLayout};
use ferese_layout::{Axis as LayoutAxis, DropPlacement, Rect, WindowId};
use smithay::desktop::Window;
use smithay::input::pointer::{CursorIcon, CursorImageStatus};
use smithay::output::Output;
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel;
use smithay::wayland::compositor::with_states;
use smithay::wayland::shell::xdg::SurfaceCachedState;
use std::collections::HashSet;
pub(crate) struct WindowInteraction {
pub id: WindowId,
window: Window,
workspace: WorkspaceId,
output: Output,
output_geometry: smithay::utils::Rectangle<i32, Logical>,
start: Point<f64, Logical>,
initial: smithay::utils::Rectangle<i32, Logical>,
placement: WindowPlacement,
applied_placement: WindowPlacement,
edges: Option<ResizeEdge>,
base_layout: WorkspaceLayout,
applied_layout: WorkspaceLayout,
revision: u64,
serial: Serial,
last_tick: std::time::Instant,
scroll: f64,
drop_floating: bool,
pub location: Point<f64, Logical>,
pub pressed: HashSet<u32>,
pub finished: bool,
pub cancelled: bool,
started: bool,
last_location: Point<f64, Logical>,
was_maximized: bool,
pub visual: Option<Rect>,
snap_x: crate::floating::AxisSnap,
snap_y: crate::floating::AxisSnap,
}
impl WindowInteraction {
pub(crate) fn toggle_drop_mode(&mut self) {
if self.edges.is_none() && !self.finished {
self.drop_floating = !self.drop_floating;
}
}
pub(crate) fn scroll(&mut self, amount: f64) {
if self.edges.is_none() && amount.is_finite() && !self.finished {
self.scroll += amount;
}
}
}
impl Ferese {
pub(crate) fn begin_window_interaction(
&mut self,
pointer: &PointerHandle<Self>,
window: Window,
start: GrabStartData<Self>,
serial: Serial,
edges: Option<ResizeEdge>,
) -> bool {
if self.session_lock.active()
|| self.input_capture.active()
|| self.overview.is_active()
|| self.pointer_operation.is_some()
|| self.keyboard_focused_layer().is_some()
|| !self.window_content_ready(&window)
|| pointer
.with_grab(|_, grab| !grab.is::<smithay::input::pointer::ClickGrab<Self>>())
.unwrap_or(false)
|| pointer.current_focus().is_some_and(|surface| {
with_pointer_constraint(&surface, pointer, |constraint| {
constraint.is_some_and(|constraint| constraint.is_active())
})
})
{
return false;
}
let Some(id) = self.windows.ids().get(&window).copied() else {
return false;
};
let Some(workspace) = self.workspaces.workspace_for_window(id) else {
return false;
};
let Some(placement) = self.workspaces.placement(id) else {
return false;
};
if !self.window_interaction_allowed(id, workspace) {
return false;
}
let Some(output) = self
.space
.outputs()
.find(|output| self.window_belongs_to_output(id, output))
.cloned()
else {
return false;
};
let Some(output_geometry) = self.space.output_geometry(&output) else {
return false;
};
let Some(mut initial) = self.visual_rect_for_window(&window) else {
return false;
};
let was_maximized = self
.windows
.record(id)
.is_some_and(|record| record.maximized);
if was_maximized {
self.set_window_maximized(id, false);
if let Some(geometry) = self.windows.geometry(&id) {
let restored = geometry.logical;
let size = (
restored.width.round() as i32,
restored.height.round() as i32,
)
.into();
let x = ((start.location.x - initial.loc.x as f64) / initial.size.w.max(1) as f64)
.clamp(0.0, 1.0);
let y = ((start.location.y - initial.loc.y as f64) / initial.size.h.max(1) as f64)
.clamp(0.0, 1.0);
initial = smithay::utils::Rectangle::new(
(
(start.location.x - x * restored.width).round() as i32,
(start.location.y - y * restored.height).round() as i32,
)
.into(),
size,
);
}
}
let layout = self
.workspaces
.workspace(workspace)
.expect("managed workspace")
.layout
.clone();
self.pointer_operation = Some(WindowInteraction {
id,
window,
workspace,
output,
output_geometry,
initial,
placement,
applied_placement: placement,
edges,
base_layout: layout.clone(),
applied_layout: layout,
revision: self.ipc_events.config_revision,
serial,
last_tick: std::time::Instant::now(),
scroll: 0.0,
drop_floating: matches!(placement, WindowPlacement::Floating { .. }),
start: start.location,
location: start.location,
last_location: start.location,
pressed: HashSet::from([start.button]),
finished: false,
cancelled: false,
started: false,
was_maximized,
visual: None,
snap_x: Default::default(),
snap_y: Default::default(),
});
self.cursor_status =
CursorImageStatus::Named(edges.map_or(CursorIcon::Grabbing, ResizeEdge::cursor));
pointer.set_grab(
self,
crate::grabs::WindowPointerGrab {
start_data: start,
finished: false,
},
serial,
Focus::Clear,
);
use smithay::reexports::calloop::timer::{TimeoutAction, Timer};
let interval = std::time::Duration::from_millis(16);
if let Err(error) =
self.loop_handle
.insert_source(Timer::from_duration(interval), move |_, _, state| {
if state
.pointer_operation
.as_ref()
.is_none_or(|operation| operation.serial != serial)
{
return TimeoutAction::Drop;
}
state.poll_window_interaction();
if state.pointer_operation.is_some() {
TimeoutAction::ToDuration(interval)
} else {
TimeoutAction::Drop
}
})
{
tracing::warn!(%error, "failed to schedule drag edge scrolling");
}
true
}
pub(crate) fn cancel_window_interaction(&mut self) {
if let Some(operation) = self.pointer_operation.as_mut() {
operation.cancelled = true;
}
self.poll_window_interaction();
}
pub(crate) fn poll_window_interaction(&mut self) {
self.update_window_interaction();
if self.window_grab_focus_pending
&& let Some(pointer) = self.seat.get_pointer()
&& !pointer.is_grabbed()
{
self.window_grab_focus_pending = false;
let location = pointer.current_location();
pointer.motion(
self,
self.surface_under(location),
&MotionEvent {
location,
serial: SERIAL_COUNTER.next_serial(),
time: self.last_pointer_time,
},
);
pointer.frame(self);
}
}
fn update_window_interaction(&mut self) {
let Some(mut operation) = self.pointer_operation.take() else {
return;
};
let valid = !self.session_lock.active()
&& !self.input_capture.active()
&& self.keyboard_focused_layer().is_none()
&& self
.windows
.record(operation.id)
.is_some_and(|record| !record.maximized)
&& operation.location.x.is_finite()
&& operation.location.y.is_finite()
&& self
.windows
.window(operation.id)
.is_some_and(|window| self.window_content_ready(window))
&& self.workspaces.workspace_for_window(operation.id) == Some(operation.workspace)
&& self.space.output_geometry(&operation.output) == Some(operation.output_geometry)
&& self
.output_id(&operation.output)
.and_then(|id| self.output_workspaces.active_workspace(id))
== Some(operation.workspace)
&& self.window_interaction_allowed(operation.id, operation.workspace)
&& self
.workspaces
.placement(operation.id)
.is_some_and(|placement| {
matches!(
(placement, operation.placement),
(WindowPlacement::Tiled, WindowPlacement::Tiled)
| (
WindowPlacement::Floating { .. },
WindowPlacement::Floating { .. }
)
)
})
&& self.ipc_events.config_revision == operation.revision;
if !valid {
operation.cancelled = true;
}
if self.overview.is_active() {
operation.cancelled = true;
}
let layout_matches = operation.edges.is_none()
|| operation.placement != WindowPlacement::Tiled
|| self
.workspaces
.workspace(operation.workspace)
.is_some_and(|workspace| same_layout(&workspace.layout, &operation.applied_layout));
if operation.edges.is_some()
&& operation.placement == WindowPlacement::Tiled
&& !layout_matches
{
operation.cancelled = true;
}
let placement_matches =
self.workspaces.placement(operation.id) == Some(operation.applied_placement);
if operation.edges.is_some() && !placement_matches {
operation.cancelled = true;
}
let delta = operation.location - operation.start;
let distance = delta.x.hypot(delta.y);
operation.started |= distance > 0.0 && distance >= self.input_settings.mouse.drag_threshold;
if operation.cancelled {
if operation.started
&& self.workspaces.workspace_for_window(operation.id) == Some(operation.workspace)
{
match operation.placement {
WindowPlacement::Tiled if operation.edges.is_some() && layout_matches => {
if let Some(workspace) = self.workspaces.workspace_mut(operation.workspace)
{
workspace.layout = operation.base_layout.clone();
}
}
WindowPlacement::Floating { rect }
if operation.edges.is_some() && placement_matches =>
{
self.set_floating_window_rect(&operation.window, rect);
}
_ => {}
}
}
if let Some(pointer) = self.seat.get_pointer() {
// Flush only a compositor-owned grab. The physical releases are
// then consumed, preserving button state without sending them to clients.
if pointer
.with_grab(|_, grab| grab.is::<crate::grabs::WindowPointerGrab>())
.unwrap_or(false)
{
for button in &operation.pressed {
pointer.button(
self,
&ButtonEvent {
button: *button,
state: ButtonState::Released,
serial: SERIAL_COUNTER.next_serial(),
time: self.last_pointer_time,
},
);
self.consumed_pointer_buttons.insert(*button);
}
if pointer
.with_grab(|_, grab| grab.is::<crate::grabs::WindowPointerGrab>())
.unwrap_or(false)
{
pointer.unset_grab(
self,
SERIAL_COUNTER.next_serial(),
self.last_pointer_time,
);
}
}
}
self.finish_window_interaction(&operation, true);
return;
}
if operation.started && operation.location != operation.last_location {
let previous_visual = operation.visual;
if let Some(edges) = operation.edges {
self.resize_interaction(&mut operation, edges, delta);
} else {
let mut rect = Rect::new(
operation.initial.loc.x as f64 + delta.x,
operation.initial.loc.y as f64 + delta.y,
operation.initial.size.w as f64,
operation.initial.size.h as f64,
);
if operation.drop_floating {
let (xs, ys) = self.floating_snap_lines(&operation.window, rect);
if self.floating_snap_bypassed() {
operation.snap_x.clear();
operation.snap_y.clear();
} else {
rect.x = operation.snap_x.apply(rect.x, rect.width, &xs);
rect.y = operation.snap_y.apply(rect.y, rect.height, &ys);
}
}
operation.visual = Some(rect);
self.space.raise_element(&operation.window, false);
}
operation.last_location = operation.location;
let outputs = self
.space
.outputs()
.filter(|output| {
*output == &operation.output
|| self.space.output_geometry(output).is_some_and(|geometry| {
let bounds = Rect::new(
geometry.loc.x as f64,
geometry.loc.y as f64,
geometry.size.w as f64,
geometry.size.h as f64,
);
[previous_visual, operation.visual]
.into_iter()
.flatten()
.any(|rect| crate::floating::intersection(rect, bounds).is_some())
})
})
.cloned()
.collect::<Vec<_>>();
for output in outputs {
self.defer_output_redraw(output);
}
self.cursor_redraw_pending = true;
}
if operation.started && operation.edges.is_none() && !operation.finished {
self.scroll_drag_viewport(&mut operation);
}
if operation.finished {
let committed = operation.started
&& (operation.edges.is_some() || self.drop_interaction(&operation));
self.finish_window_interaction(&operation, !committed);
} else {
self.pointer_operation = Some(operation);
}
}
fn scroll_drag_viewport(&mut self, operation: &mut WindowInteraction) {
let now = std::time::Instant::now();
let elapsed = now
.duration_since(operation.last_tick)
.as_secs_f64()
.min(0.05);
operation.last_tick = now;
let wheel = std::mem::take(&mut operation.scroll);
let Some(output) = self.space.output_under(operation.location).next().cloned() else {
return;
};
let Some(bounds) = self.output_bounds_for(&output) else {
return;
};
let Some(workspace) = self
.output_id(&output)
.and_then(|id| self.output_workspaces.active_workspace(id))
else {
return;
};
// The pointer can be held at an edge to reach columns outside the viewport.
let edge = if operation.drop_floating
|| self
.layer_under(operation.location)
.is_some_and(|(layer, ..)| {
matches!(
layer.layer(),
smithay::wayland::shell::wlr_layer::Layer::Top
| smithay::wayland::shell::wlr_layer::Layer::Overlay
)
}) {
0.0
} else if operation.location.x < bounds.x + 32.0 {
-1.0
} else if operation.location.x > bounds.x + bounds.width - 32.0 {
1.0
} else {
0.0
};
if let Some(workspace) = self.workspaces.workspace_mut(workspace)
&& workspace.fullscreen.is_none()
&& let WorkspaceLayout::Scrolling(layout) = &mut workspace.layout
&& layout.scroll_viewport(wheel + edge * elapsed * 800.0)
{
self.relayout();
}
}
fn resize_interaction(
&mut self,
operation: &mut WindowInteraction,
edges: ResizeEdge,
delta: Point<f64, Logical>,
) {
if operation.placement == WindowPlacement::Tiled {
let Some(bounds) = self.output_bounds_for(&operation.output) else {
return;
};
let mut layout = operation.base_layout.clone();
if edges.left() || edges.right() {
let edge = if edges.left() {
Direction::Left
} else {
Direction::Right
};
let _ = layout.resize_edge(operation.id, edge, delta.x, bounds);
}
if edges.top() || edges.bottom() {
let edge = if edges.top() {
Direction::Up
} else {
Direction::Down
};
let _ = layout.resize_edge(operation.id, edge, delta.y, bounds);
}
if let Some(workspace) = self.workspaces.workspace_mut(operation.workspace) {
workspace.layout = layout;
}
self.relayout();
if let Some(workspace) = self.workspaces.workspace(operation.workspace) {
operation.applied_layout = workspace.layout.clone();
}
} else if let Some(surface) = operation.window.toplevel() {
let initial = operation.initial;
let mut delta = delta;
let left = initial.loc.x as f64 + if edges.left() { delta.x } else { 0.0 };
let top = initial.loc.y as f64 + if edges.top() { delta.y } else { 0.0 };
let right = initial.loc.x as f64
+ initial.size.w as f64
+ if edges.right() { delta.x } else { 0.0 };
let bottom = initial.loc.y as f64
+ initial.size.h as f64
+ if edges.bottom() { delta.y } else { 0.0 };
let raw = Rect::new(left, top, (right - left).max(1.0), (bottom - top).max(1.0));
let (xs, ys) = self.floating_snap_lines(&operation.window, raw);
if self.floating_snap_bypassed() {
operation.snap_x.clear();
operation.snap_y.clear();
} else {
if edges.left() || edges.right() {
let moving = if edges.left() { left } else { right };
delta.x += operation.snap_x.apply(moving, 0.0, &xs) - moving;
}
if edges.top() || edges.bottom() {
let moving = if edges.top() { top } else { bottom };
delta.y += operation.snap_y.apply(moving, 0.0, &ys) - moving;
}
}
let (min, max) = with_states(surface.wl_surface(), |states| {
let mut cache = states.cached_state.get::<SurfaceCachedState>();
let cached = cache.current();
(cached.min_size, cached.max_size)
});
let (min, max) =
self.effective_size_constraints(&operation.window, (min.w, min.h), (max.w, max.h));
let size =
crate::grabs::constrained_size(operation.initial.size, delta, edges, min, max);
let rect = crate::grabs::resized_rect(operation.initial, size, edges);
self.set_floating_window_geometry(&operation.window, rect.loc, rect.size);
operation.applied_placement = self
.workspaces
.placement(operation.id)
.unwrap_or(operation.applied_placement);
let initial = operation.initial;
self.windows.update(operation.id, |record| {
record.resize_anchor = Some((
edges.left(),
edges.top(),
Rect::new(
initial.loc.x as f64,
initial.loc.y as f64,
initial.size.w as f64,
initial.size.h as f64,
),
))
});
}
if let Some(surface) = operation.window.toplevel() {
surface.with_pending_state(|state| {
state.states.set(xdg_toplevel::State::Resizing);
});
surface.send_pending_configure();
}
}
fn drop_interaction(&mut self, operation: &WindowInteraction) -> bool {
if self
.layer_under(operation.location)
.is_some_and(|(layer, ..)| {
matches!(
layer.layer(),
smithay::wayland::shell::wlr_layer::Layer::Top
| smithay::wayland::shell::wlr_layer::Layer::Overlay
)
})
{
return false;
}
let Some(output) = self.space.output_under(operation.location).next().cloned() else {
return false;
};
let Some(destination) = self
.output_id(&output)
.and_then(|id| self.output_workspaces.active_workspace(id))
else {
return false;
};
if self
.workspaces
.workspace(destination)
.is_none_or(|workspace| workspace.fullscreen.is_some())
&& !(destination == operation.workspace
&& operation.drop_floating
&& self.window_interaction_allowed(operation.id, destination))
{
return false;
}
let target = self
.space
.elements()
.rev()
.filter_map(|window| {
let id = self.windows.ids().get(window).copied()?;
if id == operation.id
|| self.workspaces.workspace_for_window(id) != Some(destination)
|| self.workspaces.placement(id) != Some(WindowPlacement::Tiled)
{
return None;
}
let rect = self.visual_rect_for_window(window)?;
rect.to_f64()
.contains(operation.location)
.then_some((id, rect))
})
.next();
// Apply a drop transactionally, preserving all changes made while dragging.
let mut next = self.workspaces.clone();
let floating = matches!(operation.placement, WindowPlacement::Floating { .. });
if floating != operation.drop_floating
&& next
.toggle_floating(
operation.id,
operation
.visual
.expect("started move has a visual rectangle"),
LayoutAxis::Horizontal,
0.5,
)
.is_err()
{
return false;
}
if next
.move_window_to_workspace(operation.id, destination, LayoutAxis::Horizontal, 0.5)
.is_err()
{
return false;
}
match next
.placement(operation.id)
.expect("validated dragged window")
{
WindowPlacement::Tiled => {
if let Some((target, rect)) = target {
let placement = drop_placement(operation.location, rect);
if next
.workspace_mut(destination)
.expect("validated destination")
.layout
.place_window(operation.id, target, placement)
.is_err()
{
return false;
}
}
}
WindowPlacement::Floating { .. } => {
if let Some(rect) = operation.visual
&& next.set_floating_rect(operation.id, rect).is_err()
{
return false;
}
}
}
if next.validate().is_err() {
return false;
}
self.workspaces = next;
// Start the landing animation at the position actually being displayed.
if let Some(rect) = operation.visual {
self.windows.clear_transaction(&operation.id);
self.windows.update(operation.id, |record| {
record.world_x = None;
record.coupled_width = None;
if let Some(geometry) = record.geometry.as_mut() {
geometry.visual = ferese_animation::AnimatedRect::new(rect);
geometry.presentation_changed = true;
}
});
}
self.activate_managed_window(operation.id);
true
}
fn finish_window_interaction(&mut self, operation: &WindowInteraction, cancelled: bool) {
if let Some(surface) = operation.window.toplevel() {
surface.with_pending_state(|state| {
state.states.unset(xdg_toplevel::State::Resizing);
});
surface.send_pending_configure();
}
if cancelled && operation.was_maximized && self.windows.window(operation.id).is_some() {
self.set_window_maximized(operation.id, true);
}
if !cancelled {
self.remember_floating(&operation.window);
}
self.cursor_status = CursorImageStatus::default_named();
self.relayout();
self.cursor_redraw_pending = true;
}
}
fn drop_placement(
location: Point<f64, Logical>,
rect: smithay::utils::Rectangle<i32, Logical>,
) -> DropPlacement {
let x = (location.x - rect.loc.x as f64) / rect.size.w.max(1) as f64;
let y = (location.y - rect.loc.y as f64) / rect.size.h.max(1) as f64;
if (0.25..=0.75).contains(&x) && (0.25..=0.75).contains(&y) {
return DropPlacement::Tab;
}
let edges = [
(x, DropPlacement::Left),
(1.0 - x, DropPlacement::Right),
(y, DropPlacement::Above),
(1.0 - y, DropPlacement::Below),
];
edges
.into_iter()
.min_by(|a, b| a.0.total_cmp(&b.0))
.expect("four edges")
.1
}
fn same_layout(a: &WorkspaceLayout, b: &WorkspaceLayout) -> bool {
match (a, b) {
(WorkspaceLayout::Scrolling(a), WorkspaceLayout::Scrolling(b)) => {
a.columns() == b.columns()
}
_ => a == b,
}
}
+1
View File
@@ -212,6 +212,7 @@ fn after_dispatch_with_redraw(
mut redraw: impl FnMut(&mut Ferese, RedrawRequest<'_>),
) {
crate::backends::direct::gamma::sync_night_light(state);
state.poll_window_interaction();
state.process_pending_dmabuf_imports();
state.prune_screencopies();
state.refresh_capture_sessions();
+11 -1
View File
@@ -232,7 +232,7 @@ fn scene_elements(
presentation_alpha,
))
};
let windows = if workspace.is_none() && state.overview.is_active() {
let mut windows = if workspace.is_none() && state.overview.is_active() {
state
.windows
.overview_windows()
@@ -258,6 +258,16 @@ fn scene_elements(
.collect::<Vec<_>>()
};
if let Some(operation) = state
.pointer_operation
.as_ref()
.filter(|operation| operation.visual.is_some())
&& let Some(index) = windows.iter().position(|(_, id, ..)| *id == operation.id)
{
// A dragged tile stays above floating windows, even after a relayout.
windows[..=index].rotate_right(1);
}
let mut closing = if workspace.is_some() {
Default::default()
} else {
+2
View File
@@ -299,6 +299,7 @@ impl Ferese {
}
pub(crate) fn prepare_lock_input(&mut self) {
self.cancel_window_interaction();
// Reject desktop grabs installed after acquisition, but keep the lock
// owner's implicit click grab intact until its button release.
if let Some(keyboard) = self.seat.get_keyboard()
@@ -482,6 +483,7 @@ impl SessionLockHandler for Ferese {
);
}
self.set_overview_active(false);
self.cancel_window_interaction();
let serial = SERIAL_COUNTER.next_serial();
if let Some(pointer) = self.seat.get_pointer() {
pointer.unset_grab(self, serial, smithay::backend::input::InputTime::now());
+12 -2
View File
@@ -11,6 +11,8 @@ mod hit_testing;
mod layout;
mod navigation;
mod outputs;
#[cfg(test)]
mod pointer_tests;
mod reconciliation;
pub(crate) mod tab_indicator;
mod tabs;
@@ -488,6 +490,9 @@ pub struct Ferese {
pub(crate) tab_indicator_settings: crate::config::TabIndicatorSettings,
pub(crate) tab_bars: HashMap<WorkspaceId, Vec<ferese_layout::TabBar>>,
pub(crate) consumed_pointer_buttons: HashSet<u32>,
pub(crate) pointer_scroll: crate::input::ScrollBindings,
pub(crate) pointer_operation: Option<crate::input::WindowInteraction>,
pub(crate) window_grab_focus_pending: bool,
pub(crate) pending_layer_focus: Option<(WlSurface, Instant)>,
pub(crate) consumed_touch_slots: HashSet<smithay::backend::input::TouchSlot>,
pub(crate) focus_history: ferese_core::FocusHistory,
@@ -812,6 +817,9 @@ impl Ferese {
tab_indicator_settings: config.tab_indicator,
tab_bars: HashMap::new(),
consumed_pointer_buttons: HashSet::new(),
pointer_scroll: Default::default(),
pointer_operation: None,
window_grab_focus_pending: false,
pending_layer_focus: None,
consumed_touch_slots: HashSet::new(),
focus_history: Default::default(),
@@ -1022,7 +1030,9 @@ impl Ferese {
self.advance_animations(Instant::now());
}
let touchpad_changed = self.input_settings.touchpad != config.input_settings.touchpad;
let pointer_settings_changed = self.input_settings.touchpad
!= config.input_settings.touchpad
|| self.input_settings.mouse != config.input_settings.mouse;
let outputs_changed = self.output_profiles != config.output_profiles;
if let Some(workspaces) = workspaces {
self.workspaces = workspaces;
@@ -1089,7 +1099,7 @@ impl Ferese {
crate::daemon::Runner::refresh(&runner, self);
}
if touchpad_changed {
if pointer_settings_changed {
crate::backends::direct::reload_input_devices(self);
}
+35 -7
View File
@@ -6,6 +6,25 @@ use super::{
};
impl Ferese {
pub(crate) fn window_interaction_allowed(
&self,
id: WindowId,
workspace: ferese_core::WorkspaceId,
) -> bool {
self.workspaces
.workspace(workspace)
.is_some_and(|workspace| {
workspace.fullscreen.is_none_or(|fullscreen| {
id != fullscreen
&& self.floating_above_fullscreen.get(&id) == Some(&fullscreen)
&& matches!(
self.workspaces.placement(id),
Some(WindowPlacement::Floating { .. })
)
})
})
}
pub(super) fn adjust_floating_direction(
&mut self,
id: WindowId,
@@ -238,18 +257,24 @@ impl Ferese {
location: Point<i32, Logical>,
size: Size<i32, Logical>,
) {
self.set_floating_window_rect(
window,
Rect::new(
location.x as f64,
location.y as f64,
size.w.max(1) as f64,
size.h.max(1) as f64,
),
);
}
pub(crate) fn set_floating_window_rect(&mut self, window: &Window, rect: Rect) {
let Some(id) = self.windows.ids().get(window).copied() else {
return;
};
self.windows
.update(id, |w| w.resize_anchor.take())
.flatten();
let rect = Rect::new(
location.x as f64,
location.y as f64,
size.w.max(1) as f64,
size.h.max(1) as f64,
);
if let Err(error) = self.workspaces.set_floating_rect(id, rect) {
tracing::error!(%error, ?id, "failed to update floating window geometry");
@@ -288,7 +313,10 @@ impl Ferese {
self.windows.update(id, |record| record.world_x = None);
self.windows
.update(id, |record| record.coupled_width = None);
self.map_window_geometry(window.clone(), location);
self.map_window_geometry(
window.clone(),
(rect.x.round() as i32, rect.y.round() as i32).into(),
);
self.sync_window_stacking();
// Move/resize/cancel callbacks run with Smithay's pointer mutex held.
// Cursor rendering reads current_location(), which would lock it again.
+13
View File
@@ -214,6 +214,19 @@ impl Ferese {
}
pub(crate) fn window_belongs_to_output(&self, window: WindowId, output: &Output) -> bool {
if let Some(rect) = self
.pointer_operation
.as_ref()
.filter(|operation| operation.id == window)
.and_then(|operation| operation.visual)
{
return self.space.output_geometry(output).is_some_and(|area| {
rect.x < (area.loc.x + area.size.w) as f64
&& rect.x + rect.width > area.loc.x as f64
&& rect.y < (area.loc.y + area.size.h) as f64
&& rect.y + rect.height > area.loc.y as f64
});
}
let Some(output_id) = self.output_id(output) else {
return false;
};
+539
View File
@@ -0,0 +1,539 @@
use super::capture_privacy::tests::window;
use super::window_lifecycle_tests::fixture;
use super::*;
use smithay::input::pointer::{GrabStartData, MotionEvent};
use smithay::utils::SERIAL_COUNTER;
fn pointer_interaction_begin(
state: &mut Ferese,
window: Window,
resize: bool,
) -> smithay::input::pointer::PointerHandle<Ferese> {
use smithay::backend::input::{ButtonState, InputTime};
let pointer = state.seat.get_pointer().unwrap();
let rect = state.visual_rect_for_window(&window).unwrap();
let location = (
rect.loc.x as f64 + rect.size.w as f64 / 2.0,
rect.loc.y as f64 + rect.size.h as f64 / 2.0,
)
.into();
assert!(state.begin_window_interaction(
&pointer,
window,
GrabStartData {
focus: None,
button: 0x110,
location
},
SERIAL_COUNTER.next_serial(),
resize.then(|| xdg_toplevel::ResizeEdge::BottomRight.into())
));
pointer.button(
state,
&smithay::input::pointer::ButtonEvent {
button: 0x110,
state: ButtonState::Pressed,
serial: SERIAL_COUNTER.next_serial(),
time: InputTime::from_millis(1),
},
);
pointer
}
#[test]
fn pointer_drag_keeps_layout_until_drop_and_escape_preserves_new_windows() {
if !crate::startup_tests::private_runtime(
"state::pointer_tests::pointer_drag_keeps_layout_until_drop_and_escape_preserves_new_windows",
) {
return;
}
let (mut events, mut state, _) = fixture();
let (first, _wire) = window(&mut state, &mut events, 0xff112233);
let first_id = state.windows.ids()[&first];
let before = state.workspaces.active().layout.clone();
let pointer = pointer_interaction_begin(&mut state, first, false);
pointer.motion(
&mut state,
None,
&MotionEvent {
location: (700.0, 300.0).into(),
serial: SERIAL_COUNTER.next_serial(),
time: smithay::backend::input::InputTime::from_millis(2),
},
);
state.poll_window_interaction();
assert_eq!(state.workspaces.active().layout, before);
assert!(state.pointer_operation.as_ref().unwrap().visual.is_some());
let (second, _wire2) = window(&mut state, &mut events, 0xff445566);
let second_id = state.windows.ids()[&second];
state.cancel_window_interaction();
assert!(state.pointer_operation.is_none());
assert!(!pointer.is_grabbed());
assert!(state.workspaces.active().layout.contains(first_id));
assert!(state.workspaces.active().layout.contains(second_id));
state.workspaces.validate().unwrap();
}
#[test]
fn pointer_resize_cancels_cleanly_and_finishes_on_its_own_button() {
use smithay::backend::input::{ButtonState, InputTime};
if !crate::startup_tests::private_runtime(
"state::pointer_tests::pointer_resize_cancels_cleanly_and_finishes_on_its_own_button",
) {
return;
}
let (mut events, mut state, _) = fixture();
let (first, _wire) = window(&mut state, &mut events, 0xff112233);
let id = state.windows.ids()[&first];
state.focused_window = Some(id);
state.toggle_focused_floating();
let before = state.workspaces.placement(id);
let pointer = pointer_interaction_begin(&mut state, first.clone(), true);
pointer.motion(
&mut state,
None,
&MotionEvent {
location: (700.0, 500.0).into(),
serial: SERIAL_COUNTER.next_serial(),
time: InputTime::from_millis(2),
},
);
state.poll_window_interaction();
assert_ne!(state.workspaces.placement(id), before);
state.cancel_window_interaction();
assert_eq!(state.workspaces.placement(id), before);
let pointer = pointer_interaction_begin(&mut state, first, false);
for (button, pressed) in [(0x111, true), (0x110, false)] {
pointer.button(
&mut state,
&smithay::input::pointer::ButtonEvent {
button,
state: if pressed {
ButtonState::Pressed
} else {
ButtonState::Released
},
serial: SERIAL_COUNTER.next_serial(),
time: InputTime::from_millis(3),
},
);
}
state.poll_window_interaction();
assert!(state.pointer_operation.is_none());
assert!(
pointer.is_grabbed(),
"remaining button is drained without moving the window"
);
pointer.button(
&mut state,
&smithay::input::pointer::ButtonEvent {
button: 0x111,
state: ButtonState::Released,
serial: SERIAL_COUNTER.next_serial(),
time: InputTime::from_millis(4),
},
);
assert!(!pointer.is_grabbed());
}
fn motion(state: &mut Ferese, point: (f64, f64)) {
let pointer = state.seat.get_pointer().unwrap();
pointer.motion(
state,
None,
&MotionEvent {
location: point.into(),
serial: SERIAL_COUNTER.next_serial(),
time: smithay::backend::input::InputTime::from_millis(2),
},
);
state.poll_window_interaction();
}
fn button(state: &mut Ferese, code: u32, pressed: bool) {
let pointer = state.seat.get_pointer().unwrap();
pointer.button(
state,
&smithay::input::pointer::ButtonEvent {
button: code,
state: if pressed {
smithay::backend::input::ButtonState::Pressed
} else {
smithay::backend::input::ButtonState::Released
},
serial: SERIAL_COUNTER.next_serial(),
time: smithay::backend::input::InputTime::from_millis(3),
},
);
state.poll_window_interaction();
}
#[test]
fn pointer_drop_joins_tabs_and_can_float_and_retile_in_both_layouts() {
if !crate::startup_tests::private_runtime(
"state::pointer_tests::pointer_drop_joins_tabs_and_can_float_and_retile_in_both_layouts",
) {
return;
}
for tree in [false, true] {
let (mut events, mut state, _) = fixture();
let (first, _wire1) = window(&mut state, &mut events, 0xff112233);
let (second, _wire2) = window(&mut state, &mut events, 0xff445566);
let a = state.windows.ids()[&first];
let b = state.windows.ids()[&second];
if tree {
let mut layout = ferese_layout::LayoutTree::default();
layout.insert(a, None, Axis::Horizontal, 0.5).unwrap();
layout.insert(b, Some(a), Axis::Horizontal, 0.5).unwrap();
state.workspaces.active_mut().layout = WorkspaceLayout::Tree(layout);
state.relayout();
}
let target = state.visual_rect_for_window(&second).unwrap();
pointer_interaction_begin(&mut state, first.clone(), false);
motion(
&mut state,
(target.loc.x as f64 + target.size.w as f64 / 2.0, 250.0),
);
button(&mut state, 0x110, false);
let group = state.workspaces.active().layout.tab_group(a).unwrap();
assert!(group.windows.contains(&b));
assert_eq!(group.active, a);
assert!(state.space.element_location(&second).is_none());
pointer_interaction_begin(&mut state, first.clone(), false);
button(&mut state, 0x111, true);
motion(&mut state, (200.0, 180.0));
button(&mut state, 0x111, false);
button(&mut state, 0x110, false);
assert!(matches!(
state.workspaces.placement(a),
Some(WindowPlacement::Floating { .. })
));
assert!(state.space.element_location(&second).is_some());
let target = state.visual_rect_for_window(&second).unwrap();
pointer_interaction_begin(&mut state, first.clone(), false);
button(&mut state, 0x111, true);
motion(&mut state, (target.loc.x as f64 + 10.0, 200.0));
button(&mut state, 0x111, false);
button(&mut state, 0x110, false);
assert_eq!(state.workspaces.placement(a), Some(WindowPlacement::Tiled));
assert!(state.workspaces.active().layout.tab_group(a).is_none());
state.workspaces.validate().unwrap();
}
}
#[test]
fn pointer_drop_crosses_negative_rotated_scaled_output_and_preserves_visual_origin() {
if !crate::startup_tests::private_runtime(
"state::pointer_tests::pointer_drop_crosses_negative_rotated_scaled_output_and_preserves_visual_origin",
) {
return;
}
let (mut events, mut state, first_output) = fixture();
let second = Output::new("second".into(), first_output.physical_properties());
second.change_current_state(
first_output.current_mode(),
Some(smithay::utils::Transform::_90),
Some(smithay::output::Scale::Fractional(1.5)),
None,
);
state.space.map_output(&second, (-400, -150));
state.register_output(&second, "second".into());
let unaffected = Output::new("unaffected".into(), first_output.physical_properties());
unaffected.change_current_state(first_output.current_mode(), None, None, None);
state.space.map_output(&unaffected, (1600, 0));
state.register_output(&unaffected, "unaffected".into());
let (window, _wire) = window(&mut state, &mut events, 0xff112233);
let id = state.windows.ids()[&window];
state.animations_enabled = true;
pointer_interaction_begin(&mut state, window.clone(), false);
state.output_redraw_pending.clear();
motion(&mut state, (-200.0, 100.0));
assert!(
!state.output_redraw_pending.contains(&unaffected),
"dragging must not redraw an unrelated output"
);
let dragged = state.pointer_operation.as_ref().unwrap().visual.unwrap();
let frame = state.sample_frame(&second, Duration::ZERO);
assert!(
frame.windows.contains_key(&id),
"dragged window must be sampled on the destination output"
);
assert_eq!(frame.windows[&id].presentation.bounds.current, dragged);
button(&mut state, 0x110, false);
assert_eq!(
state.workspaces.workspace_for_window(id),
state
.output_id(&second)
.and_then(|id| state.output_workspaces.active_workspace(id))
);
assert_eq!(
state.windows.geometry(&id).unwrap().visual.current,
dragged,
"landing must animate from the pointer, not the old tile"
);
let pointer = state.seat.get_pointer().unwrap();
assert_eq!(
pointer.current_focus(),
state
.surface_under(pointer.current_location())
.map(|(surface, _)| surface)
);
state.workspaces.validate().unwrap();
}
#[test]
fn pointer_grab_cancels_on_reload_output_loss_and_unmap() {
if !crate::startup_tests::private_runtime(
"state::pointer_tests::pointer_grab_cancels_on_reload_output_loss_and_unmap",
) {
return;
}
for reason in ["reload", "output", "unmap", "overview", "escape"] {
let (mut events, mut state, output) = fixture();
let (window, _wire) = window(&mut state, &mut events, 0xff112233);
let pointer = pointer_interaction_begin(&mut state, window.clone(), false);
motion(&mut state, (700.0, 300.0));
match reason {
"reload" => {
state.reload_config_source("input { touchpad { accel-speed 0.4; disable-while-mouse-connected #true; } }".into()).unwrap();
}
"output" => state.unregister_output(&output),
"unmap" => state.remove_tiled_window(&window),
"overview" => state.set_overview_active(true),
"escape" => {
crate::input::binding_mode_tests::key(&mut state, 9, true);
crate::input::binding_mode_tests::key(&mut state, 9, false);
}
_ => unreachable!(),
}
state.poll_window_interaction();
assert!(state.pointer_operation.is_none(), "{reason}");
assert!(!pointer.is_grabbed(), "{reason}");
state.workspaces.validate().unwrap();
if reason == "reload" {
assert_eq!(state.input_settings.touchpad.accel_speed, 0.4);
assert!(state.input_settings.touchpad.disable_while_mouse_connected);
}
}
}
#[test]
fn pointer_tiled_resize_rolls_back_without_losing_concurrent_layout_changes() {
if !crate::startup_tests::private_runtime(
"state::pointer_tests::pointer_tiled_resize_rolls_back_without_losing_concurrent_layout_changes",
) {
return;
}
let (mut events, mut state, _) = fixture();
let (first, _wire1) = window(&mut state, &mut events, 0xff112233);
let (second, _wire2) = window(&mut state, &mut events, 0xff445566);
let a = state.windows.ids()[&first];
let before = state.workspaces.active().layout.clone();
pointer_interaction_begin(&mut state, first.clone(), true);
motion(&mut state, (750.0, 500.0));
assert_ne!(state.workspaces.active().layout, before);
state.cancel_window_interaction();
assert_eq!(
state
.workspaces
.active()
.layout
.geometry(Rect::new(0., 0., 800., 600.))
.unwrap(),
before.geometry(Rect::new(0., 0., 800., 600.)).unwrap()
);
pointer_interaction_begin(&mut state, first, true);
motion(&mut state, (750.0, 500.0));
let (third, _wire3) = window(&mut state, &mut events, 0xff778899);
state.poll_window_interaction();
assert!(state.pointer_operation.is_none());
for id in [a, state.windows.ids()[&second], state.windows.ids()[&third]] {
assert!(state.workspaces.active().layout.contains(id));
}
state.workspaces.validate().unwrap();
}
#[test]
fn pointer_fullscreen_and_maximize_preserve_restore_geometry_and_zero_threshold_is_safe() {
if !crate::startup_tests::private_runtime(
"state::pointer_tests::pointer_fullscreen_and_maximize_preserve_restore_geometry_and_zero_threshold_is_safe",
) {
return;
}
let (mut events, mut state, _) = fixture();
let (window, _wire) = window(&mut state, &mut events, 0xff112233);
let id = state.windows.ids()[&window];
state.focused_window = Some(id);
state.toggle_focused_floating();
state.set_floating_window_geometry(&window, (200, 150).into(), (300, 200).into());
let original = state.workspaces.placement(id);
state.set_window_fullscreen(id, true);
let pointer = state.seat.get_pointer().unwrap();
assert!(!state.begin_window_interaction(
&pointer,
window.clone(),
GrabStartData {
focus: None,
button: 0x110,
location: (300.0, 200.0).into()
},
SERIAL_COUNTER.next_serial(),
None
));
state.set_window_fullscreen(id, false);
state.animations_enabled = true;
state.set_window_maximized(id, true);
pointer_interaction_begin(&mut state, window.clone(), false);
motion(&mut state, (500.0, 350.0));
let visual = state.pointer_operation.as_ref().unwrap().visual.unwrap();
assert_eq!((visual.width, visual.height), (300.0, 200.0));
state.cancel_window_interaction();
assert!(state.windows.record(id).unwrap().maximized);
assert_eq!(state.workspaces.placement(id), original);
state.input_settings.mouse.drag_threshold = 0.0;
pointer_interaction_begin(&mut state, window, false);
button(&mut state, 0x111, true);
button(&mut state, 0x111, false);
button(&mut state, 0x110, false);
assert!(state.windows.record(id).unwrap().maximized);
assert_eq!(state.workspaces.placement(id), original);
}
#[test]
fn pointer_scroll_reaches_hidden_columns_without_changing_window_membership() {
if !crate::startup_tests::private_runtime(
"state::pointer_tests::pointer_scroll_reaches_hidden_columns_without_changing_window_membership",
) {
return;
}
let (mut events, mut state, _) = fixture();
let clients = (0..5)
.map(|_| window(&mut state, &mut events, 0xff112233))
.collect::<Vec<_>>();
let before = state.workspaces.active().layout.viewport_x().unwrap();
assert!(before > 0.0);
let window = clients.last().unwrap().0.clone();
let pointer = pointer_interaction_begin(&mut state, window, false);
motion(&mut state, (400.0, 200.0));
pointer.axis(
&mut state,
smithay::input::pointer::AxisFrame::new(smithay::backend::input::InputTime::from_millis(4))
.value(smithay::backend::input::Axis::Vertical, -200.0),
);
state.poll_window_interaction();
assert!(state.workspaces.active().layout.viewport_x().unwrap() < before);
assert_eq!(state.workspaces.active().layout.window_ids().count(), 5);
state.cancel_window_interaction();
state.workspaces.validate().unwrap();
}
#[test]
fn pointer_lock_cancels_drag_and_releases_buttons_before_lock_surfaces_receive_input() {
use smithay::reexports::wayland_server::Resource;
use smithay::reexports::wayland_protocols::ext::session_lock::v1::server::ext_session_lock_manager_v1::ExtSessionLockManagerV1;
if !crate::startup_tests::private_runtime(
"state::pointer_tests::pointer_lock_cancels_drag_and_releases_buttons_before_lock_surfaces_receive_input",
) {
return;
}
let (mut events, mut state, _) = fixture();
let (window, _window_wire) = window(&mut state, &mut events, 0xff112233);
let (server, mut wire) = std::os::unix::net::UnixStream::pair().unwrap();
let client = state
.display_handle
.insert_client(server, std::sync::Arc::new(ClientState::default()))
.unwrap();
let pointer = pointer_interaction_begin(&mut state, window, false);
motion(&mut state, (700.0, 200.0));
let manager = client
.create_resource::<ExtSessionLockManagerV1, smithay::wayland::GlobalData, Ferese>(
&state.display_handle,
1,
smithay::wayland::GlobalData,
)
.unwrap();
super::capture_privacy::tests::request(&mut wire, manager.id().protocol_id(), 1, &[2], None);
for _ in 0..20 {
super::capture_privacy::tests::dispatch(&mut events, &mut state);
if state.session_lock.active() {
break;
}
}
state.poll_window_interaction();
assert!(state.session_lock.active());
assert!(state.pointer_operation.is_none());
assert!(!pointer.is_grabbed());
assert!(state.consumed_pointer_buttons.contains(&0x110));
assert_eq!(pointer.current_focus(), None);
}
#[test]
fn pointer_floating_resize_keeps_edge_snapping_and_shift_bypass() {
if !crate::startup_tests::private_runtime(
"state::pointer_tests::pointer_floating_resize_keeps_edge_snapping_and_shift_bypass",
) {
return;
}
let (mut events, mut state, _) = fixture();
let (window, _wire) = window(&mut state, &mut events, 0xff112233);
let id = state.windows.ids()[&window];
state.focused_window = Some(id);
state.toggle_focused_floating();
state.set_floating_window_geometry(&window, (100, 100).into(), (200, 200).into());
pointer_interaction_begin(&mut state, window, true);
// The helper starts at (200, 200). A 493px delta puts the right edge 7px
// from the output edge, inside the existing 10px snap distance.
motion(&mut state, (693.0, 210.0));
let Some(WindowPlacement::Floating { rect }) = state.workspaces.placement(id) else {
panic!("floating window");
};
assert_eq!(rect.x + rect.width, 800.0);
crate::input::binding_mode_tests::key(&mut state, 50, true);
motion(&mut state, (694.0, 210.0));
let Some(WindowPlacement::Floating { rect }) = state.workspaces.placement(id) else {
panic!("floating window");
};
assert_eq!(rect.x + rect.width, 794.0);
crate::input::binding_mode_tests::key(&mut state, 50, false);
state.cancel_window_interaction();
}
#[test]
fn pointer_scrolling_left_resize_anchors_the_right_edge_and_cancels() {
if !crate::startup_tests::private_runtime(
"state::pointer_tests::pointer_scrolling_left_resize_anchors_the_right_edge_and_cancels",
) {
return;
}
let (mut events, mut state, _) = fixture();
let (window, _wire) = window(&mut state, &mut events, 0xff112233);
let id = state.windows.ids()[&window];
let before = state.windows.geometry(&id).unwrap().logical;
let pointer = state.seat.get_pointer().unwrap();
let start = Point::from((before.x, before.y + before.height / 2.0));
assert!(state.begin_window_interaction(
&pointer,
window,
GrabStartData {
focus: None,
button: 0x111,
location: start,
},
SERIAL_COUNTER.next_serial(),
Some(xdg_toplevel::ResizeEdge::Left.into())
));
button(&mut state, 0x111, true);
motion(&mut state, (start.x + 100.0, start.y));
let after = state.windows.geometry(&id).unwrap().logical;
assert_eq!(
after.x + after.width,
before.x + before.width,
"dragging the left edge moved the opposite edge: {before:?} -> {after:?}"
);
assert_eq!(after.x, before.x + 100.0);
state.cancel_window_interaction();
assert!(!pointer.is_grabbed());
assert_eq!(state.windows.geometry(&id).unwrap().logical, before);
}
+24
View File
@@ -155,6 +155,15 @@ impl Ferese {
0.03 * opacity.velocity,
);
bounds.current = crate::presentation::scaled_visual_rect(bounds.current, presence_scale);
if let Some(rect) = self
.pointer_operation
.as_ref()
.filter(|operation| operation.id == id)
.and_then(|operation| operation.visual)
{
bounds.current = rect;
bounds.velocity = Default::default();
}
bounds.target = bounds.current;
let (focus_alpha, dim) = self.window_focus_effect(id, delta, overview.opacity());
crate::presentation::WindowPresentation {
@@ -195,6 +204,21 @@ impl Ferese {
))
})
})
.chain(
self.pointer_operation
.as_ref()
.filter(|operation| {
operation.visual.is_some()
&& self.workspaces.workspace_for_window(operation.id).and_then(
|workspace| self.output_workspaces.output_for_workspace(workspace),
) != self.output_id(output)
})
.and_then(|operation| {
self.windows
.record(operation.id)
.map(|record| (operation.id, record, false))
}),
)
}
pub(crate) fn output_has_pending_visual_changes(&self, output: &Output) -> bool {
+13 -16
View File
@@ -1,10 +1,10 @@
use super::capture_privacy::tests::{ack_configure, dispatch, request, window};
use super::*;
use smithay::input::pointer::{Focus, GrabStartData, MotionEvent};
use smithay::input::pointer::{GrabStartData, MotionEvent};
use smithay::reexports::wayland_server::Resource;
use smithay::utils::SERIAL_COUNTER;
fn fixture() -> (
pub(super) fn fixture() -> (
smithay::reexports::calloop::EventLoop<'static, Ferese>,
Ferese,
Output,
@@ -1228,21 +1228,17 @@ fn malformed_resize_limits_disconnect_only_the_client_and_motion_survives() {
state.focused_window = Some(id);
state.toggle_focused_floating();
let pointer = state.seat.get_pointer().unwrap();
pointer.set_grab(
&mut state,
crate::grabs::ResizeSurfaceGrab::new(
GrabStartData {
focus: None,
button: 0x110,
location: (100.0, 100.0).into(),
},
window,
xdg_toplevel::ResizeEdge::BottomRight.into(),
Rectangle::new((100, 100).into(), (64, 48).into()),
),
assert!(state.begin_window_interaction(
&pointer,
window,
GrabStartData {
focus: None,
button: 0x110,
location: (100.0, 100.0).into()
},
SERIAL_COUNTER.next_serial(),
Focus::Clear,
);
Some(xdg_toplevel::ResizeEdge::BottomRight.into()),
));
request(&mut wire, 4, 8, &[minimum.0 as u32, minimum.1 as u32], None);
request(&mut wire, 4, 7, &[maximum.0 as u32, maximum.1 as u32], None);
request(&mut wire, 2, 6, &[], None);
@@ -1261,6 +1257,7 @@ fn malformed_resize_limits_disconnect_only_the_client_and_motion_survives() {
SERIAL_COUNTER.next_serial(),
smithay::backend::input::InputTime::from_millis(2),
);
state.poll_window_interaction();
wire.set_nonblocking(true).unwrap();
let mut messages = Vec::new();
let _ = wire.read_to_end(&mut messages);