diff --git a/src/backend/kms/device.rs b/src/backend/kms/device.rs index 3409a9545..19ed58184 100644 --- a/src/backend/kms/device.rs +++ b/src/backend/kms/device.rs @@ -2,7 +2,7 @@ use crate::{ backend::render::{output_elements, CursorMode, GlMultiRenderer, CLEAR_COLOR}, - config::{AdaptiveSync, OutputConfig, OutputState, ScreenFilter}, + config::{AdaptiveSync, EdidProduct, OutputConfig, OutputState, ScreenFilter}, shell::Shell, utils::prelude::*, wayland::protocols::screencopy::Frame as ScreencopyFrame, @@ -743,7 +743,7 @@ fn create_output_for_conn(drm: &mut DrmDevice, conn: connector::Handle) -> Resul .ok(); let (phys_w, phys_h) = conn_info.size().unwrap_or((0, 0)); - Ok(Output::new( + let output = Output::new( interface, PhysicalProperties { size: (phys_w as i32, phys_h as i32).into(), @@ -764,7 +764,13 @@ fn create_output_for_conn(drm: &mut DrmDevice, conn: connector::Handle) -> Resul .and_then(|info| info.model()) .unwrap_or_else(|| String::from("Unknown")), }, - )) + ); + if let Some(edid) = edid_info.as_ref().and_then(|x| x.edid()) { + output + .user_data() + .insert_if_missing(|| EdidProduct::from(edid.vendor_product())); + } + Ok(output) } fn populate_modes( diff --git a/src/config/mod.rs b/src/config/mod.rs index 6d33d9551..bc72918e3 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -94,6 +94,29 @@ impl From for OutputInfo { } } +#[derive(Debug, Deserialize, Serialize, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct EdidProduct { + pub manufacturer: [char; 3], + pub product: u16, + pub serial: Option, + pub manufacture_week: i32, + pub manufacture_year: i32, + pub model_year: Option, +} + +impl From for EdidProduct { + fn from(vp: libdisplay_info::edid::VendorProduct) -> Self { + Self { + manufacturer: vp.manufacturer, + product: vp.product, + serial: vp.serial, + manufacture_week: vp.manufacture_week, + manufacture_year: vp.manufacture_year, + model_year: vp.model_year, + } + } +} + #[derive(Default, Debug, Deserialize, Serialize)] pub struct NumlockStateConfig { pub last_state: bool, diff --git a/src/shell/mod.rs b/src/shell/mod.rs index 69099c699..d64182ca4 100644 --- a/src/shell/mod.rs +++ b/src/shell/mod.rs @@ -518,14 +518,14 @@ impl WorkspaceSet { } } - fn set_output(&mut self, new_output: &Output) { + fn set_output(&mut self, new_output: &Output, explicit: bool) { self.sticky_layer.set_output(new_output); for window in self.sticky_layer.windows() { toplevel_leave_output(&window, &self.output); toplevel_enter_output(&window, &new_output); } for workspace in &mut self.workspaces { - workspace.set_output(new_output); + workspace.set_output(new_output, explicit); } self.output = new_output.clone(); } @@ -711,7 +711,7 @@ impl Workspaces { .backup_set .take() .map(|mut set| { - set.set_output(output); + set.set_output(output, false); set }) .unwrap_or_else(|| { @@ -738,7 +738,7 @@ impl Workspaces { } set.update_workspace_idxs(workspace_state); for (i, workspace) in set.workspaces.iter_mut().enumerate() { - workspace.set_output(output); + workspace.set_output(output, false); workspace.refresh(); if i == set.active { workspace_state.add_workspace_state(&workspace.handle, WState::Active); @@ -790,7 +790,7 @@ impl Workspaces { move_workspace_to_group(&mut workspace, &workspace_group, workspace_state); // update mapping - workspace.set_output(&new_output); + workspace.set_output(&new_output, false); workspace.refresh(); new_set.workspaces.push(workspace); @@ -840,6 +840,7 @@ impl Workspaces { } } + // Move workspace from one output to another, explicitly by the user fn migrate_workspace( &mut self, from: &Output, @@ -858,7 +859,7 @@ impl Workspaces { { let new_set = self.sets.get_mut(to).unwrap(); move_workspace_to_group(&mut workspace, &new_set.group, workspace_state); - workspace.set_output(to); + workspace.set_output(to, true); workspace.refresh(); new_set.workspaces.insert(new_set.active + 1, workspace); new_set.update_workspace_idxs(workspace_state); diff --git a/src/shell/workspace.rs b/src/shell/workspace.rs index bfbd672a8..f69ee73b8 100644 --- a/src/shell/workspace.rs +++ b/src/shell/workspace.rs @@ -4,6 +4,7 @@ use crate::{ element::{AsGlowRenderer, FromGlesError}, BackdropShader, }, + config::EdidProduct, shell::{ layout::{floating::FloatingLayout, tiling::TilingLayout}, OverviewMode, ANIMATION_DURATION, @@ -73,6 +74,33 @@ use super::{ const FULLSCREEN_ANIMATION_DURATION: Duration = Duration::from_millis(200); +#[derive(Debug, Clone, PartialEq, Eq)] +struct OutputMatch { + name: String, + edid: Option, +} + +impl OutputMatch { + fn for_output(output: &Output) -> Self { + Self { + name: output.name(), + edid: output.edid().cloned(), + } + } + + // If `disambguate` is true, check that edid *and* connector name match. + // Otherwise, match only edid (if it exists) + fn matches(&self, output: &Output, disambiguate: bool) -> bool { + if self.edid.as_ref() != output.edid() { + false + } else if disambiguate || self.edid.is_none() { + self.name == output.name() + } else { + true + } + } +} + #[derive(Debug)] pub struct Workspace { pub output: Output, @@ -85,7 +113,7 @@ pub struct Workspace { pub handle: WorkspaceHandle, pub focus_stack: FocusStacks, pub screencopy: ScreencopySessions, - pub output_stack: VecDeque, + output_stack: VecDeque, pub(super) backdrop_id: Id, pub dirty: AtomicBool, } @@ -241,7 +269,7 @@ impl Workspace { ) -> Workspace { let tiling_layer = TilingLayout::new(theme.clone(), &output); let floating_layer = FloatingLayout::new(theme, &output); - let output_name = output.name(); + let output_match = OutputMatch::for_output(&output); Workspace { output, @@ -255,7 +283,7 @@ impl Workspace { screencopy: ScreencopySessions::default(), output_stack: { let mut queue = VecDeque::new(); - queue.push_back(output_name); + queue.push_back(output_match); queue }, backdrop_id: Id::new(), @@ -361,7 +389,11 @@ impl Workspace { &self.output } - pub fn set_output(&mut self, output: &Output) { + // Set output the workspace is on + // + // If `explicit` is `true`, the user has explicitly moved the workspace + // to this output, so previous outputs it was on can be forgotten. + pub fn set_output(&mut self, output: &Output, explicit: bool) { self.tiling_layer.set_output(output); self.floating_layer.set_output(output); for mapped in self.mapped() { @@ -376,21 +408,39 @@ impl Workspace { toplevel_enter_output(&surface, output); } } - let output_name = output.name(); + if explicit { + self.output_stack.clear(); + } if let Some(pos) = self .output_stack .iter() - .position(|name| name == &output_name) + .position(|i| i.matches(output, true)) + { + // Matched edid and connector name + self.output_stack.truncate(pos + 1); + } else if let Some(pos) = self + .output_stack + .iter() + .position(|i| i.matches(output, false)) { + // Matched edid but not connector name; truncate entries that don't match edid, + // but keep old entry in case we see two outputs with the same edid. self.output_stack.truncate(pos + 1); + self.output_stack.push_back(OutputMatch::for_output(output)); } else { - self.output_stack.push_back(output.name()); + self.output_stack.push_back(OutputMatch::for_output(output)); } self.output = output.clone(); } pub fn prefers_output(&self, output: &Output) -> bool { - self.output_stack.contains(&output.name()) + // Disambiguate match by connector name if existing output has same edid + let disambiguate = output + .edid() + .is_some_and(|edid| self.output().edid() == Some(edid)); + self.output_stack + .iter() + .any(|i| i.matches(output, disambiguate)) } pub fn unmap(&mut self, mapped: &CosmicMapped) -> Option { diff --git a/src/utils/prelude.rs b/src/utils/prelude.rs index f7952459d..bb30272a7 100644 --- a/src/utils/prelude.rs +++ b/src/utils/prelude.rs @@ -9,7 +9,7 @@ pub use crate::shell::{SeatExt, Shell, Workspace}; pub use crate::state::{Common, State}; pub use crate::wayland::handlers::xdg_shell::popup::update_reactive_popups; use crate::{ - config::{AdaptiveSync, OutputConfig, OutputState}, + config::{AdaptiveSync, EdidProduct, OutputConfig, OutputState}, shell::zoom::OutputZoomState, }; @@ -35,6 +35,8 @@ pub trait OutputExt { fn is_enabled(&self) -> bool; fn config(&self) -> Ref<'_, OutputConfig>; fn config_mut(&self) -> RefMut<'_, OutputConfig>; + + fn edid(&self) -> Option<&EdidProduct>; } struct Vrr(AtomicU8); @@ -158,4 +160,8 @@ impl OutputExt for Output { .unwrap() .borrow_mut() } + + fn edid(&self) -> Option<&EdidProduct> { + self.user_data().get() + } }