Open floating windows on the focused output
This commit is contained in:
1 parent
1b51ba83b4
commit
1a484adead
4 files changed
+188
-24
No files matched your search
@@ -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(
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -52,9 +52,10 @@ an oversized window keeps its size with its top-left corner reachable.
|
||||
Successful move/resize completion saves ordinary floating geometry by app ID in
|
||||
`$XDG_STATE_HOME/ferese/floating.json` (normally `~/.local/state/ferese/floating.json`).
|
||||
Geometry is stored with the output name and as fractions of its work area, so
|
||||
placement survives resolution changes. Restores are skipped if the output is
|
||||
missing or less than 25% of the window would be visible. A worker saves the
|
||||
latest snapshot, combining pending updates. Completing a drag updates memory
|
||||
placement survives resolution changes. New independent windows open on the
|
||||
focused output; saved geometry applies only when it belongs to that output and
|
||||
at least 25% of the window would be visible. A worker saves the latest snapshot,
|
||||
combining pending updates. Completing a drag updates memory
|
||||
immediately without waiting for storage.
|
||||
Transient dialogs and cancelled drags do not update this memory. Floating → tiled
|
||||
→ floating also restores the window's last floating geometry. Nested previews
|
||||
|
||||
Reference in new issue
Block a user