1181 lines
47 KiB
Rust
1181 lines
47 KiB
Rust
use super::capture_privacy::tests::{ack_configure, dispatch, request, window};
|
|
use super::*;
|
|
use smithay::input::pointer::{Focus, GrabStartData, MotionEvent};
|
|
use smithay::reexports::wayland_server::Resource;
|
|
use smithay::utils::SERIAL_COUNTER;
|
|
|
|
fn fixture() -> (smithay::reexports::calloop::EventLoop<'static, Ferese>, Ferese, Output) {
|
|
let mut events = smithay::reexports::calloop::EventLoop::try_new().unwrap();
|
|
let mut state = crate::startup_tests::state(&mut events);
|
|
state.animations_enabled = false;
|
|
let output = Output::new(
|
|
"test".into(),
|
|
smithay::output::PhysicalProperties {
|
|
size: (0, 0).into(),
|
|
subpixel: smithay::output::Subpixel::Unknown,
|
|
make: "test".into(),
|
|
model: "test".into(),
|
|
},
|
|
);
|
|
output.change_current_state(
|
|
Some(smithay::output::Mode {
|
|
size: (800, 600).into(),
|
|
refresh: 60_000,
|
|
}),
|
|
None,
|
|
None,
|
|
None,
|
|
);
|
|
state.space.map_output(&output, (0, 0));
|
|
state.register_output(&output, "test".into());
|
|
(events, state, output)
|
|
}
|
|
|
|
#[test]
|
|
fn width_cycle_keeps_stacked_rows_together_through_holds_forecasts_and_reversal() {
|
|
if !crate::startup_tests::private_runtime(
|
|
"state::window_lifecycle_tests::width_cycle_keeps_stacked_rows_together_through_holds_forecasts_and_reversal",
|
|
) {
|
|
return;
|
|
}
|
|
for animations in [true, false] {
|
|
let (mut events, mut state, output) = fixture();
|
|
state.animation_test_time = Some(Duration::ZERO);
|
|
state.gap_config = GapConfig {
|
|
inner: 10.0,
|
|
outer: 10.0,
|
|
smart: false,
|
|
};
|
|
state.column_width_presets = vec![ColumnWidth::Proportion(0.5), ColumnWidth::Full];
|
|
let WorkspaceLayout::Scrolling(layout) = &mut state.workspaces.active_mut().layout else {
|
|
panic!("expected scrolling layout");
|
|
};
|
|
layout.set_default_width(ColumnWidth::Proportion(0.5));
|
|
layout.set_focus_strategy(ferese_layout::ViewportFocusStrategy::Minimal);
|
|
let (neighbor, _neighbor_wire) = window(&mut state, &mut events, 0xff112233);
|
|
let (first, _first_wire) = window(&mut state, &mut events, 0xff445566);
|
|
let (second, _second_wire) = window(&mut state, &mut events, 0xff778899);
|
|
let a = state.windows.ids()[&first];
|
|
let b = state.windows.ids()[&second];
|
|
state.workspaces.stack_window(b, a).unwrap();
|
|
state.focused_window = Some(state.windows.ids()[&neighbor]);
|
|
state.relayout();
|
|
state.focused_window = Some(b);
|
|
state.relayout();
|
|
let ids = [a, b];
|
|
for id in state.windows.resizing().copied().collect::<Vec<_>>() {
|
|
state.windows.clear_transaction(&id);
|
|
}
|
|
state.release_presentation_dependencies();
|
|
state.advance_animations_at(Duration::ZERO, Duration::ZERO);
|
|
let before = state.windows.geometry(&a).unwrap().visual.current.width;
|
|
assert_eq!(state.windows.geometry(&b).unwrap().visual.current.width, before);
|
|
state.animations_enabled = animations;
|
|
state.spring_config = SpringConfig {
|
|
stiffness: 900.0,
|
|
damping: 65.0,
|
|
..Default::default()
|
|
};
|
|
state.viewport_spring_config = SpringConfig {
|
|
stiffness: 200.0,
|
|
damping: 30.0,
|
|
..Default::default()
|
|
};
|
|
state.focused_window = Some(b);
|
|
let workspace = state.workspaces.active_id();
|
|
let viewport_before = state.viewports[&workspace].motion().target;
|
|
// An interrupted width motion must provide one source and velocity for
|
|
// the column, even if a sibling previously used its general spring.
|
|
if animations {
|
|
state.windows.update(a, |record| {
|
|
let mut width = AnimatedValue::new(before);
|
|
width.velocity = 25.0;
|
|
record.coupled_width = Some((workspace, width));
|
|
});
|
|
}
|
|
state.cycle_focused_column_width();
|
|
assert!(state.viewports[&workspace].motion().target > viewport_before);
|
|
for id in ids {
|
|
assert!(state.windows.record(id).unwrap().coupled_width.is_some());
|
|
assert_eq!(state.windows.transaction(&id).is_some(), animations);
|
|
}
|
|
let assert_equal = |state: &Ferese| {
|
|
let first = *state.windows.geometry(&a).unwrap();
|
|
let second = *state.windows.geometry(&b).unwrap();
|
|
assert_eq!(first.visual.current.width, second.visual.current.width);
|
|
assert_eq!(first.visual.velocity.width, second.visual.velocity.width);
|
|
for horizon in [Duration::ZERO, Duration::from_millis(4), Duration::from_millis(16)] {
|
|
let scene = state.sample_frame(&output, horizon);
|
|
assert_eq!(
|
|
scene.windows[&a].geometry.visual.current.width,
|
|
scene.windows[&b].geometry.visual.current.width
|
|
);
|
|
}
|
|
assert_eq!(*state.windows.geometry(&a).unwrap(), first);
|
|
assert_eq!(*state.windows.geometry(&b).unwrap(), second);
|
|
};
|
|
assert_equal(&state);
|
|
state.windows.clear_transaction(&a);
|
|
state.release_presentation_dependencies();
|
|
assert_eq!(
|
|
state.presentation_dependencies.reflow_blocked(a, &state.windows),
|
|
animations
|
|
);
|
|
assert_eq!(
|
|
state.presentation_dependencies.reflow_blocked(b, &state.windows),
|
|
animations
|
|
);
|
|
for millis in [16, 100, 299, 300, 316, 332] {
|
|
state.advance_animations_at(Duration::from_millis(16), Duration::from_millis(millis));
|
|
assert_equal(&state);
|
|
}
|
|
assert!(
|
|
state.windows.transaction(&b).is_none(),
|
|
"existing deadline releases the last sibling"
|
|
);
|
|
if animations {
|
|
assert!(state.windows.geometry(&a).unwrap().visual.current.width > before);
|
|
}
|
|
// Retarget the same column while its spring is moving.
|
|
state.cycle_focused_column_width();
|
|
assert_equal(&state);
|
|
for id in ids {
|
|
state.windows.clear_transaction(&id);
|
|
}
|
|
state.release_presentation_dependencies();
|
|
for frame in 1..=150 {
|
|
state.advance_animations_at(Duration::from_millis(16), Duration::from_millis(332 + frame * 16));
|
|
assert_equal(&state);
|
|
}
|
|
assert_eq!(state.windows.geometry(&a).unwrap().visual.current.width, before);
|
|
assert_eq!(state.windows.geometry(&b).unwrap().visual.current.width, before);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn floating_placement_reserves_the_full_opening_window_footprint() {
|
|
if !crate::startup_tests::private_runtime(
|
|
"state::window_lifecycle_tests::floating_placement_reserves_the_full_opening_window_footprint",
|
|
) {
|
|
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 bounds = state.output_bounds().unwrap();
|
|
let logical = state.windows.geometry(&id).unwrap().logical;
|
|
state.animations_enabled = true;
|
|
state
|
|
.windows
|
|
.update(id, |record| record.opening = Some(AnimatedValue::new(0.0)));
|
|
let visual = state.presented_window_rect(id).unwrap();
|
|
assert!(visual.width < logical.width);
|
|
let obstacles = state.floating_obstacles(None, bounds);
|
|
assert_eq!(
|
|
obstacles,
|
|
vec![(id.0, crate::floating::intersection(logical, bounds).unwrap())]
|
|
);
|
|
let placed = crate::floating::min_overlap((80., 80.), bounds, &[obstacles[0].1], (logical.x, logical.y)).unwrap();
|
|
assert!(crate::floating::intersection(placed, logical).is_none());
|
|
}
|
|
|
|
#[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]
|
|
fn late_keymap_failure_preserves_capture_sessions_and_runtime_settings() {
|
|
if !crate::startup_tests::private_runtime(
|
|
"state::window_lifecycle_tests::late_keymap_failure_preserves_capture_sessions_and_runtime_settings",
|
|
) {
|
|
return;
|
|
}
|
|
|
|
let (mut events, mut state, _) = fixture();
|
|
let (window, _wire) = window(&mut state, &mut events, 0xff112233);
|
|
let focus = state.focused_window;
|
|
let session = state.input_capture.register(42, 1).unwrap();
|
|
state.input_capture.enable(session).unwrap();
|
|
let previous_layout = state.input_settings.xkb_layout.clone();
|
|
let previous_repeat = state.input_settings.repeat_rate;
|
|
let mut runtime = crate::config::Config::default().runtime_config().unwrap();
|
|
// Preparation succeeded; force the real late application failure.
|
|
runtime.input_settings.xkb_layout = "ferese-nonexistent-layout-for-regression".into();
|
|
runtime.input_settings.repeat_rate = previous_repeat + 1;
|
|
let result = state.apply_runtime_config(runtime, &serde_json::json!({"input": "changed"}));
|
|
assert!(result.unwrap_err().contains("keymap reload failed"));
|
|
assert!(state.input_capture.has_sessions());
|
|
state.input_capture.enable(session).unwrap();
|
|
assert!(!state.input_capture.restore_focus);
|
|
assert_eq!(state.focused_window, focus);
|
|
assert_eq!(state.input_settings.xkb_layout, previous_layout);
|
|
assert_eq!(state.input_settings.repeat_rate, previous_repeat);
|
|
assert!(state.windows.ids().contains_key(&window));
|
|
}
|
|
|
|
#[test]
|
|
fn dmabuf_validation_uses_the_originating_global_without_a_desktop_frame() {
|
|
use smithay::backend::allocator::{Buffer, Format, Fourcc, Modifier};
|
|
use std::io::Read;
|
|
use std::os::fd::AsRawFd;
|
|
use std::os::unix::net::UnixStream;
|
|
use std::sync::Arc;
|
|
|
|
if !crate::startup_tests::private_runtime(
|
|
"state::window_lifecycle_tests::dmabuf_validation_uses_the_originating_global_without_a_desktop_frame",
|
|
) {
|
|
return;
|
|
}
|
|
|
|
let mut events = smithay::reexports::calloop::EventLoop::try_new().unwrap();
|
|
let mut state = crate::startup_tests::state(&mut events);
|
|
let formats = [Format {
|
|
code: Fourcc::Argb8888,
|
|
modifier: Modifier::Linear,
|
|
}];
|
|
let a = state
|
|
.dmabuf_state
|
|
.create_global::<Ferese>(&state.display_handle, formats);
|
|
state.dmabuf_imports.register(a);
|
|
let (server, mut wire) = UnixStream::pair().unwrap();
|
|
let client = state
|
|
.display_handle
|
|
.insert_client(server, Arc::new(ClientState::default()))
|
|
.unwrap();
|
|
request(&mut wire, 1, 1, &[2], None);
|
|
dispatch(&mut events, &mut state);
|
|
let read = |wire: &mut UnixStream| {
|
|
wire.set_nonblocking(true).unwrap();
|
|
let mut bytes = Vec::new();
|
|
let _ = wire.read_to_end(&mut bytes);
|
|
wire.set_nonblocking(false).unwrap();
|
|
bytes
|
|
};
|
|
let mut registry = read(&mut wire);
|
|
// Register B after the initial registry roundtrip so notification order
|
|
// identifies A and B without depending on backend map iteration order.
|
|
let b = state
|
|
.dmabuf_state
|
|
.create_global::<Ferese>(&state.display_handle, formats);
|
|
state.dmabuf_imports.register(b);
|
|
state.display_handle.flush_clients().unwrap();
|
|
registry.extend(read(&mut wire));
|
|
let mut offset = 0;
|
|
let mut names = Vec::new();
|
|
while offset < registry.len() {
|
|
let header = u32::from_ne_bytes(registry[offset + 4..offset + 8].try_into().unwrap());
|
|
let length = (header >> 16) as usize;
|
|
let message = ®istry[offset..offset + length];
|
|
if message
|
|
.windows(b"zwp_linux_dmabuf_v1\0".len())
|
|
.any(|text| text == b"zwp_linux_dmabuf_v1\0")
|
|
{
|
|
names.push(u32::from_ne_bytes(message[8..12].try_into().unwrap()));
|
|
}
|
|
offset += length;
|
|
}
|
|
assert_eq!(names.len(), 2);
|
|
let interface = b"zwp_linux_dmabuf_v1\0";
|
|
for (name, id) in names.into_iter().zip([3, 4]) {
|
|
let mut args = vec![name, interface.len() as u32];
|
|
let mut padded = interface.to_vec();
|
|
padded.resize(padded.len().next_multiple_of(4), 0);
|
|
args.extend(
|
|
padded
|
|
.chunks_exact(4)
|
|
.map(|word| u32::from_ne_bytes(word.try_into().unwrap())),
|
|
);
|
|
args.extend([3, id]);
|
|
request(&mut wire, 2, 0, &args, None);
|
|
}
|
|
dispatch(&mut events, &mut state);
|
|
read(&mut wire);
|
|
let file = tempfile::tempfile().unwrap();
|
|
file.set_len(64).unwrap();
|
|
// Both asynchronous and immediate imports must survive the wrong GPU
|
|
// being visited first. The mock capability accepts only B's buffers.
|
|
for (params, buffer) in [(5, None), (6, Some(7))] {
|
|
request(&mut wire, 4, 1, &[params], None);
|
|
request(&mut wire, params, 1, &[0, 0, 16, 0, 0], Some(file.as_raw_fd()));
|
|
let mut args = vec![4, 4, Fourcc::Argb8888 as u32, 0];
|
|
if let Some(buffer) = buffer {
|
|
args.insert(0, buffer);
|
|
}
|
|
request(&mut wire, params, if buffer.is_some() { 3 } else { 2 }, &args, None);
|
|
dispatch(&mut events, &mut state);
|
|
assert!(state.space.outputs().next().is_none());
|
|
state.process_pending_dmabuf_imports();
|
|
assert!(
|
|
read(&mut wire).is_empty(),
|
|
"unavailable renderer must defer notification"
|
|
);
|
|
state
|
|
.dmabuf_imports
|
|
.process_with(a, None, |_| panic!("B's request reached A's renderer"));
|
|
let mut calls = 0;
|
|
state.dmabuf_imports.process_with(b, None, |dmabuf| {
|
|
calls += 1;
|
|
assert_eq!(dmabuf.size(), (4, 4).into());
|
|
true
|
|
});
|
|
assert_eq!(calls, 1);
|
|
state.display_handle.flush_clients().unwrap();
|
|
let response = read(&mut wire);
|
|
if buffer.is_none() {
|
|
assert_eq!(u32::from_ne_bytes(response[..4].try_into().unwrap()), params);
|
|
assert_eq!(u32::from_ne_bytes(response[4..8].try_into().unwrap()) & 0xffff, 0);
|
|
} else {
|
|
assert!(response.is_empty(), "create_immed success must not emit a failed event");
|
|
}
|
|
assert!(client.get_credentials(&state.display_handle).is_ok());
|
|
}
|
|
|
|
// Deferred requests receive one terminal failure on timeout or removal.
|
|
for params in [8, 9] {
|
|
request(&mut wire, 4, 1, &[params], None);
|
|
request(&mut wire, params, 1, &[0, 0, 16, 0, 0], Some(file.as_raw_fd()));
|
|
request(&mut wire, params, 2, &[4, 4, Fourcc::Argb8888 as u32, 0], None);
|
|
dispatch(&mut events, &mut state);
|
|
state.process_pending_dmabuf_imports();
|
|
if params == 8 {
|
|
state.dmabuf_imports.expire(Instant::now() + Duration::from_secs(5));
|
|
} else {
|
|
state.dmabuf_imports.remove(b);
|
|
}
|
|
|
|
state.display_handle.flush_clients().unwrap();
|
|
let response = read(&mut wire);
|
|
assert_eq!(response.len(), 8);
|
|
assert_eq!(u32::from_ne_bytes(response[..4].try_into().unwrap()), params);
|
|
assert_eq!(u32::from_ne_bytes(response[4..8].try_into().unwrap()) & 0xffff, 1);
|
|
state
|
|
.dmabuf_imports
|
|
.process_with(b, None, |_| panic!("failed request survived"));
|
|
assert!(client.get_credentials(&state.display_handle).is_ok());
|
|
}
|
|
|
|
state.dmabuf_imports.register(b);
|
|
request(&mut wire, 4, 1, &[10], None);
|
|
request(&mut wire, 10, 1, &[0, 0, 16, 0, 0], Some(file.as_raw_fd()));
|
|
request(&mut wire, 10, 2, &[4, 4, Fourcc::Argb8888 as u32, 0], None);
|
|
dispatch(&mut events, &mut state);
|
|
drop(wire);
|
|
dispatch(&mut events, &mut state);
|
|
state.process_pending_dmabuf_imports();
|
|
state
|
|
.dmabuf_imports
|
|
.process_with(b, None, |_| panic!("disconnected client's request survived"));
|
|
}
|
|
|
|
#[test]
|
|
fn hidden_null_commit_removes_membership_and_remaps_without_workspace_switch() {
|
|
if !crate::startup_tests::private_runtime(
|
|
"state::window_lifecycle_tests::hidden_null_commit_removes_membership_and_remaps_without_workspace_switch",
|
|
) {
|
|
return;
|
|
}
|
|
|
|
let (mut events, mut state, _) = fixture();
|
|
let (window, mut wire) = window(&mut state, &mut events, 0xff112233);
|
|
let id = state.windows.ids()[&window];
|
|
state.switch_workspace(2);
|
|
let active = state.workspaces.active_id();
|
|
assert!(state.space.element_location(&window).is_none());
|
|
assert_eq!(
|
|
state.window_for_surface(window.toplevel().unwrap().wl_surface()),
|
|
Some(window.clone())
|
|
);
|
|
|
|
request(&mut wire, 2, 1, &[0, 0, 0], None);
|
|
request(&mut wire, 2, 6, &[], None);
|
|
dispatch(&mut events, &mut state);
|
|
assert!(!state.windows.ids().contains_key(&window));
|
|
assert!(state.workspaces.workspace_for_window(id).is_none());
|
|
|
|
request(&mut wire, 2, 6, &[], None);
|
|
dispatch(&mut events, &mut state);
|
|
ack_configure(&mut wire, 3);
|
|
request(&mut wire, 2, 1, &[6, 0, 0], None);
|
|
request(&mut wire, 2, 6, &[], None);
|
|
dispatch(&mut events, &mut state);
|
|
let remapped = state.windows.ids()[&window];
|
|
assert_ne!(id, remapped);
|
|
assert_eq!(state.workspaces.workspace_for_window(remapped), Some(active));
|
|
assert_eq!(state.workspaces.active_id(), active);
|
|
}
|
|
|
|
#[test]
|
|
fn hidden_commits_update_client_geometry_and_release_resize_transactions() {
|
|
if !crate::startup_tests::private_runtime(
|
|
"state::window_lifecycle_tests::hidden_commits_update_client_geometry_and_release_resize_transactions",
|
|
) {
|
|
return;
|
|
}
|
|
|
|
let (mut events, mut state, _) = fixture();
|
|
let (window, mut wire) = window(&mut state, &mut events, 0xff112233);
|
|
let id = state.windows.ids()[&window];
|
|
state.switch_workspace(2);
|
|
assert!(state.space.element_location(&window).is_none());
|
|
let toplevel = window.toplevel().unwrap();
|
|
toplevel.with_pending_state(|pending| pending.size = Some((80, 48).into()));
|
|
let serial = toplevel.send_configure();
|
|
state.windows.set_transaction(
|
|
id,
|
|
crate::resize_transaction::ResizeTransaction::new(serial, state.presentation_now()),
|
|
);
|
|
state.display_handle.flush_clients().unwrap();
|
|
assert_eq!(ack_configure(&mut wire, 3), u32::from(serial));
|
|
request(&mut wire, 3, 3, &[0, 0, 32, 48], None);
|
|
request(&mut wire, 2, 1, &[6, 0, 0], None);
|
|
request(&mut wire, 2, 6, &[], None);
|
|
dispatch(&mut events, &mut state);
|
|
assert_eq!(window.geometry().size, (32, 48).into());
|
|
assert!(state.windows.transaction(&id).is_none());
|
|
assert_eq!(
|
|
state.windows.geometry(&id).unwrap().client.committed_size,
|
|
Some(ClientSize { width: 32, height: 48 })
|
|
);
|
|
assert!(state.space.element_location(&window).is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn floating_maximize_and_restore_work_in_both_layouts() {
|
|
if !crate::startup_tests::private_runtime(
|
|
"state::window_lifecycle_tests::floating_maximize_and_restore_work_in_both_layouts",
|
|
) {
|
|
return;
|
|
}
|
|
|
|
let (mut events, mut state, _) = fixture();
|
|
let (window, mut wire) = window(&mut state, &mut events, 0xff112233);
|
|
let id = state.windows.ids()[&window];
|
|
state.focused_window = Some(id);
|
|
state.toggle_focused_floating();
|
|
state.set_floating_window_geometry(&window, (120, 100).into(), (64, 48).into());
|
|
state.display_handle.flush_clients().unwrap();
|
|
ack_configure(&mut wire, 3);
|
|
request(&mut wire, 2, 6, &[], None);
|
|
dispatch(&mut events, &mut state);
|
|
let normal = match state.workspaces.placement(id).unwrap() {
|
|
WindowPlacement::Floating { rect } => rect,
|
|
_ => panic!("expected floating window"),
|
|
};
|
|
|
|
let mut next_object = 7;
|
|
for mode in [ferese_core::LayoutMode::Scrolling, ferese_core::LayoutMode::Tree] {
|
|
let bounds = state.output_bounds().unwrap();
|
|
state.workspaces.set_active_layout_mode(mode, bounds).unwrap();
|
|
state.relayout();
|
|
state.set_window_maximized(id, true);
|
|
let maximized = maximized_rect(bounds, state.gap_config.outer);
|
|
assert_eq!(state.windows.geometry(&id).unwrap().logical, maximized, "{mode:?}");
|
|
assert_eq!(
|
|
state.workspaces.placement(id),
|
|
Some(WindowPlacement::Floating { rect: normal })
|
|
);
|
|
state.display_handle.flush_clients().unwrap();
|
|
ack_configure(&mut wire, 3);
|
|
// Commit the configured raster instead of keeping the original tiny buffer.
|
|
use smithay::reexports::wayland_server::protocol::wl_shm::WlShm;
|
|
use std::io::Write;
|
|
use std::os::fd::AsRawFd;
|
|
let client = window.toplevel().unwrap().wl_surface().client().unwrap();
|
|
let shm = client
|
|
.create_resource::<WlShm, (), Ferese>(&state.display_handle, 1, ())
|
|
.unwrap();
|
|
let (width, height) = (maximized.width.round() as u32, maximized.height.round() as u32);
|
|
let mut file = tempfile::tempfile().unwrap();
|
|
file.write_all(&0xff112233u32.to_ne_bytes().repeat((width * height) as usize))
|
|
.unwrap();
|
|
request(
|
|
&mut wire,
|
|
shm.id().protocol_id(),
|
|
0,
|
|
&[next_object, width * height * 4],
|
|
Some(file.as_raw_fd()),
|
|
);
|
|
request(
|
|
&mut wire,
|
|
next_object,
|
|
0,
|
|
&[next_object + 1, 0, width, height, width * 4, 0],
|
|
None,
|
|
);
|
|
request(&mut wire, 3, 3, &[0, 0, width, height], None);
|
|
request(&mut wire, 2, 1, &[next_object + 1, 0, 0], None);
|
|
next_object += 2;
|
|
request(&mut wire, 2, 6, &[], None);
|
|
dispatch(&mut events, &mut state);
|
|
assert!(
|
|
window
|
|
.toplevel()
|
|
.unwrap()
|
|
.current_state()
|
|
.states
|
|
.contains(xdg_toplevel::State::Maximized)
|
|
);
|
|
|
|
state.set_window_maximized(id, false);
|
|
assert_eq!(state.windows.geometry(&id).unwrap().logical, normal, "{mode:?}");
|
|
state.display_handle.flush_clients().unwrap();
|
|
ack_configure(&mut wire, 3);
|
|
request(&mut wire, 3, 3, &[0, 0, 64, 48], None);
|
|
request(&mut wire, 2, 1, &[6, 0, 0], None);
|
|
request(&mut wire, 2, 6, &[], None);
|
|
dispatch(&mut events, &mut state);
|
|
assert!(
|
|
!window
|
|
.toplevel()
|
|
.unwrap()
|
|
.current_state()
|
|
.states
|
|
.contains(xdg_toplevel::State::Maximized)
|
|
);
|
|
assert_eq!(
|
|
state.workspaces.placement(id),
|
|
Some(WindowPlacement::Floating { rect: normal })
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn malformed_resize_limits_disconnect_only_the_client_and_motion_survives() {
|
|
use std::io::Read;
|
|
if !crate::startup_tests::private_runtime(
|
|
"state::window_lifecycle_tests::malformed_resize_limits_disconnect_only_the_client_and_motion_survives",
|
|
) {
|
|
return;
|
|
}
|
|
|
|
let (mut events, mut state, _) = fixture();
|
|
for (minimum, maximum) in [
|
|
((800, 0), (400, 0)),
|
|
((0, 800), (0, 400)),
|
|
((-1, 0), (0, 0)),
|
|
((0, 0), (-1, 0)),
|
|
((0, -1), (0, 0)),
|
|
((0, 0), (0, -1)),
|
|
] {
|
|
let (window, mut wire) = window(&mut state, &mut events, 0xff112233);
|
|
let id = state.windows.ids()[&window];
|
|
state.focused_window = Some(id);
|
|
state.toggle_focused_floating();
|
|
let pointer = state.seat.get_pointer().unwrap();
|
|
pointer.set_grab(
|
|
&mut state,
|
|
crate::grabs::ResizeSurfaceGrab::new(
|
|
GrabStartData {
|
|
focus: None,
|
|
button: 0x110,
|
|
location: (100.0, 100.0).into(),
|
|
},
|
|
window,
|
|
xdg_toplevel::ResizeEdge::BottomRight.into(),
|
|
Rectangle::new((100, 100).into(), (64, 48).into()),
|
|
),
|
|
SERIAL_COUNTER.next_serial(),
|
|
Focus::Clear,
|
|
);
|
|
request(&mut wire, 4, 8, &[minimum.0 as u32, minimum.1 as u32], None);
|
|
request(&mut wire, 4, 7, &[maximum.0 as u32, maximum.1 as u32], None);
|
|
request(&mut wire, 2, 6, &[], None);
|
|
dispatch(&mut events, &mut state);
|
|
pointer.motion(
|
|
&mut state,
|
|
None,
|
|
&MotionEvent {
|
|
location: (120.0, 130.0).into(),
|
|
serial: SERIAL_COUNTER.next_serial(),
|
|
time: 1,
|
|
},
|
|
);
|
|
pointer.unset_grab(&mut state, SERIAL_COUNTER.next_serial(), 2);
|
|
wire.set_nonblocking(true).unwrap();
|
|
let mut messages = Vec::new();
|
|
let _ = wire.read_to_end(&mut messages);
|
|
let mut offset = 0;
|
|
let mut rejected = false;
|
|
while offset + 8 <= messages.len() {
|
|
let object = u32::from_ne_bytes(messages[offset..offset + 4].try_into().unwrap());
|
|
let header = u32::from_ne_bytes(messages[offset + 4..offset + 8].try_into().unwrap());
|
|
if object == 1 && header & 0xffff == 0 {
|
|
assert_eq!(
|
|
u32::from_ne_bytes(messages[offset + 12..offset + 16].try_into().unwrap()),
|
|
xdg_toplevel::Error::InvalidSize as u32
|
|
);
|
|
rejected = true;
|
|
}
|
|
|
|
offset += (header >> 16) as usize;
|
|
}
|
|
assert!(rejected, "invalid limits {minimum:?}/{maximum:?} were not rejected");
|
|
}
|
|
|
|
let (healthy, _wire) = window(&mut state, &mut events, 0xff112233);
|
|
assert!(state.windows.ids().contains_key(&healthy));
|
|
}
|
|
|
|
#[test]
|
|
fn window_rule_minimum_size_replaces_the_client_advertised_minimum() {
|
|
if !crate::startup_tests::private_runtime(
|
|
"state::window_lifecycle_tests::window_rule_minimum_size_replaces_the_client_advertised_minimum",
|
|
) {
|
|
return;
|
|
}
|
|
|
|
let (mut events, mut state, _) = fixture();
|
|
let (window, mut wire) = window(&mut state, &mut events, 0xff112233);
|
|
let id = state.windows.ids()[&window];
|
|
request(&mut wire, 4, 8, &[800, 600], None);
|
|
request(&mut wire, 2, 6, &[], None);
|
|
dispatch(&mut events, &mut state);
|
|
assert_eq!(
|
|
state.effective_size_constraints(&window, (800, 600), (0, 0)).0,
|
|
(800, 600)
|
|
);
|
|
assert_eq!(state.window_constraints()[&id].min_width, 800.0);
|
|
assert_eq!(state.window_constraints()[&id].min_height, 600.0);
|
|
|
|
state.window_rules =
|
|
crate::config::Config::parse_source("window-rule transient=#false min-width=500 min-height=400\n")
|
|
.unwrap()
|
|
.window_rules()
|
|
.unwrap();
|
|
assert_eq!(
|
|
state.effective_size_constraints(&window, (800, 600), (0, 0)).0,
|
|
(500, 400)
|
|
);
|
|
let constraints = state.window_constraints();
|
|
assert_eq!(constraints[&id].min_width, 500.0);
|
|
assert_eq!(constraints[&id].min_height, 400.0);
|
|
|
|
state.window_rules = crate::config::Config::parse_source("window-rule transient=#false min-height=400\n")
|
|
.unwrap()
|
|
.window_rules()
|
|
.unwrap();
|
|
assert_eq!(
|
|
state.effective_size_constraints(&window, (800, 600), (0, 0)).0,
|
|
(800, 400)
|
|
);
|
|
|
|
state.window_rules.clear();
|
|
assert_eq!(
|
|
state.effective_size_constraints(&window, (800, 600), (0, 0)).0,
|
|
(800, 600)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn resize_binding_mode_repeats_resize_a_mapped_window_and_stop_on_escape() {
|
|
if !crate::startup_tests::private_runtime(
|
|
"state::window_lifecycle_tests::resize_binding_mode_repeats_resize_a_mapped_window_and_stop_on_escape",
|
|
) {
|
|
return;
|
|
}
|
|
use crate::input::binding_mode_tests::key;
|
|
let (mut events, mut state, _) = fixture();
|
|
state
|
|
.reload_config_source("binding-mode resize { binding h resize left; }".into())
|
|
.unwrap();
|
|
let (window, _wire) = window(&mut state, &mut events, 0xff112233);
|
|
let id = state.windows.ids()[&window];
|
|
state.toggle_focused_floating();
|
|
state
|
|
.workspaces
|
|
.set_floating_rect(id, Rect::new(120., 100., 224., 128.))
|
|
.unwrap();
|
|
state.relayout_window(id);
|
|
state.set_binding_mode("resize");
|
|
key(&mut state, 43, true);
|
|
assert_eq!(
|
|
state.workspaces.placement(id),
|
|
Some(WindowPlacement::Floating {
|
|
rect: Rect::new(120., 100., 192., 128.)
|
|
})
|
|
);
|
|
key(&mut state, 43, true);
|
|
assert_eq!(
|
|
state.workspaces.placement(id),
|
|
Some(WindowPlacement::Floating {
|
|
rect: Rect::new(120., 100., 160., 128.)
|
|
})
|
|
);
|
|
key(&mut state, 9, true);
|
|
key(&mut state, 9, false);
|
|
key(&mut state, 43, true);
|
|
assert_eq!(
|
|
state.workspaces.placement(id),
|
|
Some(WindowPlacement::Floating {
|
|
rect: Rect::new(120., 100., 160., 128.)
|
|
})
|
|
);
|
|
key(&mut state, 43, false);
|
|
assert!(state.intercepted_keys.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn resize_binding_mode_repeats_require_the_original_modifiers() {
|
|
if !crate::startup_tests::private_runtime(
|
|
"state::window_lifecycle_tests::resize_binding_mode_repeats_require_the_original_modifiers",
|
|
) {
|
|
return;
|
|
}
|
|
use crate::input::binding_mode_tests::key;
|
|
let (mut events, mut state, _) = fixture();
|
|
state
|
|
.reload_config_source("binding-mode resize { binding h resize left; }".into())
|
|
.unwrap();
|
|
let (window, _wire) = window(&mut state, &mut events, 0xff112233);
|
|
let id = state.windows.ids()[&window];
|
|
state.toggle_focused_floating();
|
|
state
|
|
.workspaces
|
|
.set_floating_rect(id, Rect::new(120., 100., 320., 128.))
|
|
.unwrap();
|
|
state.relayout_window(id);
|
|
state.set_binding_mode("resize");
|
|
key(&mut state, 43, true);
|
|
let resized = state.workspaces.placement(id);
|
|
key(&mut state, 133, true);
|
|
key(&mut state, 43, true);
|
|
assert_eq!(
|
|
state.workspaces.placement(id),
|
|
resized,
|
|
"Super+h is not a resize-mode chord"
|
|
);
|
|
assert!(
|
|
state
|
|
.intercepted_keys
|
|
.contains(&smithay::input::keyboard::Keycode::new(43))
|
|
);
|
|
key(&mut state, 133, false);
|
|
key(&mut state, 43, true);
|
|
assert_eq!(
|
|
state.workspaces.placement(id),
|
|
Some(WindowPlacement::Floating {
|
|
rect: Rect::new(120., 100., 256., 128.),
|
|
})
|
|
);
|
|
key(&mut state, 43, false);
|
|
assert!(state.intercepted_keys.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn reloading_mode_bindings_cancels_the_modes_focus_preview() {
|
|
if !crate::startup_tests::private_runtime(
|
|
"state::window_lifecycle_tests::reloading_mode_bindings_cancels_the_modes_focus_preview",
|
|
) {
|
|
return;
|
|
}
|
|
use crate::input::binding_mode_tests::key;
|
|
let (mut events, mut state, _) = fixture();
|
|
let source = "binding-mode cycle { binding Alt+Tab focus-mru-next; }";
|
|
state.reload_config_source(source.into()).unwrap();
|
|
let (_first, _first_wire) = window(&mut state, &mut events, 0xff112233);
|
|
let (_second, _second_wire) = window(&mut state, &mut events, 0xff223344);
|
|
let origin = state.focused_window;
|
|
state.set_binding_mode("cycle");
|
|
key(&mut state, 64, true);
|
|
key(&mut state, 23, true);
|
|
assert!(state.focus_cycle.is_some());
|
|
state
|
|
.reload_config_source(format!("{source}\ninput {{ repeat-rate 30; }}"))
|
|
.unwrap();
|
|
assert_eq!(state.binding_modes.active(), None);
|
|
assert!(
|
|
state.focus_cycle.is_none(),
|
|
"the exited mode cannot keep its focus preview"
|
|
);
|
|
assert_eq!(state.focused_window, origin);
|
|
assert!(
|
|
state
|
|
.intercepted_keys
|
|
.contains(&smithay::input::keyboard::Keycode::new(23))
|
|
);
|
|
key(&mut state, 23, false);
|
|
key(&mut state, 64, false);
|
|
assert!(state.intercepted_keys.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn tree_tabs_join_switch_leave_and_hide_inactive_maximized_windows() {
|
|
if !crate::startup_tests::private_runtime(
|
|
"state::window_lifecycle_tests::tree_tabs_join_switch_leave_and_hide_inactive_maximized_windows",
|
|
) {
|
|
return;
|
|
}
|
|
let (mut events, mut state, _output) = fixture();
|
|
state.toggle_layout_mode();
|
|
let (first, _first_wire) = window(&mut state, &mut events, 0xff112233);
|
|
let (second, _second_wire) = window(&mut state, &mut events, 0xff445566);
|
|
let a = state.windows.ids()[&first];
|
|
let b = state.windows.ids()[&second];
|
|
state.focused_window = Some(b);
|
|
state.consume_focused_window();
|
|
let workspace = state.workspaces.active_id();
|
|
assert_eq!(state.tab_bars[&workspace].len(), 1);
|
|
assert!(state.space.element_location(&first).is_none());
|
|
assert!(state.space.element_location(&second).is_some());
|
|
let bar = &state.tab_bars[&workspace][0];
|
|
let header = bar.rect;
|
|
let content = state.windows.geometry(&b).unwrap().visual.target;
|
|
assert_eq!(content.y, header.y + header.height);
|
|
let hit = Point::from((header.x + header.width * 0.25, header.y + header.height * 0.5));
|
|
assert_eq!(state.tab_at(hit), Some(a));
|
|
assert!(state.surface_under(hit).is_none());
|
|
state.cycle_tab(false);
|
|
assert_eq!(state.focused_window, Some(a));
|
|
assert!(state.space.element_location(&first).is_some());
|
|
assert!(state.space.element_location(&second).is_none());
|
|
state.toggle_focused_maximized();
|
|
assert_eq!(state.visible_tab_bars(workspace).count(), 0);
|
|
state.cycle_tab(true);
|
|
assert_eq!(state.focused_window, Some(b));
|
|
assert!(
|
|
state.space.element_location(&first).is_none(),
|
|
"inactive maximized tab must remain hidden"
|
|
);
|
|
assert_eq!(state.visible_tab_bars(workspace).count(), 1);
|
|
state.toggle_focused_fullscreen();
|
|
let tree = state.workspaces.active().layout.clone();
|
|
state.leave_tab();
|
|
state.expel_focused_window();
|
|
state.cycle_tab(false);
|
|
assert_eq!(state.workspaces.active().layout, tree);
|
|
assert_eq!(state.visible_tab_bars(workspace).count(), 0);
|
|
state.toggle_focused_fullscreen();
|
|
state.leave_tab();
|
|
assert!(state.tab_bars[&workspace].is_empty());
|
|
assert!(state.space.element_location(&first).is_some());
|
|
assert!(state.space.element_location(&second).is_some());
|
|
assert!(state.workspaces.active().layout.validate().is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn scrolling_tabs_switch_with_directional_focus_and_drop_hidden_resize_holds() {
|
|
if !crate::startup_tests::private_runtime(
|
|
"state::window_lifecycle_tests::scrolling_tabs_switch_with_directional_focus_and_drop_hidden_resize_holds",
|
|
) {
|
|
return;
|
|
}
|
|
let (mut events, mut state, _output) = fixture();
|
|
let (first, _first_wire) = window(&mut state, &mut events, 0xff112233);
|
|
state.toggle_column_tabbed();
|
|
let (second, _second_wire) = window(&mut state, &mut events, 0xff445566);
|
|
state.consume_focused_window();
|
|
let (third, _third_wire) = window(&mut state, &mut events, 0xff778899);
|
|
state.consume_focused_window();
|
|
let a = state.windows.ids()[&first];
|
|
let b = state.windows.ids()[&second];
|
|
let c = state.windows.ids()[&third];
|
|
let workspace = state.workspaces.active_id();
|
|
assert_eq!(state.tab_bars[&workspace][0].group.windows, [a, b, c]);
|
|
assert_eq!(state.space.elements().count(), 1);
|
|
let position = state.workspaces.active().layout.viewport_x();
|
|
state.windows.set_transaction(
|
|
c,
|
|
crate::resize_transaction::ResizeTransaction::new(SERIAL_COUNTER.next_serial(), state.presentation_now()),
|
|
);
|
|
state.focus_direction(Direction::Up);
|
|
assert_eq!(state.focused_window, Some(b));
|
|
assert!(state.windows.transaction(&c).is_none());
|
|
assert!(state.space.element_location(&third).is_none());
|
|
assert!(state.space.element_location(&second).is_some());
|
|
assert_eq!(state.workspaces.active().layout.viewport_x(), position);
|
|
state.focus_direction(Direction::Down);
|
|
assert_eq!(state.focused_window, Some(c));
|
|
state.toggle_column_tabbed();
|
|
assert_eq!(state.space.elements().count(), 3);
|
|
assert!(state.tab_bars[&workspace].is_empty());
|
|
state.toggle_column_tabbed();
|
|
assert_eq!(state.space.elements().count(), 1);
|
|
state.leave_tab();
|
|
assert_eq!(state.space.elements().count(), 2);
|
|
assert_eq!(state.tab_bars[&workspace][0].group.windows, [a, b]);
|
|
}
|
|
|
|
#[test]
|
|
fn tab_indicator_hover_switch_and_flash_preserve_window_geometry() {
|
|
if !crate::startup_tests::private_runtime(
|
|
"state::window_lifecycle_tests::tab_indicator_hover_switch_and_flash_preserve_window_geometry",
|
|
) {
|
|
return;
|
|
}
|
|
let (mut events, mut state, _output) = fixture();
|
|
let (first, _first_wire) = window(&mut state, &mut events, 0xff112233);
|
|
state.toggle_column_tabbed();
|
|
let (second, _second_wire) = window(&mut state, &mut events, 0xff445566);
|
|
state.consume_focused_window();
|
|
let a = state.windows.ids()[&first];
|
|
let b = state.windows.ids()[&second];
|
|
let workspace = state.workspaces.active_id();
|
|
let bar = state.tab_bars[&workspace][0].clone();
|
|
let before = state.windows.geometry(&b).unwrap().logical;
|
|
let g = state
|
|
.tab_indicator_geometry(&bar, state.presented_window_rect(b).unwrap())
|
|
.unwrap();
|
|
let point = (g.hit.x + g.hit.width * 0.5, g.rail.y + g.segment_height * 0.5).into();
|
|
assert_eq!(state.tab_at(point), Some(a));
|
|
assert!(
|
|
state.surface_under(point).is_none(),
|
|
"the rail must not deliver input to the client"
|
|
);
|
|
state.update_tab_indicator_hover(point);
|
|
assert_eq!(state.tab_indicator.hover.as_ref().unwrap().opacity.current, 1.);
|
|
assert_eq!(state.windows.geometry(&b).unwrap().logical, before);
|
|
let g = state
|
|
.tab_indicator_geometry(&bar, state.presented_window_rect(b).unwrap())
|
|
.unwrap();
|
|
assert!(
|
|
g.list.width < state.tab_indicator_settings.list_width,
|
|
"short titles should not fill the width cap"
|
|
);
|
|
let row = g.row(0);
|
|
let point = (row.x + row.width * 0.5, row.y + row.height * 0.5).into();
|
|
state.update_tab_indicator_hover(point);
|
|
assert_eq!(state.tab_at(point), Some(a));
|
|
state.activate_managed_window(a);
|
|
assert_eq!(state.windows.geometry(&a).unwrap().logical, before);
|
|
assert!(state.tab_indicator.flash.is_none(), "pointer activation does not flash");
|
|
state.update_tab_indicator_hover((799., 599.).into());
|
|
assert_eq!(state.tab_indicator.hover.as_ref().unwrap().opacity.current, 0.);
|
|
state.tab_indicator_settings.flash_duration = Duration::from_millis(1);
|
|
state.cycle_tab(false);
|
|
assert_eq!(state.focused_window, Some(b));
|
|
assert_eq!(state.tab_indicator.flash.as_ref().unwrap().opacity.current, 1.);
|
|
assert_eq!(state.windows.geometry(&b).unwrap().logical, before);
|
|
let deadline = Instant::now() + Duration::from_secs(1);
|
|
while state
|
|
.tab_indicator
|
|
.flash
|
|
.as_ref()
|
|
.is_some_and(|f| f.opacity.target != 0.)
|
|
&& Instant::now() < deadline
|
|
{
|
|
events.dispatch(Duration::from_millis(5), &mut state).unwrap();
|
|
}
|
|
assert_eq!(state.tab_indicator.flash.as_ref().unwrap().opacity.current, 0.);
|
|
state.toggle_column_tabbed();
|
|
assert!(state.tab_indicator.hover.is_none());
|
|
assert!(state.tab_indicator.flash.is_none());
|
|
assert!(state.tab_at(point).is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn tab_indicator_yields_to_a_floating_window_and_disappears_with_one_tab() {
|
|
if !crate::startup_tests::private_runtime(
|
|
"state::window_lifecycle_tests::tab_indicator_yields_to_a_floating_window_and_disappears_with_one_tab",
|
|
) {
|
|
return;
|
|
}
|
|
let (mut events, mut state, _output) = fixture();
|
|
let (first, _first_wire) = window(&mut state, &mut events, 0xff112233);
|
|
state.toggle_column_tabbed();
|
|
let (second, _second_wire) = window(&mut state, &mut events, 0xff445566);
|
|
state.consume_focused_window();
|
|
let a = state.windows.ids()[&first];
|
|
let b = state.windows.ids()[&second];
|
|
let workspace = state.workspaces.active_id();
|
|
let (overlay, _overlay_wire) = window(&mut state, &mut events, 0xff778899);
|
|
let overlay_id = state.windows.ids()[&overlay];
|
|
state.toggle_focused_floating();
|
|
let bar = state.tab_bars[&workspace][0].clone();
|
|
let g = state
|
|
.tab_indicator_geometry(&bar, state.presented_window_rect(b).unwrap())
|
|
.unwrap();
|
|
let point = (g.hit.x + 6., g.hit.y + 5.).into();
|
|
state
|
|
.workspaces
|
|
.set_floating_rect(overlay_id, Rect::new(g.hit.x, g.hit.y, 200., 100.))
|
|
.unwrap();
|
|
state.relayout();
|
|
assert!(
|
|
state.tab_at(point).is_none(),
|
|
"floating window above the rail keeps its pointer input"
|
|
);
|
|
state.activate_managed_window(b);
|
|
state.leave_tab();
|
|
assert!(state.scrolling_tab_bar(a).is_none());
|
|
assert!(state.tab_indicator.hover.is_none());
|
|
}
|