Support keyboard movement and resizing for floating windows
This commit is contained in:
1 parent
76112b59ac
commit
4a9880eaad
5 files changed
+261
-1
No files matched your search
@@ -1,6 +1,57 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
impl Ferese {
|
impl Ferese {
|
||||||
|
pub(super) fn adjust_floating_direction(&mut self, id: WindowId, direction: Direction, resize: bool) {
|
||||||
|
const STEP: f64 = 32.0;
|
||||||
|
let workspace = self.workspaces.active();
|
||||||
|
if self.session_lock.active()
|
||||||
|
|| self.input_capture.captures(1)
|
||||||
|
|| self.workspaces.workspace_for_window(id) != Some(workspace.id)
|
||||||
|
|| workspace.fullscreen == Some(id)
|
||||||
|
|| self.windows.record(id).is_some_and(|record| record.maximized)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let Some(WindowPlacement::Floating { rect }) = self.workspaces.placement(id) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(work) = self.floating_bounds_for_window(id) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let mut destination = rect;
|
||||||
|
match (resize, direction) {
|
||||||
|
(false, Direction::Left) => destination.x -= STEP,
|
||||||
|
(false, Direction::Right) => destination.x += STEP,
|
||||||
|
(false, Direction::Up) => destination.y -= STEP,
|
||||||
|
(false, Direction::Down) => destination.y += STEP,
|
||||||
|
(true, Direction::Left) => destination.width -= STEP,
|
||||||
|
(true, Direction::Right) => destination.width += STEP,
|
||||||
|
(true, Direction::Up) => destination.height -= STEP,
|
||||||
|
(true, Direction::Down) => destination.height += STEP,
|
||||||
|
}
|
||||||
|
if resize {
|
||||||
|
destination = constrained_floating_rect(
|
||||||
|
destination,
|
||||||
|
self.window_constraints().get(&id).copied().unwrap_or_default(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
destination = crate::floating::clamp_to_work(destination, work);
|
||||||
|
if destination == rect {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if let Err(error) = self.workspaces.set_floating_rect(id, destination) {
|
||||||
|
tracing::error!(%error, ?id, "failed to adjust floating window geometry");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.windows.update(id, |record| {
|
||||||
|
record.resize_anchor = resize.then_some((false, false, destination))
|
||||||
|
});
|
||||||
|
self.relayout_window(id);
|
||||||
|
if let Some(window) = self.windows.window(id).cloned() {
|
||||||
|
self.remember_floating(&window);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn toggle_focused_floating(&mut self) {
|
pub fn toggle_focused_floating(&mut self) {
|
||||||
let Some(window) = self.focused_window else {
|
let Some(window) = self.focused_window else {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -197,6 +197,13 @@ impl Ferese {
|
|||||||
let Some(current) = self.focused_window else {
|
let Some(current) = self.focused_window else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
if matches!(
|
||||||
|
self.workspaces.placement(current),
|
||||||
|
Some(WindowPlacement::Floating { .. })
|
||||||
|
) {
|
||||||
|
self.adjust_floating_direction(current, direction, false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
let Some(bounds) = self.output_bounds() else {
|
let Some(bounds) = self.output_bounds() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
@@ -220,6 +227,14 @@ impl Ferese {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if matches!(
|
||||||
|
self.workspaces.placement(current),
|
||||||
|
Some(WindowPlacement::Floating { .. })
|
||||||
|
) {
|
||||||
|
self.adjust_floating_direction(current, direction, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
match self
|
match self
|
||||||
.workspaces
|
.workspaces
|
||||||
.active_mut()
|
.active_mut()
|
||||||
|
|||||||
@@ -31,6 +31,188 @@ fn fixture() -> (smithay::reexports::calloop::EventLoop<'static, Ferese>, Ferese
|
|||||||
(events, state, output)
|
(events, state, output)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn keyboard_floating_move_and_resize_work_in_both_layouts() {
|
||||||
|
if !crate::startup_tests::private_runtime(
|
||||||
|
"state::window_lifecycle_tests::keyboard_floating_move_and_resize_work_in_both_layouts",
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let (mut events, mut state, _) = fixture();
|
||||||
|
let (window, _wire) = window(&mut state, &mut events, 0xff112233);
|
||||||
|
let id = state.windows.ids()[&window];
|
||||||
|
state.toggle_focused_floating();
|
||||||
|
let initial = Rect::new(120., 100., 160., 128.);
|
||||||
|
let workspace = state.workspaces.active_id();
|
||||||
|
for mode in [LayoutMode::Scrolling, LayoutMode::Tree] {
|
||||||
|
state
|
||||||
|
.workspaces
|
||||||
|
.set_active_layout_mode(mode, state.output_bounds().unwrap())
|
||||||
|
.unwrap();
|
||||||
|
for (resize, direction, expected) in [
|
||||||
|
(false, Direction::Left, Rect::new(88., 100., 160., 128.)),
|
||||||
|
(false, Direction::Right, Rect::new(152., 100., 160., 128.)),
|
||||||
|
(false, Direction::Up, Rect::new(120., 68., 160., 128.)),
|
||||||
|
(false, Direction::Down, Rect::new(120., 132., 160., 128.)),
|
||||||
|
(true, Direction::Left, Rect::new(120., 100., 128., 128.)),
|
||||||
|
(true, Direction::Right, Rect::new(120., 100., 192., 128.)),
|
||||||
|
(true, Direction::Up, Rect::new(120., 100., 160., 96.)),
|
||||||
|
(true, Direction::Down, Rect::new(120., 100., 160., 160.)),
|
||||||
|
] {
|
||||||
|
state.workspaces.set_floating_rect(id, initial).unwrap();
|
||||||
|
state.relayout_window(id);
|
||||||
|
if resize {
|
||||||
|
state.resize_direction(direction);
|
||||||
|
} else {
|
||||||
|
state.move_direction(direction);
|
||||||
|
}
|
||||||
|
assert_eq!(
|
||||||
|
state.workspaces.placement(id),
|
||||||
|
Some(WindowPlacement::Floating { rect: expected })
|
||||||
|
);
|
||||||
|
assert_eq!(state.windows.geometry(&id).unwrap().logical, expected);
|
||||||
|
assert_eq!(state.focused_window, Some(id));
|
||||||
|
assert_eq!(state.workspaces.active_id(), workspace);
|
||||||
|
assert_eq!(state.workspaces.workspace_for_window(id), Some(workspace));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn keyboard_floating_geometry_stays_reachable_and_respects_client_limits() {
|
||||||
|
if !crate::startup_tests::private_runtime(
|
||||||
|
"state::window_lifecycle_tests::keyboard_floating_geometry_stays_reachable_and_respects_client_limits",
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let (mut events, mut state, _) = fixture();
|
||||||
|
let (window, mut wire) = window(&mut state, &mut events, 0xff112233);
|
||||||
|
let id = state.windows.ids()[&window];
|
||||||
|
state.toggle_focused_floating();
|
||||||
|
request(&mut wire, 4, 8, &[100, 80], None);
|
||||||
|
request(&mut wire, 4, 7, &[200, 160], None);
|
||||||
|
request(&mut wire, 2, 6, &[], None);
|
||||||
|
dispatch(&mut events, &mut state);
|
||||||
|
state
|
||||||
|
.workspaces
|
||||||
|
.set_floating_rect(id, Rect::new(630., 470., 160., 128.))
|
||||||
|
.unwrap();
|
||||||
|
state.relayout_window(id);
|
||||||
|
for _ in 0..5 {
|
||||||
|
state.move_direction(Direction::Right);
|
||||||
|
state.move_direction(Direction::Down);
|
||||||
|
state.resize_direction(Direction::Right);
|
||||||
|
state.resize_direction(Direction::Down);
|
||||||
|
}
|
||||||
|
assert_eq!(
|
||||||
|
state.workspaces.placement(id),
|
||||||
|
Some(WindowPlacement::Floating {
|
||||||
|
rect: Rect::new(600., 440., 200., 160.),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
for _ in 0..30 {
|
||||||
|
state.move_direction(Direction::Left);
|
||||||
|
state.move_direction(Direction::Up);
|
||||||
|
state.resize_direction(Direction::Left);
|
||||||
|
state.resize_direction(Direction::Up);
|
||||||
|
}
|
||||||
|
assert_eq!(
|
||||||
|
state.workspaces.placement(id),
|
||||||
|
Some(WindowPlacement::Floating {
|
||||||
|
rect: Rect::new(0., 0., 100., 80.),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
request(&mut wire, 4, 7, &[0, 0], None);
|
||||||
|
request(&mut wire, 4, 8, &[900, 700], None);
|
||||||
|
request(&mut wire, 2, 6, &[], None);
|
||||||
|
dispatch(&mut events, &mut state);
|
||||||
|
state.resize_direction(Direction::Left);
|
||||||
|
state.resize_direction(Direction::Up);
|
||||||
|
state.move_direction(Direction::Right);
|
||||||
|
state.move_direction(Direction::Down);
|
||||||
|
assert_eq!(
|
||||||
|
state.workspaces.placement(id),
|
||||||
|
Some(WindowPlacement::Floating {
|
||||||
|
rect: Rect::new(0., 0., 900., 700.),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn keyboard_floating_actions_preserve_maximized_and_fullscreen_restore_geometry() {
|
||||||
|
if !crate::startup_tests::private_runtime(
|
||||||
|
"state::window_lifecycle_tests::keyboard_floating_actions_preserve_maximized_and_fullscreen_restore_geometry",
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let (mut events, mut state, _) = fixture();
|
||||||
|
let (window, _wire) = window(&mut state, &mut events, 0xff112233);
|
||||||
|
let id = state.windows.ids()[&window];
|
||||||
|
state.toggle_focused_floating();
|
||||||
|
let normal = state.workspaces.placement(id);
|
||||||
|
for fullscreen in [false, true] {
|
||||||
|
if fullscreen {
|
||||||
|
state.set_window_fullscreen(id, true);
|
||||||
|
} else {
|
||||||
|
state.set_window_maximized(id, true);
|
||||||
|
}
|
||||||
|
let geometry = state.windows.geometry(&id).unwrap().logical;
|
||||||
|
for direction in [Direction::Left, Direction::Right, Direction::Up, Direction::Down] {
|
||||||
|
state.move_direction(direction);
|
||||||
|
state.resize_direction(direction);
|
||||||
|
assert_eq!(state.workspaces.placement(id), normal);
|
||||||
|
assert_eq!(state.windows.geometry(&id).unwrap().logical, geometry);
|
||||||
|
}
|
||||||
|
if fullscreen {
|
||||||
|
state.set_window_fullscreen(id, false);
|
||||||
|
} else {
|
||||||
|
state.set_window_maximized(id, false);
|
||||||
|
}
|
||||||
|
let WindowPlacement::Floating { rect } = normal.unwrap() else {
|
||||||
|
panic!("floating window")
|
||||||
|
};
|
||||||
|
assert_eq!(state.windows.geometry(&id).unwrap().logical, rect);
|
||||||
|
}
|
||||||
|
state.focused_window = None;
|
||||||
|
state.move_direction(Direction::Right);
|
||||||
|
state.resize_direction(Direction::Down);
|
||||||
|
assert_eq!(state.workspaces.placement(id), normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn keyboard_floating_resize_reuses_the_client_barrier_and_move_keeps_it_pending() {
|
||||||
|
if !crate::startup_tests::private_runtime(
|
||||||
|
"state::window_lifecycle_tests::keyboard_floating_resize_reuses_the_client_barrier_and_move_keeps_it_pending",
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let (mut events, mut state, _) = fixture();
|
||||||
|
let (window, _wire) = window(&mut state, &mut events, 0xff112233);
|
||||||
|
let id = state.windows.ids()[&window];
|
||||||
|
state.toggle_focused_floating();
|
||||||
|
state.set_floating_window_geometry(&window, (120, 100).into(), (64, 48).into());
|
||||||
|
state.animations_enabled = true;
|
||||||
|
let before = state.windows.geometry(&id).unwrap().visual.current;
|
||||||
|
let client = state.windows.geometry(&id).unwrap().client.committed_size;
|
||||||
|
state.resize_direction(Direction::Right);
|
||||||
|
let transaction = *state.windows.transaction(&id).expect("existing resize barrier");
|
||||||
|
assert_eq!(transaction.source_geometry(), Some(window.geometry()));
|
||||||
|
assert_eq!(state.windows.geometry(&id).unwrap().client.committed_size, client);
|
||||||
|
assert_eq!(state.windows.geometry(&id).unwrap().visual.current, before);
|
||||||
|
assert_eq!(
|
||||||
|
state.windows.geometry(&id).unwrap().logical,
|
||||||
|
Rect::new(120., 100., 96., 48.)
|
||||||
|
);
|
||||||
|
state.move_direction(Direction::Right);
|
||||||
|
assert_eq!(state.windows.transaction(&id).unwrap().serial(), transaction.serial());
|
||||||
|
assert_eq!(state.windows.geometry(&id).unwrap().client.committed_size, client);
|
||||||
|
assert_eq!(state.windows.geometry(&id).unwrap().visual.current, before);
|
||||||
|
assert_eq!(
|
||||||
|
state.windows.geometry(&id).unwrap().logical,
|
||||||
|
Rect::new(152., 100., 96., 48.)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn late_keymap_failure_preserves_capture_sessions_and_runtime_settings() {
|
fn late_keymap_failure_preserves_capture_sessions_and_runtime_settings() {
|
||||||
if !crate::startup_tests::private_runtime(
|
if !crate::startup_tests::private_runtime(
|
||||||
|
|||||||
@@ -551,7 +551,13 @@ selects an edge or corner using a 3×3 grid; the center selects the nearest corn
|
|||||||
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.
|
||||||
|
|
||||||
Moving and resizing snap to work-area edges and nearby visible window edges at
|
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
|
||||||
|
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
|
||||||
|
floating windows ignore these actions.
|
||||||
|
|
||||||
|
Mouse 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.
|
||||||
|
|||||||
@@ -45,6 +45,12 @@ feresectl focus-floating
|
|||||||
tiled/floating layer on the active workspace. It does nothing when that layer
|
tiled/floating layer on the active workspace. It does nothing when that layer
|
||||||
has no candidate. `toggle-floating` changes placement; `focus-floating` changes focus.
|
has no candidate. `toggle-floating` changes placement; `focus-floating` changes focus.
|
||||||
|
|
||||||
|
`feresectl move left|right|up|down` and `feresectl resize left|right|up|down`
|
||||||
|
also handle floating windows. Moves use 32 logical pixels. Resizing shrinks
|
||||||
|
width/height with left/up and grows them with right/down by 32 logical pixels,
|
||||||
|
subject to client size limits. Fullscreen and maximized floating windows ignore
|
||||||
|
these actions.
|
||||||
|
|
||||||
## Choose a session
|
## Choose a session
|
||||||
|
|
||||||
Every feresectl IPC command, including theme commands and subscriptions, resolves
|
Every feresectl IPC command, including theme commands and subscriptions, resolves
|
||||||
|
|||||||
Reference in new issue
Block a user