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::>() { 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::(&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::(&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::(&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()); } #[test] fn shell_can_move_a_window_to_the_current_workspace_on_another_output() { if !crate::startup_tests::private_runtime( "state::window_lifecycle_tests::shell_can_move_a_window_to_the_current_workspace_on_another_output", ) { return; } let (mut events, mut state, _) = fixture(); let (window, _wire) = window(&mut state, &mut events, 0xff112233); let id = state.windows.ids()[&window]; let source = state.workspaces.workspace_for_window(id).unwrap(); let second = Output::new( "second".into(), smithay::output::PhysicalProperties { size: (0, 0).into(), subpixel: smithay::output::Subpixel::Unknown, make: "test".into(), model: "test".into(), }, ); second.change_current_state( Some(smithay::output::Mode { size: (800, 600).into(), refresh: 60_000, }), None, None, None, ); state.space.map_output(&second, (800, 0)); state.register_output(&second, "second".into()); let output = state.output_ids[&second]; let destination = state.output_workspaces.active_workspace(output).unwrap(); assert_ne!(source, destination); assert!(state.move_managed_window_to_workspace(id, destination)); assert_eq!(state.workspaces.workspace_for_window(id), Some(destination)); assert!(state.move_managed_window_to_workspace(id, destination)); }