use super::*; /// Output-local presentation values. Sampling never changes authoritative /// geometry, configure barriers, animation clocks, or another output's view. pub(crate) struct FrameScene { pub windows: HashMap, pub overview: crate::overview::OverviewMotion, // Plane selection and scheduling reuse the activity sampled with this scene. pub animating: bool, pub delta: Duration, } pub(crate) struct WindowFrame { pub geometry: WindowGeometry, pub presentation: crate::presentation::WindowPresentation, } impl Ferese { pub(crate) fn current_window_presentation(&self, id: WindowId) -> Option { let geometry = *self.windows.geometry(&id)?; Some(self.compose_window_presentation( id, geometry, self.overview.motion(), self.workspace_slide_offset(id), Duration::ZERO, )) } fn window_focus_effect(&self, id: WindowId, delta: Duration, overview_opacity: f32) -> (f32, f64) { let target = crate::focus_effect::target(self.focus_effect, self.focused_window, id); let mut focus = self .windows .record(id) .and_then(|record| record.focus_transition.clone()) .unwrap_or_else(|| crate::focus_effect::BoundedFade::new(target)); focus.advance_visual( target, delta, if self.animations_enabled && self.focus_effect.enabled { self.focus_effect.duration_ms } else { 0.0 }, ); self.focus_effect .resolve(focus.current, self.focused_window.is_some(), overview_opacity) } fn compose_window_presentation( &self, id: WindowId, geometry: WindowGeometry, overview: &crate::overview::OverviewMotion, offset: (f64, f64), delta: Duration, ) -> crate::presentation::WindowPresentation { let record = self.windows.record(id).expect("presentation has a window record"); let mut visual = geometry.visual; if self.presentation_dependencies.reflow_blocked(id, &self.windows) { let predicted_x_velocity = visual.velocity.x; visual.velocity = Default::default(); if !geometry.is_zooming() && let Some((workspace, _)) = record.world_x.as_ref() && !self .presentation_dependencies .viewport_blocked(*workspace, &self.windows) && let Some(viewport) = self.viewports.get(workspace) { visual.velocity.x = if delta.is_zero() { -viewport.presented_motion().velocity } else { predicted_x_velocity }; } } let mut bounds = overview.presented_bounds(id, visual); bounds.current.x += offset.0; bounds.current.y += offset.1; if let Some(workspace) = self.workspaces.workspace_for_window(id) && let Some((output_id, slide)) = self .workspace_slides .iter() .find(|(_, slide)| slide.contains(workspace)) && let Some(item) = slide.items.iter().find(|item| item.workspace == workspace) && let Some(area) = self .outputs_by_id .get(output_id) .and_then(|output| self.space.output_geometry(output)) { let (_, velocity) = slide.sample(item, delta); bounds.velocity.x += velocity.x * f64::from(area.size.w); bounds.velocity.y += velocity.y * f64::from(area.size.h); } let focused = if self.overview.is_active() { self.overview_selected(id) } else { self.focused_window == Some(id) }; let mut opacity = record.opening.unwrap_or_else(|| AnimatedValue::new(1.0)); let mut emphasis = record .focus .unwrap_or_else(|| AnimatedValue::new(if focused { 1.0 } else { 0.0 })); let mut shadow = record.shadow.unwrap_or(emphasis); if !self.animations_enabled { opacity.snap(); emphasis.snap(); shadow.snap(); } else if !delta.is_zero() { opacity.advance( delta, SpringConfig { position_tolerance: 0.00001, velocity_tolerance: 0.00001, ..self.spring_config }, ); emphasis.advance(delta, crate::presentation::emphasis_spring(self.spring_config)); shadow.advance(delta, crate::presentation::shadow_spring(self.spring_config)); } let presence_scale = 0.97 + 0.03 * opacity.current; let native_size = (!overview.is_presenting() && presence_scale != 1.0).then(|| ClientSize::from_rect(bounds.current)); bounds.velocity = crate::presentation::scaled_visual_velocity( bounds.current, bounds.velocity, presence_scale, 0.03 * opacity.velocity, ); bounds.current = crate::presentation::scaled_visual_rect(bounds.current, presence_scale); bounds.target = bounds.current; let (focus_alpha, dim) = self.window_focus_effect(id, delta, overview.opacity()); crate::presentation::WindowPresentation { id, focus_alpha, dim, bounds, opacity, emphasis, shadow, scale_content: overview.is_presenting(), native_size, } } fn output_window_records<'a>( &'a self, output: &'a Output, block_resizes: bool, ) -> impl Iterator + 'a { self.output_id(output) .into_iter() .flat_map(move |output| self.output_workspaces.assigned_workspaces(output)) .filter_map(move |id| self.workspaces.workspace(id)) .flat_map(move |workspace| { workspace .layout .window_ids() .chain(workspace.floating.iter().copied()) .filter_map(move |id| { Some(( id, self.windows.record(id)?, block_resizes && self.presentation_dependencies.reflow_blocked(id, &self.windows), )) }) }) } pub(crate) fn output_has_pending_visual_changes(&self, output: &Output) -> bool { let selected = if self.overview.is_active() { self.overview.selected() } else { self.focused_window }; self.output_window_records(output, false).any(|(id, record, _)| { self.window_belongs_to_output(id, output) && (record .focus .as_ref() .is_some_and(|focus| focus.needs_update(if selected == Some(id) { 1.0 } else { 0.0 })) || record .shadow .as_ref() .is_some_and(|shadow| shadow.needs_update(if selected == Some(id) { 1.0 } else { 0.0 })) || record.focus_transition.as_ref().is_some_and(|transition| { transition.needs_update(crate::focus_effect::target(self.focus_effect, self.focused_window, id)) })) }) } pub(crate) fn output_resize_deadline(&self, output: &Output) -> Option { self.output_window_records(output, false) .filter(|(id, _, _)| self.window_belongs_to_output(*id, output)) .flat_map(|(id, record, _)| { // Client-size waits are retired while visiting mapped surfaces. // Hidden tabs/workspaces must not arm an already-expired timer. let mapped = self .windows .window(id) .is_some_and(|window| self.space.element_location(window).is_some()); [ record.resize.map(|wait| wait.deadline()), record .geometry .filter(|_| mapped) .and_then(|geometry| geometry.client.commit_deadline()), ] }) .flatten() .min() } fn snapshot_is_waiting(&self, id: WindowId, geometry: &WindowGeometry) -> bool { self.presentation_dependencies.reflow_blocked(id, &self.windows) || geometry.client.committed_size.is_some_and(|size| { crate::presentation::resize_needs_old_frame( geometry.visual.current, geometry.logical, size.width, size.height, ) }) } pub(crate) fn output_has_animations(&self, output: &Output) -> bool { if self .render .closing .iter() .any(|window| Some(window.output) == self.output_id(output)) || self.overview.is_animating(self.spring_config) || self.tab_indicator_output_animating(output) || self .dismissing_popups .iter() .any(|(root, _, _)| self.surface_outputs(root).contains(output)) { return true; } if self .output_id(output) .and_then(|id| self.workspace_slides.get(&id)) .is_some_and(|slide| slide.held_progress.is_none() && slide.moving()) { return true; } self.output_window_records(output, true).any(|(id, record, blocked)| { let Some(mut geometry) = record.geometry else { return false; }; if !self.window_belongs_to_output(id, output) { return false; } if record.opening.is_some() || (self.render.snapshot(&id).is_some() && !self.snapshot_is_waiting(id, &geometry)) || record.focus.as_ref().is_some_and(|focus| focus.is_animating()) || record.shadow.as_ref().is_some_and(|shadow| shadow.is_animating()) || record .focus_transition .as_ref() .is_some_and(|transition| transition.is_animating()) { return true; } if !blocked && geometry.advance(Duration::ZERO, self.spring_config, self.animations_enabled) { return true; } let Some(workspace) = self.workspaces.workspace_for_window(id) else { return false; }; (!self .presentation_dependencies .viewport_blocked(workspace, &self.windows) && self .viewports .get(&workspace) .is_some_and(ViewportPresentation::needs_tick)) || (!blocked && record.coupled_width.as_ref().is_some_and(|(_, width)| { let mut width = *width; width.advance(Duration::ZERO, self.viewport_spring_config) })) }) } pub(crate) fn sample_frame(&self, output: &Output, horizon: Duration) -> FrameScene { let animating = self.output_has_animations(output); let delta = if self.animations_enabled && animating { horizon.mul_f64(self.animation_speed) } else { Duration::ZERO }; let overview = self.overview.sample(delta, self.spring_config); let predicted_slide_offsets = (!delta.is_zero()).then(|| self.sample_workspace_slide_offsets(self.output_id(output), delta)); let slide_offsets = predicted_slide_offsets .as_ref() .unwrap_or(&self.workspace_slide_offsets); let mut windows = HashMap::new(); for (id, record, blocked) in self.output_window_records(output, !delta.is_zero()) { let Some(original) = record.geometry else { continue }; // The workspace strip also draws inactive workspaces on this output. // Keep their samples through the overview exit transition; the main // grid still filters by window_belongs_to_output in the render path. if !overview.is_presenting() && !self.window_belongs_to_output(id, output) { continue; } let world = record.world_x.as_ref().and_then(|(workspace, world)| { let viewport = self.viewports.get(workspace)?; Some(( *world, viewport.sample( delta, self.viewport_spring_config, self.animations_enabled, self.presentation_dependencies .viewport_blocked(*workspace, &self.windows), ), )) }); let width = record.coupled_width.as_ref().map(|(_, width)| *width); let geometry = if delta.is_zero() { original } else { predict_geometry( original, delta, self.spring_config, self.viewport_spring_config, world, width, blocked, ) }; let offset = self .workspaces .workspace_for_window(id) .and_then(|workspace| slide_offsets.get(&workspace).copied()) .unwrap_or_default(); let presentation = self.compose_window_presentation(id, geometry, &overview, offset, delta); windows.insert(id, WindowFrame { geometry, presentation }); } FrameScene { windows, overview, animating, delta, } } } fn predict_geometry( original: WindowGeometry, delta: Duration, spring: SpringConfig, viewport_spring: SpringConfig, world: Option<(AnimatedValue, AnimatedValue)>, width: Option, reflow_held: bool, ) -> WindowGeometry { let mut predicted = original; let zooming = predicted.is_zooming(); if width.is_some() { predicted.visual.target.width = predicted.visual.current.width; predicted.visual.velocity.width = 0.0; } if !reflow_held { predicted.advance(delta, spring, true); } else { predicted.visual.velocity = Default::default(); } predicted.visual.target.width = original.visual.target.width; if let Some((mut world, viewport)) = world && !zooming { if !reflow_held { world.advance(delta, spring); } else { world.velocity = 0.0; } super::animation::sync_scrolling_coordinates(&mut predicted, &mut world, &viewport, false); } if let Some(mut width) = width && !reflow_held { width.advance(delta, viewport_spring); predicted.visual.current.width = width.current; predicted.visual.velocity.width = width.velocity; } predicted } #[cfg(test)] mod tests { use super::*; #[test] fn pending_resize_waits_without_refresh_cadence_and_recovers() { if !crate::startup_tests::private_runtime( "state::prediction::tests::pending_resize_waits_without_refresh_cadence_and_recovers", ) { return; } use crate::frame_scheduler::FrameScheduler; let mut event_loop = EventLoop::try_new().unwrap(); let display = Display::new().unwrap(); let config = crate::config::Config::default().runtime_config().unwrap(); let mut state = Ferese::new(&mut event_loop, display, config).unwrap(); let output = Output::new( "resize-idle".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: (1920, 1080).into(), refresh: 60_000, }), None, None, Some((0, 0).into()), ); state.space.map_output(&output, (0, 0)); state.register_output(&output, "resize-idle".into()); let id = WindowId(1); state.workspaces.insert_window(id, Axis::Horizontal, 0.5).unwrap(); let settled = WindowGeometry::new(Rect::new(0., 0., 400., 300.), None); state.windows.records.insert( id, super::super::window_registry::WindowRecord { geometry: Some(settled), ..Default::default() }, ); state.windows.set_transaction( id, crate::resize_transaction::ResizeTransaction::new(9.into(), Duration::ZERO), ); state.rebuild_presentation_dependencies(); assert_eq!(state.output_resize_deadline(&output), Some(Duration::from_millis(300))); let mut held = settled; held.visual.set_target(Rect::new(100., 0., 400., 300.)); state.windows.set_geometry(id, held); // Geometry which cannot advance is not visual activity either. assert!(!state.output_has_animations(&output)); for hz in [60, 120, 240] { let interval = Duration::from_nanos(1_000_000_000 / hz); let mut scheduler = FrameScheduler::new(interval); scheduler.request(Duration::ZERO); let mut attempts = 0; for tick in 0..(hz * 3 / 10) { let now = interval * tick as u32; if scheduler.plan(now).is_some() { let plan = scheduler.begin().unwrap(); attempts += 1; scheduler.rendered(plan, now, Duration::ZERO, false); if state.sample_frame(&output, Duration::ZERO).animating { scheduler.request(now); } } } assert_eq!(attempts, 1, "pending-only render attempts at {hz} Hz"); } // Real fades remain live even while buffer-dependent geometry is held. state.windows.update(id, |record| { let mut opening = AnimatedValue::new(0.0); opening.set_target(1.0); record.opening = Some(opening); }); assert!(state.output_has_animations(&output)); state.windows.update(id, |record| record.opening = None); // A client commit removes the wait and wakes its held geometry. let mut moving = settled; moving.visual.set_target(Rect::new(100., 0., 400., 300.)); state.windows.set_geometry(id, moving); state.windows.clear_transaction(&id); state.release_presentation_dependencies(); assert_eq!(state.output_resize_deadline(&output), None); assert!(state.output_has_animations(&output)); state.windows.geometry_mut(&id).unwrap().client.request_size( ClientSize { width: 800, height: 300, }, Duration::ZERO, ); assert_eq!( state.output_resize_deadline(&output), None, "unmapped client-size waits cannot arm expired wakeups" ); // Timeout processing still releases a pending wait. state.windows.set_transaction( id, crate::resize_transaction::ResizeTransaction::new(10.into(), Duration::ZERO), ); state.advance_animations_at(Duration::ZERO, Duration::from_millis(300)); assert!(state.windows.transaction(&id).is_none()); state.unregister_output(&output); assert_eq!(state.output_resize_deadline(&output), None); assert!(!state.output_has_animations(&output)); } #[test] #[ignore = "requires a private XDG_RUNTIME_DIR and permission to bind test sockets"] fn focus_effect_sampling_preserves_activation_and_output_local_progress() { let runtime = std::path::PathBuf::from(std::env::var_os("XDG_RUNTIME_DIR").unwrap()); assert!(runtime.starts_with(std::env::temp_dir())); let mut event_loop = EventLoop::try_new().unwrap(); let mut state = Ferese::new( &mut event_loop, Display::new().unwrap(), crate::config::Config::default().runtime_config().unwrap(), ) .unwrap(); state.focus_effect = crate::config::FocusEffectSettings { enabled: true, blur: true, active_opacity: 0.9, inactive_opacity: 0.6, inactive_dim: 0.2, duration_ms: 150.0, }; let mut outputs = Vec::new(); for index in 0..2 { let output = Output::new( format!("focus-{index}"), 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, }), Some(smithay::utils::Transform::Normal), None, Some((index * 800, 0).into()), ); state.space.map_output(&output, (index * 800, 0)); state.register_output(&output, format!("focus-{index}")); let workspace = state .output_workspaces .active_workspace(state.output_id(&output).unwrap()) .unwrap(); let id = WindowId(index as u64 + 1); let rect = Rect::new(index as f64 * 800.0, 0.0, 400.0, 300.0); state .workspaces .insert_floating_window(id, workspace, rect, false) .unwrap(); state.windows.records.insert( id, super::super::window_registry::WindowRecord { geometry: Some(WindowGeometry::new(rect, Some(ClientSize::from_rect(rect)))), focus_transition: Some(crate::focus_effect::BoundedFade::new(if index == 0 { 1.0 } else { 0.0 })), ..Default::default() }, ); outputs.push(output); } let first = WindowId(1); let second = WindowId(2); state.focused_window = Some(first); assert_eq!(state.current_window_presentation(first).unwrap().alpha(), 0.9); assert_eq!(state.current_window_presentation(second).unwrap().alpha(), 0.6); // Only emphasis follows overview selection; activation and its fade remain unchanged. state.set_overview_active(true); state.overview.select_window(second); assert_eq!(state.focused_window, Some(first)); assert_eq!(state.current_window_presentation(first).unwrap().alpha(), 0.9); assert_eq!(state.current_window_presentation(second).unwrap().alpha(), 0.6); state.set_overview_active(false); state.focused_window = Some(second); let transition = state .windows .record_mut(first) .unwrap() .focus_transition .as_mut() .unwrap(); transition.advance_visual(0.0, Duration::ZERO, 150.0); let previous = transition.current; assert!(state.output_has_pending_visual_changes(&outputs[0])); let frame = state.sample_frame(&outputs[0], Duration::from_millis(75)); assert!((frame.windows[&first].presentation.alpha() - 0.75).abs() < 1e-6); assert!(!frame.windows.contains_key(&second)); assert_eq!( state .windows .record(first) .unwrap() .focus_transition .as_ref() .unwrap() .current, previous ); // A pending client configure must not stop focus presentation from progressing. state.windows.set_transaction( first, crate::resize_transaction::ResizeTransaction::new(9.into(), Duration::ZERO), ); let waiting = state.sample_frame(&outputs[0], Duration::from_millis(75)); assert_eq!( waiting.windows[&first].presentation.alpha(), frame.windows[&first].presentation.alpha() ); assert!(state.windows.transaction(&first).is_some()); state.animations_enabled = false; assert_eq!(state.current_window_presentation(first).unwrap().alpha(), 0.6); assert_eq!(state.current_window_presentation(second).unwrap().alpha(), 0.9); } #[test] #[ignore = "requires a private XDG_RUNTIME_DIR and permission to bind test sockets"] fn overview_strip_switch_slides_stable_grids_and_keeps_strip_fixed() { let runtime = std::path::PathBuf::from(std::env::var_os("XDG_RUNTIME_DIR").unwrap()); assert!(runtime.starts_with(std::env::temp_dir())); let mut event_loop = EventLoop::try_new().unwrap(); let mut state = Ferese::new( &mut event_loop, Display::new().unwrap(), crate::config::Config::default().runtime_config().unwrap(), ) .unwrap(); let output = Output::new( "overview-slide-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: (1200, 800).into(), refresh: 120_000, }), Some(smithay::utils::Transform::Normal), None, Some((0, 0).into()), ); state.space.map_output(&output, (0, 0)); state.register_output(&output, "overview-slide-test".into()); let output_id = state.output_id(&output).unwrap(); let from = state.output_workspaces.active_workspace(output_id).unwrap(); let to = state.workspaces.create_workspace(); state.output_workspaces.assign_workspace(output_id, to).unwrap(); for (id, workspace) in [(WindowId(1), from), (WindowId(2), to)] { let normal = Rect::new(100., 160., 640., 480.); state .workspaces .insert_floating_window(id, workspace, normal, false) .unwrap(); state.windows.records.insert( id, super::super::window_registry::WindowRecord { geometry: Some(WindowGeometry::new(normal, None)), ..Default::default() }, ); } state.set_overview_active(true); state.advance_animations_by(Duration::from_secs(10)); let old = state.current_window_presentation(WindowId(1)).unwrap().bounds.current; let cards = state.overview_workspace_cards(&output); let card = cards.iter().find(|card| card.workspace == to).unwrap(); assert!( state.click_overview_workspace( ( card.rect.x + card.rect.width * 0.5, card.rect.y + card.rect.height * 0.5 ) .into() ) ); assert!(state.workspace_slides.contains_key(&output_id)); assert!(state.window_belongs_to_output(WindowId(1), &output)); let incoming = state.current_window_presentation(WindowId(2)).unwrap().bounds.current; assert_eq!(incoming.width, old.width); assert_eq!(incoming.height, old.height); assert!(incoming.x > old.x + 1000.); let predicted = state.sample_frame(&output, Duration::from_millis(60)); assert!(predicted.windows[&WindowId(1)].presentation.bounds.current.x < old.x); assert!(predicted.windows[&WindowId(2)].presentation.bounds.current.x < incoming.x); assert_eq!( predicted.windows[&WindowId(2)].presentation.bounds.current.width, incoming.width ); // Activation can add a trailing empty workspace. Once that layout is // established, slide prediction must not translate the strip or previews. let current_cards = state.overview_workspace_cards(&output); let predicted_cards = state.overview_workspace_cards_for_frame(&output, Some(&predicted)); for (before, after) in current_cards.iter().zip(predicted_cards) { assert_eq!(before.rect, after.rect); assert_eq!(before.windows, after.windows); } state.advance_animations_by(Duration::from_millis(80)); let before = state.workspace_slides[&output_id].clone(); state.update_workspace_slide(output_id, output_id, Some(to), from, None); let reversed = &state.workspace_slides[&output_id]; assert_eq!(reversed.speed, crate::overview::OVERVIEW_MOTION_SPEED); for item in &before.items { let after = reversed .items .iter() .find(|other| other.workspace == item.workspace) .unwrap(); assert_eq!(item.start, after.start); assert_eq!(item.velocity, after.velocity); } // Finish on the selected workspace and retire all outgoing previews. state.output_workspaces.switch_workspace(output_id, from).unwrap(); state.retarget_overview(); state.advance_animations_by(Duration::from_secs(10)); assert!(state.workspace_slides.is_empty()); assert!(!state.overview.has_window_preview(WindowId(2))); assert!(!state.overview.needs_tick()); state.animations_enabled = false; let card = state .overview_workspace_cards(&output) .into_iter() .find(|card| card.workspace == to) .unwrap(); state.click_overview_workspace( ( card.rect.x + card.rect.width * 0.5, card.rect.y + card.rect.height * 0.5, ) .into(), ); assert!(state.workspace_slides.is_empty()); assert!(!state.overview.needs_tick()); } #[test] #[ignore = "requires a private XDG_RUNTIME_DIR and permission to bind test sockets"] fn frame_sampling_preserves_compositor_state_between_output_deadlines() { let runtime = std::path::PathBuf::from(std::env::var_os("XDG_RUNTIME_DIR").unwrap()); assert!( runtime.starts_with(std::env::temp_dir()), "use a disposable runtime directory" ); let mut event_loop = EventLoop::try_new().unwrap(); let display = Display::new().unwrap(); let config = crate::config::Config::default().runtime_config().unwrap(); let mut state = Ferese::new(&mut event_loop, display, config).unwrap(); let output = Output::new( "forecast-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: (1920, 1080).into(), refresh: 60_000, }), Some(smithay::utils::Transform::Normal), None, Some((0, 0).into()), ); state.space.map_output(&output, (0, 0)); state.register_output(&output, "forecast-test".into()); let id = WindowId(1); state.workspaces.insert_window(id, Axis::Horizontal, 0.5).unwrap(); let mut geometry = WindowGeometry::new(Rect::new(0., 0., 400., 300.), None); geometry.visual.set_target(Rect::new(600., 0., 400., 300.)); // This test isolates frame sampling from the protocol client fixture. state.windows.records.insert( id, super::super::window_registry::WindowRecord { geometry: Some(geometry), ..Default::default() }, ); let tick = state.last_animation_tick; for horizon in [ Duration::from_millis(16), Duration::from_millis(4), Duration::from_millis(8), ] { { let forecast = state.sample_frame(&output, horizon); assert!(forecast.windows[&id].geometry.visual.current.x > geometry.visual.current.x); assert!(forecast.animating); } assert_eq!(*state.windows.geometry(&id).unwrap(), geometry); assert_eq!(state.last_animation_tick, tick); } // Sampling one output must not visit or include another output's records. let other = Output::new( "other-forecast".into(), smithay::output::PhysicalProperties { size: (0, 0).into(), subpixel: smithay::output::Subpixel::Unknown, make: "test".into(), model: "test".into(), }, ); other.change_current_state( Some(smithay::output::Mode { size: (1280, 720).into(), refresh: 240_000, }), Some(smithay::utils::Transform::Normal), None, Some((1920, 0).into()), ); state.space.map_output(&other, (1920, 0)); state.register_output(&other, "other-forecast".into()); let other_workspace = state .output_workspaces .active_workspace(state.output_id(&other).unwrap()) .unwrap(); let other_id = WindowId(2); state .workspaces .insert_floating_window(other_id, other_workspace, geometry.visual.current, false) .unwrap(); state.windows.records.insert( other_id, super::super::window_registry::WindowRecord { geometry: Some(geometry), ..Default::default() }, ); assert_eq!( state .output_window_records(&output, false) .map(|(id, _, _)| id) .collect::>(), vec![id] ); assert_eq!( state .output_window_records(&other, false) .map(|(id, _, _)| id) .collect::>(), vec![other_id] ); assert!( !state .sample_frame(&output, Duration::from_millis(8)) .windows .contains_key(&other_id) ); assert!( !state .sample_frame(&other, Duration::from_millis(4)) .windows .contains_key(&id) ); let output_id = state.output_id(&output).unwrap(); let from = state.workspaces.workspace_for_window(id).unwrap(); let to = state.workspaces.create_workspace(); state.output_workspaces.assign_workspace(output_id, to).unwrap(); let mut slide = WorkspaceSlide::new(None, from, to, SwipeDirection::Left); slide.held_progress = Some(0.25); state.workspace_slides.insert(output_id, slide); state.refresh_workspace_slide_offsets(); let offsets = state.workspace_slide_offsets.clone(); assert_eq!(offsets[&from].0, -480.0); assert_eq!(offsets[&to].0, 1440.0); assert_eq!( state.sample_frame(&output, Duration::from_millis(8)).windows[&id] .presentation .bounds .current .x, state.sample_frame(&output, Duration::from_millis(8)).windows[&id] .geometry .visual .current .x - 480.0 ); assert_eq!(state.workspace_slide_offsets, offsets); assert!( state .sample_workspace_slide_offsets(state.output_id(&other), Duration::from_millis(8)) .is_empty() ); state.cancel_workspace_slides(); assert!(state.workspace_slide_offsets.is_empty()); // A prediction must neither advance nor expire a client resize barrier. state.windows.set_transaction( id, crate::resize_transaction::ResizeTransaction::new(9.into(), Duration::ZERO), ); state.rebuild_presentation_dependencies(); let blocked = state.sample_frame(&output, Duration::from_millis(16)); assert_eq!(blocked.windows[&id].geometry, geometry); assert!(state.windows.transaction(&id).is_some()); state.windows.clear_transaction(&id); state.animations_enabled = false; let forecast = state.sample_frame(&output, Duration::from_millis(16)); assert_eq!(forecast.windows[&id].geometry, geometry); let settled = WindowGeometry::new(Rect::new(0., 0., 400., 300.), None); state.windows.set_geometry(id, settled); state.animations_enabled = true; assert!(!state.sample_frame(&output, Duration::ZERO).animating); assert!(state.sample_frame(&other, Duration::ZERO).animating); // Target changes with animations disabled still need their final frame // on both displays, even though no spring/fade remains active afterward. state.focused_window = Some(id); state .windows .update(id, |record| record.focus = Some(AnimatedValue::new(1.0))); state.focused_window = Some(other_id); assert!(state.output_has_pending_visual_changes(&output)); state.animations_enabled = false; state.advance_animations(Instant::now()); assert!(!state.output_has_pending_visual_changes(&output)); state.animations_enabled = true; state.windows.set_geometry(id, geometry); assert!(state.output_has_animations(&output)); state.unregister_output(&other); assert!(!state.output_has_animations(&other)); assert!( state.output_has_animations(&output), "remaining output keeps its active animation" ); let inactive = state.workspaces.create_workspace(); state.output_workspaces.assign_workspace(output_id, inactive).unwrap(); let preview_id = WindowId(3); state .workspaces .insert_floating_window(preview_id, inactive, settled.visual.current, false) .unwrap(); state.windows.records.insert( preview_id, super::super::window_registry::WindowRecord { geometry: Some(settled), ..Default::default() }, ); assert!( !state .sample_frame(&output, Duration::ZERO) .windows .contains_key(&preview_id) ); state.set_overview_active(true); let frame = state.sample_frame(&output, Duration::ZERO); assert!( frame.windows.contains_key(&preview_id), "inactive workspace needs a thumbnail sample" ); assert!( !state.window_belongs_to_output(preview_id, &output), "thumbnail must stay out of the main grid" ); for card in state.overview_workspace_cards_for_frame(&output, Some(&frame)) { for (id, _) in card.windows { assert!( frame.windows.contains_key(&id), "workspace card has no presentation for {id:?}" ); } } state.set_overview_active(false); assert!( state.overview.is_presenting(), "exit transition keeps the strip visible" ); assert!( state .sample_frame(&output, Duration::ZERO) .windows .contains_key(&preview_id) ); } #[test] fn mixed_refresh_forecasts_do_not_change_authoritative_geometry_or_client_state() { let mut geometry = WindowGeometry::new(Rect::new(0., 0., 400., 300.), None); geometry.visual.set_target(Rect::new(600., 0., 500., 300.)); let original = geometry; let sample = |delta| { predict_geometry( geometry, delta, SpringConfig::default(), SpringConfig::default(), None, None, false, ) }; let fast = sample(Duration::from_nanos(1_000_000_000 / 240)); let slow = sample(Duration::from_nanos(1_000_000_000 / 60)); assert!(fast.visual.current.x > 0.); assert!(slow.visual.current.x > fast.visual.current.x); assert_eq!(sample(Duration::from_nanos(1_000_000_000 / 240)), fast); assert_eq!(geometry, original); assert_eq!(slow.client, original.client); assert_eq!(slow.logical, original.logical); } #[test] fn scrolling_coordinates_and_coupled_width_use_the_same_forecast_time() { let mut geometry = WindowGeometry::new(Rect::new(500., 0., 400., 300.), None); geometry.set_logical_target(Rect::new(400., 0., 500., 300.), Duration::ZERO); let mut world = AnimatedValue::new(500.); world.set_target(600.); let mut viewport = AnimatedValue::new(0.); viewport.set_target(200.); let mut width = AnimatedValue::new(400.); width.set_target(500.); let spring = SpringConfig::default(); let delta = Duration::from_millis(10); let sampled_viewport = ViewportPresentation::from_motion(viewport).sample(delta, spring, true, false); let predicted = predict_geometry( geometry, delta, spring, spring, Some((world, sampled_viewport)), Some(width), false, ); world.advance_with_policy(delta, spring, CrossingPolicy::NoCrossing); viewport.advance_with_policy(delta, spring, CrossingPolicy::NoCrossing); width.advance_with_policy(delta, spring, CrossingPolicy::NoCrossing); assert_eq!(predicted.visual.current.x, world.current - viewport.current); assert_eq!(predicted.visual.current.width, width.current); assert_eq!(predicted.visual.target, geometry.visual.target); } }