diff options
| author | tet <gmorer@pm.me> | 2024-05-21 22:43:42 +0000 |
|---|---|---|
| committer | Ivan Molodetskikh <yalterz@gmail.com> | 2024-07-05 07:35:01 +0300 |
| commit | 43df7fad46033692abc6a2b223e6d98f8b7a5aed (patch) | |
| tree | aa7bf3fee968b97650a84b626615f8edb6fa44d4 /src | |
| parent | d2087a2cd9f30e40778861666370df56b532af63 (diff) | |
| download | niri-43df7fad46033692abc6a2b223e6d98f8b7a5aed.tar.gz niri-43df7fad46033692abc6a2b223e6d98f8b7a5aed.tar.bz2 niri-43df7fad46033692abc6a2b223e6d98f8b7a5aed.zip | |
Implement wlr-output-management protocol
fix: wlr_output_management use WeakOutput
Diffstat (limited to 'src')
| -rw-r--r-- | src/handlers/mod.rs | 18 | ||||
| -rw-r--r-- | src/niri.rs | 16 | ||||
| -rw-r--r-- | src/protocols/mod.rs | 1 | ||||
| -rw-r--r-- | src/protocols/output_management.rs | 891 |
4 files changed, 924 insertions, 2 deletions
diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index 845faf08..f288e24f 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -68,9 +68,13 @@ use crate::protocols::foreign_toplevel::{ self, ForeignToplevelHandler, ForeignToplevelManagerState, }; use crate::protocols::gamma_control::{GammaControlHandler, GammaControlManagerState}; +use crate::protocols::output_management::{OutputManagementHandler, OutputManagementManagerState}; use crate::protocols::screencopy::{Screencopy, ScreencopyHandler}; use crate::utils::{output_size, send_scale_transform}; -use crate::{delegate_foreign_toplevel, delegate_gamma_control, delegate_screencopy}; +use crate::{ + delegate_foreign_toplevel, delegate_gamma_control, delegate_output_management, + delegate_screencopy, +}; impl SeatHandler for State { type KeyboardFocus = WlSurface; @@ -545,3 +549,15 @@ delegate_xdg_activation!(State); impl FractionalScaleHandler for State {} delegate_fractional_scale!(State); + +impl OutputManagementHandler for State { + fn output_management_state(&mut self) -> &mut OutputManagementManagerState { + &mut self.niri.output_management_state + } + + fn apply_output_config(&mut self, config: Vec<niri_config::Output>) { + self.niri.config.borrow_mut().outputs = config; + self.reload_output_config(); + } +} +delegate_output_management!(State); diff --git a/src/niri.rs b/src/niri.rs index b017d90e..24a328a0 100644 --- a/src/niri.rs +++ b/src/niri.rs @@ -114,6 +114,7 @@ use crate::ipc::server::IpcServer; use crate::layout::{Layout, LayoutElement as _, MonitorRenderElement}; use crate::protocols::foreign_toplevel::{self, ForeignToplevelManagerState}; use crate::protocols::gamma_control::GammaControlManagerState; +use crate::protocols::output_management::OutputManagementManagerState; use crate::protocols::screencopy::{Screencopy, ScreencopyManagerState}; use crate::pw_utils::{Cast, PipeWire}; #[cfg(feature = "xdp-gnome-screencast")] @@ -202,6 +203,7 @@ pub struct Niri { pub session_lock_state: SessionLockManagerState, pub foreign_toplevel_state: ForeignToplevelManagerState, pub screencopy_state: ScreencopyManagerState, + pub output_management_state: OutputManagementManagerState, pub viewporter_state: ViewporterState, pub xdg_foreign_state: XdgForeignState, pub shm_state: ShmState, @@ -1051,7 +1053,7 @@ impl State { self.niri.queue_redraw_all(); } - fn reload_output_config(&mut self) { + pub fn reload_output_config(&mut self) { let mut resized_outputs = vec![]; for output in self.niri.global_space.outputs() { let name = output.name(); @@ -1103,6 +1105,9 @@ impl State { if let Some(touch) = self.niri.seat.get_touch() { touch.cancel(self); } + + let config = self.niri.config.borrow().outputs.clone(); + self.niri.output_management_state.on_config_changed(config); } pub fn apply_transient_output_config(&mut self, name: &str, action: niri_ipc::OutputAction) { @@ -1178,6 +1183,9 @@ impl State { #[cfg(feature = "dbus")] self.niri.on_ipc_outputs_changed(); + + let new_config = self.backend.ipc_outputs().lock().unwrap().clone(); + self.niri.output_management_state.notify_changes(new_config); } #[cfg(feature = "xdp-gnome-screencast")] @@ -1482,6 +1490,11 @@ impl Niri { ForeignToplevelManagerState::new::<State, _>(&display_handle, |client| { !client.get_data::<ClientState>().unwrap().restricted }); + let mut output_management_state = + OutputManagementManagerState::new::<State, _>(&display_handle, |client| { + !client.get_data::<ClientState>().unwrap().restricted + }); + output_management_state.on_config_changed(config_.outputs.clone()); let screencopy_state = ScreencopyManagerState::new::<State, _>(&display_handle, |client| { !client.get_data::<ClientState>().unwrap().restricted }); @@ -1626,6 +1639,7 @@ impl Niri { layer_shell_state, session_lock_state, foreign_toplevel_state, + output_management_state, screencopy_state, viewporter_state, xdg_foreign_state, 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<OutputId, (ZwlrOutputHeadV1, Vec<ZwlrOutputModeV1>)>, + confs: HashMap<ZwlrOutputConfigurationV1, OutputConfigurationState>, + manager: ZwlrOutputManagerV1, +} + +pub struct OutputManagementManagerState { + display: DisplayHandle, + serial: u32, + clients: HashMap<ClientId, ClientData>, + current_state: HashMap<OutputId, niri_ipc::Output>, + current_config: Vec<niri_config::Output>, +} + +pub struct OutputManagementManagerGlobalData { + filter: Box<dyn for<'c> 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<niri_config::Output>); +} + +#[derive(Debug)] +enum OutputConfigurationState { + Ongoing(HashMap<OutputId, niri_config::Output>), + Finished, +} + +pub enum OutputConfigurationHeadState { + Cancelled, + Ok(OutputId, ZwlrOutputConfigurationV1), +} + +impl OutputManagementManagerState { + pub fn new<D, F>(display: &DisplayHandle, filter: F) -> Self + where + D: GlobalDispatch<ZwlrOutputManagerV1, OutputManagementManagerGlobalData>, + D: Dispatch<ZwlrOutputManagerV1, ()>, + D: Dispatch<ZwlrOutputHeadV1, OutputId>, + D: Dispatch<ZwlrOutputConfigurationV1, u32>, + D: Dispatch<ZwlrOutputConfigurationHeadV1, OutputConfigurationHeadState>, + D: Dispatch<ZwlrOutputModeV1, ()>, + 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::<D, ZwlrOutputManagerV1, _>(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<niri_config::Output>) { + self.current_config = new_config; + } + + pub fn notify_changes(&mut self, new_state: HashMap<OutputId, niri_ipc::Output>) { + 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<D> GlobalDispatch<ZwlrOutputManagerV1, OutputManagementManagerGlobalData, D> + for OutputManagementManagerState +where + D: GlobalDispatch<ZwlrOutputManagerV1, OutputManagementManagerGlobalData>, + D: Dispatch<ZwlrOutputManagerV1, ()>, + D: Dispatch<ZwlrOutputHeadV1, OutputId>, + D: Dispatch<ZwlrOutputConfigurationV1, u32>, + D: Dispatch<ZwlrOutputConfigurationHeadV1, OutputConfigurationHeadState>, + D: Dispatch<ZwlrOutputModeV1, ()>, + D: OutputManagementHandler, + D: 'static, +{ + fn bind( + state: &mut D, + display: &DisplayHandle, + client: &Client, + manager: New<ZwlrOutputManagerV1>, + _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::<D>(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<D> Dispatch<ZwlrOutputManagerV1, (), D> for OutputManagementManagerState +where + D: GlobalDispatch<ZwlrOutputManagerV1, OutputManagementManagerGlobalData>, + D: Dispatch<ZwlrOutputManagerV1, ()>, + D: Dispatch<ZwlrOutputHeadV1, OutputId>, + D: Dispatch<ZwlrOutputConfigurationV1, u32>, + D: Dispatch<ZwlrOutputConfigurationHeadV1, OutputConfigurationHeadState>, + D: Dispatch<ZwlrOutputModeV1, ()>, + 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<D> Dispatch<ZwlrOutputConfigurationV1, u32, D> for OutputManagementManagerState +where + D: GlobalDispatch<ZwlrOutputManagerV1, OutputManagementManagerGlobalData>, + D: Dispatch<ZwlrOutputManagerV1, ()>, + D: Dispatch<ZwlrOutputHeadV1, OutputId>, + D: Dispatch<ZwlrOutputConfigurationV1, u32>, + D: Dispatch<ZwlrOutputConfigurationHeadV1, OutputConfigurationHeadState>, + D: Dispatch<ZwlrOutputModeV1, ()>, + 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::<OutputId>() 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::<OutputId>() 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<D> Dispatch<ZwlrOutputConfigurationHeadV1, OutputConfigurationHeadState, D> + for OutputManagementManagerState +where + D: GlobalDispatch<ZwlrOutputManagerV1, OutputManagementManagerGlobalData>, + D: Dispatch<ZwlrOutputManagerV1, ()>, + D: Dispatch<ZwlrOutputHeadV1, OutputId>, + D: Dispatch<ZwlrOutputConfigurationV1, u32>, + D: Dispatch<ZwlrOutputConfigurationHeadV1, OutputConfigurationHeadState>, + D: Dispatch<ZwlrOutputModeV1, ()>, + 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::<u32>() 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<D> Dispatch<ZwlrOutputHeadV1, OutputId, D> for OutputManagementManagerState +where + D: GlobalDispatch<ZwlrOutputManagerV1, OutputManagementManagerGlobalData>, + D: Dispatch<ZwlrOutputManagerV1, ()>, + D: Dispatch<ZwlrOutputHeadV1, OutputId>, + D: Dispatch<ZwlrOutputConfigurationV1, u32>, + D: Dispatch<ZwlrOutputConfigurationHeadV1, OutputConfigurationHeadState>, + D: Dispatch<ZwlrOutputModeV1, ()>, + 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<D> Dispatch<ZwlrOutputModeV1, (), D> for OutputManagementManagerState +where + D: GlobalDispatch<ZwlrOutputManagerV1, OutputManagementManagerGlobalData>, + D: Dispatch<ZwlrOutputManagerV1, ()>, + D: Dispatch<ZwlrOutputHeadV1, OutputId>, + D: Dispatch<ZwlrOutputConfigurationV1, u32>, + D: Dispatch<ZwlrOutputConfigurationHeadV1, OutputConfigurationHeadState>, + D: Dispatch<ZwlrOutputModeV1, ()>, + D: OutputManagementHandler, + D: 'static, +{ + fn request( + _state: &mut D, + _client: &Client, + _mode: &ZwlrOutputModeV1, + request: zwlr_output_m |
