Open floating windows on the focused output

This commit is contained in:
Samuel Onoja committed 2026-10-23 01:17:20 +01:00
1 parent 1b51ba83b4
commit 1a484adead
4 files changed
+188 -24

No files matched your search

+54 -7
View File
@@ -70,15 +70,29 @@ pub(crate) fn dispatch(events: &mut EventLoop<'static, Ferese>, state: &mut Fere
}
pub(crate) fn ack_configure(wire: &mut UnixStream, xdg: u32) -> u32 {
ack_configure_with_size(wire, xdg, None).0
}
fn ack_configure_with_size(
wire: &mut UnixStream,
xdg: u32,
toplevel: Option<u32>,
) -> (u32, Option<(u32, u32)>) {
wire.set_nonblocking(true).unwrap();
let mut bytes = Vec::new();
let _ = wire.read_to_end(&mut bytes);
wire.set_nonblocking(false).unwrap();
let mut serial = None;
let mut size = None;
let mut offset = 0;
while offset < bytes.len() {
let object = u32::from_ne_bytes(bytes[offset..offset + 4].try_into().unwrap());
let header = u32::from_ne_bytes(bytes[offset + 4..offset + 8].try_into().unwrap());
if Some(object) == toplevel && header & 0xffff == 0 {
let width = u32::from_ne_bytes(bytes[offset + 8..offset + 12].try_into().unwrap());
let height = u32::from_ne_bytes(bytes[offset + 12..offset + 16].try_into().unwrap());
size = Some((width, height));
}
if object == xdg && header & 0xffff == 0 {
serial = Some(u32::from_ne_bytes(
bytes[offset + 8..offset + 12].try_into().unwrap(),
@@ -88,13 +102,22 @@ pub(crate) fn ack_configure(wire: &mut UnixStream, xdg: u32) -> u32 {
}
assert!(serial.is_some(), "missing configure for {xdg}: {bytes:?}");
request(wire, xdg, 4, &[serial.unwrap()], None);
serial.unwrap()
(serial.unwrap(), size)
}
pub(crate) fn window(
state: &mut Ferese,
events: &mut EventLoop<'static, Ferese>,
color: u32,
) -> (Window, UnixStream) {
window_with_app_id(state, events, color, "")
}
pub(crate) fn window_with_app_id(
state: &mut Ferese,
events: &mut EventLoop<'static, Ferese>,
color: u32,
app_id: &str,
) -> (Window, UnixStream) {
let (server, mut wire) = UnixStream::pair().unwrap();
let client = state
@@ -125,23 +148,47 @@ pub(crate) fn window(
request(&mut wire, compositor.id().protocol_id(), 0, &[2], None);
request(&mut wire, shell.id().protocol_id(), 2, &[3, 2], None);
request(&mut wire, 3, 1, &[4], None);
if !app_id.is_empty() {
let mut bytes = app_id.as_bytes().to_vec();
bytes.push(0);
let mut args = vec![bytes.len() as u32];
bytes.resize(bytes.len().next_multiple_of(4), 0);
args.extend(
bytes
.chunks_exact(4)
.map(|word| u32::from_ne_bytes(word.try_into().unwrap())),
);
request(&mut wire, 4, 3, &args, None);
}
request(&mut wire, 2, 6, &[], None);
dispatch(events, state);
ack_configure(&mut wire, 3);
let (_, configured) = ack_configure_with_size(&mut wire, 3, Some(4));
// Application fixtures honor the first configured size, as Settings does.
// Existing capture fixtures keep their fixed 64x48 buffer.
let (width, height) = if app_id.is_empty() {
(64, 48)
} else {
let (width, height) = configured.expect("initial toplevel configure");
(
if width > 0 { width } else { 64 },
if height > 0 { height } else { 48 },
)
};
let buffer_size = width * height * 4;
let mut file = tempfile::tempfile().unwrap();
file.write_all(&color.to_ne_bytes().repeat(64 * 48))
file.write_all(&color.to_ne_bytes().repeat((width * height) as usize))
.unwrap();
request(
&mut wire,
shm.id().protocol_id(),
0,
&[5, 64 * 48 * 4],
&[5, buffer_size],
Some(file.as_raw_fd()),
);
request(&mut wire, 5, 0, &[6, 0, 64, 48, 64 * 4, 0], None);
request(&mut wire, 3, 3, &[0, 0, 64, 48], None);
request(&mut wire, 5, 0, &[6, 0, width, height, width * 4, 0], None);
request(&mut wire, 3, 3, &[0, 0, width, height], None);
request(&mut wire, 2, 1, &[6, 0, 0], None);
request(&mut wire, 2, 2, &[0, 0, 64, 48], None);
request(&mut wire, 2, 2, &[0, 0, width, height], None);
request(&mut wire, 2, 6, &[], None);
dispatch(events, state);
let window = state
@@ -359,6 +359,119 @@ fn floating_placement_reserves_the_full_opening_window_footprint() {
assert!(crate::floating::intersection(placed, logical).is_none());
}
fn floating_launch_fixture() -> (
smithay::reexports::calloop::EventLoop<'static, Ferese>,
Ferese,
Output,
Output,
) {
let (events, mut state, first) = fixture();
let second = Output::new("second".into(), first.physical_properties());
second.change_current_state(first.current_mode(), None, None, None);
state.space.map_output(&second, (800, 0));
state.register_output(&second, "second".into());
state.window_rules = crate::config::Config::parse_source(
"window-rule app-id=dev.ferese.Settings floating=#true\n",
)
.unwrap()
.window_rules()
.unwrap();
(events, state, first, second)
}
#[test]
fn floating_launch_keeps_keyboard_focused_output_when_pointer_is_elsewhere() {
if !crate::startup_tests::private_runtime(
"state::window_lifecycle_tests::floating_launch_keeps_keyboard_focused_output_when_pointer_is_elsewhere",
) {
return;
}
let (mut events, mut state, first, second) = floating_launch_fixture();
let pointer = state.seat.get_pointer().unwrap();
pointer.motion(
&mut state,
None,
&MotionEvent {
location: (100., 100.).into(),
serial: SERIAL_COUNTER.next_serial(),
time: smithay::backend::input::InputTime::from_millis(1),
},
);
let output_id = state.output_ids[&second];
let destination = state.output_workspaces.active_workspace(output_id).unwrap();
state.activate_output_workspace(output_id, destination);
assert_eq!(pointer.current_location(), (100., 100.).into());
assert_ne!(state.output_ids[&first], output_id);
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 bounds = state.output_bounds_for(&second).unwrap();
let rect = state.windows.geometry(&id).unwrap().logical;
assert_eq!(crate::floating::intersection(rect, bounds), Some(rect));
}
#[test]
fn floating_launch_restores_geometry_only_on_the_focused_output() {
if !crate::startup_tests::private_runtime(
"state::window_lifecycle_tests::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.));
}
}
}
#[test]
fn keyboard_floating_move_and_resize_work_in_both_layouts() {
if !crate::startup_tests::private_runtime(
+17 -14
View File
@@ -58,13 +58,17 @@ impl Ferese {
return;
}
let bounds = self.output_bounds();
let outputs = self.floating_outputs();
let Some((remembered, work)) = app_id
.as_deref()
.and_then(|app| self.floating_memory.get(app))
.and_then(|entry| {
let rect = entry.restore(&outputs)?;
let work = outputs.iter().find(|(name, _)| *name == entry.output)?.1;
let work = outputs
.iter()
.find(|(name, work)| *name == entry.output && Some(*work) == bounds)?
.1;
Some((rect, work))
})
else {
@@ -106,10 +110,13 @@ impl Ferese {
return;
}
if let Some(pointer) = self.seat.get_pointer() {
self.focus_output_at(pointer.current_location());
}
let Some(bounds) = self.output_bounds() else {
self.add_tiled_window(window);
return;
};
// Saved geometry may restore a position on this output, but must not
// redirect a new window away from the output the user focused.
let remembered_work = app_id
.filter(|app| !crate::window_rules::is_native_dialog(Some(app)))
.and_then(|app| self.floating_memory.get(app))
@@ -118,18 +125,9 @@ impl Ferese {
entry.restore(&outputs)?;
outputs
.into_iter()
.find(|(name, _)| *name == entry.output)
.find(|(name, work)| *name == entry.output && *work == bounds)
.map(|(_, work)| work)
});
if let Some(work) = remembered_work {
self.focus_output_at((work.x + work.width / 2.0, work.y + work.height / 2.0).into());
}
let Some(bounds) = self.output_bounds() else {
self.add_tiled_window(window);
return;
};
let id = self.windows.allocate_id();
let size_rect = client_size(&window)
.map(|size| natural_floating_rect(bounds, size))
@@ -434,6 +432,11 @@ impl Ferese {
.0
.as_deref()
.and_then(|app| self.floating_memory.get(app))
.filter(|entry| {
self.floating_outputs()
.iter()
.any(|(name, work)| *name == entry.output && *work == bounds)
})
.and_then(|entry| entry.restore(&self.floating_outputs()))
.is_none()
{