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 {
|
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();
|
wire.set_nonblocking(true).unwrap();
|
||||||
let mut bytes = Vec::new();
|
let mut bytes = Vec::new();
|
||||||
let _ = wire.read_to_end(&mut bytes);
|
let _ = wire.read_to_end(&mut bytes);
|
||||||
wire.set_nonblocking(false).unwrap();
|
wire.set_nonblocking(false).unwrap();
|
||||||
let mut serial = None;
|
let mut serial = None;
|
||||||
|
let mut size = None;
|
||||||
let mut offset = 0;
|
let mut offset = 0;
|
||||||
while offset < bytes.len() {
|
while offset < bytes.len() {
|
||||||
let object = u32::from_ne_bytes(bytes[offset..offset + 4].try_into().unwrap());
|
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());
|
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 {
|
if object == xdg && header & 0xffff == 0 {
|
||||||
serial = Some(u32::from_ne_bytes(
|
serial = Some(u32::from_ne_bytes(
|
||||||
bytes[offset + 8..offset + 12].try_into().unwrap(),
|
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:?}");
|
assert!(serial.is_some(), "missing configure for {xdg}: {bytes:?}");
|
||||||
request(wire, xdg, 4, &[serial.unwrap()], None);
|
request(wire, xdg, 4, &[serial.unwrap()], None);
|
||||||
serial.unwrap()
|
(serial.unwrap(), size)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn window(
|
pub(crate) fn window(
|
||||||
state: &mut Ferese,
|
state: &mut Ferese,
|
||||||
events: &mut EventLoop<'static, Ferese>,
|
events: &mut EventLoop<'static, Ferese>,
|
||||||
color: u32,
|
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) {
|
) -> (Window, UnixStream) {
|
||||||
let (server, mut wire) = UnixStream::pair().unwrap();
|
let (server, mut wire) = UnixStream::pair().unwrap();
|
||||||
let client = state
|
let client = state
|
||||||
@@ -125,23 +148,47 @@ pub(crate) fn window(
|
|||||||
request(&mut wire, compositor.id().protocol_id(), 0, &[2], None);
|
request(&mut wire, compositor.id().protocol_id(), 0, &[2], None);
|
||||||
request(&mut wire, shell.id().protocol_id(), 2, &[3, 2], None);
|
request(&mut wire, shell.id().protocol_id(), 2, &[3, 2], None);
|
||||||
request(&mut wire, 3, 1, &[4], 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);
|
request(&mut wire, 2, 6, &[], None);
|
||||||
dispatch(events, state);
|
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();
|
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();
|
.unwrap();
|
||||||
request(
|
request(
|
||||||
&mut wire,
|
&mut wire,
|
||||||
shm.id().protocol_id(),
|
shm.id().protocol_id(),
|
||||||
0,
|
0,
|
||||||
&[5, 64 * 48 * 4],
|
&[5, buffer_size],
|
||||||
Some(file.as_raw_fd()),
|
Some(file.as_raw_fd()),
|
||||||
);
|
);
|
||||||
request(&mut wire, 5, 0, &[6, 0, 64, 48, 64 * 4, 0], None);
|
request(&mut wire, 5, 0, &[6, 0, width, height, width * 4, 0], None);
|
||||||
request(&mut wire, 3, 3, &[0, 0, 64, 48], None);
|
request(&mut wire, 3, 3, &[0, 0, width, height], None);
|
||||||
request(&mut wire, 2, 1, &[6, 0, 0], 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);
|
request(&mut wire, 2, 6, &[], None);
|
||||||
dispatch(events, state);
|
dispatch(events, state);
|
||||||
let window = 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());
|
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]
|
#[test]
|
||||||
fn keyboard_floating_move_and_resize_work_in_both_layouts() {
|
fn keyboard_floating_move_and_resize_work_in_both_layouts() {
|
||||||
if !crate::startup_tests::private_runtime(
|
if !crate::startup_tests::private_runtime(
|
||||||
|
|||||||
@@ -58,13 +58,17 @@ impl Ferese {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let bounds = self.output_bounds();
|
||||||
let outputs = self.floating_outputs();
|
let outputs = self.floating_outputs();
|
||||||
let Some((remembered, work)) = app_id
|
let Some((remembered, work)) = app_id
|
||||||
.as_deref()
|
.as_deref()
|
||||||
.and_then(|app| self.floating_memory.get(app))
|
.and_then(|app| self.floating_memory.get(app))
|
||||||
.and_then(|entry| {
|
.and_then(|entry| {
|
||||||
let rect = entry.restore(&outputs)?;
|
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))
|
Some((rect, work))
|
||||||
})
|
})
|
||||||
else {
|
else {
|
||||||
@@ -106,10 +110,13 @@ impl Ferese {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(pointer) = self.seat.get_pointer() {
|
let Some(bounds) = self.output_bounds() else {
|
||||||
self.focus_output_at(pointer.current_location());
|
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
|
let remembered_work = app_id
|
||||||
.filter(|app| !crate::window_rules::is_native_dialog(Some(app)))
|
.filter(|app| !crate::window_rules::is_native_dialog(Some(app)))
|
||||||
.and_then(|app| self.floating_memory.get(app))
|
.and_then(|app| self.floating_memory.get(app))
|
||||||
@@ -118,18 +125,9 @@ impl Ferese {
|
|||||||
entry.restore(&outputs)?;
|
entry.restore(&outputs)?;
|
||||||
outputs
|
outputs
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.find(|(name, _)| *name == entry.output)
|
.find(|(name, work)| *name == entry.output && *work == bounds)
|
||||||
.map(|(_, work)| work)
|
.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 id = self.windows.allocate_id();
|
||||||
let size_rect = client_size(&window)
|
let size_rect = client_size(&window)
|
||||||
.map(|size| natural_floating_rect(bounds, size))
|
.map(|size| natural_floating_rect(bounds, size))
|
||||||
@@ -434,6 +432,11 @@ impl Ferese {
|
|||||||
.0
|
.0
|
||||||
.as_deref()
|
.as_deref()
|
||||||
.and_then(|app| self.floating_memory.get(app))
|
.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()))
|
.and_then(|entry| entry.restore(&self.floating_outputs()))
|
||||||
.is_none()
|
.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
|
Successful move/resize completion saves ordinary floating geometry by app ID in
|
||||||
`$XDG_STATE_HOME/ferese/floating.json` (normally `~/.local/state/ferese/floating.json`).
|
`$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
|
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
|
placement survives resolution changes. New independent windows open on the
|
||||||
missing or less than 25% of the window would be visible. A worker saves the
|
focused output; saved geometry applies only when it belongs to that output and
|
||||||
latest snapshot, combining pending updates. Completing a drag updates memory
|
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.
|
immediately without waiting for storage.
|
||||||
Transient dialogs and cancelled drags do not update this memory. Floating → tiled
|
Transient dialogs and cancelled drags do not update this memory. Floating → tiled
|
||||||
→ floating also restores the window's last floating geometry. Nested previews
|
→ floating also restores the window's last floating geometry. Nested previews
|
||||||
|
|||||||
Reference in new issue
Block a user