aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authortet <gmorer@pm.me>2024-05-21 22:43:42 +0000
committerIvan Molodetskikh <yalterz@gmail.com>2024-07-05 07:35:01 +0300
commit43df7fad46033692abc6a2b223e6d98f8b7a5aed (patch)
treeaa7bf3fee968b97650a84b626615f8edb6fa44d4
parentd2087a2cd9f30e40778861666370df56b532af63 (diff)
downloadniri-43df7fad46033692abc6a2b223e6d98f8b7a5aed.tar.gz
niri-43df7fad46033692abc6a2b223e6d98f8b7a5aed.tar.bz2
niri-43df7fad46033692abc6a2b223e6d98f8b7a5aed.zip
Implement wlr-output-management protocol
fix: wlr_output_management use WeakOutput
-rw-r--r--niri-ipc/src/lib.rs4
-rw-r--r--src/handlers/mod.rs18
-rw-r--r--src/niri.rs16
-rw-r--r--src/protocols/mod.rs1
-rw-r--r--src/protocols/output_management.rs891
5 files changed, 926 insertions, 4 deletions
diff --git a/niri-ipc/src/lib.rs b/niri-ipc/src/lib.rs
index 4db883e5..572b4a83 100644
--- a/niri-ipc/src/lib.rs
+++ b/niri-ipc/src/lib.rs
@@ -433,7 +433,7 @@ pub struct Output {
}
/// Output mode.
-#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
+#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq)]
pub struct Mode {
/// Width in physical pixels.
pub width: u16,
@@ -446,7 +446,7 @@ pub struct Mode {
}
/// Logical output in the compositor's coordinate space.
-#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
+#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq)]
pub struct LogicalOutput {
/// Logical X position.
pub x: i32,
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(&current_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(&current_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: &a