Files
feresaria/compositor/src/state/reconciliation.rs
T
Samuel Onoja 979afbd213 Reduce idle CPU use and prioritize compositor rendering (#62)
Avoid background title updates when no shell window list is visible, and skip output and capture invalidation for titles outside compositor decorations. Preserve focused titles, fresh window lists and capture privacy.

Request high EGL context priority with fallback, cover rendering and native title behavior in CI, and keep audio recording flowing while video capture is idle.
2026-10-25 14:54:50 +01:00

787 lines
31 KiB
Rust

//! Exceptional topology publication. Ordinary input/damage never constructs a plan.
#[cfg(test)]
use calloop::EventLoop;
#[cfg(test)]
use ferese_animation::AnimatedValue;
#[cfg(test)]
use ferese_animation::ClientSize;
#[cfg(test)]
use ferese_animation::WindowGeometry;
#[cfg(test)]
use ferese_layout::WindowId;
#[cfg(test)]
use ferese_protocols::shell::v1::server::ferese_shell_v1::FereseShellV1;
#[cfg(test)]
use smithay::desktop::Space;
#[cfg(test)]
use smithay::desktop::Window;
use super::{
Ferese, HashSet, Logical, Output, OutputGeometry, Point, Rect, WindowPlacement,
moved_floating_rect,
};
use smithay::output::{Mode, Scale};
use smithay::utils::Transform;
/// Mutable resource bookkeeping lives outside the pure ownership plan.
#[derive(Default)]
pub(crate) struct DesktopTransition {
pub retired: HashSet<Output>,
pub redraw: HashSet<Output>,
pub publishing: bool,
pub shell_pending: bool,
}
/// The backend reports hardware that actually survived application/rollback.
pub(crate) struct DesktopOutput {
pub output: Output,
pub identity: String,
pub mode: Mode,
pub transform: Transform,
pub scale: Scale,
pub position: Point<i32, Logical>,
}
impl DesktopOutput {
fn geometry(&self) -> OutputGeometry {
let size = self
.transform
.transform_size(self.mode.size)
.to_f64()
.to_logical(self.scale.fractional_scale())
.to_i32_ceil();
OutputGeometry::new(self.position.x, self.position.y, size.w, size.h)
}
}
#[derive(Default)]
pub(crate) struct DesktopChanges {
pub layout: bool,
pub focus: bool,
pub shell: bool,
pub outputs: HashSet<Output>,
}
impl Ferese {
pub(crate) fn begin_desktop_transition(&mut self) {
assert!(
self.desktop_transition.is_none(),
"nested desktop transition"
);
self.desktop_transition = Some(DesktopTransition::default());
}
/// No event-loop dispatch occurs between planning and publication. Only the
/// small ownership map is staged; layout and animation remain authoritative.
pub(crate) fn publish_desktop(
&mut self,
runtime: Vec<DesktopOutput>,
) -> Result<DesktopChanges, ferese_core::OutputError> {
assert!(self.desktop_transition.is_some());
let inventory = runtime
.iter()
.map(|output| {
(
self.ensure_output_identity(&output.identity),
output.geometry(),
)
})
.collect::<Vec<_>>();
let workspaces = self
.workspaces
.iter()
.map(|workspace| workspace.id)
.collect::<Vec<_>>();
let plan = self.output_workspaces.plan_desktop(
&inventory,
&workspaces,
self.workspaces.next_workspace_id(),
)?;
let mut changes = DesktopChanges {
layout: !plan.affected_outputs.is_empty(),
focus: plan.focus_changed,
shell: !plan.affected_outputs.is_empty(),
outputs: self
.desktop_transition
.as_ref()
.expect("desktop transition must be started before publication or retirement")
.redraw
.clone(),
};
let floats = self
.windows
.ids()
.values()
.filter_map(|id| {
let workspace = self.workspaces.workspace_for_window(*id)?;
let WindowPlacement::Floating { rect } = self.workspaces.placement(*id)? else {
return None;
};
let destination = plan.outputs.output_for_workspace(workspace)?;
let new = plan.outputs.geometry(destination)?;
let old_owner = self.output_workspaces.output_for_workspace(workspace);
let old = old_owner.and_then(|owner| self.output_workspaces.geometry(owner));
(old_owner != Some(destination) || old != Some(new)).then(|| {
let bounds = |g: OutputGeometry| {
Rect::new(g.x as f64, g.y as f64, g.width as f64, g.height as f64)
};
(
*id,
moved_floating_rect(rect, bounds(old.unwrap_or(new)), bounds(new)),
)
})
})
.collect::<Vec<_>>();
// All fallible planning precedes publication. Resource retirement may
// already have invalidated unusable scanouts, but not desktop policy.
for id in plan.created_workspaces {
assert_eq!(self.workspaces.create_workspace(), id);
}
let surviving = runtime
.iter()
.map(|output| output.output.clone())
.collect::<Vec<_>>();
let old_outputs = self.output_ids.keys().cloned().collect::<Vec<_>>();
for output in old_outputs {
if !surviving.contains(&output) {
self.retire_output_resources(&output);
self.space.unmap_output(&output);
}
}
if self.output_ids.is_empty() && !runtime.is_empty() {
self.reset_animation_clock();
}
self.output_ids.clear();
self.outputs_by_id.clear();
self.output_names.clear();
for (output, (id, geometry)) in runtime.into_iter().zip(inventory) {
let state_changed = output.output.current_mode() != Some(output.mode)
|| output.output.current_transform() != output.transform
|| output.output.current_scale().fractional_scale()
!= output.scale.fractional_scale()
|| self
.space
.output_geometry(&output.output)
.is_none_or(|old| {
old.loc != output.position
|| old.size.w != geometry.width
|| old.size.h != geometry.height
});
if state_changed {
output.output.change_current_state(
Some(output.mode),
Some(output.transform),
Some(output.scale),
Some(output.position),
);
self.space.map_output(&output.output, output.position);
changes.layout = true;
changes.shell = true;
}
if state_changed || plan.affected_outputs.contains(&id) {
changes.outputs.insert(output.output.clone());
}
if state_changed && self.session_lock.active() {
self.session_lock.output_added(&output.output);
}
self.output_names.insert(id, output.output.name());
self.outputs_by_id.insert(id, output.output.clone());
self.output_ids.insert(output.output, id);
}
// Slides referencing evacuated workspaces cannot survive ownership
// changes. Unaffected slide/world/viewport/resize owners remain intact.
self.workspace_slides.retain(|id, slide| {
plan.outputs.geometry(*id).is_some()
&& slide
.items
.iter()
.all(|item| plan.outputs.output_for_workspace(item.workspace) == Some(*id))
});
self.output_workspaces = plan.outputs;
for (id, rect) in floats {
self.workspaces
.set_floating_rect(id, rect)
.expect("planned floating window exists");
}
self.refresh_workspace_slide_offsets();
if changes.focus {
if let Some(output) = self.output_workspaces.focused_output()
&& let Some(workspace) = self.output_workspaces.active_workspace(output)
{
self.activate_output_workspace(output, workspace);
} else {
self.focused_window = None;
}
}
Ok(changes)
}
pub(crate) fn finish_desktop_transition(&mut self, mut changes: DesktopChanges) {
self.desktop_transition
.as_mut()
.expect("desktop transition must be started before publication or retirement")
.publishing = true;
let activity_output = self.space.outputs().next().map(|output| output.name());
if let Some(output) = activity_output
&& (self.activities.store.attach_output(&output)
| self.activities.pomodoro_attach_output(&output))
{
self.activities.changed();
}
if changes.layout {
self.relayout_desktop();
}
if changes.focus || self.session_lock.active() {
self.restore_keyboard_focus();
}
self.reconcile_layer_keyboard_focus();
self.refresh_lock_outputs();
self.refresh_idle_inhibition();
let batch = self
.desktop_transition
.take()
.expect("desktop transition must be started before publication or retirement");
changes.outputs.extend(batch.redraw);
// Animation/layout helpers may request snapshots, but only this point
// can emit the complete topology generation.
if changes.shell || changes.layout || changes.focus || batch.shell_pending {
self.send_shell_snapshots();
}
changes.outputs.retain(|output| {
self.output_ids.contains_key(output)
|| self.direct_backend.as_ref().is_some_and(|backend| {
backend
.physical_outputs()
.any(|physical| physical == output)
})
});
if !changes.outputs.is_empty() {
crate::backends::direct::render_on(
self,
&changes.outputs.into_iter().collect::<Vec<_>>(),
);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::state::ClientState;
use crate::state::SwipeDirection;
use crate::state::ViewportPresentation;
use crate::state::WorkspaceSlide;
use smithay::output::{PhysicalProperties, Subpixel};
use smithay::reexports::wayland_server::Display;
use smithay::reexports::wayland_server::Resource;
use std::io::{Read, Write};
use std::os::unix::net::UnixStream;
use std::sync::Arc;
use std::time::Duration;
fn output(name: &str, x: i32, width: i32) -> DesktopOutput {
DesktopOutput {
output: Output::new(
name.into(),
PhysicalProperties {
serial_number: String::new(),
size: (0, 0).into(),
subpixel: Subpixel::Unknown,
make: "test".into(),
model: "test".into(),
},
),
identity: name.into(),
mode: Mode {
size: (width, 800).into(),
refresh: 60_000,
},
transform: Transform::Normal,
scale: Scale::Fractional(1.0),
position: (x, 0).into(),
}
}
fn request(wire: &mut UnixStream, object: u32, opcode: u32, args: &[u32]) {
for word in [object, (((args.len() + 2) * 4) as u32) << 16 | opcode]
.into_iter()
.chain(args.iter().copied())
{
wire.write_all(&word.to_ne_bytes()).unwrap();
}
}
fn window(
state: &mut Ferese,
event_loop: &mut EventLoop<'static, Ferese>,
) -> (Window, UnixStream) {
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_wm_base::XdgWmBase;
use smithay::reexports::wayland_server::protocol::wl_compositor::WlCompositor;
use smithay::wayland::shell::xdg::XdgWmBaseUserData;
let (server, mut wire) = UnixStream::pair().unwrap();
let client = state
.display_handle
.insert_client(server, Arc::new(ClientState::default()))
.unwrap();
let compositor = client
.create_resource::<WlCompositor, smithay::wayland::GlobalData, Ferese>(
&state.display_handle,
6,
smithay::wayland::GlobalData,
)
.unwrap();
let shell = client
.create_resource::<XdgWmBase, XdgWmBaseUserData, Ferese>(
&state.display_handle,
6,
Default::default(),
)
.unwrap();
request(&mut wire, compositor.id().protocol_id(), 0, &[2]);
request(&mut wire, shell.id().protocol_id(), 2, &[3, 2]);
request(&mut wire, 3, 1, &[4]);
event_loop
.dispatch(Duration::from_millis(10), state)
.unwrap();
let window = state
.space
.elements()
.find(|window| {
window
.toplevel()
.unwrap()
.wl_surface()
.client()
.is_some_and(|owner| owner.id() == client.id())
})
.unwrap()
.clone();
(window, wire)
}
fn events(state: &mut Ferese, wire: &mut UnixStream) -> Vec<(u32, u32, Vec<u32>)> {
state.display_handle.flush_clients().unwrap();
wire.set_nonblocking(true).unwrap();
let mut bytes = Vec::new();
match wire.read_to_end(&mut bytes) {
Ok(_) => {}
Err(error) => assert_eq!(error.kind(), std::io::ErrorKind::WouldBlock),
}
let mut result = Vec::new();
let mut offset = 0;
while offset < bytes.len() {
let word = |at| u32::from_ne_bytes(bytes[at..at + 4].try_into().unwrap());
let header = word(offset + 4);
let end = offset + (header >> 16) as usize;
result.push((
word(offset),
header & 0xffff,
(offset + 8..end).step_by(4).map(word).collect(),
));
offset = end;
}
result
}
#[test]
fn shell_title_interest_suppresses_background_churn_and_refreshes_on_subscription() {
if !crate::startup_tests::private_runtime(
"state::reconciliation::tests::shell_title_interest_suppresses_background_churn_and_refreshes_on_subscription",
) {
return;
}
use crate::shell_control::ShellData;
use crate::state::capture_privacy::tests::window;
use ferese_protocols::shell::v1::server::ferese_shell_v1::FereseShellV1;
let mut event_loop = EventLoop::try_new().unwrap();
let mut state = crate::startup_tests::state(&mut event_loop);
state.begin_desktop_transition();
let changes = state
.publish_desktop(vec![output("titles", 0, 1200)])
.unwrap();
state.finish_desktop_transition(changes);
let (background, _background_wire) = window(&mut state, &mut event_loop, 0xff112233);
let (focused, _focused_wire) = window(&mut state, &mut event_loop, 0xff334455);
let background = state.windows.ids()[&background];
let focused = state.windows.ids()[&focused];
assert!(state.activate_managed_window(focused));
let (server, mut wire) = UnixStream::pair().unwrap();
let client = state
.display_handle
.insert_client(server, Arc::new(ClientState::default()))
.unwrap();
let shell = client
.create_resource::<FereseShellV1, ShellData, Ferese>(
&state.display_handle,
11,
ShellData::default(),
)
.unwrap();
state.shell_resources.push(shell.downgrade());
state.send_shell_snapshots();
events(&mut state, &mut wire);
let initial = state.shell_snapshot_serial;
state.windows.record_mut(background).unwrap().title = "Default subscription".into();
state.send_shell_snapshots();
assert!(
state.shell_snapshot_serial > initial,
"new controllers initially receive all titles"
);
events(&mut state, &mut wire);
let opcode = FereseShellV1::interface()
.requests
.iter()
.position(|request| request.name == "set_background_title_updates")
.unwrap() as u32;
request(&mut wire, shell.id().protocol_id(), opcode, &[0]);
event_loop
.dispatch(Duration::from_millis(10), &mut state)
.unwrap();
events(&mut state, &mut wire);
let idle = state.shell_snapshot_serial;
for number in 0..100 {
state.windows.record_mut(background).unwrap().title = format!("spinner {number}");
state.send_shell_snapshots();
}
assert_eq!(state.shell_snapshot_serial, idle);
assert!(events(&mut state, &mut wire).is_empty());
request(&mut wire, shell.id().protocol_id(), opcode, &[1]);
event_loop
.dispatch(Duration::from_millis(10), &mut state)
.unwrap();
assert!(state.shell_snapshot_serial > idle);
let snapshot = events(&mut state, &mut wire);
let bytes: Vec<_> = snapshot
.iter()
.flat_map(|(_, _, words)| words.iter().flat_map(|word| word.to_ne_bytes()))
.collect();
assert!(
bytes
.windows(b"spinner 99".len())
.any(|bytes| bytes == b"spinner 99"),
"opening a list must receive the latest suppressed title"
);
request(&mut wire, shell.id().protocol_id(), opcode, &[0]);
event_loop
.dispatch(Duration::from_millis(10), &mut state)
.unwrap();
events(&mut state, &mut wire);
let before = state.shell_snapshot_serial;
state.windows.record_mut(focused).unwrap().title = "Focused title".into();
state.send_shell_snapshots();
assert!(state.shell_snapshot_serial > before);
let before = state.shell_snapshot_serial;
assert!(state.activate_managed_window(background));
state.send_shell_snapshots();
assert!(state.shell_snapshot_serial > before);
}
#[test]
fn planned_geometry_matches_smithay_at_fractional_scale_and_rotation() {
for transform in [Transform::Normal, Transform::_90, Transform::Flipped270] {
let mut entry = output("geometry", -500, 1920);
entry.mode.size.h = 1080;
entry.scale = Scale::Fractional(1.75);
entry.transform = transform;
entry.output.change_current_state(
Some(entry.mode),
Some(transform),
Some(entry.scale),
Some(entry.position),
);
let mut space = Space::<Window>::default();
space.map_output(&entry.output, entry.position);
let actual = space.output_geometry(&entry.output).unwrap();
assert_eq!(
entry.geometry(),
OutputGeometry::new(actual.loc.x, actual.loc.y, actual.size.w, actual.size.h)
);
}
}
#[test]
fn topology_publication_protocol_and_readiness_regressions() {
// The subprocess owns private sockets and config; never touches a live session.
if std::env::var_os("FERESE_DESKTOP_TEST_CHILD").is_none() {
let runtime = tempfile::tempdir().unwrap();
let result = std::process::Command::new(std::env::current_exe().unwrap())
.args([
"--exact",
"state::reconciliation::tests::topology_publication_protocol_and_readiness_regressions",
"--nocapture",
])
.env("FERESE_DESKTOP_TEST_CHILD", "1")
.env("XDG_RUNTIME_DIR", runtime.path())
.env("XDG_CONFIG_HOME", runtime.path())
.env("XDG_STATE_HOME", runtime.path())
.output()
.unwrap();
assert!(
result.status.success(),
"{}\n{}",
String::from_utf8_lossy(&result.stdout),
String::from_utf8_lossy(&result.stderr)
);
return;
}
let mut event_loop = EventLoop::try_new().unwrap();
let config = crate::config::Config::default().runtime_config().unwrap();
let mut state = Ferese::new(&mut event_loop, Display::new().unwrap(), config).unwrap();
let a = output("a", 0, 1200);
let b = output("b", 1200, 800);
let a_handle = a.output.clone();
let b_handle = b.output.clone();
state.begin_desktop_transition();
let changes = state.publish_desktop(vec![a, b]).unwrap();
state.finish_desktop_transition(changes);
let a_id = state.output_ids[&a_handle];
let b_id = state.output_ids[&b_handle];
let b_workspace = state.output_workspaces.active_workspace(b_id).unwrap();
// Real protocol resources let us observe exactly what clients receive.
let (server, mut shell_wire) = UnixStream::pair().unwrap();
let client = state
.display_handle
.insert_client(server, Arc::new(ClientState::default()))
.unwrap();
let shell = client
.create_resource::<FereseShellV1, crate::shell_control::ShellData, Ferese>(
&state.display_handle,
6,
Default::default(),
)
.unwrap();
state.shell_resources.push(shell.downgrade());
state.send_shell_snapshots();
events(&mut state, &mut shell_wire);
let original_serial = state.shell_snapshot_serial;
// Invalid planning leaves output metadata, ownership and shell unchanged.
let original = state.output_workspaces.clone();
state.begin_desktop_transition();
let mut invalid = state.current_desktop_outputs();
invalid[0].mode.size.w = 0;
assert!(state.publish_desktop(invalid).is_err());
assert_eq!(state.output_workspaces, original);
assert_eq!(a_handle.current_mode().unwrap().size.w, 1200);
assert!(events(&mut state, &mut shell_wire).is_empty());
state.finish_desktop_transition(DesktopChanges::default());
assert_eq!(state.shell_snapshot_serial, original_serial);
// A real XDG toplevel needs a new size; it deliberately never acknowledges it.
let (slow, _slow_wire) = window(&mut state, &mut event_loop);
let slow_id = WindowId(101);
let initial = Rect::new(1500.0, 500.0, 600.0, 600.0);
state
.workspaces
.insert_floating_window(slow_id, b_workspace, initial, true)
.unwrap();
state.windows.register_window(slow.clone(), slow_id);
let mut geometry = WindowGeometry::new(initial, None);
geometry.client.committed_size = Some(ClientSize::from_rect(initial));
state.windows.set_geometry(slow_id, geometry);
state.output_workspaces.focus_output(b_id).unwrap();
state.restore_output_focus();
state.relayout();
events(&mut state, &mut shell_wire);
let before_serial = state.shell_snapshot_serial;
let spare = state.workspaces.create_workspace();
state
.output_workspaces
.assign_workspace(b_id, spare)
.unwrap();
state.workspace_slides.insert(
b_id,
WorkspaceSlide::new(None, spare, b_workspace, SwipeDirection::Right),
);
state.begin_desktop_transition();
state.unregister_output(&b_handle);
state.unregister_output(&b_handle); // Duplicate retirement is harmless.
assert_eq!(
state.workspaces.placement(slow_id),
Some(WindowPlacement::Floating { rect: initial })
);
assert_eq!(
state.output_workspaces.output_for_workspace(b_workspace),
Some(b_id)
);
state.send_shell_snapshots();
assert!(events(&mut state, &mut shell_wire).is_empty());
let mut runtime = state.current_desktop_outputs();
runtime.retain(|entry| entry.output == a_handle);
runtime[0].mode.size = (500, 400).into();
let changes = state.publish_desktop(runtime).unwrap();
assert_eq!(
state.output_workspaces.output_for_workspace(b_workspace),
Some(a_id)
);
assert_eq!(state.focused_window, Some(slow_id));
assert!(!state.workspace_slides.contains_key(&b_id));
state.send_shell_snapshots();
assert!(events(&mut state, &mut shell_wire).is_empty());
state.finish_desktop_transition(changes);
assert_eq!(state.shell_snapshot_serial, before_serial + 1);
let published = events(&mut state, &mut shell_wire);
let shell_events = published
.iter()
.filter(|(id, _, _)| *id == shell.id().protocol_id())
.collect::<Vec<_>>();
assert_eq!(
shell_events
.iter()
.filter(|(_, opcode, _)| *opcode == 0)
.count(),
1
);
assert_eq!(
shell_events
.iter()
.filter(|(_, opcode, _)| *opcode == 4)
.count(),
1
);
assert_eq!(
shell_events
.iter()
.filter(|(_, opcode, _)| *opcode == 2)
.count(),
1
);
for (_, opcode, args) in &shell_events {
if *opcode == 3 {
assert_eq!(args[3], a_id.0 as u32);
if args[1] == b_workspace.0 as u32 {
assert_eq!(&args[args.len() - 2..], &[1, 1]);
}
} else if *opcode == 2 {
let active = 3 + args[2].div_ceil(4) as usize;
assert_eq!(args[active + 1], b_workspace.0 as u32);
assert_eq!(args[active + 2], 1);
} else if *opcode == 1 {
assert_eq!(args[1], slow_id.0 as u32);
assert_eq!(args[3], b_workspace.0 as u32);
}
}
let WindowPlacement::Floating { rect } = state.workspaces.placement(slow_id).unwrap()
else {
panic!()
};
assert!(
rect.x >= 0.0
&& rect.y >= 0.0
&& rect.x + rect.width <= 500.0
&& rect.y + rect.height <= 400.0
);
assert!(
state.windows.transaction(&slow_id).is_some(),
"topology commit must not await XDG acknowledgement"
);
assert_eq!(
state
.windows
.geometry(&slow_id)
.unwrap()
.client
.committed_size,
Some(ClientSize::from_rect(initial))
);
// A separate workspace/output keeps progressing while the slow client waits.
state.begin_desktop_transition();
let mut runtime = state.current_desktop_outputs();
runtime.push(output("c", 500, 800));
let changes = state.publish_desktop(runtime).unwrap();
state.finish_desktop_transition(changes);
let c_id = state.persistent_output_id("c").unwrap();
let c_workspace = state.output_workspaces.active_workspace(c_id).unwrap();
let mut viewport = AnimatedValue::new(0.0);
viewport.set_target(200.0);
state
.viewports
.insert(c_workspace, ViewportPresentation::from_motion(viewport));
let now = state.start_time.elapsed();
state.advance_animations_at(Duration::from_millis(16), now + Duration::from_millis(16));
assert!(state.viewports[&c_workspace].motion().current > 0.0);
assert!(state.windows.transaction(&slow_id).is_some());
state.advance_animations_at(Duration::from_millis(334), now + Duration::from_millis(350));
assert!(state.windows.transaction(&slow_id).is_none());
// A no-op/resume inventory keeps ownership and emits no duplicate generation.
events(&mut state, &mut shell_wire);
let before = state.output_workspaces.clone();
for _ in 0..3 {
state.begin_desktop_transition();
let runtime = state.current_desktop_outputs();
let changes = state.publish_desktop(runtime).unwrap();
assert!(!changes.layout && !changes.focus && !changes.shell);
assert!(changes.outputs.is_empty());
state.finish_desktop_transition(changes);
}
assert_eq!(state.output_workspaces, before);
assert!(events(&mut state, &mut shell_wire).is_empty());
assert_eq!(
state.workspaces.workspace_for_window(slow_id),
Some(b_workspace)
);
let unfocused = state.outputs_by_id[&c_id].clone();
let focused = state.focused_window;
state.unregister_output(&unfocused);
assert_eq!(
state.focused_window, focused,
"removing a non-focused output preserves window focus"
);
// Lock protection remains authoritative through headless loss/recovery.
use smithay::reexports::wayland_protocols::ext::session_lock::v1::server::ext_session_lock_manager_v1::ExtSessionLockManagerV1;
let (server, mut lock_wire) = UnixStream::pair().unwrap();
let client = state
.display_handle
.insert_client(server, Arc::new(ClientState::default()))
.unwrap();
let lock = client
.create_resource::<ExtSessionLockManagerV1, smithay::wayland::GlobalData, Ferese>(
&state.display_handle,
1,
smithay::wayland::GlobalData,
)
.unwrap();
request(&mut lock_wire, lock.id().protocol_id(), 1, &[2]);
event_loop
.dispatch(Duration::from_millis(10), &mut state)
.unwrap();
assert!(state.session_lock.active());
state.begin_desktop_transition();
for output in state.output_ids.keys().cloned().collect::<Vec<_>>() {
state.unregister_output(&output);
}
let changes = state.publish_desktop(Vec::new()).unwrap();
state.finish_desktop_transition(changes);
assert!(state.output_ids.is_empty());
assert!(state.space.outputs().next().is_none());
assert!(state.output_workspaces.focused_output().is_none());
assert!(state.focused_window.is_none());
assert!(state.workspace_slides.is_empty());
assert!(state.seat.get_keyboard().unwrap().current_focus().is_none());
assert_eq!(
state.workspaces.workspace_for_window(slow_id),
Some(b_workspace)
);
state.begin_desktop_transition();
let changes = state.publish_desktop(vec![output("a", 0, 1200)]).unwrap();
state.finish_desktop_transition(changes);
assert_eq!(
state.output_workspaces.output_for_workspace(b_workspace),
Some(a_id)
);
assert!(state.session_lock.active());
assert!(state.surface_under((20.0, 20.0).into()).is_none());
}
}