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