From 43df7fad46033692abc6a2b223e6d98f8b7a5aed Mon Sep 17 00:00:00 2001 From: tet Date: Tue, 21 May 2024 22:43:42 +0000 Subject: Implement wlr-output-management protocol fix: wlr_output_management use WeakOutput --- src/protocols/mod.rs | 1 + src/protocols/output_management.rs | 891 +++++++++++++++++++++++++++++++++++++ 2 files changed, 892 insertions(+) create mode 100644 src/protocols/output_management.rs (limited to 'src/protocols') diff --git a/src/protocols/mod.rs b/src/protocols/mod.rs index d1779b7e..a58b48bf 100644 --- a/src/protocols/mod.rs +++ b/src/protocols/mod.rs @@ -1,3 +1,4 @@ pub mod foreign_toplevel; pub mod gamma_control; +pub mod output_management; pub mod screencopy; diff --git a/src/protocols/output_management.rs b/src/protocols/output_management.rs new file mode 100644 index 00000000..2842fced --- /dev/null +++ b/src/protocols/output_management.rs @@ -0,0 +1,891 @@ +use std::collections::hash_map::Entry; +use std::collections::HashMap; +use std::iter::zip; +use std::mem; + +use niri_config::FloatOrInt; +use niri_ipc::Transform; +use smithay::reexports::wayland_protocols_wlr::output_management::v1::server::{ + zwlr_output_configuration_head_v1, zwlr_output_configuration_v1, zwlr_output_head_v1, + zwlr_output_manager_v1, zwlr_output_mode_v1, +}; +use smithay::reexports::wayland_server::backend::ClientId; +use smithay::reexports::wayland_server::protocol::wl_output::Transform as WlTransform; +use smithay::reexports::wayland_server::{ + Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, WEnum, +}; +use zwlr_output_configuration_head_v1::ZwlrOutputConfigurationHeadV1; +use zwlr_output_configuration_v1::ZwlrOutputConfigurationV1; +use zwlr_output_head_v1::{AdaptiveSyncState, ZwlrOutputHeadV1}; +use zwlr_output_manager_v1::ZwlrOutputManagerV1; +use zwlr_output_mode_v1::ZwlrOutputModeV1; + +use crate::backend::OutputId; +use crate::niri::State; +use crate::utils::ipc_transform_to_smithay; + +const VERSION: u32 = 4; + +#[derive(Debug)] +struct ClientData { + heads: HashMap)>, + confs: HashMap, + manager: ZwlrOutputManagerV1, +} + +pub struct OutputManagementManagerState { + display: DisplayHandle, + serial: u32, + clients: HashMap, + current_state: HashMap, + current_config: Vec, +} + +pub struct OutputManagementManagerGlobalData { + filter: Box Fn(&'c Client) -> bool + Send + Sync>, +} + +pub trait OutputManagementHandler { + fn output_management_state(&mut self) -> &mut OutputManagementManagerState; + fn apply_output_config(&mut self, config: Vec); +} + +#[derive(Debug)] +enum OutputConfigurationState { + Ongoing(HashMap), + Finished, +} + +pub enum OutputConfigurationHeadState { + Cancelled, + Ok(OutputId, ZwlrOutputConfigurationV1), +} + +impl OutputManagementManagerState { + pub fn new(display: &DisplayHandle, filter: F) -> Self + where + D: GlobalDispatch, + D: Dispatch, + D: Dispatch, + D: Dispatch, + D: Dispatch, + D: Dispatch, + D: OutputManagementHandler, + D: 'static, + F: for<'c> Fn(&'c Client) -> bool + Send + Sync + 'static, + { + let global_data = OutputManagementManagerGlobalData { + filter: Box::new(filter), + }; + display.create_global::(VERSION, global_data); + + Self { + display: display.clone(), + clients: HashMap::new(), + serial: 0, + current_state: HashMap::new(), + current_config: Vec::new(), + } + } + + pub fn on_config_changed(&mut self, new_config: Vec) { + self.current_config = new_config; + } + + pub fn notify_changes(&mut self, new_state: HashMap) { + let mut changed = false; /* most likely to end up true */ + for (output, conf) in new_state.iter() { + if let Some(old) = self.current_state.get(output) { + if old.vrr_enabled != conf.vrr_enabled { + changed = true; + for client in self.clients.values() { + if let Some((head, _)) = client.heads.get(output) { + if head.version() >= zwlr_output_head_v1::EVT_ADAPTIVE_SYNC_SINCE { + head.adaptive_sync(match conf.vrr_enabled { + true => AdaptiveSyncState::Enabled, + false => AdaptiveSyncState::Disabled, + }); + } + } + } + } + + // TTY outputs can't change modes I think, however, winit and virtual outputs can. + let modes_changed = old.modes != conf.modes; + if modes_changed { + changed = true; + if old.modes.len() != conf.modes.len() { + error!("output's old mode count doesn't match new modes"); + } else { + for client in self.clients.values() { + if let Some((_, modes)) = client.heads.get(output) { + for (wl_mode, mode) in zip(modes, &conf.modes) { + wl_mode.size(i32::from(mode.width), i32::from(mode.height)); + if let Ok(refresh_rate) = mode.refresh_rate.try_into() { + wl_mode.refresh(refresh_rate); + } + } + } + } + } + } + + match (old.current_mode, conf.current_mode) { + (Some(old_index), Some(new_index)) => { + if old.modes.len() == conf.modes.len() + && (modes_changed || old_index != new_index) + { + changed = true; + for client in self.clients.values() { + if let Some((head, modes)) = client.heads.get(output) { + if let Some(new_mode) = modes.get(new_index) { + head.current_mode(new_mode); + } else { + error!( + "output new mode doesnt exist for the client's output" + ); + } + } + } + } + } + (Some(_), None) => { + changed = true; + for client in self.clients.values() { + if let Some((head, _)) = client.heads.get(output) { + head.enabled(0); + } + } + } + (None, Some(new_index)) => { + if old.modes.len() == conf.modes.len() { + changed = true; + for client in self.clients.values() { + if let Some((head, modes)) = client.heads.get(output) { + head.enabled(1); + if let Some(mode) = modes.get(new_index) { + head.current_mode(mode); + } else { + error!( + "output new mode doesnt exist for the client's output" + ); + } + } + } + } + } + (None, None) => {} + } + match (old.logical, conf.logical) { + (Some(old_logical), Some(new_logical)) => { + if old_logical != new_logical { + changed = true; + for client in self.clients.values() { + if let Some((head, _)) = client.heads.get(output) { + if old_logical.x != new_logical.x + || old_logical.y != new_logical.y + { + head.position(new_logical.x, new_logical.y); + } + if old_logical.scale != new_logical.scale { + head.scale(new_logical.scale); + } + if old_logical.transform != new_logical.transform { + head.transform( + ipc_transform_to_smithay(new_logical.transform).into(), + ); + } + } + } + } + } + (None, Some(new_logical)) => { + changed = true; + for client in self.clients.values() { + if let Some((head, _)) = client.heads.get(output) { + // head enable in the mode diff check + head.position(new_logical.x, new_logical.y); + head.transform( + ipc_transform_to_smithay(new_logical.transform).into(), + ); + head.scale(new_logical.scale); + } + } + } + (Some(_), None) => { + // heads disabled in the mode diff check + } + (None, None) => {} + } + } else { + changed = true; + notify_new_head(self, output, conf); + } + } + for (old, _) in self.current_state.iter() { + if !new_state.contains_key(old) { + changed = true; + notify_removed_head(&mut self.clients, old); + } + } + if changed { + self.current_state = new_state; + self.serial += 1; + for data in self.clients.values() { + data.manager.done(self.serial); + for conf in data.confs.keys() { + conf.cancelled(); + } + } + } + } +} + +impl GlobalDispatch + for OutputManagementManagerState +where + D: GlobalDispatch, + D: Dispatch, + D: Dispatch, + D: Dispatch, + D: Dispatch, + D: Dispatch, + D: OutputManagementHandler, + D: 'static, +{ + fn bind( + state: &mut D, + display: &DisplayHandle, + client: &Client, + manager: New, + _manager_state: &OutputManagementManagerGlobalData, + data_init: &mut DataInit<'_, D>, + ) { + let manager = data_init.init(manager, ()); + let g_state = state.output_management_state(); + let mut client_data = ClientData { + heads: HashMap::new(), + confs: HashMap::new(), + manager: manager.clone(), + }; + for (output, conf) in &g_state.current_state { + send_new_head::(display, client, &mut client_data, *output, conf); + } + g_state.clients.insert(client.id(), client_data); + manager.done(g_state.serial); + } + + fn can_view(client: Client, global_data: &OutputManagementManagerGlobalData) -> bool { + (global_data.filter)(&client) + } +} + +impl Dispatch for OutputManagementManagerState +where + D: GlobalDispatch, + D: Dispatch, + D: Dispatch, + D: Dispatch, + D: Dispatch, + D: Dispatch, + D: OutputManagementHandler, + D: 'static, +{ + fn request( + state: &mut D, + client: &Client, + _manager: &ZwlrOutputManagerV1, + request: zwlr_output_manager_v1::Request, + _data: &(), + _display: &DisplayHandle, + data_init: &mut DataInit<'_, D>, + ) { + match request { + zwlr_output_manager_v1::Request::CreateConfiguration { id, serial } => { + let g_state = state.output_management_state(); + let conf = data_init.init(id, serial); + if let Some(client_data) = g_state.clients.get_mut(&client.id()) { + if serial != g_state.serial { + conf.cancelled(); + } + let state = OutputConfigurationState::Ongoing(HashMap::new()); + client_data.confs.insert(conf, state); + } else { + error!("CreateConfiguration: missing client data"); + } + } + zwlr_output_manager_v1::Request::Stop => { + if let Some(c) = state.output_management_state().clients.remove(&client.id()) { + c.manager.finished() + } + } + _ => unreachable!(), + } + } + fn destroyed(state: &mut D, client: ClientId, _resource: &ZwlrOutputManagerV1, _data: &()) { + state.output_management_state().clients.remove(&client); + } +} + +impl Dispatch for OutputManagementManagerState +where + D: GlobalDispatch, + D: Dispatch, + D: Dispatch, + D: Dispatch, + D: Dispatch, + D: Dispatch, + D: OutputManagementHandler, + D: 'static, +{ + fn request( + state: &mut D, + client: &Client, + conf: &ZwlrOutputConfigurationV1, + request: zwlr_output_configuration_v1::Request, + serial: &u32, + _display: &DisplayHandle, + data_init: &mut DataInit<'_, D>, + ) { + let g_state = state.output_management_state(); + let outdated = *serial != g_state.serial; + if outdated { + debug!("OutputConfiguration: request from an outdated configuration"); + } + + let new_config = g_state + .clients + .get_mut(&client.id()) + .and_then(|data| data.confs.get_mut(conf)); + if new_config.is_none() { + error!("OutputConfiguration: request from unknown configuration object"); + } + + match request { + zwlr_output_configuration_v1::Request::EnableHead { id, head } => { + let Some(output) = head.data::() else { + error!("EnableHead: Missing attached output"); + let _fail = data_init.init(id, OutputConfigurationHeadState::Cancelled); + return; + }; + if outdated { + let _fail = data_init.init(id, OutputConfigurationHeadState::Cancelled); + return; + } + + let Some(new_config) = new_config else { + let _fail = data_init.init(id, OutputConfigurationHeadState::Cancelled); + return; + }; + + let OutputConfigurationState::Ongoing(new_config) = new_config else { + let _fail = data_init.init(id, OutputConfigurationHeadState::Cancelled); + conf.post_error( + zwlr_output_configuration_v1::Error::AlreadyUsed, + "configuration had already been used", + ); + return; + }; + + let Some(current_config) = g_state.current_state.get(output) else { + error!("EnableHead: output missing from current config"); + let _fail = data_init.init(id, OutputConfigurationHeadState::Cancelled); + return; + }; + + match new_config.entry(*output) { + Entry::Occupied(_) => { + let _fail = data_init.init(id, OutputConfigurationHeadState::Cancelled); + conf.post_error( + zwlr_output_configuration_v1::Error::AlreadyConfiguredHead, + "head has been already configured", + ); + return; + } + Entry::Vacant(entry) => { + let mut config = g_state + .current_config + .iter() + .find(|o| o.name.eq_ignore_ascii_case(¤t_config.name)) + .cloned() + .unwrap_or_else(|| niri_config::Output { + name: current_config.name.clone(), + ..Default::default() + }); + config.off = false; + entry.insert(config); + } + }; + + data_init.init(id, OutputConfigurationHeadState::Ok(*output, conf.clone())); + } + zwlr_output_configuration_v1::Request::DisableHead { head } => { + if outdated { + return; + } + let Some(output) = head.data::() else { + error!("DisableHead: missing attached output head name"); + return; + }; + + let Some(new_config) = new_config else { + return; + }; + + let OutputConfigurationState::Ongoing(new_config) = new_config else { + conf.post_error( + zwlr_output_configuration_v1::Error::AlreadyUsed, + "configuration had already been used", + ); + return; + }; + + let Some(current_config) = g_state.current_state.get(output) else { + error!("EnableHead: output missing from current config"); + return; + }; + + match new_config.entry(*output) { + Entry::Occupied(_) => { + conf.post_error( + zwlr_output_configuration_v1::Error::AlreadyConfiguredHead, + "head has been already configured", + ); + } + Entry::Vacant(entry) => { + let mut config = g_state + .current_config + .iter() + .find(|o| o.name.eq_ignore_ascii_case(¤t_config.name)) + .cloned() + .unwrap_or_else(|| niri_config::Output { + name: current_config.name.clone(), + ..Default::default() + }); + config.off = true; + entry.insert(config); + } + }; + } + zwlr_output_configuration_v1::Request::Apply => { + if outdated { + conf.cancelled(); + return; + } + + let Some(new_config) = new_config else { + return; + }; + + let OutputConfigurationState::Ongoing(new_config) = + mem::replace(new_config, OutputConfigurationState::Finished) + else { + conf.post_error( + zwlr_output_configuration_v1::Error::AlreadyUsed, + "configuration had already been used", + ); + return; + }; + + let any_enabled = new_config.values().any(|c| !c.off); + if !any_enabled { + conf.failed(); + return; + } + + state.apply_output_config(new_config.into_values().collect()); + // FIXME: verify that it had been applied successfully (which may be difficult). + conf.succeeded(); + } + zwlr_output_configuration_v1::Request::Test => { + if outdated { + conf.cancelled(); + return; + } + + let Some(new_config) = new_config else { + return; + }; + + let OutputConfigurationState::Ongoing(new_config) = + mem::replace(new_config, OutputConfigurationState::Finished) + else { + conf.post_error( + zwlr_output_configuration_v1::Error::AlreadyUsed, + "configuration had already been used", + ); + return; + }; + + let any_enabled = new_config.values().any(|c| !c.off); + if !any_enabled { + conf.failed(); + return; + } + + // FIXME: actually test the configuration with TTY. + conf.succeeded() + } + zwlr_output_configuration_v1::Request::Destroy => { + g_state + .clients + .get_mut(&client.id()) + .map(|d| d.confs.remove(conf)); + } + _ => unreachable!(), + } + } +} + +impl Dispatch + for OutputManagementManagerState +where + D: GlobalDispatch, + D: Dispatch, + D: Dispatch, + D: Dispatch, + D: Dispatch, + D: Dispatch, + D: OutputManagementHandler, + D: 'static, +{ + fn request( + state: &mut D, + client: &Client, + conf_head: &ZwlrOutputConfigurationHeadV1, + request: zwlr_output_configuration_head_v1::Request, + data: &OutputConfigurationHeadState, + _display: &DisplayHandle, + _data_init: &mut DataInit<'_, D>, + ) { + let g_state = state.output_management_state(); + let Some(client_data) = g_state.clients.get_mut(&client.id()) else { + error!("ConfigurationHead: missing client data"); + return; + }; + let OutputConfigurationHeadState::Ok(output_id, conf) = data else { + warn!("ConfigurationHead: request sent to a cancelled head"); + return; + }; + let Some(serial) = conf.data::() else { + error!("ConfigurationHead: missing serial"); + return; + }; + if *serial != g_state.serial { + warn!("ConfigurationHead: request sent to an outdated"); + return; + } + let Some(new_config) = client_data.confs.get_mut(conf) else { + error!("ConfigurationHead: unknown configuration"); + return; + }; + let OutputConfigurationState::Ongoing(new_config) = new_config else { + conf.post_error( + zwlr_output_configuration_v1::Error::AlreadyUsed, + "configuration had already been used", + ); + return; + }; + let Some(new_config) = new_config.get_mut(output_id) else { + error!("ConfigurationHead: config missing from enabled heads"); + return; + }; + + match request { + zwlr_output_configuration_head_v1::Request::SetMode { mode } => { + let index = match client_data + .heads + .get(output_id) + .map(|(_, mods)| mods.iter().position(|m| m.id() == mode.id())) + { + Some(Some(index)) => index, + _ => { + warn!("SetMode: failed to find requested mode"); + conf_head.post_error( + zwlr_output_configuration_head_v1::Error::InvalidMode, + "failed to find requested mode", + ); + return; + } + }; + + let Some(current_config) = g_state.current_state.get(output_id) else { + warn!("SetMode: output missing from the current config"); + return; + }; + + let Some(mode) = current_config.modes.get(index) else { + error!("SetMode: requested mode is out of range"); + return; + }; + + new_config.mode = Some(niri_ipc::ConfiguredMode { + width: mode.width, + height: mode.height, + refresh: Some(mode.refresh_rate as f64 / 1000.), + }); + } + zwlr_output_configuration_head_v1::Request::SetCustomMode { + width, + height, + refresh, + } => { + // FIXME: Support custom mode + let (width, height, refresh): (u16, u16, u32) = + match (width.try_into(), height.try_into(), refresh.try_into()) { + (Ok(width), Ok(height), Ok(refresh)) => (width, height, refresh), + _ => { + warn!("SetCustomMode: invalid input data"); + return; + } + }; + + let Some(current_config) = g_state.current_state.get(output_id) else { + warn!("SetMode: output missing from the current config"); + return; + }; + + let Some(mode) = current_config.modes.iter().find(|m| { + m.width == width + && m.height == height + && (refresh == 0 || m.refresh_rate == refresh) + }) else { + warn!("SetCustomMode: no matching mode"); + return; + }; + + new_config.mode = Some(niri_ipc::ConfiguredMode { + width: mode.width, + height: mode.height, + refresh: Some(mode.refresh_rate as f64 / 1000.), + }); + } + zwlr_output_configuration_head_v1::Request::SetPosition { x, y } => { + new_config.position = Some(niri_config::Position { x, y }); + } + zwlr_output_configuration_head_v1::Request::SetTransform { transform } => { + let transform = match transform { + WEnum::Value(WlTransform::Normal) => Transform::Normal, + WEnum::Value(WlTransform::_90) => Transform::_90, + WEnum::Value(WlTransform::_180) => Transform::_180, + WEnum::Value(WlTransform::_270) => Transform::_270, + WEnum::Value(WlTransform::Flipped) => Transform::Flipped, + WEnum::Value(WlTransform::Flipped90) => Transform::Flipped90, + WEnum::Value(WlTransform::Flipped180) => Transform::Flipped180, + WEnum::Value(WlTransform::Flipped270) => Transform::Flipped270, + _ => { + warn!("SetTransform: unknown requested transform"); + conf_head.post_error( + zwlr_output_configuration_head_v1::Error::InvalidTransform, + "unknown transform value", + ); + return; + } + }; + new_config.transform = transform; + } + zwlr_output_configuration_head_v1::Request::SetScale { scale } => { + if scale <= 0. { + conf_head.post_error( + zwlr_output_configuration_head_v1::Error::InvalidScale, + "scale is negative or zero", + ); + return; + } + new_config.scale = Some(FloatOrInt(scale)); + } + zwlr_output_configuration_head_v1::Request::SetAdaptiveSync { state } => { + let enabled = match state { + WEnum::Value(AdaptiveSyncState::Enabled) => true, + WEnum::Value(AdaptiveSyncState::Disabled) => false, + _ => { + warn!("SetAdaptativeSync: unknown requested adaptative sync"); + conf_head.post_error( + zwlr_output_configuration_head_v1::Error::InvalidAdaptiveSyncState, + "unknown adaptive sync value", + ); + return; + } + }; + new_config.variable_refresh_rate = enabled; + } + _ => unreachable!(), + } + } +} + +impl Dispatch for OutputManagementManagerState +where + D: GlobalDispatch, + D: Dispatch, + D: Dispatch, + D: Dispatch, + D: Dispatch, + D: Dispatch, + D: OutputManagementHandler, + D: 'static, +{ + fn request( + _state: &mut D, + _client: &Client, + _output_head: &ZwlrOutputHeadV1, + request: zwlr_output_head_v1::Request, + _data: &OutputId, + _display: &DisplayHandle, + _data_init: &mut DataInit<'_, D>, + ) { + match request { + zwlr_output_head_v1::Request::Release => {} + _ => unreachable!(), + } + } + fn destroyed(state: &mut D, client: ClientId, _resource: &ZwlrOutputHeadV1, data: &OutputId) { + if let Some(c) = state.output_management_state().clients.get_mut(&client) { + c.heads.remove(data); + } + } +} + +impl Dispatch for OutputManagementManagerState +where + D: GlobalDispatch, + D: Dispatch, + D: Dispatch, + D: Dispatch, + D: Dispatch, + D: Dispatch, + D: OutputManagementHandler, + D: 'static, +{ + fn request( + _state: &mut D, + _client: &Client, + _mode: &ZwlrOutputModeV1, + request: zwlr_output_mode_v1::Request, + _data: &(), + _display: &DisplayHandle, + _data_init: &mut DataInit<'_, D>, + ) { + match request { + zwlr_output_mode_v1::Request::Release => {} + _ => unreachable!(), + } + } +} + +#[macro_export] +macro_rules! delegate_output_management{ + ($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => { + smithay::reexports::wayland_server::delegate_global_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + smithay::reexports::wayland_protocols_wlr::output_management::v1::server::zwlr_output_manager_v1::ZwlrOutputManagerV1: $crate::protocols::output_management::OutputManagementManagerGlobalData + ] => $crate::protocols::output_management::OutputManagementManagerState); + smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + smithay::reexports::wayland_protocols_wlr::output_management::v1::server::zwlr_output_manager_v1::ZwlrOutputManagerV1: () + ] => $crate::protocols::output_management::OutputManagementManagerState); + smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + smithay::reexports::wayland_protocols_wlr::output_management::v1::server::zwlr_output_configuration_v1::ZwlrOutputConfigurationV1: u32 + ] => $crate::protocols::output_management::OutputManagementManagerState); + smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + smithay::reexports::wayland_protocols_wlr::output_management::v1::server::zwlr_output_head_v1::ZwlrOutputHeadV1: $crate::backend::OutputId + ] => $crate::protocols::output_management::OutputManagementManagerState); + smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + smithay::reexports::wayland_protocols_wlr::output_management::v1::server::zwlr_output_mode_v1::ZwlrOutputModeV1: () + ] => $crate::protocols::output_management::OutputManagementManagerState); + smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + smithay::reexports::wayland_protocols_wlr::output_management::v1::server::zwlr_output_configuration_head_v1::ZwlrOutputConfigurationHeadV1: $crate::protocols::output_management::OutputConfigurationHeadState + ] => $crate::protocols::output_management::OutputManagementManagerState); + }; +} + +fn notify_removed_head(clients: &mut HashMap, head: &OutputId) { + for data in clients.values_mut() { + if let Some((head, mods)) = data.heads.remove(head) { + mods.iter().for_each(|m| m.finished()); + head.finished(); + } + } +} + +fn notify_new_head( + state: &mut OutputManagementManagerState, + output: &OutputId, + conf: &niri_ipc::Output, +) { + let display = &state.display; + let clients = &mut state.clients; + for data in clients.values_mut() { + if let Some(client) = data.manager.client() { + send_new_head::(display, &client, data, *output, conf); + } + } +} + +fn send_new_head( + display: &DisplayHandle, + client: &Client, + client_data: &mut ClientData, + output: OutputId, + conf: &niri_ipc::Output, +) where + D: GlobalDispatch, + D: Dispatch, + D: Dispatch, + D: Dispatch, + D: Dispatch, + D: OutputManagementHandler, + D: 'static, + D: Dispatch, + D: Dispatch, + D: 'static, +{ + let new_head = client + .create_resource::(display, client_data.manager.version(), output) + .unwrap(); + client_data.manager.head(&new_head); + new_head.name(conf.name.clone()); + new_head.description(format!("{} - {} - {}", conf.make, conf.model, conf.name)); + if let Some((width, height)) = conf.physical_size { + if let (Ok(a), Ok(b)) = (width.try_into(), height.try_into()) { + new_head.physical_size(a, b); + } + } + let mut new_modes = Vec::with_capacity(conf.modes.len()); + for (index, mode) in conf.modes.iter().enumerate() { + let new_mode = client + .create_resource::(display, new_head.version(), ()) + .unwrap(); + new_head.mode(&new_mode); + new_mode.size(i32::from(mode.width), i32::from(mode.height)); + if mode.is_preferred { + new_mode.preferred(); + } + if let Ok(refresh_rate) = mode.refresh_rate.try_into() { + new_mode.refresh(refresh_rate); + } + if Some(index) == conf.current_mode { + new_head.current_mode(&new_mode); + } + new_modes.push(new_mode); + } + if let Some(logical) = conf.logical { + new_head.position(logical.x, logical.y); + new_head.transform(ipc_transform_to_smithay(logical.transform).into()); + new_head.scale(logical.scale); + } + new_head.enabled(conf.current_mode.is_some() as i32); + if new_head.version() >= zwlr_output_head_v1::EVT_MAKE_SINCE { + new_head.make(conf.make.clone()); + } + if new_head.version() >= zwlr_output_head_v1::EVT_MODEL_SINCE { + new_head.model(conf.model.clone()); + } + + if new_head.version() >= zwlr_output_head_v1::EVT_ADAPTIVE_SYNC_SINCE { + new_head.adaptive_sync(match conf.vrr_enabled { + true => AdaptiveSyncState::Enabled, + false => AdaptiveSyncState::Disabled, + }); + } + // new_head.serial_number(output.serial); + client_data.heads.insert(output, (new_head, new_modes)); +} -- cgit