Fix floating placement and restore on overlapping outputs (#60)

Carry output identity through floating placement and restore, and retain the owning output when saved geometry overlaps multiple monitors equally.

Fixes #59.
This commit is contained in:
Samuel Onoja authored and GitHub committed 2026-10-24 19:48:16 +01:00
1 parent 2248ee6846
commit df36d1c9b0
3 files changed
+220 -88

No files matched your search

+160 -44
View File
@@ -424,54 +424,170 @@ fn floating_launch_restores_geometry_only_on_the_focused_output() {
) {
return;
}
for restore_on_focused_output in [false, true] {
let (mut events, mut state, first, second) = floating_launch_fixture();
let saved_output = if restore_on_focused_output {
&second
} else {
&first
};
let saved_work = state.output_bounds_for(saved_output).unwrap();
let saved = Rect::new(saved_work.x + 100., saved_work.y + 80., 240., 160.);
state.floating_memory.save(
"dev.ferese.Settings".into(),
crate::floating::Remembered::new(saved_output.name(), saved, saved_work),
);
// Keep the pointer on the launch output to isolate saved-placement
// redirection from the separate keyboard-versus-pointer regression.
let pointer = state.seat.get_pointer().unwrap();
pointer.motion(
&mut state,
None,
&MotionEvent {
location: (900., 100.).into(),
serial: SERIAL_COUNTER.next_serial(),
time: smithay::backend::input::InputTime::from_millis(1),
},
);
state.focus_output_at(pointer.current_location());
let output_id = state.output_ids[&second];
let destination = state.output_workspaces.active_workspace(output_id).unwrap();
let (window, _wire) = super::capture_privacy::tests::window_with_app_id(
&mut state,
&mut events,
0xff112233,
"dev.ferese.Settings",
);
let id = state.windows.ids()[&window];
assert_eq!(state.output_workspaces.focused_output(), Some(output_id));
assert_eq!(state.workspaces.workspace_for_window(id), Some(destination));
let rect = state.windows.geometry(&id).unwrap().logical;
let bounds = state.output_bounds_for(&second).unwrap();
assert_eq!(crate::floating::intersection(rect, bounds), Some(rect));
if restore_on_focused_output {
assert_eq!(rect, saved);
} else {
assert_eq!((rect.width, rect.height), (64., 48.));
for second_x in [800, 400, 0] {
for restore_on_focused_output in [true, false] {
let (mut events, mut state, first, second) = floating_launch_fixture();
state.space.map_output(&second, (second_x, 0));
let saved_output = if restore_on_focused_output {
&second
} else {
&first
};
let saved_work = state.output_bounds_for(saved_output).unwrap();
let saved = Rect::new(saved_work.x + 100., saved_work.y + 80., 240., 160.);
state.floating_memory.save(
"dev.ferese.Settings".into(),
crate::floating::Remembered::new(saved_output.name(), saved, saved_work),
);
let output_id = state.output_ids[&second];
let destination = state.output_workspaces.active_workspace(output_id).unwrap();
state.activate_output_workspace(output_id, destination);
let (window, _wire) = super::capture_privacy::tests::window_with_app_id(
&mut state,
&mut events,
0xff112233,
"dev.ferese.Settings",
);
let id = state.windows.ids()[&window];
assert_eq!(state.output_workspaces.focused_output(), Some(output_id));
assert_eq!(state.workspaces.workspace_for_window(id), Some(destination));
let rect = state.windows.geometry(&id).unwrap().logical;
let bounds = state.output_bounds_for(&second).unwrap();
assert_eq!(crate::floating::intersection(rect, bounds), Some(rect));
if restore_on_focused_output {
assert_eq!(rect, saved, "second output at x={second_x}");
} else {
assert_eq!(
(rect.width, rect.height),
(64., 48.),
"memory belongs to the other output; second output at x={second_x}"
);
}
}
}
}
#[test]
fn floating_rule_restores_geometry_on_the_windows_output() {
if !crate::startup_tests::private_runtime(
"state::window_lifecycle_tests::floating_rule_restores_geometry_on_the_windows_output",
) {
return;
}
for second_x in [800, 0] {
let (mut events, mut state, first, second) = floating_launch_fixture();
state.space.map_output(&second, (second_x, 0));
let owner = state.output_ids[&second];
let workspace = state.output_workspaces.active_workspace(owner).unwrap();
state.activate_output_workspace(owner, workspace);
let (window, _wire) = window(&mut state, &mut events, 0xff112233);
let id = state.windows.ids()[&window];
let work = state.output_bounds_for(&second).unwrap();
let saved = Rect::new(work.x + 100., work.y + 80., 240., 160.);
state.windows.update(id, |record| {
record.floating_memory =
Some(crate::floating::Remembered::new(second.name(), saved, work));
});
let focused = state.output_ids[&first];
state.activate_output_workspace(
focused,
state.output_workspaces.active_workspace(focused).unwrap(),
);
state.apply_window_rule_result(
&window,
crate::window_rules::WindowRuleResult {
floating: Some(true),
..Default::default()
},
);
assert_eq!(state.output_workspaces.focused_output(), Some(focused));
assert_eq!(state.workspaces.workspace_for_window(id), Some(workspace));
assert_eq!(state.windows.geometry(&id).unwrap().logical, saved);
}
}
#[test]
fn floating_memory_round_trip_preserves_output_identity() {
if !crate::startup_tests::private_runtime(
"state::window_lifecycle_tests::floating_memory_round_trip_preserves_output_identity",
) {
return;
}
for second_x in [0, 400] {
for on_second in [false, true] {
let (mut events, mut state, first, second) = floating_launch_fixture();
state.space.map_output(&second, (second_x, 0));
let owner = if on_second { &second } else { &first };
let output_id = state.output_ids[owner];
let workspace = state.output_workspaces.active_workspace(output_id).unwrap();
state.activate_output_workspace(output_id, workspace);
let (window, wire) = super::capture_privacy::tests::window_with_app_id(
&mut state,
&mut events,
0xff112233,
"dev.ferese.Settings",
);
let id = state.windows.ids()[&window];
// The whole rectangle fits both outputs, so area alone cannot
// decide which monitor's placement memory should own it.
let saved = Rect::new(f64::from(second_x) + 100., 80., 240., 160.);
state.workspaces.set_floating_rect(id, saved).unwrap();
state.relayout_window(id);
state.remember_floating(&window);
assert_eq!(
state
.floating_memory
.get("dev.ferese.Settings")
.unwrap()
.output,
owner.name(),
"owner={} second_x={second_x}",
owner.name(),
);
drop(wire);
dispatch(&mut events, &mut state);
assert!(state.windows.ids().is_empty());
state.activate_output_workspace(output_id, workspace);
let (restored, _wire) = super::capture_privacy::tests::window_with_app_id(
&mut state,
&mut events,
0xff112233,
"dev.ferese.Settings",
);
let id = state.windows.ids()[&restored];
assert_eq!(state.windows.geometry(&id).unwrap().logical, saved);
}
}
}
#[test]
fn floating_memory_follows_a_drag_to_another_output() {
if !crate::startup_tests::private_runtime(
"state::window_lifecycle_tests::floating_memory_follows_a_drag_to_another_output",
) {
return;
}
let (mut events, mut state, first, second) = floating_launch_fixture();
let owner = state.output_ids[&first];
let workspace = state.output_workspaces.active_workspace(owner).unwrap();
state.activate_output_workspace(owner, workspace);
let (window, _wire) = super::capture_privacy::tests::window_with_app_id(
&mut state,
&mut events,
0xff112233,
"dev.ferese.Settings",
);
let id = state.windows.ids()[&window];
let saved = Rect::new(900., 80., 240., 160.);
state.workspaces.set_floating_rect(id, saved).unwrap();
state.relayout_window(id);
state.remember_floating(&window);
assert_eq!(state.workspaces.workspace_for_window(id), Some(workspace));
let memory = state.floating_memory.get("dev.ferese.Settings").unwrap();
assert_eq!(memory.output, second.name());
assert_eq!(memory.restore(&state.floating_outputs()), Some(saved));
}
#[test]
fn keyboard_floating_move_and_resize_work_in_both_layouts() {
if !crate::startup_tests::private_runtime(