//! The state of all the outputs at one time. //! //! The compositor gives the full state of the outputs in one reply. A snapshot //! holds that reply. The program compares two snapshots to find a change. use std::collections::{BTreeMap, HashMap}; use dynamonix_geom::Rect; use niri_ipc::Output; use crate::config::OutputConfig; use crate::identity::Signature; use crate::layout::Layout; use crate::output::OutputExt; /// The state of all the outputs at one time. /// /// The snapshot keeps the outputs in the order of their names. The order does /// not change between two snapshots of the same outputs. #[derive(Debug, Clone, Default)] pub struct Snapshot { outputs: BTreeMap, } impl PartialEq for Snapshot { fn eq(&self, other: &Self) -> bool { self.outputs.len() == other.outputs.len() && self.outputs.iter().all(|(name, output)| { other .outputs .get(name) .is_some_and(|found| crate::output::same_state(output, found)) }) } } impl Snapshot { /// Make a snapshot from the reply of the compositor. #[must_use] pub fn from_ipc(outputs: HashMap) -> Self { Self { outputs: outputs.into_iter().collect(), } } /// Make a snapshot from a list of the outputs. #[must_use] pub fn new(outputs: impl IntoIterator) -> Self { Self { outputs: outputs .into_iter() .map(|output| (output.name.clone(), output)) .collect(), } } /// Give the output with the name. #[must_use] pub fn get(&self, name: &str) -> Option<&Output> { self.outputs.get(name) } /// Give all the outputs in the order of their names. pub fn iter(&self) -> impl Iterator { self.outputs.values() } /// Give the names of all the outputs in order. #[must_use] pub fn names(&self) -> Vec { self.outputs.keys().cloned().collect() } /// Give the outputs that the compositor shows. pub fn enabled(&self) -> impl Iterator { self.outputs.values().filter(|output| output.is_enabled()) } /// Give the number of the outputs that the compositor shows. #[must_use] pub fn enabled_count(&self) -> usize { self.enabled().count() } /// Give the number of the outputs. #[must_use] pub fn len(&self) -> usize { self.outputs.len() } /// Tell if the compositor reports no output. #[must_use] pub fn is_empty(&self) -> bool { self.outputs.is_empty() } /// Give the area of each output that the compositor shows. #[must_use] pub fn rects(&self) -> Vec<(String, Rect)> { self .enabled() .filter_map(|output| Some((output.name.clone(), output.rect()?))) .collect() } /// Give the identities of all the monitors. #[must_use] pub fn signature(&self) -> Signature { Signature::new(self.outputs.values().map(OutputExt::identity)) } /// Make the layout that repeats the current state. /// /// The program applies this layout to make no change. #[must_use] pub fn to_layout(&self) -> Layout { Layout::new( self .outputs .iter() .map(|(name, output)| (name.clone(), OutputConfig::of(output))), ) } } #[cfg(test)] mod tests { use niri_ipc::{LogicalOutput, Mode, Transform}; use super::*; fn output(name: &str, x: i32, enabled: bool) -> Output { Output { name: name.to_owned(), make: "Make".to_owned(), model: format!("Model-{name}"), serial: Some(name.to_owned()), physical_size: None, modes: vec![Mode { width: 1920, height: 1080, refresh_rate: 60_000, is_preferred: true, }], current_mode: Some(0), is_custom_mode: false, vrr_supported: false, vrr_enabled: false, logical: enabled.then_some(LogicalOutput { x, y: 0, width: 1920, height: 1080, scale: 1.0, transform: Transform::Normal, }), } } #[test] fn an_empty_snapshot_reports_no_output() { let snapshot = Snapshot::default(); assert!(snapshot.is_empty()); assert_eq!(snapshot.enabled_count(), 0); assert!(snapshot.rects().is_empty()); } #[test] fn the_snapshot_counts_only_the_enabled_outputs() { let snapshot = Snapshot::new([output("DP-1", 0, true), output("DP-2", 1920, false)]); assert_eq!(snapshot.len(), 2); assert_eq!(snapshot.enabled_count(), 1); assert_eq!(snapshot.rects().len(), 1); } #[test] fn the_outputs_stay_in_the_order_of_their_names() { let snapshot = Snapshot::new([output("DP-2", 1920, true), output("DP-1", 0, true)]); assert_eq!(snapshot.names(), vec!["DP-1", "DP-2"]); } #[test] fn the_layout_of_a_snapshot_repeats_the_state_of_the_snapshot() { let snapshot = Snapshot::new([output("DP-1", 0, true), output("DP-2", 1920, true)]); let layout = snapshot.to_layout(); assert_eq!(layout.len(), 2); assert_eq!(layout.rects(&snapshot), snapshot.rects()); } #[test] fn the_signature_does_not_change_with_the_order_of_the_outputs() { let first = Snapshot::new([output("DP-1", 0, true), output("DP-2", 1920, true)]); let second = Snapshot::new([output("DP-2", 1920, true), output("DP-1", 0, true)]); assert_eq!(first.signature(), second.signature()); } }