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 } => {
|
InputEvent::DeviceRemoved { device } => {
|
||||||
|
state.cancel_window_interaction();
|
||||||
if let Some(backend) = state.direct_backend.as_mut() {
|
if let Some(backend) = state.direct_backend.as_mut() {
|
||||||
backend
|
backend
|
||||||
.input_devices
|
.input_devices
|
||||||
@@ -437,6 +438,7 @@ pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<()
|
|||||||
.handle()
|
.handle()
|
||||||
.insert_source(notifier, move |event, _, state| match event {
|
.insert_source(notifier, move |event, _, state| match event {
|
||||||
SessionEvent::PauseSession => {
|
SessionEvent::PauseSession => {
|
||||||
|
state.cancel_window_interaction();
|
||||||
state.set_binding_mode("default");
|
state.set_binding_mode("default");
|
||||||
let handle = state.loop_handle.clone();
|
let handle = state.loop_handle.clone();
|
||||||
if let Some(backend) = state.direct_backend.as_mut() {
|
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) {
|
fn configure_libinput_device(state: &Ferese, device: &mut LibinputDevice) {
|
||||||
let settings = state.input_settings.touchpad;
|
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
|
if device.config_tap_finger_count() > 0
|
||||||
&& let Err(error) = device.config_tap_set_enabled(settings.tap)
|
&& 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");
|
tracing::warn!(device = %device.name(), ?error, "failed to configure tap-to-click");
|
||||||
}
|
}
|
||||||
if device.config_scroll_has_natural_scroll()
|
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");
|
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};
|
pub use bindings::{Binding, BindingAction};
|
||||||
use bindings::{BindingConfig, BindingMatch, BindingModeConfig, BindingModifiers};
|
use bindings::{BindingConfig, BindingMatch, BindingModeConfig, BindingModifiers};
|
||||||
pub(crate) use bindings::{BindingModes, BindingSet, physical_keymap};
|
pub(crate) use bindings::{BindingModes, BindingSet, physical_keymap};
|
||||||
|
pub(crate) use bindings::{PointerTrigger, ScrollDirection};
|
||||||
use ferese_animation::SpringConfig;
|
use ferese_animation::SpringConfig;
|
||||||
use ferese_core::LayoutMode;
|
use ferese_core::LayoutMode;
|
||||||
use ferese_layout::{ColumnWidth, Direction, GapConfig, ViewportFocusStrategy};
|
use ferese_layout::{ColumnWidth, Direction, GapConfig, ViewportFocusStrategy};
|
||||||
pub use input_motion::InputSettings;
|
pub use input_motion::{AccelerationProfile, InputSettings};
|
||||||
use input_motion::{AnimationsConfig, InputConfig};
|
use input_motion::{AnimationsConfig, InputConfig};
|
||||||
use outputs::OutputProfileConfig;
|
use outputs::OutputProfileConfig;
|
||||||
pub use outputs::{
|
pub use outputs::{
|
||||||
@@ -1134,13 +1135,16 @@ mod tests {
|
|||||||
config.input_settings().unwrap(),
|
config.input_settings().unwrap(),
|
||||||
InputSettings {
|
InputSettings {
|
||||||
focus_follows_mouse: true,
|
focus_follows_mouse: true,
|
||||||
|
mouse: Default::default(),
|
||||||
xkb_layout: "us,de".to_owned(),
|
xkb_layout: "us,de".to_owned(),
|
||||||
xkb_variant: ",nodeadkeys".to_owned(),
|
xkb_variant: ",nodeadkeys".to_owned(),
|
||||||
xkb_options: vec!["grp:alt_shift_toggle".to_owned()],
|
xkb_options: vec!["grp:alt_shift_toggle".to_owned()],
|
||||||
repeat_rate: 30,
|
repeat_rate: 30,
|
||||||
repeat_delay_ms: 450,
|
repeat_delay_ms: 450,
|
||||||
touchpad: TouchpadSettings {
|
touchpad: TouchpadSettings {
|
||||||
|
accel_speed: 0.0,
|
||||||
tap: false,
|
tap: false,
|
||||||
|
disable_while_mouse_connected: false,
|
||||||
natural_scroll: false,
|
natural_scroll: false,
|
||||||
disable_while_typing: true,
|
disable_while_typing: true,
|
||||||
swipe_threshold: 80,
|
swipe_threshold: 80,
|
||||||
@@ -1303,7 +1307,7 @@ mod tests {
|
|||||||
let input = config.input_settings().unwrap();
|
let input = config.input_settings().unwrap();
|
||||||
let bindings = config.bindings(&input).unwrap();
|
let bindings = config.bindings(&input).unwrap();
|
||||||
|
|
||||||
assert_eq!(bindings.len(), 63);
|
assert_eq!(bindings.len(), 65);
|
||||||
for (shift, action) in [
|
for (shift, action) in [
|
||||||
(false, BindingAction::ToggleMaximized),
|
(false, BindingAction::ToggleMaximized),
|
||||||
(true, BindingAction::ToggleFullscreen),
|
(true, BindingAction::ToggleFullscreen),
|
||||||
@@ -1362,7 +1366,7 @@ mod tests {
|
|||||||
|
|
||||||
let input = replaced.input_settings().unwrap();
|
let input = replaced.input_settings().unwrap();
|
||||||
let bindings = replaced.bindings(&input).unwrap();
|
let bindings = replaced.bindings(&input).unwrap();
|
||||||
assert_eq!(bindings.len(), 63);
|
assert_eq!(bindings.len(), 65);
|
||||||
assert!(bindings.iter().any(|binding| {
|
assert!(bindings.iter().any(|binding| {
|
||||||
binding.action
|
binding.action
|
||||||
== BindingAction::Spawn(
|
== BindingAction::Spawn(
|
||||||
@@ -1372,7 +1376,7 @@ mod tests {
|
|||||||
|
|
||||||
let input = unbound.input_settings().unwrap();
|
let input = unbound.input_settings().unwrap();
|
||||||
let bindings = unbound.bindings(&input).unwrap();
|
let bindings = unbound.bindings(&input).unwrap();
|
||||||
assert_eq!(bindings.len(), 62);
|
assert_eq!(bindings.len(), 64);
|
||||||
assert!(
|
assert!(
|
||||||
!bindings
|
!bindings
|
||||||
.iter()
|
.iter()
|
||||||
|
|||||||
@@ -7,9 +7,11 @@ use super::{
|
|||||||
|
|
||||||
mod index;
|
mod index;
|
||||||
mod modes;
|
mod modes;
|
||||||
|
mod pointer;
|
||||||
pub(crate) use index::BindingSet;
|
pub(crate) use index::BindingSet;
|
||||||
pub(super) use modes::BindingModeConfig;
|
pub(super) use modes::BindingModeConfig;
|
||||||
pub(crate) use modes::BindingModes;
|
pub(crate) use modes::BindingModes;
|
||||||
|
pub(crate) use pointer::{PointerTrigger, ScrollDirection};
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
fn binding_commands(&self) -> HashMap<String, Vec<String>> {
|
fn binding_commands(&self) -> HashMap<String, Vec<String>> {
|
||||||
@@ -120,6 +122,7 @@ pub(super) fn parse_binding(
|
|||||||
let trigger = match identity.match_mode {
|
let trigger = match identity.match_mode {
|
||||||
BindingMatch::Keysym => BindingTrigger::Keysym(identity.key),
|
BindingMatch::Keysym => BindingTrigger::Keysym(identity.key),
|
||||||
BindingMatch::Physical => BindingTrigger::Physical(Keycode::new(identity.key)),
|
BindingMatch::Physical => BindingTrigger::Physical(Keycode::new(identity.key)),
|
||||||
|
BindingMatch::Pointer => BindingTrigger::Pointer(PointerTrigger::from_key(identity.key)),
|
||||||
BindingMatch::Swipe => BindingTrigger::Swipe {
|
BindingMatch::Swipe => BindingTrigger::Swipe {
|
||||||
fingers: identity.key / 4,
|
fingers: identity.key / 4,
|
||||||
direction: [
|
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 {
|
Ok(Binding {
|
||||||
modifiers: identity.modifiers,
|
modifiers: identity.modifiers,
|
||||||
trigger,
|
trigger,
|
||||||
@@ -143,6 +155,18 @@ pub(super) fn binding_identity(
|
|||||||
keymap: &xkb::Keymap,
|
keymap: &xkb::Keymap,
|
||||||
) -> Result<BindingIdentity, ConfigError> {
|
) -> Result<BindingIdentity, ConfigError> {
|
||||||
let (modifiers, key) = parse_chord(&configured.keys)?;
|
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 let Some((fingers, direction)) = parse_swipe(&key) {
|
||||||
if configured.match_mode == BindingMatch::Physical
|
if configured.match_mode == BindingMatch::Physical
|
||||||
|| modifiers != BindingModifiers::default()
|
|| modifiers != BindingModifiers::default()
|
||||||
@@ -168,7 +192,7 @@ pub(super) fn binding_identity(
|
|||||||
configured.keys
|
configured.keys
|
||||||
))
|
))
|
||||||
})?,
|
})?,
|
||||||
BindingMatch::Swipe => {
|
BindingMatch::Swipe | BindingMatch::Pointer => {
|
||||||
return Err(ConfigError::Binding(
|
return Err(ConfigError::Binding(
|
||||||
"gesture keys must use Swipe3Left/Right/Up/Down (3–5 fingers)".into(),
|
"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 {
|
let (match_mode, key) = match binding.trigger {
|
||||||
BindingTrigger::Keysym(key) => (BindingMatch::Keysym, key),
|
BindingTrigger::Keysym(key) => (BindingMatch::Keysym, key),
|
||||||
BindingTrigger::Physical(key) => (BindingMatch::Physical, key.raw()),
|
BindingTrigger::Physical(key) => (BindingMatch::Physical, key.raw()),
|
||||||
|
BindingTrigger::Pointer(trigger) => (BindingMatch::Pointer, trigger.key()),
|
||||||
BindingTrigger::Swipe { fingers, direction } => {
|
BindingTrigger::Swipe { fingers, direction } => {
|
||||||
(BindingMatch::Swipe, fingers * 4 + direction as u32)
|
(BindingMatch::Swipe, fingers * 4 + direction as u32)
|
||||||
}
|
}
|
||||||
@@ -292,6 +317,14 @@ pub(super) fn parse_action(
|
|||||||
};
|
};
|
||||||
|
|
||||||
match 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())),
|
"binding-mode" => Ok(BindingAction::BindingMode(required_argument()?.to_owned())),
|
||||||
"none" => {
|
"none" => {
|
||||||
no_argument()?;
|
no_argument()?;
|
||||||
@@ -447,6 +480,8 @@ pub(super) fn parse_workspace(argument: &str) -> Result<u8, ConfigError> {
|
|||||||
|
|
||||||
pub(super) fn default_bindings() -> Vec<BindingConfig> {
|
pub(super) fn default_bindings() -> Vec<BindingConfig> {
|
||||||
let mut bindings = vec![
|
let mut bindings = vec![
|
||||||
|
binding("Super+MouseLeft", "pointer-move", None),
|
||||||
|
binding("Super+MouseRight", "pointer-resize", None),
|
||||||
binding("Super+Space", "toggle-launcher", None),
|
binding("Super+Space", "toggle-launcher", None),
|
||||||
binding("Super+Enter", "spawn", Some("terminal")),
|
binding("Super+Enter", "spawn", Some("terminal")),
|
||||||
binding("Super+Shift+S", "spawn", Some("screenshot")),
|
binding("Super+Shift+S", "spawn", Some("screenshot")),
|
||||||
@@ -538,6 +573,7 @@ pub struct Binding {
|
|||||||
pub(super) enum BindingTrigger {
|
pub(super) enum BindingTrigger {
|
||||||
Keysym(u32),
|
Keysym(u32),
|
||||||
Physical(Keycode),
|
Physical(Keycode),
|
||||||
|
Pointer(PointerTrigger),
|
||||||
Swipe {
|
Swipe {
|
||||||
fingers: u32,
|
fingers: u32,
|
||||||
direction: crate::gestures::SwipeDirection,
|
direction: crate::gestures::SwipeDirection,
|
||||||
@@ -563,6 +599,8 @@ pub enum BindingAction {
|
|||||||
FocusLastWindow,
|
FocusLastWindow,
|
||||||
FocusFloating,
|
FocusFloating,
|
||||||
FocusMru(bool),
|
FocusMru(bool),
|
||||||
|
PointerMove,
|
||||||
|
PointerResize,
|
||||||
Move(ferese_layout::Direction),
|
Move(ferese_layout::Direction),
|
||||||
Resize(ferese_layout::Direction),
|
Resize(ferese_layout::Direction),
|
||||||
SwitchWorkspace(u8),
|
SwitchWorkspace(u8),
|
||||||
@@ -664,7 +702,7 @@ impl Binding {
|
|||||||
&& match self.trigger {
|
&& match self.trigger {
|
||||||
BindingTrigger::Keysym(expected) => keysyms.contains(&expected),
|
BindingTrigger::Keysym(expected) => keysyms.contains(&expected),
|
||||||
BindingTrigger::Physical(expected) => expected == keycode,
|
BindingTrigger::Physical(expected) => expected == keycode,
|
||||||
BindingTrigger::Swipe { .. } => false,
|
BindingTrigger::Swipe { .. } | BindingTrigger::Pointer(_) => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -704,6 +742,8 @@ impl Binding {
|
|||||||
"Next window in focus history"
|
"Next window in focus history"
|
||||||
}
|
}
|
||||||
.into(),
|
.into(),
|
||||||
|
BindingAction::PointerMove => "Drag window".into(),
|
||||||
|
BindingAction::PointerResize => "Resize window with pointer".into(),
|
||||||
BindingAction::Move(direction) => format!("Move window {direction:?}"),
|
BindingAction::Move(direction) => format!("Move window {direction:?}"),
|
||||||
BindingAction::Resize(direction) => format!("Resize window {direction:?}"),
|
BindingAction::Resize(direction) => format!("Resize window {direction:?}"),
|
||||||
BindingAction::SwitchWorkspace(index) => format!("Go to workspace {index}"),
|
BindingAction::SwitchWorkspace(index) => format!("Go to workspace {index}"),
|
||||||
@@ -757,6 +797,7 @@ impl Binding {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
parts.push(match self.trigger {
|
parts.push(match self.trigger {
|
||||||
|
BindingTrigger::Pointer(trigger) => trigger.label(),
|
||||||
BindingTrigger::Keysym(symbol) => {
|
BindingTrigger::Keysym(symbol) => {
|
||||||
let name = xkb::keysym_get_name(xkb::Keysym::new(symbol));
|
let name = xkb::keysym_get_name(xkb::Keysym::new(symbol));
|
||||||
match name.as_str() {
|
match name.as_str() {
|
||||||
@@ -798,6 +839,8 @@ pub(super) enum BindingMatch {
|
|||||||
Keysym,
|
Keysym,
|
||||||
Physical,
|
Physical,
|
||||||
Swipe,
|
Swipe,
|
||||||
|
#[serde(skip)]
|
||||||
|
Pointer,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
|||||||
@@ -10,27 +10,30 @@ use super::{
|
|||||||
binding_identity_for_runtime,
|
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.
|
/// The ordered slice remains available for gestures and the shortcut guide.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub(crate) struct BindingSet {
|
pub(crate) struct BindingSet {
|
||||||
ordered: Vec<Binding>,
|
ordered: Vec<Binding>,
|
||||||
keyboard: HashMap<BindingIdentity, usize>,
|
index: HashMap<BindingIdentity, usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Vec<Binding>> for BindingSet {
|
impl From<Vec<Binding>> for BindingSet {
|
||||||
fn from(ordered: Vec<Binding>) -> Self {
|
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);
|
let identity = binding_identity_for_runtime(binding);
|
||||||
|
|
||||||
if identity.match_mode != BindingMatch::Swipe {
|
if matches!(
|
||||||
keyboard.entry(identity).or_insert(index);
|
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 {
|
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(
|
pub(crate) fn binding_for_key(
|
||||||
&self,
|
&self,
|
||||||
keycode: Keycode,
|
keycode: Keycode,
|
||||||
@@ -58,13 +81,13 @@ impl BindingSet {
|
|||||||
alt,
|
alt,
|
||||||
shift,
|
shift,
|
||||||
};
|
};
|
||||||
let physical = self.keyboard.get(&BindingIdentity {
|
let physical = self.index.get(&BindingIdentity {
|
||||||
modifiers,
|
modifiers,
|
||||||
match_mode: BindingMatch::Physical,
|
match_mode: BindingMatch::Physical,
|
||||||
key: keycode.raw(),
|
key: keycode.raw(),
|
||||||
});
|
});
|
||||||
let symbolic = symbols.iter().filter_map(|&key| {
|
let symbolic = symbols.iter().filter_map(|&key| {
|
||||||
self.keyboard.get(&BindingIdentity {
|
self.index.get(&BindingIdentity {
|
||||||
modifiers,
|
modifiers,
|
||||||
match_mode: BindingMatch::Keysym,
|
match_mode: BindingMatch::Keysym,
|
||||||
key,
|
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(),
|
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 {
|
Ok(InputSettings {
|
||||||
|
mouse,
|
||||||
focus_follows_mouse: self.input.focus_follows_mouse,
|
focus_follows_mouse: self.input.focus_follows_mouse,
|
||||||
xkb_layout: self.input.xkb_layout.clone(),
|
xkb_layout: self.input.xkb_layout.clone(),
|
||||||
xkb_variant: self.input.xkb_variant.clone(),
|
xkb_variant: self.input.xkb_variant.clone(),
|
||||||
@@ -39,6 +76,8 @@ impl Config {
|
|||||||
repeat_delay_ms: self.input.repeat_delay_ms,
|
repeat_delay_ms: self.input.repeat_delay_ms,
|
||||||
touchpad: TouchpadSettings {
|
touchpad: TouchpadSettings {
|
||||||
tap: self.input.touchpad.tap,
|
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,
|
natural_scroll: self.input.touchpad.natural_scroll,
|
||||||
disable_while_typing: self.input.touchpad.disable_while_typing,
|
disable_while_typing: self.input.touchpad.disable_while_typing,
|
||||||
swipe_threshold: self.input.touchpad.swipe_threshold,
|
swipe_threshold: self.input.touchpad.swipe_threshold,
|
||||||
@@ -85,11 +124,14 @@ pub struct InputSettings {
|
|||||||
pub repeat_rate: i32,
|
pub repeat_rate: i32,
|
||||||
pub repeat_delay_ms: i32,
|
pub repeat_delay_ms: i32,
|
||||||
pub touchpad: TouchpadSettings,
|
pub touchpad: TouchpadSettings,
|
||||||
|
pub mouse: MouseSettings,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
pub struct TouchpadSettings {
|
pub struct TouchpadSettings {
|
||||||
pub tap: bool,
|
pub tap: bool,
|
||||||
|
pub accel_speed: f64,
|
||||||
|
pub disable_while_mouse_connected: bool,
|
||||||
pub natural_scroll: bool,
|
pub natural_scroll: bool,
|
||||||
pub disable_while_typing: bool,
|
pub disable_while_typing: bool,
|
||||||
pub swipe_threshold: u16,
|
pub swipe_threshold: u16,
|
||||||
@@ -115,6 +157,8 @@ pub(super) struct InputConfig {
|
|||||||
pub(super) repeat_delay_ms: i32,
|
pub(super) repeat_delay_ms: i32,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub(super) touchpad: TouchpadConfig,
|
pub(super) touchpad: TouchpadConfig,
|
||||||
|
#[serde(default)]
|
||||||
|
pub(super) mouse: MouseSettings,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for InputConfig {
|
impl Default for InputConfig {
|
||||||
@@ -127,14 +171,19 @@ impl Default for InputConfig {
|
|||||||
repeat_rate: default_repeat_rate(),
|
repeat_rate: default_repeat_rate(),
|
||||||
repeat_delay_ms: default_repeat_delay(),
|
repeat_delay_ms: default_repeat_delay(),
|
||||||
touchpad: TouchpadConfig::default(),
|
touchpad: TouchpadConfig::default(),
|
||||||
|
mouse: MouseSettings::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub(super) struct TouchpadConfig {
|
pub(super) struct TouchpadConfig {
|
||||||
|
#[serde(default)]
|
||||||
|
pub(super) accel_speed: f64,
|
||||||
#[serde(default = "enabled_by_default")]
|
#[serde(default = "enabled_by_default")]
|
||||||
pub(super) tap: bool,
|
pub(super) tap: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
pub(super) disable_while_mouse_connected: bool,
|
||||||
#[serde(default = "enabled_by_default")]
|
#[serde(default = "enabled_by_default")]
|
||||||
pub(super) natural_scroll: bool,
|
pub(super) natural_scroll: bool,
|
||||||
#[serde(default = "enabled_by_default")]
|
#[serde(default = "enabled_by_default")]
|
||||||
@@ -147,6 +196,8 @@ impl Default for TouchpadConfig {
|
|||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
tap: true,
|
tap: true,
|
||||||
|
accel_speed: 0.0,
|
||||||
|
disable_while_mouse_connected: false,
|
||||||
natural_scroll: true,
|
natural_scroll: true,
|
||||||
disable_while_typing: true,
|
disable_while_typing: true,
|
||||||
swipe_threshold: default_swipe_threshold(),
|
swipe_threshold: default_swipe_threshold(),
|
||||||
@@ -160,6 +211,42 @@ pub(super) type AnimationsConfig = ferese_animation::MotionSettings;
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
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]
|
#[test]
|
||||||
fn compositor_speed_uses_shared_motion_bounds() {
|
fn compositor_speed_uses_shared_motion_bounds() {
|
||||||
for speed in [1e-100, 0.009, 101.0, 1e100] {
|
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;
|
mod resize_grab;
|
||||||
|
|
||||||
pub use move_grab::MoveSurfaceGrab;
|
pub use resize_grab::ResizeEdge;
|
||||||
pub use resize_grab::{ResizeEdge, ResizeSurfaceGrab};
|
|
||||||
|
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_protocols::xdg::shell::server::xdg_toplevel;
|
||||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
|
||||||
use smithay::utils::{Logical, Point, Rectangle, Size};
|
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)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
pub struct ResizeEdge(xdg_toplevel::ResizeEdge);
|
pub struct ResizeEdge(xdg_toplevel::ResizeEdge);
|
||||||
@@ -23,6 +11,15 @@ impl From<xdg_toplevel::ResizeEdge> for ResizeEdge {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl 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 {
|
pub fn at(point: Point<f64, Logical>, rect: Rectangle<i32, Logical>) -> Self {
|
||||||
use xdg_toplevel::ResizeEdge::*;
|
use xdg_toplevel::ResizeEdge::*;
|
||||||
let x = (point.x - rect.loc.x as f64) / rect.size.w.max(1) as f64;
|
let x = (point.x - rect.loc.x as f64) / rect.size.w.max(1) as f64;
|
||||||
@@ -46,7 +43,7 @@ impl ResizeEdge {
|
|||||||
_ => Bottom,
|
_ => Bottom,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
fn left(self) -> bool {
|
pub(crate) fn left(self) -> bool {
|
||||||
matches!(
|
matches!(
|
||||||
self.0,
|
self.0,
|
||||||
xdg_toplevel::ResizeEdge::Left
|
xdg_toplevel::ResizeEdge::Left
|
||||||
@@ -55,7 +52,7 @@ impl ResizeEdge {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn right(self) -> bool {
|
pub(crate) fn right(self) -> bool {
|
||||||
matches!(
|
matches!(
|
||||||
self.0,
|
self.0,
|
||||||
xdg_toplevel::ResizeEdge::Right
|
xdg_toplevel::ResizeEdge::Right
|
||||||
@@ -64,7 +61,7 @@ impl ResizeEdge {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn top(self) -> bool {
|
pub(crate) fn top(self) -> bool {
|
||||||
matches!(
|
matches!(
|
||||||
self.0,
|
self.0,
|
||||||
xdg_toplevel::ResizeEdge::Top
|
xdg_toplevel::ResizeEdge::Top
|
||||||
@@ -73,7 +70,7 @@ impl ResizeEdge {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn bottom(self) -> bool {
|
pub(crate) fn bottom(self) -> bool {
|
||||||
matches!(
|
matches!(
|
||||||
self.0,
|
self.0,
|
||||||
xdg_toplevel::ResizeEdge::Bottom
|
xdg_toplevel::ResizeEdge::Bottom
|
||||||
@@ -83,274 +80,7 @@ impl ResizeEdge {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ResizeSurfaceGrab {
|
pub(crate) fn constrained_size(
|
||||||
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(
|
|
||||||
initial: Size<i32, Logical>,
|
initial: Size<i32, Logical>,
|
||||||
delta: Point<f64, Logical>,
|
delta: Point<f64, Logical>,
|
||||||
edges: ResizeEdge,
|
edges: ResizeEdge,
|
||||||
@@ -392,7 +122,7 @@ fn constrained_size(
|
|||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resized_rect(
|
pub(crate) fn resized_rect(
|
||||||
initial: Rectangle<i32, Logical>,
|
initial: Rectangle<i32, Logical>,
|
||||||
size: Size<i32, Logical>,
|
size: Size<i32, Logical>,
|
||||||
edges: ResizeEdge,
|
edges: ResizeEdge,
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
use smithay::desktop::Window;
|
|
||||||
use smithay::input::pointer::{
|
use smithay::input::pointer::{
|
||||||
AxisFrame, ButtonEvent, GestureHoldBeginEvent, GestureHoldEndEvent, GesturePinchBeginEvent,
|
AxisFrame, ButtonEvent, GestureHoldBeginEvent, GestureHoldEndEvent, GesturePinchBeginEvent,
|
||||||
GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent, GestureSwipeEndEvent,
|
GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent, GestureSwipeEndEvent,
|
||||||
@@ -6,22 +5,16 @@ use smithay::input::pointer::{
|
|||||||
RelativeMotionEvent,
|
RelativeMotionEvent,
|
||||||
};
|
};
|
||||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
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::Ferese;
|
||||||
use crate::floating::AxisSnap;
|
|
||||||
|
|
||||||
pub struct MoveSurfaceGrab {
|
pub(crate) struct WindowPointerGrab {
|
||||||
pub start_data: GrabStartData<Ferese>,
|
pub start_data: GrabStartData<Ferese>,
|
||||||
pub window: Window,
|
|
||||||
pub initial_location: Point<i32, Logical>,
|
|
||||||
pub initial_size: Size<i32, Logical>,
|
|
||||||
pub finished: bool,
|
pub finished: bool,
|
||||||
pub snap_x: AxisSnap,
|
|
||||||
pub snap_y: AxisSnap,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PointerGrab<Ferese> for MoveSurfaceGrab {
|
impl PointerGrab<Ferese> for WindowPointerGrab {
|
||||||
fn motion(
|
fn motion(
|
||||||
&mut self,
|
&mut self,
|
||||||
data: &mut Ferese,
|
data: &mut Ferese,
|
||||||
@@ -30,36 +23,21 @@ impl PointerGrab<Ferese> for MoveSurfaceGrab {
|
|||||||
event: &MotionEvent,
|
event: &MotionEvent,
|
||||||
) {
|
) {
|
||||||
handle.motion(data, None, event);
|
handle.motion(data, None, event);
|
||||||
let location = self.initial_location.to_f64() + (event.location - self.start_data.location);
|
if !self.finished
|
||||||
let raw = ferese_layout::Rect::new(
|
&& let Some(operation) = data.pointer_operation.as_mut()
|
||||||
location.x,
|
{
|
||||||
location.y,
|
operation.location = event.location;
|
||||||
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),
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
.to_i32_round();
|
|
||||||
data.set_floating_window_geometry(&self.window, location, self.initial_size);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn relative_motion(
|
fn relative_motion(
|
||||||
&mut self,
|
&mut self,
|
||||||
data: &mut Ferese,
|
data: &mut Ferese,
|
||||||
handle: &mut PointerInnerHandle<'_, Ferese>,
|
handle: &mut PointerInnerHandle<'_, Ferese>,
|
||||||
focus: Option<(WlSurface, Point<f64, Logical>)>,
|
_focus: Option<(WlSurface, Point<f64, Logical>)>,
|
||||||
event: &RelativeMotionEvent,
|
event: &RelativeMotionEvent,
|
||||||
) {
|
) {
|
||||||
handle.relative_motion(data, focus, event);
|
handle.relative_motion(data, None, event);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn button(
|
fn button(
|
||||||
@@ -68,21 +46,46 @@ impl PointerGrab<Ferese> for MoveSurfaceGrab {
|
|||||||
handle: &mut PointerInnerHandle<'_, Ferese>,
|
handle: &mut PointerInnerHandle<'_, Ferese>,
|
||||||
event: &ButtonEvent,
|
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);
|
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;
|
self.finished = true;
|
||||||
data.remember_floating(&self.window);
|
if let Some(operation) = data.pointer_operation.as_mut() {
|
||||||
handle.unset_grab(self, data, event.serial, event.time, true);
|
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(
|
fn axis(
|
||||||
&mut self,
|
&mut self,
|
||||||
data: &mut Ferese,
|
data: &mut Ferese,
|
||||||
handle: &mut PointerInnerHandle<'_, Ferese>,
|
_handle: &mut PointerInnerHandle<'_, Ferese>,
|
||||||
frame: AxisFrame,
|
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>) {
|
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) {
|
fn unset(&mut self, data: &mut Ferese) {
|
||||||
if self.finished {
|
// Resolve focus after the layout settles and the pointer mutex is released.
|
||||||
return;
|
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::Ferese;
|
||||||
use crate::grabs::{MoveSurfaceGrab, ResizeEdge, ResizeSurfaceGrab};
|
use crate::grabs::ResizeEdge;
|
||||||
|
|
||||||
impl XdgShellHandler for Ferese {
|
impl XdgShellHandler for Ferese {
|
||||||
fn xdg_shell_state(&mut self) -> &mut XdgShellState {
|
fn xdg_shell_state(&mut self) -> &mut XdgShellState {
|
||||||
@@ -150,26 +150,7 @@ impl XdgShellHandler for Ferese {
|
|||||||
else {
|
else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let Some(initial_rect) = self.visual_rect_for_window(&window) else {
|
self.begin_window_interaction(&pointer, window, start_data, serial, None);
|
||||||
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,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resize_request(
|
fn resize_request(
|
||||||
@@ -203,18 +184,15 @@ impl XdgShellHandler for Ferese {
|
|||||||
else {
|
else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let Some(rect) = self.visual_rect_for_window(&window) else {
|
if edges != xdg_toplevel::ResizeEdge::None {
|
||||||
return;
|
self.begin_window_interaction(
|
||||||
};
|
&pointer,
|
||||||
if !self.is_floating_window(&window) {
|
window,
|
||||||
return;
|
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) {
|
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;
|
use ferese_layout::Direction;
|
||||||
|
pub(crate) use pointer::ScrollBindings;
|
||||||
use smithay::backend::input::{
|
use smithay::backend::input::{
|
||||||
AbsolutePositionEvent, Axis, AxisSource, ButtonState, Event, GestureBeginEvent,
|
AbsolutePositionEvent, Axis, AxisSource, ButtonState, Device, Event, GestureBeginEvent,
|
||||||
GestureEndEvent, GestureSwipeUpdateEvent as _, InputBackend, InputEvent, KeyState,
|
GestureEndEvent, GestureSwipeUpdateEvent as _, InputBackend, InputEvent, KeyState,
|
||||||
KeyboardKeyEvent, PointerAxisEvent, PointerButtonEvent, PointerMotionEvent, Switch,
|
KeyboardKeyEvent, PointerAxisEvent, PointerButtonEvent, PointerMotionEvent, Switch,
|
||||||
SwitchState, SwitchToggleEvent, TouchEvent,
|
SwitchState, SwitchToggleEvent, TouchEvent,
|
||||||
@@ -20,11 +24,10 @@ use smithay::wayland::pointer_constraints::{PointerConstraint, with_pointer_cons
|
|||||||
use crate::Ferese;
|
use crate::Ferese;
|
||||||
use crate::config::BindingAction;
|
use crate::config::BindingAction;
|
||||||
use crate::gestures::SwipeDirection;
|
use crate::gestures::SwipeDirection;
|
||||||
use crate::grabs::{MoveSurfaceGrab, ResizeEdge, ResizeSurfaceGrab};
|
use crate::grabs::ResizeEdge;
|
||||||
use crate::process::spawn_client;
|
use crate::process::spawn_client;
|
||||||
|
|
||||||
const BTN_LEFT: u32 = 0x110;
|
const BTN_LEFT: u32 = 0x110;
|
||||||
const BTN_RIGHT: u32 = 0x111;
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub(crate) mod binding_mode_tests;
|
pub(crate) mod binding_mode_tests;
|
||||||
@@ -130,6 +133,11 @@ impl Ferese {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn process_input_event<I: InputBackend>(&mut self, event: InputEvent<I>) {
|
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() {
|
if self.input_capture.active() {
|
||||||
self.set_binding_mode("default");
|
self.set_binding_mode("default");
|
||||||
}
|
}
|
||||||
@@ -372,6 +380,11 @@ impl Ferese {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
let symbol = keysym.modified_sym().raw();
|
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 data.binding_modes.active().is_some() && symbol == keysyms::KEY_Escape {
|
||||||
if state == KeyState::Pressed {
|
if state == KeyState::Pressed {
|
||||||
data.intercepted_keys.insert(keycode);
|
data.intercepted_keys.insert(keycode);
|
||||||
@@ -586,17 +599,21 @@ impl Ferese {
|
|||||||
}
|
}
|
||||||
InputEvent::PointerButton { event, .. } => {
|
InputEvent::PointerButton { event, .. } => {
|
||||||
self.last_pointer_time = event.time();
|
self.last_pointer_time = event.time();
|
||||||
if self.input_capture.active() {
|
if event.state() == ButtonState::Pressed {
|
||||||
if self.input_capture.captures(2) {
|
// A disconnected device may never deliver its consumed release.
|
||||||
self.input_capture.push(serde_json::json!({"type":"button", "button":event.button_code(), "pressed":event.state() == ButtonState::Pressed}));
|
self.consumed_pointer_buttons.remove(&event.button_code());
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
if event.state() == ButtonState::Released
|
if event.state() == ButtonState::Released
|
||||||
&& self.consumed_pointer_buttons.remove(&event.button_code())
|
&& self.consumed_pointer_buttons.remove(&event.button_code())
|
||||||
{
|
{
|
||||||
return;
|
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 pointer = self.seat.get_pointer().expect("seat has a pointer");
|
||||||
let serial = SERIAL_COUNTER.next_serial();
|
let serial = SERIAL_COUNTER.next_serial();
|
||||||
if self.session_lock.active() {
|
if self.session_lock.active() {
|
||||||
@@ -709,8 +726,11 @@ impl Ferese {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if event.state() == ButtonState::Pressed
|
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(
|
pointer.button(
|
||||||
self,
|
self,
|
||||||
&ButtonEvent {
|
&ButtonEvent {
|
||||||
@@ -764,47 +784,82 @@ impl Ferese {
|
|||||||
}
|
}
|
||||||
return;
|
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");
|
let pointer = self.seat.get_pointer().expect("seat has a pointer");
|
||||||
if self.scroll_tab_indicator(
|
if !pointer.is_grabbed()
|
||||||
pointer.current_location(),
|
&& self.scroll_tab_indicator(
|
||||||
if vertical != 0.0 {
|
pointer.current_location(),
|
||||||
vertical
|
if vertical != 0.0 {
|
||||||
} else {
|
vertical
|
||||||
horizontal
|
} else {
|
||||||
},
|
horizontal
|
||||||
) {
|
},
|
||||||
|
)
|
||||||
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if self.scroll_overview_strip(
|
if !pointer.is_grabbed()
|
||||||
pointer.current_location(),
|
&& self.scroll_overview_strip(
|
||||||
if horizontal != 0.0 {
|
pointer.current_location(),
|
||||||
horizontal
|
if horizontal != 0.0 {
|
||||||
} else {
|
horizontal
|
||||||
vertical
|
} else {
|
||||||
},
|
vertical
|
||||||
) {
|
},
|
||||||
|
)
|
||||||
|
{
|
||||||
return;
|
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);
|
let mut frame = AxisFrame::new(event.time()).source(source);
|
||||||
if horizontal != 0.0 {
|
if horizontal != 0.0 {
|
||||||
frame = frame.value(Axis::Horizontal, horizontal);
|
frame = frame.value(Axis::Horizontal, horizontal);
|
||||||
if let Some(value) = event.amount_v120(Axis::Horizontal) {
|
if let Some(value) = scaled_v120[0] {
|
||||||
frame = frame.v120(Axis::Horizontal, value as i32);
|
frame = frame.v120(Axis::Horizontal, value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if vertical != 0.0 {
|
if vertical != 0.0 {
|
||||||
frame = frame.value(Axis::Vertical, vertical);
|
frame = frame.value(Axis::Vertical, vertical);
|
||||||
if let Some(value) = event.amount_v120(Axis::Vertical) {
|
if let Some(value) = scaled_v120[1] {
|
||||||
frame = frame.v120(Axis::Vertical, value as i32);
|
frame = frame.v120(Axis::Vertical, value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if source == AxisSource::Finger {
|
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);
|
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);
|
frame = frame.stop(Axis::Vertical);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -925,23 +980,13 @@ impl Ferese {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
fn start_floating_pointer_grab(
|
fn start_pointer_window_grab(
|
||||||
&mut self,
|
&mut self,
|
||||||
pointer: &PointerHandle<Self>,
|
pointer: &PointerHandle<Self>,
|
||||||
button: u32,
|
button: u32,
|
||||||
serial: Serial,
|
serial: Serial,
|
||||||
|
resize: bool,
|
||||||
) -> 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();
|
let location = pointer.current_location();
|
||||||
if self.layer_under(location).is_some() {
|
if self.layer_under(location).is_some() {
|
||||||
return false;
|
return false;
|
||||||
@@ -949,46 +994,21 @@ impl Ferese {
|
|||||||
let Some(window) = self.window_under_visual(location) else {
|
let Some(window) = self.window_under_visual(location) else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
if !self.is_floating_window(&window) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
let Some(rect) = self.visual_rect_for_window(&window) else {
|
let Some(rect) = self.visual_rect_for_window(&window) else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
self.focus_window_at(location, serial, true);
|
self.focus_window_at(location, serial, true);
|
||||||
let start_data = GrabStartData {
|
self.begin_window_interaction(
|
||||||
focus: None,
|
pointer,
|
||||||
button,
|
window,
|
||||||
location,
|
GrabStartData {
|
||||||
};
|
focus: None,
|
||||||
|
button,
|
||||||
if button == BTN_LEFT {
|
location,
|
||||||
pointer.set_grab(
|
},
|
||||||
self,
|
serial,
|
||||||
MoveSurfaceGrab {
|
resize.then(|| ResizeEdge::at(location, rect)),
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn absolute_event_position<I, E>(&self, event: &E) -> Option<Point<f64, Logical>>
|
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) {
|
fn execute_binding(&mut self, action: BindingAction) {
|
||||||
match action {
|
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::BindingMode(name) => self.set_binding_mode(&name),
|
||||||
BindingAction::Spawn(argv) => {
|
BindingAction::Spawn(argv) => {
|
||||||
if let Some(child) = spawn_client(
|
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<'_>),
|
mut redraw: impl FnMut(&mut Ferese, RedrawRequest<'_>),
|
||||||
) {
|
) {
|
||||||
crate::backends::direct::gamma::sync_night_light(state);
|
crate::backends::direct::gamma::sync_night_light(state);
|
||||||
|
state.poll_window_interaction();
|
||||||
state.process_pending_dmabuf_imports();
|
state.process_pending_dmabuf_imports();
|
||||||
state.prune_screencopies();
|
state.prune_screencopies();
|
||||||
state.refresh_capture_sessions();
|
state.refresh_capture_sessions();
|
||||||
|
|||||||
@@ -232,7 +232,7 @@ fn scene_elements(
|
|||||||
presentation_alpha,
|
presentation_alpha,
|
||||||
))
|
))
|
||||||
};
|
};
|
||||||
let windows = if workspace.is_none() && state.overview.is_active() {
|
let mut windows = if workspace.is_none() && state.overview.is_active() {
|
||||||
state
|
state
|
||||||
.windows
|
.windows
|
||||||
.overview_windows()
|
.overview_windows()
|
||||||
@@ -258,6 +258,16 @@ fn scene_elements(
|
|||||||
.collect::<Vec<_>>()
|
.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() {
|
let mut closing = if workspace.is_some() {
|
||||||
Default::default()
|
Default::default()
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -299,6 +299,7 @@ impl Ferese {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn prepare_lock_input(&mut self) {
|
pub(crate) fn prepare_lock_input(&mut self) {
|
||||||
|
self.cancel_window_interaction();
|
||||||
// Reject desktop grabs installed after acquisition, but keep the lock
|
// Reject desktop grabs installed after acquisition, but keep the lock
|
||||||
// owner's implicit click grab intact until its button release.
|
// owner's implicit click grab intact until its button release.
|
||||||
if let Some(keyboard) = self.seat.get_keyboard()
|
if let Some(keyboard) = self.seat.get_keyboard()
|
||||||
@@ -482,6 +483,7 @@ impl SessionLockHandler for Ferese {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
self.set_overview_active(false);
|
self.set_overview_active(false);
|
||||||
|
self.cancel_window_interaction();
|
||||||
let serial = SERIAL_COUNTER.next_serial();
|
let serial = SERIAL_COUNTER.next_serial();
|
||||||
if let Some(pointer) = self.seat.get_pointer() {
|
if let Some(pointer) = self.seat.get_pointer() {
|
||||||
pointer.unset_grab(self, serial, smithay::backend::input::InputTime::now());
|
pointer.unset_grab(self, serial, smithay::backend::input::InputTime::now());
|
||||||
|
|||||||
+12
-2
@@ -11,6 +11,8 @@ mod hit_testing;
|
|||||||
mod layout;
|
mod layout;
|
||||||
mod navigation;
|
mod navigation;
|
||||||
mod outputs;
|
mod outputs;
|
||||||
|
#[cfg(test)]
|
||||||
|
mod pointer_tests;
|
||||||
mod reconciliation;
|
mod reconciliation;
|
||||||
pub(crate) mod tab_indicator;
|
pub(crate) mod tab_indicator;
|
||||||
mod tabs;
|
mod tabs;
|
||||||
@@ -488,6 +490,9 @@ pub struct Ferese {
|
|||||||
pub(crate) tab_indicator_settings: crate::config::TabIndicatorSettings,
|
pub(crate) tab_indicator_settings: crate::config::TabIndicatorSettings,
|
||||||
pub(crate) tab_bars: HashMap<WorkspaceId, Vec<ferese_layout::TabBar>>,
|
pub(crate) tab_bars: HashMap<WorkspaceId, Vec<ferese_layout::TabBar>>,
|
||||||
pub(crate) consumed_pointer_buttons: HashSet<u32>,
|
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) pending_layer_focus: Option<(WlSurface, Instant)>,
|
||||||
pub(crate) consumed_touch_slots: HashSet<smithay::backend::input::TouchSlot>,
|
pub(crate) consumed_touch_slots: HashSet<smithay::backend::input::TouchSlot>,
|
||||||
pub(crate) focus_history: ferese_core::FocusHistory,
|
pub(crate) focus_history: ferese_core::FocusHistory,
|
||||||
@@ -812,6 +817,9 @@ impl Ferese {
|
|||||||
tab_indicator_settings: config.tab_indicator,
|
tab_indicator_settings: config.tab_indicator,
|
||||||
tab_bars: HashMap::new(),
|
tab_bars: HashMap::new(),
|
||||||
consumed_pointer_buttons: HashSet::new(),
|
consumed_pointer_buttons: HashSet::new(),
|
||||||
|
pointer_scroll: Default::default(),
|
||||||
|
pointer_operation: None,
|
||||||
|
window_grab_focus_pending: false,
|
||||||
pending_layer_focus: None,
|
pending_layer_focus: None,
|
||||||
consumed_touch_slots: HashSet::new(),
|
consumed_touch_slots: HashSet::new(),
|
||||||
focus_history: Default::default(),
|
focus_history: Default::default(),
|
||||||
@@ -1022,7 +1030,9 @@ impl Ferese {
|
|||||||
self.advance_animations(Instant::now());
|
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;
|
let outputs_changed = self.output_profiles != config.output_profiles;
|
||||||
if let Some(workspaces) = workspaces {
|
if let Some(workspaces) = workspaces {
|
||||||
self.workspaces = workspaces;
|
self.workspaces = workspaces;
|
||||||
@@ -1089,7 +1099,7 @@ impl Ferese {
|
|||||||
crate::daemon::Runner::refresh(&runner, self);
|
crate::daemon::Runner::refresh(&runner, self);
|
||||||
}
|
}
|
||||||
|
|
||||||
if touchpad_changed {
|
if pointer_settings_changed {
|
||||||
crate::backends::direct::reload_input_devices(self);
|
crate::backends::direct::reload_input_devices(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,25 @@ use super::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
impl Ferese {
|
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(
|
pub(super) fn adjust_floating_direction(
|
||||||
&mut self,
|
&mut self,
|
||||||
id: WindowId,
|
id: WindowId,
|
||||||
@@ -238,18 +257,24 @@ impl Ferese {
|
|||||||
location: Point<i32, Logical>,
|
location: Point<i32, Logical>,
|
||||||
size: Size<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 {
|
let Some(id) = self.windows.ids().get(window).copied() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
self.windows
|
self.windows
|
||||||
.update(id, |w| w.resize_anchor.take())
|
.update(id, |w| w.resize_anchor.take())
|
||||||
.flatten();
|
.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) {
|
if let Err(error) = self.workspaces.set_floating_rect(id, rect) {
|
||||||
tracing::error!(%error, ?id, "failed to update floating window geometry");
|
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.world_x = None);
|
||||||
self.windows
|
self.windows
|
||||||
.update(id, |record| record.coupled_width = None);
|
.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();
|
self.sync_window_stacking();
|
||||||
// Move/resize/cancel callbacks run with Smithay's pointer mutex held.
|
// Move/resize/cancel callbacks run with Smithay's pointer mutex held.
|
||||||
// Cursor rendering reads current_location(), which would lock it again.
|
// 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 {
|
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 {
|
let Some(output_id) = self.output_id(output) else {
|
||||||
return false;
|
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,
|
0.03 * opacity.velocity,
|
||||||
);
|
);
|
||||||
bounds.current = crate::presentation::scaled_visual_rect(bounds.current, presence_scale);
|
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;
|
bounds.target = bounds.current;
|
||||||
let (focus_alpha, dim) = self.window_focus_effect(id, delta, overview.opacity());
|
let (focus_alpha, dim) = self.window_focus_effect(id, delta, overview.opacity());
|
||||||
crate::presentation::WindowPresentation {
|
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 {
|
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::capture_privacy::tests::{ack_configure, dispatch, request, window};
|
||||||
use super::*;
|
use super::*;
|
||||||
use smithay::input::pointer::{Focus, GrabStartData, MotionEvent};
|
use smithay::input::pointer::{GrabStartData, MotionEvent};
|
||||||
use smithay::reexports::wayland_server::Resource;
|
use smithay::reexports::wayland_server::Resource;
|
||||||
use smithay::utils::SERIAL_COUNTER;
|
use smithay::utils::SERIAL_COUNTER;
|
||||||
|
|
||||||
fn fixture() -> (
|
pub(super) fn fixture() -> (
|
||||||
smithay::reexports::calloop::EventLoop<'static, Ferese>,
|
smithay::reexports::calloop::EventLoop<'static, Ferese>,
|
||||||
Ferese,
|
Ferese,
|
||||||
Output,
|
Output,
|
||||||
@@ -1228,21 +1228,17 @@ fn malformed_resize_limits_disconnect_only_the_client_and_motion_survives() {
|
|||||||
state.focused_window = Some(id);
|
state.focused_window = Some(id);
|
||||||
state.toggle_focused_floating();
|
state.toggle_focused_floating();
|
||||||
let pointer = state.seat.get_pointer().unwrap();
|
let pointer = state.seat.get_pointer().unwrap();
|
||||||
pointer.set_grab(
|
assert!(state.begin_window_interaction(
|
||||||
&mut state,
|
&pointer,
|
||||||
crate::grabs::ResizeSurfaceGrab::new(
|
window,
|
||||||
GrabStartData {
|
GrabStartData {
|
||||||
focus: None,
|
focus: None,
|
||||||
button: 0x110,
|
button: 0x110,
|
||||||
location: (100.0, 100.0).into(),
|
location: (100.0, 100.0).into()
|
||||||
},
|
},
|
||||||
window,
|
|
||||||
xdg_toplevel::ResizeEdge::BottomRight.into(),
|
|
||||||
Rectangle::new((100, 100).into(), (64, 48).into()),
|
|
||||||
),
|
|
||||||
SERIAL_COUNTER.next_serial(),
|
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, 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, 4, 7, &[maximum.0 as u32, maximum.1 as u32], None);
|
||||||
request(&mut wire, 2, 6, &[], 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(),
|
SERIAL_COUNTER.next_serial(),
|
||||||
smithay::backend::input::InputTime::from_millis(2),
|
smithay::backend::input::InputTime::from_millis(2),
|
||||||
);
|
);
|
||||||
|
state.poll_window_interaction();
|
||||||
wire.set_nonblocking(true).unwrap();
|
wire.set_nonblocking(true).unwrap();
|
||||||
let mut messages = Vec::new();
|
let mut messages = Vec::new();
|
||||||
let _ = wire.read_to_end(&mut messages);
|
let _ = wire.read_to_end(&mut messages);
|
||||||
|
|||||||
@@ -123,6 +123,31 @@ impl WorkspaceLayout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn place_window(
|
||||||
|
&mut self,
|
||||||
|
window: WindowId,
|
||||||
|
target: WindowId,
|
||||||
|
placement: ferese_layout::DropPlacement,
|
||||||
|
) -> Result<(), LayoutError> {
|
||||||
|
match self {
|
||||||
|
Self::Tree(layout) => layout.place_window(window, target, placement),
|
||||||
|
Self::Scrolling(layout) => layout.place_window(window, target, placement),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resize_edge(
|
||||||
|
&mut self,
|
||||||
|
window: WindowId,
|
||||||
|
edge: Direction,
|
||||||
|
delta: f64,
|
||||||
|
bounds: Rect,
|
||||||
|
) -> Result<bool, LayoutError> {
|
||||||
|
match self {
|
||||||
|
Self::Tree(layout) => layout.resize_edge(window, edge, delta, bounds),
|
||||||
|
Self::Scrolling(layout) => layout.resize_edge(window, edge, delta, bounds),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn remove(&mut self, window: WindowId) -> Result<(), LayoutError> {
|
pub fn remove(&mut self, window: WindowId) -> Result<(), LayoutError> {
|
||||||
match self {
|
match self {
|
||||||
Self::Scrolling(layout) => layout.remove(window),
|
Self::Scrolling(layout) => layout.remove(window),
|
||||||
|
|||||||
@@ -279,6 +279,134 @@ impl LayoutTree {
|
|||||||
Ok(window_node)
|
Ok(window_node)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Reinsert a window beside a target, or into its tab group.
|
||||||
|
pub fn place_window(
|
||||||
|
&mut self,
|
||||||
|
window: WindowId,
|
||||||
|
target: WindowId,
|
||||||
|
placement: crate::DropPlacement,
|
||||||
|
) -> Result<(), LayoutError> {
|
||||||
|
use crate::DropPlacement::*;
|
||||||
|
if !self.contains(window) {
|
||||||
|
return Err(LayoutError::UnknownWindow(window));
|
||||||
|
}
|
||||||
|
if !self.contains(target) {
|
||||||
|
return Err(LayoutError::UnknownWindow(target));
|
||||||
|
}
|
||||||
|
if window == target {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
if placement == Tab {
|
||||||
|
return self.stack_window(window, target);
|
||||||
|
}
|
||||||
|
let mut next = self.clone();
|
||||||
|
next.remove(window)?;
|
||||||
|
let axis = if matches!(placement, Left | Right) {
|
||||||
|
Axis::Horizontal
|
||||||
|
} else {
|
||||||
|
Axis::Vertical
|
||||||
|
};
|
||||||
|
let leaf = next.insert(window, Some(target), axis, 0.5)?;
|
||||||
|
if matches!(placement, Left | Above) {
|
||||||
|
let parent = next.parents[&leaf];
|
||||||
|
if let Some(Node::Split { first, second, .. }) = next.nodes.get_mut(&parent) {
|
||||||
|
std::mem::swap(first, second);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
next.validate()?;
|
||||||
|
*self = next;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Move a selected split edge by a signed logical-pixel distance.
|
||||||
|
pub fn resize_edge(
|
||||||
|
&mut self,
|
||||||
|
window: WindowId,
|
||||||
|
edge: Direction,
|
||||||
|
delta: f64,
|
||||||
|
bounds: Rect,
|
||||||
|
) -> Result<bool, LayoutError> {
|
||||||
|
let mut child = *self
|
||||||
|
.windows
|
||||||
|
.get(&window)
|
||||||
|
.ok_or(LayoutError::UnknownWindow(window))?;
|
||||||
|
if !delta.is_finite() {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
while let Some(parent) = self.parents.get(&child).copied() {
|
||||||
|
let matches = match self.nodes.get(&parent) {
|
||||||
|
Some(Node::Split {
|
||||||
|
axis: Axis::Horizontal,
|
||||||
|
first,
|
||||||
|
second,
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
|
(edge == Direction::Right && *first == child)
|
||||||
|
|| (edge == Direction::Left && *second == child)
|
||||||
|
}
|
||||||
|
Some(Node::Split {
|
||||||
|
axis: Axis::Vertical,
|
||||||
|
first,
|
||||||
|
second,
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
|
(edge == Direction::Down && *first == child)
|
||||||
|
|| (edge == Direction::Up && *second == child)
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
if matches {
|
||||||
|
let mut path = Vec::new();
|
||||||
|
let mut node = parent;
|
||||||
|
while let Some(ancestor) = self.parents.get(&node).copied() {
|
||||||
|
path.push((ancestor, node));
|
||||||
|
node = ancestor;
|
||||||
|
}
|
||||||
|
let mut area = bounds;
|
||||||
|
for (ancestor, descendant) in path.into_iter().rev() {
|
||||||
|
if let Some(Node::Split {
|
||||||
|
axis, ratio, first, ..
|
||||||
|
}) = self.nodes.get(&ancestor)
|
||||||
|
{
|
||||||
|
match axis {
|
||||||
|
Axis::Horizontal => {
|
||||||
|
let part = area.width * ratio;
|
||||||
|
if *first == descendant {
|
||||||
|
area.width = part;
|
||||||
|
} else {
|
||||||
|
area.x += part;
|
||||||
|
area.width -= part;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Axis::Vertical => {
|
||||||
|
let part = area.height * ratio;
|
||||||
|
if *first == descendant {
|
||||||
|
area.height = part;
|
||||||
|
} else {
|
||||||
|
area.y += part;
|
||||||
|
area.height -= part;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(Node::Split { ratio, .. }) = self.nodes.get_mut(&parent) {
|
||||||
|
let extent = if matches!(edge, Direction::Left | Direction::Right) {
|
||||||
|
area.width
|
||||||
|
} else {
|
||||||
|
area.height
|
||||||
|
};
|
||||||
|
let next = normalized_ratio(*ratio + delta / extent.max(1.0));
|
||||||
|
let changed = next != *ratio;
|
||||||
|
*ratio = next;
|
||||||
|
return Ok(changed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
child = parent;
|
||||||
|
}
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn remove(&mut self, window: WindowId) -> Result<(), LayoutError> {
|
pub fn remove(&mut self, window: WindowId) -> Result<(), LayoutError> {
|
||||||
let leaf = self
|
let leaf = self
|
||||||
.windows
|
.windows
|
||||||
|
|||||||
@@ -6,3 +6,356 @@ pub use layout_tree::*;
|
|||||||
pub use scrolling::{
|
pub use scrolling::{
|
||||||
Column, ColumnDisplay, ColumnWidth, ScrollingLayout, ViewportFocusStrategy, ViewportTarget,
|
Column, ColumnDisplay, ColumnWidth, ScrollingLayout, ViewportFocusStrategy, ViewportTarget,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// The destination of an interactive tiled-window drop.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub enum DropPlacement {
|
||||||
|
Left,
|
||||||
|
Right,
|
||||||
|
Above,
|
||||||
|
Below,
|
||||||
|
Tab,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod pointer_tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pointer_tree_drop_preserves_windows_and_is_atomic_for_unknown_targets() {
|
||||||
|
for placement in [
|
||||||
|
DropPlacement::Left,
|
||||||
|
DropPlacement::Right,
|
||||||
|
DropPlacement::Above,
|
||||||
|
DropPlacement::Below,
|
||||||
|
DropPlacement::Tab,
|
||||||
|
] {
|
||||||
|
let mut tree = LayoutTree::default();
|
||||||
|
for id in 1..=4 {
|
||||||
|
tree.insert(WindowId(id), None, Axis::Horizontal, 0.5)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
tree.place_window(WindowId(1), WindowId(3), placement)
|
||||||
|
.unwrap();
|
||||||
|
tree.validate().unwrap();
|
||||||
|
assert_eq!(tree.window_ids().count(), 4);
|
||||||
|
let before = tree.clone();
|
||||||
|
assert!(
|
||||||
|
tree.place_window(WindowId(1), WindowId(99), placement)
|
||||||
|
.is_err()
|
||||||
|
);
|
||||||
|
assert_eq!(tree, before);
|
||||||
|
tree.place_window(WindowId(1), WindowId(1), placement)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(tree, before);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pointer_scrolling_drop_preserves_window_and_row_invariants() {
|
||||||
|
for placement in [
|
||||||
|
DropPlacement::Left,
|
||||||
|
DropPlacement::Right,
|
||||||
|
DropPlacement::Above,
|
||||||
|
DropPlacement::Below,
|
||||||
|
DropPlacement::Tab,
|
||||||
|
] {
|
||||||
|
let mut layout = ScrollingLayout::default();
|
||||||
|
for id in 1..=4 {
|
||||||
|
layout.insert(WindowId(id), None).unwrap();
|
||||||
|
}
|
||||||
|
layout
|
||||||
|
.place_window(WindowId(1), WindowId(3), placement)
|
||||||
|
.unwrap();
|
||||||
|
layout.validate().unwrap();
|
||||||
|
assert_eq!(layout.window_ids().count(), 4);
|
||||||
|
let column = layout
|
||||||
|
.columns()
|
||||||
|
.iter()
|
||||||
|
.find(|column| column.windows.contains(&WindowId(1)))
|
||||||
|
.unwrap();
|
||||||
|
if placement == DropPlacement::Tab {
|
||||||
|
assert_eq!(column.display, ColumnDisplay::Tabbed);
|
||||||
|
assert_eq!(column.windows.len(), 2);
|
||||||
|
}
|
||||||
|
let before = layout.clone();
|
||||||
|
assert!(
|
||||||
|
layout
|
||||||
|
.place_window(WindowId(1), WindowId(99), placement)
|
||||||
|
.is_err()
|
||||||
|
);
|
||||||
|
assert_eq!(layout, before);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pointer_tree_resize_moves_the_selected_boundary_in_both_directions() {
|
||||||
|
let mut tree = LayoutTree::default();
|
||||||
|
tree.insert(WindowId(1), None, Axis::Horizontal, 0.5)
|
||||||
|
.unwrap();
|
||||||
|
tree.insert(WindowId(2), Some(WindowId(1)), Axis::Horizontal, 0.5)
|
||||||
|
.unwrap();
|
||||||
|
let bounds = Rect::new(-1000.0, -500.0, 1000.0, 600.0);
|
||||||
|
let before = tree.geometry(bounds).unwrap();
|
||||||
|
assert!(
|
||||||
|
tree.resize_edge(WindowId(1), Direction::Right, 100.0, bounds)
|
||||||
|
.unwrap()
|
||||||
|
);
|
||||||
|
let after = tree.geometry(bounds).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
after[&WindowId(1)].width,
|
||||||
|
before[&WindowId(1)].width + 100.0
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
after[&WindowId(2)].width,
|
||||||
|
before[&WindowId(2)].width - 100.0
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
tree.resize_edge(WindowId(2), Direction::Left, -100.0, bounds)
|
||||||
|
.unwrap()
|
||||||
|
);
|
||||||
|
assert_eq!(tree.geometry(bounds).unwrap(), before);
|
||||||
|
assert!(
|
||||||
|
!tree
|
||||||
|
.resize_edge(WindowId(1), Direction::Left, 100.0, bounds)
|
||||||
|
.unwrap()
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!tree
|
||||||
|
.resize_edge(WindowId(1), Direction::Right, f64::NAN, bounds)
|
||||||
|
.unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod pointer_sequence_tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn repeated_pointer_drops_and_resizes_keep_tabbed_layouts_valid() {
|
||||||
|
let mut tree = LayoutTree::default();
|
||||||
|
let mut scrolling = ScrollingLayout::default();
|
||||||
|
for id in 1..=8 {
|
||||||
|
tree.insert(WindowId(id), None, Axis::Horizontal, 0.5)
|
||||||
|
.unwrap();
|
||||||
|
scrolling.insert(WindowId(id), None).unwrap();
|
||||||
|
}
|
||||||
|
let bounds = Rect::new(-1200.0, -300.0, 1200.0, 800.0);
|
||||||
|
let mut seed = 42_u64;
|
||||||
|
for _ in 0..1000 {
|
||||||
|
seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1);
|
||||||
|
let source = WindowId(1 + (seed >> 32) % 8);
|
||||||
|
let target = WindowId(1 + (seed >> 40) % 8);
|
||||||
|
let placement = [
|
||||||
|
DropPlacement::Left,
|
||||||
|
DropPlacement::Right,
|
||||||
|
DropPlacement::Above,
|
||||||
|
DropPlacement::Below,
|
||||||
|
DropPlacement::Tab,
|
||||||
|
][(seed >> 48) as usize % 5];
|
||||||
|
tree.place_window(source, target, placement).unwrap();
|
||||||
|
scrolling.place_window(source, target, placement).unwrap();
|
||||||
|
let edge = [
|
||||||
|
Direction::Left,
|
||||||
|
Direction::Right,
|
||||||
|
Direction::Up,
|
||||||
|
Direction::Down,
|
||||||
|
][(seed >> 52) as usize % 4];
|
||||||
|
let delta = (seed >> 16) as i16 as f64;
|
||||||
|
tree.resize_edge(source, edge, delta, bounds).unwrap();
|
||||||
|
scrolling.resize_edge(source, edge, delta, bounds).unwrap();
|
||||||
|
tree.validate().unwrap();
|
||||||
|
scrolling.validate().unwrap();
|
||||||
|
assert_eq!(tree.window_ids().count(), 8);
|
||||||
|
assert_eq!(scrolling.window_ids().count(), 8);
|
||||||
|
for id in 1..=8 {
|
||||||
|
assert!(tree.contains(WindowId(id)));
|
||||||
|
assert!(scrolling.contains(WindowId(id)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod pointer_regression_tests {
|
||||||
|
use super::*;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn edge_drop_on_tabbed_column_shows_rows_and_center_drop_keeps_tabs() {
|
||||||
|
for placement in [
|
||||||
|
DropPlacement::Above,
|
||||||
|
DropPlacement::Below,
|
||||||
|
DropPlacement::Tab,
|
||||||
|
] {
|
||||||
|
let mut layout = ScrollingLayout::default();
|
||||||
|
for id in 1..=3 {
|
||||||
|
layout.insert(WindowId(id), None).unwrap();
|
||||||
|
}
|
||||||
|
layout
|
||||||
|
.place_window(WindowId(1), WindowId(2), DropPlacement::Tab)
|
||||||
|
.unwrap();
|
||||||
|
layout
|
||||||
|
.place_window(WindowId(3), WindowId(2), placement)
|
||||||
|
.unwrap();
|
||||||
|
let result = layout
|
||||||
|
.resolve_geometry_with_constraints(
|
||||||
|
Rect::new(0.0, 0.0, 1200.0, 800.0),
|
||||||
|
GapConfig::default(),
|
||||||
|
&HashMap::new(),
|
||||||
|
Some(WindowId(3)),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
layout.validate().unwrap();
|
||||||
|
assert_eq!(layout.window_ids().count(), 3);
|
||||||
|
if placement == DropPlacement::Tab {
|
||||||
|
assert_eq!(result.geometry.len(), 1);
|
||||||
|
assert!(result.geometry.contains_key(&WindowId(3)));
|
||||||
|
assert_eq!(layout.tab_group(WindowId(3)).unwrap().windows.len(), 3);
|
||||||
|
} else {
|
||||||
|
assert_eq!(result.geometry.len(), 3);
|
||||||
|
let dropped = result.geometry[&WindowId(3)];
|
||||||
|
let target = result.geometry[&WindowId(2)];
|
||||||
|
assert_eq!(dropped.y < target.y, placement == DropPlacement::Above);
|
||||||
|
assert!(layout.tab_group(WindowId(3)).is_none());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn left_resize_should_move_left_edge_and_preserve_right_edge() {
|
||||||
|
let mut layout = ScrollingLayout::default();
|
||||||
|
layout.insert(WindowId(1), None).unwrap();
|
||||||
|
let bounds = Rect::new(0.0, 0.0, 1200.0, 800.0);
|
||||||
|
let before = layout
|
||||||
|
.resolve_geometry_with_constraints(
|
||||||
|
bounds,
|
||||||
|
GapConfig::default(),
|
||||||
|
&HashMap::new(),
|
||||||
|
Some(WindowId(1)),
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
.geometry[&WindowId(1)];
|
||||||
|
layout
|
||||||
|
.resize_edge(WindowId(1), Direction::Left, 100.0, bounds)
|
||||||
|
.unwrap();
|
||||||
|
let after = layout
|
||||||
|
.resolve_geometry_with_constraints(
|
||||||
|
bounds,
|
||||||
|
GapConfig::default(),
|
||||||
|
&HashMap::new(),
|
||||||
|
Some(WindowId(1)),
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
.geometry[&WindowId(1)];
|
||||||
|
assert_eq!(
|
||||||
|
after.x + after.width,
|
||||||
|
before.x + before.width,
|
||||||
|
"left-edge drag changed the right edge: {before:?} -> {after:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn horizontal_resize_anchors_survive_limits_focus_policies_and_held_buffers() {
|
||||||
|
let bounds = Rect::new(-1200.0, -300.0, 1200.0, 800.0);
|
||||||
|
for strategy in [
|
||||||
|
ViewportFocusStrategy::Minimal,
|
||||||
|
ViewportFocusStrategy::Center,
|
||||||
|
ViewportFocusStrategy::Paged,
|
||||||
|
] {
|
||||||
|
for tabbed in [false, true] {
|
||||||
|
for constraint in [
|
||||||
|
SizeConstraints::default(),
|
||||||
|
SizeConstraints {
|
||||||
|
min_width: 700.0,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
SizeConstraints {
|
||||||
|
max_width: Some(250.0),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
] {
|
||||||
|
let mut base = ScrollingLayout::default();
|
||||||
|
base.set_focus_strategy(strategy);
|
||||||
|
for id in 1..=3 {
|
||||||
|
base.insert(WindowId(id), None).unwrap();
|
||||||
|
}
|
||||||
|
if tabbed {
|
||||||
|
base.place_window(WindowId(3), WindowId(2), DropPlacement::Tab)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
let window = WindowId(2);
|
||||||
|
let constraints =
|
||||||
|
HashMap::from([(if tabbed { WindowId(3) } else { window }, constraint)]);
|
||||||
|
let before = base
|
||||||
|
.resolve_geometry_with_constraints(
|
||||||
|
bounds,
|
||||||
|
GapConfig::default(),
|
||||||
|
&constraints,
|
||||||
|
Some(window),
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
.geometry[&window];
|
||||||
|
let initial_viewport = base.viewport_x();
|
||||||
|
let initial_width = base.allocated_column_width(window).unwrap();
|
||||||
|
for direction in [Direction::Left, Direction::Right] {
|
||||||
|
for delta in [-1000.0, -100.0, 100.0, 1000.0] {
|
||||||
|
let mut layout = base.clone();
|
||||||
|
layout
|
||||||
|
.resize_edge(window, direction, delta, bounds)
|
||||||
|
.unwrap();
|
||||||
|
let after = layout
|
||||||
|
.resolve_geometry_with_constraints(
|
||||||
|
bounds,
|
||||||
|
GapConfig::default(),
|
||||||
|
&constraints,
|
||||||
|
Some(window),
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
.geometry[&window];
|
||||||
|
let edge = |rect: Rect| {
|
||||||
|
if direction == Direction::Left {
|
||||||
|
rect.x + rect.width
|
||||||
|
} else {
|
||||||
|
rect.x
|
||||||
|
}
|
||||||
|
};
|
||||||
|
assert!(
|
||||||
|
(edge(after) - edge(before)).abs() < 0.001,
|
||||||
|
"{direction:?} {delta} {strategy:?} {constraint:?}: {before:?} -> {after:?}"
|
||||||
|
);
|
||||||
|
let target = layout.viewport_target().unwrap();
|
||||||
|
let held = target.with_held_widths(
|
||||||
|
&[(
|
||||||
|
window,
|
||||||
|
initial_width,
|
||||||
|
layout.allocated_column_width(window).unwrap(),
|
||||||
|
)],
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
(held - initial_viewport).abs() < 0.001,
|
||||||
|
"pending buffers must retain the old viewport"
|
||||||
|
);
|
||||||
|
let settled = layout.clone();
|
||||||
|
layout
|
||||||
|
.resolve_geometry_with_constraints(
|
||||||
|
bounds,
|
||||||
|
GapConfig::default(),
|
||||||
|
&constraints,
|
||||||
|
Some(window),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
layout, settled,
|
||||||
|
"an idle relayout must not reapply the resize"
|
||||||
|
);
|
||||||
|
layout.validate().unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -121,6 +121,12 @@ enum ViewportRequest {
|
|||||||
WidthCycle(usize),
|
WidthCycle(usize),
|
||||||
Slide(usize, usize),
|
Slide(usize, usize),
|
||||||
Center(usize, Option<f64>),
|
Center(usize, Option<f64>),
|
||||||
|
Resize {
|
||||||
|
column: usize,
|
||||||
|
anchor: f64,
|
||||||
|
anchor_right: bool,
|
||||||
|
maximum: Option<f64>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ViewportTarget {
|
impl ViewportTarget {
|
||||||
@@ -162,6 +168,18 @@ impl ViewportTarget {
|
|||||||
});
|
});
|
||||||
layout.viewport_x = positions[active].0 + width / 2.0 - self.viewport_width / 2.0
|
layout.viewport_x = positions[active].0 + width / 2.0 - self.viewport_width / 2.0
|
||||||
}
|
}
|
||||||
|
ViewportRequest::Resize {
|
||||||
|
column,
|
||||||
|
anchor,
|
||||||
|
anchor_right,
|
||||||
|
maximum,
|
||||||
|
} => {
|
||||||
|
let width = maximum.map_or(positions[column].1, |maximum| {
|
||||||
|
positions[column].1.min(maximum)
|
||||||
|
});
|
||||||
|
layout.viewport_x =
|
||||||
|
positions[column].0 + if anchor_right { width } else { 0.0 } - anchor;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
layout.viewport_x
|
layout.viewport_x
|
||||||
}
|
}
|
||||||
@@ -204,6 +222,14 @@ enum PendingViewportRequest {
|
|||||||
// Width cycling takes precedence. Keep the focus request only in case a
|
// Width cycling takes precedence. Keep the focus request only in case a
|
||||||
// subsequent no-op cycle cancels the width request before layout resolves.
|
// subsequent no-op cycle cancels the width request before layout resolves.
|
||||||
WidthCycle(Option<PendingFocusRequest>),
|
WidthCycle(Option<PendingFocusRequest>),
|
||||||
|
// Resolve the opposite edge from the previous strip position and allocation
|
||||||
|
// after applying client limits. Negative offsets keep the first column anchored.
|
||||||
|
Resize {
|
||||||
|
window: WindowId,
|
||||||
|
origin: f64,
|
||||||
|
width: f64,
|
||||||
|
anchor_right: bool,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
@@ -242,11 +268,18 @@ impl ScrollingLayout {
|
|||||||
match self.pending_viewport {
|
match self.pending_viewport {
|
||||||
Some(PendingViewportRequest::Focus(request)) => Some(request),
|
Some(PendingViewportRequest::Focus(request)) => Some(request),
|
||||||
Some(PendingViewportRequest::WidthCycle(request)) => request,
|
Some(PendingViewportRequest::WidthCycle(request)) => request,
|
||||||
None => None,
|
None | Some(PendingViewportRequest::Resize { .. }) => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn request_focus(&mut self, request: Option<PendingFocusRequest>) {
|
fn request_focus(&mut self, request: Option<PendingFocusRequest>) {
|
||||||
|
// Direct manipulation keeps its edge anchor through the next solve.
|
||||||
|
if matches!(
|
||||||
|
self.pending_viewport,
|
||||||
|
Some(PendingViewportRequest::Resize { .. })
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
self.pending_viewport = if matches!(
|
self.pending_viewport = if matches!(
|
||||||
self.pending_viewport,
|
self.pending_viewport,
|
||||||
Some(PendingViewportRequest::WidthCycle(_))
|
Some(PendingViewportRequest::WidthCycle(_))
|
||||||
@@ -371,6 +404,37 @@ impl ScrollingLayout {
|
|||||||
self.viewport_x
|
self.viewport_x
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Pan without changing focus, keeping the viewport inside the column strip.
|
||||||
|
pub fn scroll_viewport(&mut self, delta: f64) -> bool {
|
||||||
|
let Some(sizing) = self.last_column_sizing else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
if !delta.is_finite() || delta == 0.0 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let width = self
|
||||||
|
.columns
|
||||||
|
.iter()
|
||||||
|
.map(|column| {
|
||||||
|
column
|
||||||
|
.windows
|
||||||
|
.first()
|
||||||
|
.and_then(|id| self.allocated_widths.get(id))
|
||||||
|
.copied()
|
||||||
|
.unwrap_or_else(|| sizing.resolve(column.width))
|
||||||
|
})
|
||||||
|
.sum::<f64>()
|
||||||
|
+ sizing.inner_gap * self.columns.len().saturating_sub(1) as f64;
|
||||||
|
let next = (self.viewport_x + delta).clamp(0.0, (width - sizing.viewport_width).max(0.0));
|
||||||
|
if next == self.viewport_x {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
self.viewport_x = next;
|
||||||
|
self.pending_viewport = None;
|
||||||
|
self.viewport_basis = None;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn allocated_column_width(&self, window: WindowId) -> Option<f64> {
|
pub fn allocated_column_width(&self, window: WindowId) -> Option<f64> {
|
||||||
self.allocated_widths.get(&window).copied()
|
self.allocated_widths.get(&window).copied()
|
||||||
@@ -450,6 +514,133 @@ impl ScrollingLayout {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn place_window(
|
||||||
|
&mut self,
|
||||||
|
window: WindowId,
|
||||||
|
target: WindowId,
|
||||||
|
placement: crate::DropPlacement,
|
||||||
|
) -> Result<(), LayoutError> {
|
||||||
|
use crate::DropPlacement::*;
|
||||||
|
let (source, _) = self
|
||||||
|
.window_location(window)
|
||||||
|
.ok_or(LayoutError::UnknownWindow(window))?;
|
||||||
|
if !self.contains(target) {
|
||||||
|
return Err(LayoutError::UnknownWindow(target));
|
||||||
|
}
|
||||||
|
if window == target {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let width = self.columns[source].width;
|
||||||
|
self.remove(window)?;
|
||||||
|
let (column, row) = self
|
||||||
|
.window_location(target)
|
||||||
|
.expect("target survives removing another window");
|
||||||
|
match placement {
|
||||||
|
Left | Right => {
|
||||||
|
let index = column + usize::from(placement == Right);
|
||||||
|
let mut entry = Column::new(window, width);
|
||||||
|
entry.display = self.default_display;
|
||||||
|
self.columns.insert(index, entry);
|
||||||
|
self.active_column = Some(index);
|
||||||
|
}
|
||||||
|
Above | Below | Tab => {
|
||||||
|
let entry = &mut self.columns[column];
|
||||||
|
let index = row + usize::from(placement != Above);
|
||||||
|
entry.windows.insert(index, window);
|
||||||
|
// Split the destination row's weight rather than collapsing it.
|
||||||
|
let weight = entry.heights[row] / 2.0;
|
||||||
|
entry.heights[row] = weight;
|
||||||
|
entry.heights.insert(index, weight);
|
||||||
|
entry.normalize_heights();
|
||||||
|
entry.display = if placement == Tab {
|
||||||
|
ColumnDisplay::Tabbed
|
||||||
|
} else {
|
||||||
|
ColumnDisplay::Stacked
|
||||||
|
};
|
||||||
|
entry.active = index;
|
||||||
|
self.active_column = Some(column);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.request_reveal(Some(window));
|
||||||
|
debug_assert!(self.validate().is_ok());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resize_edge(
|
||||||
|
&mut self,
|
||||||
|
window: WindowId,
|
||||||
|
edge: Direction,
|
||||||
|
delta: f64,
|
||||||
|
bounds: Rect,
|
||||||
|
) -> Result<bool, LayoutError> {
|
||||||
|
let (column, row) = self
|
||||||
|
.window_location(window)
|
||||||
|
.ok_or(LayoutError::UnknownWindow(window))?;
|
||||||
|
if !delta.is_finite() {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
if matches!(edge, Direction::Left | Direction::Right) {
|
||||||
|
let size = self.last_column_sizing.unwrap_or(ColumnSizing {
|
||||||
|
viewport_width: bounds.width,
|
||||||
|
inner_gap: 0.0,
|
||||||
|
});
|
||||||
|
let width = self
|
||||||
|
.allocated_column_width(window)
|
||||||
|
.unwrap_or_else(|| size.resolve(self.columns[column].width));
|
||||||
|
let origin = self.columns[..column]
|
||||||
|
.iter()
|
||||||
|
.map(|column| {
|
||||||
|
self.allocated_column_width(column.windows[0])
|
||||||
|
.unwrap_or_else(|| size.resolve(column.width))
|
||||||
|
+ size.inner_gap
|
||||||
|
})
|
||||||
|
.sum::<f64>()
|
||||||
|
- self.viewport_x;
|
||||||
|
let change = if edge == Direction::Left {
|
||||||
|
-delta
|
||||||
|
} else {
|
||||||
|
delta
|
||||||
|
};
|
||||||
|
let next = (width + change).clamp(
|
||||||
|
size.resolve(ColumnWidth::Proportion(MIN_COLUMN_WIDTH))
|
||||||
|
.max(1.0),
|
||||||
|
size.resolve(ColumnWidth::Proportion(MAX_COLUMN_WIDTH))
|
||||||
|
.max(1.0),
|
||||||
|
);
|
||||||
|
if next == width {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
self.columns[column].width = ColumnWidth::Fixed(next);
|
||||||
|
self.pending_viewport = Some(PendingViewportRequest::Resize {
|
||||||
|
window,
|
||||||
|
origin,
|
||||||
|
width,
|
||||||
|
anchor_right: edge == Direction::Left,
|
||||||
|
});
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
let entry = &mut self.columns[column];
|
||||||
|
if entry.display == ColumnDisplay::Tabbed {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
let neighbor = match edge {
|
||||||
|
Direction::Up => row.checked_sub(1),
|
||||||
|
Direction::Down => (row + 1 < entry.windows.len()).then_some(row + 1),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
let Some(neighbor) = neighbor else {
|
||||||
|
return Ok(false);
|
||||||
|
};
|
||||||
|
let pair = entry.heights[row] + entry.heights[neighbor];
|
||||||
|
let change = (if edge == Direction::Up { -delta } else { delta }) / bounds.height.max(1.0);
|
||||||
|
let minimum = MIN_ROW_HEIGHT.min(pair / 2.0);
|
||||||
|
let height = (entry.heights[row] + change).clamp(minimum, pair - minimum);
|
||||||
|
let changed = height != entry.heights[row];
|
||||||
|
entry.heights[row] = height;
|
||||||
|
entry.heights[neighbor] = pair - height;
|
||||||
|
Ok(changed)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn remove(&mut self, window: WindowId) -> Result<(), LayoutError> {
|
pub fn remove(&mut self, window: WindowId) -> Result<(), LayoutError> {
|
||||||
let (column_index, window_index) = self
|
let (column_index, window_index) = self
|
||||||
.window_location(window)
|
.window_location(window)
|
||||||
@@ -897,6 +1088,42 @@ impl ScrollingLayout {
|
|||||||
self.last_column_sizing.map(|sizing| sizing.viewport_width) != Some(viewport_width);
|
self.last_column_sizing.map(|sizing| sizing.viewport_width) != Some(viewport_width);
|
||||||
self.last_column_sizing = Some(sizing);
|
self.last_column_sizing = Some(sizing);
|
||||||
match self.pending_viewport.take() {
|
match self.pending_viewport.take() {
|
||||||
|
Some(PendingViewportRequest::Resize {
|
||||||
|
window,
|
||||||
|
origin,
|
||||||
|
width,
|
||||||
|
anchor_right,
|
||||||
|
}) => {
|
||||||
|
if let Some((column, _)) = self.window_location(window) {
|
||||||
|
let constraint = if self.columns[column].display == ColumnDisplay::Tabbed {
|
||||||
|
tab_constraints(&self.columns[column], constraints)
|
||||||
|
} else {
|
||||||
|
layout_tree::normalized_constraints(
|
||||||
|
constraints.get(&window).copied().unwrap_or_default(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let maximum = constraint.max_width;
|
||||||
|
let previous_width = maximum.map_or(width, |maximum| width.min(maximum));
|
||||||
|
let anchor = origin + if anchor_right { previous_width } else { 0.0 };
|
||||||
|
let next_width = maximum.map_or(column_positions[column].1, |maximum| {
|
||||||
|
column_positions[column].1.min(maximum)
|
||||||
|
});
|
||||||
|
self.record_viewport_basis(
|
||||||
|
&column_positions,
|
||||||
|
inner,
|
||||||
|
viewport_width,
|
||||||
|
ViewportRequest::Resize {
|
||||||
|
column,
|
||||||
|
anchor,
|
||||||
|
anchor_right,
|
||||||
|
maximum,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
self.viewport_x = column_positions[column].0
|
||||||
|
+ if anchor_right { next_width } else { 0.0 }
|
||||||
|
- anchor;
|
||||||
|
}
|
||||||
|
}
|
||||||
Some(PendingViewportRequest::WidthCycle(_)) => {
|
Some(PendingViewportRequest::WidthCycle(_)) => {
|
||||||
if let Some(active) = self.active_column {
|
if let Some(active) = self.active_column {
|
||||||
self.record_viewport_basis(
|
self.record_viewport_basis(
|
||||||
@@ -970,23 +1197,7 @@ impl ScrollingLayout {
|
|||||||
);
|
);
|
||||||
// All tabs use one size, even when their client constraints differ.
|
// All tabs use one size, even when their client constraints differ.
|
||||||
// Keep the column allocation stable and honor compatible maximums.
|
// Keep the column allocation stable and honor compatible maximums.
|
||||||
let mut common = SizeConstraints::default();
|
let common = tab_constraints(column, constraints);
|
||||||
for id in &column.windows {
|
|
||||||
let c = layout_tree::normalized_constraints(
|
|
||||||
constraints.get(id).copied().unwrap_or_default(),
|
|
||||||
);
|
|
||||||
common.min_width = common.min_width.max(c.min_width);
|
|
||||||
common.min_height = common.min_height.max(c.min_height);
|
|
||||||
common.max_width = match (common.max_width, c.max_width) {
|
|
||||||
(Some(a), Some(b)) => Some(a.min(b)),
|
|
||||||
(a, b) => a.or(b),
|
|
||||||
};
|
|
||||||
common.max_height = match (common.max_height, c.max_height) {
|
|
||||||
(Some(a), Some(b)) => Some(a.min(b)),
|
|
||||||
(a, b) => a.or(b),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
let common = layout_tree::normalized_constraints(common);
|
|
||||||
layout_tree::record_minimum_warnings(window, rect, common, &mut result.warnings);
|
layout_tree::record_minimum_warnings(window, rect, common, &mut result.warnings);
|
||||||
apply_maximums(window, &mut rect, common, &mut result.warnings);
|
apply_maximums(window, &mut rect, common, &mut result.warnings);
|
||||||
if column.windows.len() > 1 {
|
if column.windows.len() > 1 {
|
||||||
@@ -1331,6 +1542,28 @@ fn finite_nonnegative(value: f64) -> f64 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn tab_constraints(
|
||||||
|
column: &Column,
|
||||||
|
constraints: &HashMap<WindowId, SizeConstraints>,
|
||||||
|
) -> SizeConstraints {
|
||||||
|
let mut common = SizeConstraints::default();
|
||||||
|
for id in &column.windows {
|
||||||
|
let c =
|
||||||
|
layout_tree::normalized_constraints(constraints.get(id).copied().unwrap_or_default());
|
||||||
|
common.min_width = common.min_width.max(c.min_width);
|
||||||
|
common.min_height = common.min_height.max(c.min_height);
|
||||||
|
common.max_width = match (common.max_width, c.max_width) {
|
||||||
|
(Some(a), Some(b)) => Some(a.min(b)),
|
||||||
|
(a, b) => a.or(b),
|
||||||
|
};
|
||||||
|
common.max_height = match (common.max_height, c.max_height) {
|
||||||
|
(Some(a), Some(b)) => Some(a.min(b)),
|
||||||
|
(a, b) => a.or(b),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
layout_tree::normalized_constraints(common)
|
||||||
|
}
|
||||||
|
|
||||||
fn apply_maximums(
|
fn apply_maximums(
|
||||||
window: WindowId,
|
window: WindowId,
|
||||||
rect: &mut Rect,
|
rect: &mut Rect,
|
||||||
|
|||||||
@@ -117,19 +117,23 @@ Super+BackSpace last focused window; Alt+Tab/Alt+Shift+Tab MRU switching;
|
|||||||
Super+Shift+S area screenshot; Print Screen whole-screen screenshot;
|
Super+Shift+S area screenshot; Print Screen whole-screen screenshot;
|
||||||
Super+Shift+E logout confirmation.
|
Super+Shift+E logout confirmation.
|
||||||
|
|
||||||
Hold Super and drag a floating window with the left mouse button to move it.
|
Hold Super and drag a floating or tiled window with the left mouse button to move it.
|
||||||
Hold Super and drag with the right mouse button to resize. The starting position
|
Hold Super and drag with the right mouse button to resize. The starting position
|
||||||
selects an edge or corner using a 3×3 grid; the center selects the nearest corner.
|
selects an edge or corner using a 3×3 grid; the center selects the nearest corner.
|
||||||
The selected edges stay fixed for the whole drag. These work even when an app has
|
The selected edges stay fixed for the whole drag. These work even when an app has
|
||||||
no title bar or resize border.
|
no title bar or resize border.
|
||||||
|
|
||||||
|
Tiled drops can reorder windows, join tab groups or move between monitors.
|
||||||
|
Mouse buttons and wheel directions can also use the regular binding actions;
|
||||||
|
see [mouse bindings](input.md#mouse-bindings) for configuration and drag controls.
|
||||||
|
|
||||||
For a floating window, Super+Shift+H/J/K/L moves left/down/up/right by 32 logical
|
For a floating window, Super+Shift+H/J/K/L moves left/down/up/right by 32 logical
|
||||||
pixels. Super+Ctrl+H/L shrinks/grows its width; Super+Ctrl+K/J shrinks/grows its
|
pixels. Super+Ctrl+H/L shrinks/grows its width; Super+Ctrl+K/J shrinks/grows its
|
||||||
height by the same step. Keyboard adjustments respect client size limits and
|
height by the same step. Keyboard adjustments respect client size limits and
|
||||||
keep the window reachable in its output's work area. Fullscreen and maximized
|
keep the window reachable in its output's work area. Fullscreen and maximized
|
||||||
floating windows ignore these actions.
|
floating windows ignore these actions.
|
||||||
|
|
||||||
Mouse moves and resizes snap to work-area edges and nearby visible window edges at
|
Floating moves and resizes snap to work-area edges and nearby visible window edges at
|
||||||
10 logical pixels, then resist movement until 20 pixels from the attached edge.
|
10 logical pixels, then resist movement until 20 pixels from the attached edge.
|
||||||
Window edges are eligible only when the windows overlap along the other axis,
|
Window edges are eligible only when the windows overlap along the other axis,
|
||||||
with a 10-pixel allowance. Hold Shift during a drag to bypass snapping.
|
with a 10-pixel allowance. Hold Shift during a drag to bypass snapping.
|
||||||
|
|||||||
@@ -19,9 +19,43 @@ focus keys move the selection, Enter activates it, and Escape dismisses Overview
|
|||||||
Clicking a workspace card's background switches workspaces while keeping Overview
|
Clicking a workspace card's background switches workspaces while keeping Overview
|
||||||
open. Previews follow layout order and use the configured shell font and colors.
|
open. Previews follow layout order and use the configured shell font and colors.
|
||||||
|
|
||||||
The touchpad device keys are booleans, all defaulting to `true`: `tap`,
|
The touchpad keys `tap`, `natural-scroll` and `disable-while-typing` default to
|
||||||
`natural-scroll` and `disable-while-typing`. These apply to DRM devices; nested
|
`true`. Set `disable-while-mouse-connected #true` to pause a built-in touchpad
|
||||||
previews use the host's physical input settings.
|
while an external mouse is connected. It defaults to `false` and uses libinput's
|
||||||
|
external-mouse policy on supported touchpads, including when a mouse is plugged
|
||||||
|
in or removed during the session. The toggle is also in **Settings → Keyboard & mouse**.
|
||||||
|
These device settings apply to DRM sessions; nested previews use the host's
|
||||||
|
physical input settings.
|
||||||
|
|
||||||
|
Touchpad `accel-speed` is independent of the mouse setting. It accepts −1.0 to
|
||||||
|
1.0 and defaults to 0.0. Both speed controls are available in Settings and reload
|
||||||
|
immediately in a hardware session.
|
||||||
|
|
||||||
|
```kdl
|
||||||
|
input {
|
||||||
|
touchpad {
|
||||||
|
accel-speed 0.0
|
||||||
|
disable-while-mouse-connected #true
|
||||||
|
}
|
||||||
|
mouse {
|
||||||
|
natural-scroll #false
|
||||||
|
accel-speed 0.0
|
||||||
|
// accel-profile "flat"
|
||||||
|
left-handed #false
|
||||||
|
middle-emulation #false
|
||||||
|
scroll-factor 1.0
|
||||||
|
drag-threshold 4.0
|
||||||
|
scroll-cooldown-ms 150
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Mouse settings are separate from touchpad settings and reload live. `accel-speed`
|
||||||
|
accepts -1.0 to 1.0; `accel-profile` accepts `adaptive` or `flat`, and follows the
|
||||||
|
device default when omitted. `scroll-factor` scales application wheel scrolling
|
||||||
|
(0.01–100). `drag-threshold` is the distance before a window drag starts (0–100
|
||||||
|
logical pixels). `scroll-cooldown-ms` limits repeated wheel bindings (0–5000 ms);
|
||||||
|
zero allows each accumulated tick to run its action.
|
||||||
|
|
||||||
Workspace swipes and left/right window-focus swipes follow your fingers and settle
|
Workspace swipes and left/right window-focus swipes follow your fingers and settle
|
||||||
when released. Up goes to the next workspace on the
|
when released. Up goes to the next workspace on the
|
||||||
@@ -51,3 +85,38 @@ hardware session.
|
|||||||
|
|
||||||
Adjust recognition distance in **Settings → Keyboard & mouse**, or with
|
Adjust recognition distance in **Settings → Keyboard & mouse**, or with
|
||||||
`swipe-threshold 80` under `input.touchpad` (integer 16–1000 logical pixels).
|
`swipe-threshold 80` under `input.touchpad` (integer 16–1000 logical pixels).
|
||||||
|
|
||||||
|
## Mouse bindings
|
||||||
|
|
||||||
|
`Super+MouseLeft` drags a window and `Super+MouseRight` resizes it. Both work with
|
||||||
|
floating windows and tiled windows in scrolling or tree layouts. Drag a tiled
|
||||||
|
window to another window's edge to place it beside that window, or to its center
|
||||||
|
to join a tab group. Top and bottom drops into a tabbed column show its windows as
|
||||||
|
rows. Drag across monitors to move to the workspace shown there.
|
||||||
|
While dragging, right-click to switch between floating and tiling. Scroll or hold
|
||||||
|
the pointer at a horizontal screen edge to reach other scrolling columns.
|
||||||
|
Escape cancels the operation; releasing the initiating button finishes it.
|
||||||
|
Fullscreen windows keep their current layout until you leave fullscreen.
|
||||||
|
|
||||||
|
Mouse bindings use the same actions, overrides and named binding modes as keyboard
|
||||||
|
bindings. Ordinary actions use the current keyboard focus; `pointer-move` and
|
||||||
|
`pointer-resize` operate on the window under the pointer. Client popup grabs,
|
||||||
|
pointer constraints, shortcut inhibition and session locking take precedence.
|
||||||
|
|
||||||
|
```kdl
|
||||||
|
binding "Alt+MouseLeft" action="pointer-move"
|
||||||
|
binding "Alt+MouseRight" action="pointer-resize"
|
||||||
|
binding "Super+MouseLeft" disabled=#true
|
||||||
|
binding "Super+WheelUp" action="workspace-previous"
|
||||||
|
binding "Super+WheelDown" action="workspace-next"
|
||||||
|
binding "Super+TouchpadScrollLeft" action="focus" argument="left"
|
||||||
|
binding "Super+MouseBack" action="focus-last-window"
|
||||||
|
```
|
||||||
|
|
||||||
|
Buttons include `MouseLeft`, `MouseRight`, `MouseMiddle`, `MouseBack` and
|
||||||
|
`MouseForward`, or `mouse:272` through `mouse:287` for Linux mouse button codes.
|
||||||
|
Wheel directions are `WheelUp`, `WheelDown`, `WheelLeft` and `WheelRight`.
|
||||||
|
Touchpad scroll bindings use the `TouchpadScroll` prefix with the same directions.
|
||||||
|
Only a bound scroll axis is intercepted; the other axis still reaches the client.
|
||||||
|
High-resolution wheel steps accumulate into ticks. An intercepted touchpad scroll
|
||||||
|
keeps its event ownership until the gesture stops, even if modifiers change.
|
||||||
+9
-5
@@ -22,15 +22,19 @@ Run `feresectl launcher toggle` to open or close it from a script.
|
|||||||
|
|
||||||
| Action | Result |
|
| Action | Result |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| Super + left-button drag on a floating window | Move the window |
|
| Super + left-button drag | Move a floating or tiled window |
|
||||||
| Super + right-button drag on a floating window | Resize the edge or corner selected by the starting 3×3 region |
|
| Super + right-button drag | Resize the edge or corner selected by the starting 3×3 region |
|
||||||
|
| Right-click while moving | Switch the drop between floating and tiled |
|
||||||
|
| Escape during a drag | Cancel the move or resize |
|
||||||
| Hold Shift during a move or resize | Bypass edge snapping |
|
| Hold Shift during a move or resize | Bypass edge snapping |
|
||||||
| Super + Shift + Space | Switch the focused window between tiled and floating |
|
| Super + Shift + Space | Switch the focused window between tiled and floating |
|
||||||
|
|
||||||
You can drag from inside the window; a title bar or resize border is not required.
|
You can drag from inside the window; a title bar or resize border is not required.
|
||||||
To place an app freely, toggle it to floating, then Super-drag it.
|
Drop a tiled window beside another window to reorder it, or in its center to
|
||||||
The central resize region selects the nearest corner. Moving and resizing snap
|
join a tab group. Drag across monitors to move to the workspace shown there.
|
||||||
to nearby edges; continuing to drag beyond 20 logical pixels releases the snap.
|
Scroll or hold at a horizontal screen edge to reach more scrolling columns.
|
||||||
|
The central resize region selects the nearest corner. Floating windows snap to
|
||||||
|
nearby edges; continuing to drag beyond 20 logical pixels releases the snap.
|
||||||
|
|
||||||
## Focus, move, and resize
|
## Focus, move, and resize
|
||||||
|
|
||||||
|
|||||||
@@ -61,10 +61,21 @@ input {
|
|||||||
|
|
||||||
touchpad {
|
touchpad {
|
||||||
tap #true
|
tap #true
|
||||||
|
accel-speed 0.0
|
||||||
natural-scroll #true
|
natural-scroll #true
|
||||||
disable-while-typing #true
|
disable-while-typing #true
|
||||||
|
disable-while-mouse-connected #false
|
||||||
swipe-threshold 80
|
swipe-threshold 80
|
||||||
}
|
}
|
||||||
|
mouse {
|
||||||
|
natural-scroll #false
|
||||||
|
accel-speed 0.0
|
||||||
|
left-handed #false
|
||||||
|
middle-emulation #false
|
||||||
|
scroll-factor 1.0
|
||||||
|
drag-threshold 4.0
|
||||||
|
scroll-cooldown-ms 150
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
layout {
|
layout {
|
||||||
|
|||||||
+1
@@ -74,6 +74,7 @@ static const struct wl_registry_listener registry_listener = {global, removed};
|
|||||||
static void configure(void *data, struct xdg_surface *xdg, uint32_t serial) {
|
static void configure(void *data, struct xdg_surface *xdg, uint32_t serial) {
|
||||||
(void)data;
|
(void)data;
|
||||||
xdg_surface_ack_configure(xdg, serial);
|
xdg_surface_ack_configure(xdg, serial);
|
||||||
|
if (!mapped) return;
|
||||||
xdg_surface_set_window_geometry(xdg, 0, 0, width, height);
|
xdg_surface_set_window_geometry(xdg, 0, 0, width, height);
|
||||||
wl_surface_attach(surface, buffer, 0, 0);
|
wl_surface_attach(surface, buffer, 0, 0);
|
||||||
wl_surface_damage(surface, 0, 0, width, height);
|
wl_surface_damage(surface, 0, 0, width, height);
|
||||||
|
|||||||
@@ -117,6 +117,7 @@ class Presentation(unittest.TestCase):
|
|||||||
self.assertEqual(count("idle"), idle)
|
self.assertEqual(count("idle"), idle)
|
||||||
target.stdin.write(b"u")
|
target.stdin.write(b"u")
|
||||||
target.stdin.flush()
|
target.stdin.flush()
|
||||||
|
windows(0)
|
||||||
time.sleep(.6)
|
time.sleep(.6)
|
||||||
self.assertGreater(count("idle"), idle, "unmapped inhibitor must stop inhibiting")
|
self.assertGreater(count("idle"), idle, "unmapped inhibitor must stop inhibiting")
|
||||||
target.stdin.close()
|
target.stdin.close()
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ impl App {
|
|||||||
default: 1.,
|
default: 1.,
|
||||||
min: 0.75,
|
min: 0.75,
|
||||||
max: 3.,
|
max: 3.,
|
||||||
step: 0.25,
|
step: 0.05,
|
||||||
suffix: "×",
|
suffix: "×",
|
||||||
integer: false,
|
integer: false,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -375,7 +375,7 @@ impl App {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
Page::Shortcuts => {
|
Page::Shortcuts => {
|
||||||
body = body.push(self.note("Keyboard and swipe bindings share the same actions. Use Swipe3Up, Swipe3Down, Swipe3Left, or Swipe3Right as a shortcut (also supports 4 or 5 fingers). Actions include toggle-overview, spawn, focus, move, workspace-next, workspace-previous, workspace-back-and-forth, and none. Leave Argument empty for actions such as toggle-overview or none."));
|
body = body.push(self.note("Keyboard, mouse and swipe bindings share the same actions. Mouse shortcuts can use MouseLeft, MouseRight, MouseMiddle, MouseBack, MouseForward or WheelUp/Down/Left/Right with modifiers. Use pointer-move or pointer-resize for dragging. Swipes use Swipe3Up, Swipe3Down, Swipe3Left or Swipe3Right (also supports 4 or 5 fingers). Actions such as toggle-overview and none need no argument."));
|
||||||
let mut gestures = row([]).spacing(6);
|
let mut gestures = row([]).spacing(6);
|
||||||
for (keys, label) in [
|
for (keys, label) in [
|
||||||
("Swipe3Up", "Customize swipe up"),
|
("Swipe3Up", "Customize swipe up"),
|
||||||
|
|||||||
@@ -827,6 +827,69 @@ pub fn fields(page: Page) -> Vec<Field> {
|
|||||||
"Avoid accidental pointer movement.",
|
"Avoid accidental pointer movement.",
|
||||||
true,
|
true,
|
||||||
),
|
),
|
||||||
|
toggle(
|
||||||
|
"input.touchpad.disable_while_mouse_connected",
|
||||||
|
"Disable touchpad with a mouse",
|
||||||
|
"Pause the built-in touchpad while an external mouse is connected.",
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
range(
|
||||||
|
"input.touchpad.accel_speed",
|
||||||
|
"Touchpad speed",
|
||||||
|
"Adjust pointer acceleration for the touchpad.",
|
||||||
|
RangeSpec {
|
||||||
|
default: 0.0,
|
||||||
|
min: -1.0,
|
||||||
|
max: 1.0,
|
||||||
|
step: 0.05,
|
||||||
|
suffix: "",
|
||||||
|
integer: false,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
toggle(
|
||||||
|
"input.mouse.natural_scroll",
|
||||||
|
"Mouse natural scrolling",
|
||||||
|
"Reverse the direction of the mouse wheel.",
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
toggle(
|
||||||
|
"input.mouse.left_handed",
|
||||||
|
"Left-handed mouse",
|
||||||
|
"Swap the primary and secondary mouse buttons.",
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
toggle(
|
||||||
|
"input.mouse.middle_emulation",
|
||||||
|
"Emulate middle click",
|
||||||
|
"Press both mouse buttons together to middle-click.",
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
range(
|
||||||
|
"input.mouse.accel_speed",
|
||||||
|
"Mouse speed",
|
||||||
|
"Adjust pointer acceleration for connected mice.",
|
||||||
|
RangeSpec {
|
||||||
|
default: 0.0,
|
||||||
|
min: -1.0,
|
||||||
|
max: 1.0,
|
||||||
|
step: 0.05,
|
||||||
|
suffix: "",
|
||||||
|
integer: false,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
range(
|
||||||
|
"input.mouse.scroll_factor",
|
||||||
|
"Mouse scroll speed",
|
||||||
|
"Scale wheel scrolling in applications.",
|
||||||
|
RangeSpec {
|
||||||
|
default: 1.0,
|
||||||
|
min: 0.1,
|
||||||
|
max: 5.0,
|
||||||
|
step: 0.1,
|
||||||
|
suffix: "×",
|
||||||
|
integer: false,
|
||||||
|
},
|
||||||
|
),
|
||||||
range(
|
range(
|
||||||
"input.touchpad.swipe_threshold",
|
"input.touchpad.swipe_threshold",
|
||||||
"Swipe distance",
|
"Swipe distance",
|
||||||
|
|||||||
Reference in new issue
Block a user