From d2087a2cd9f30e40778861666370df56b532af63 Mon Sep 17 00:00:00 2001 From: Ivan Molodetskikh Date: Thu, 4 Jul 2024 13:49:33 +0400 Subject: [PATCH 1/6] Add output ID tracking --- src/backend/mod.rs | 18 +++++++++++++++- src/backend/tty.rs | 36 +++++++++++++++++++++++++++---- src/backend/winit.rs | 6 +++--- src/dbus/mutter_display_config.rs | 7 +++--- src/dbus/mutter_screen_cast.rs | 6 +++++- src/ipc/server.rs | 10 +++++---- src/niri.rs | 4 ++-- 7 files changed, 69 insertions(+), 18 deletions(-) diff --git a/src/backend/mod.rs b/src/backend/mod.rs index ccdda34..c566218 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -9,6 +9,7 @@ use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; use crate::input::CompositorMod; use crate::niri::Niri; +use crate::utils::id::IdCounter; pub mod tty; pub use tty::Tty; @@ -31,7 +32,22 @@ pub enum RenderResult { Skipped, } -pub type IpcOutputMap = HashMap; +pub type IpcOutputMap = HashMap; + +static OUTPUT_ID_COUNTER: IdCounter = IdCounter::new(); + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct OutputId(u32); + +impl OutputId { + fn next() -> OutputId { + OutputId(OUTPUT_ID_COUNTER.next()) + } + + pub fn get(self) -> u32 { + self.0 + } +} impl Backend { pub fn init(&mut self, niri: &mut Niri) { diff --git a/src/backend/tty.rs b/src/backend/tty.rs index 54fc97c..cb04c71 100644 --- a/src/backend/tty.rs +++ b/src/backend/tty.rs @@ -57,6 +57,7 @@ use wayland_protocols::wp::linux_dmabuf::zv1::server::zwp_linux_dmabuf_feedback_ use wayland_protocols::wp::presentation_time::server::wp_presentation_feedback; use super::{IpcOutputMap, RenderResult}; +use crate::backend::OutputId; use crate::frame_clock::FrameClock; use crate::niri::{Niri, RedrawState, State}; use crate::render_helpers::debug::draw_damage; @@ -118,6 +119,7 @@ pub struct OutputDevice { render_node: DrmNode, drm_scanner: DrmScanner, surfaces: HashMap, + output_ids: HashMap, // SAFETY: drop after all the objects used with them are dropped. // See https://github.com/Smithay/smithay/issues/1102. drm: DrmDevice, @@ -575,6 +577,7 @@ impl Tty { gbm, drm_scanner: DrmScanner::new(), surfaces: HashMap::new(), + output_ids: HashMap::new(), drm_lease_state, active_leases: Vec::new(), non_desktop_connectors: HashSet::new(), @@ -599,6 +602,7 @@ impl Tty { return; }; + let mut removed = Vec::new(); for event in device.drm_scanner.scan_connectors(&device.drm) { match event { DrmScanEvent::Connected { @@ -611,11 +615,27 @@ impl Tty { } DrmScanEvent::Disconnected { crtc: Some(crtc), .. - } => self.connector_disconnected(niri, node, crtc), + } => { + self.connector_disconnected(niri, node, crtc); + removed.push(crtc); + } _ => (), } } + // FIXME: this is better done in connector_disconnected(), but currently we call that to + // turn off outputs temporarily, too. So we can't do this there. + let Some(device) = self.devices.get_mut(&node) else { + error!("device disappeared"); + return; + }; + + for crtc in removed { + if device.output_ids.remove(&crtc).is_none() { + error!("output ID missing for disconnected crtc: {crtc:?}"); + } + } + self.refresh_ipc_outputs(niri); } @@ -724,6 +744,10 @@ impl Tty { return Ok(()); } + // This should be unique per CRTC connection, however currently we can call + // connector_connected() multiple times for turning the output off and on. + device.output_ids.entry(crtc).or_insert_with(OutputId::next); + let config = self .config .borrow() @@ -1464,7 +1488,7 @@ impl Tty { for (node, device) in &self.devices { for (connector, crtc) in device.drm_scanner.crtcs() { - let connector_name = format!( + let name = format!( "{}-{}", connector.interface().as_str(), connector.interface_id(), @@ -1527,7 +1551,7 @@ impl Tty { .map(logical_output); let ipc_output = niri_ipc::Output { - name: connector_name.clone(), + name, make, model, physical_size, @@ -1538,7 +1562,11 @@ impl Tty { logical, }; - ipc_outputs.insert(connector_name, ipc_output); + let id = device.output_ids.get(&crtc).copied().unwrap_or_else(|| { + error!("output ID missing for crtc: {crtc:?}"); + OutputId::next() + }); + ipc_outputs.insert(id, ipc_output); } } diff --git a/src/backend/winit.rs b/src/backend/winit.rs index 677cb10..61744e5 100644 --- a/src/backend/winit.rs +++ b/src/backend/winit.rs @@ -17,7 +17,7 @@ use smithay::reexports::wayland_protocols::wp::presentation_time::server::wp_pre use smithay::reexports::winit::dpi::LogicalSize; use smithay::reexports::winit::window::Window; -use super::{IpcOutputMap, RenderResult}; +use super::{IpcOutputMap, OutputId, RenderResult}; use crate::niri::{Niri, RedrawState, State}; use crate::render_helpers::debug::draw_damage; use crate::render_helpers::{resources, shaders, RenderTarget}; @@ -61,7 +61,7 @@ impl Winit { let physical_properties = output.physical_properties(); let ipc_outputs = Arc::new(Mutex::new(HashMap::from([( - "winit".to_owned(), + OutputId::next(), niri_ipc::Output { name: output.name(), make: physical_properties.make, @@ -98,7 +98,7 @@ impl Winit { { let mut ipc_outputs = winit.ipc_outputs.lock().unwrap(); - let output = ipc_outputs.get_mut("winit").unwrap(); + let output = ipc_outputs.values_mut().next().unwrap(); let mode = &mut output.modes[0]; mode.width = size.w.clamp(0, u16::MAX as i32) as u16; mode.height = size.h.clamp(0, u16::MAX as i32) as u16; diff --git a/src/dbus/mutter_display_config.rs b/src/dbus/mutter_display_config.rs index 783c249..146174f 100644 --- a/src/dbus/mutter_display_config.rs +++ b/src/dbus/mutter_display_config.rs @@ -57,11 +57,12 @@ impl DisplayConfig { .ipc_outputs .lock() .unwrap() - .iter() + .values() // Take only enabled outputs. - .filter(|(_, output)| output.current_mode.is_some() && output.logical.is_some()) - .map(|(c, output)| { + .filter(|output| output.current_mode.is_some() && output.logical.is_some()) + .map(|output| { // Loosely matches the check in Mutter. + let c = &output.name; let is_laptop_panel = matches!(c.get(..4), Some("eDP-" | "LVDS" | "DSI-")); // FIXME: use proper serial when we have libdisplay-info. diff --git a/src/dbus/mutter_screen_cast.rs b/src/dbus/mutter_screen_cast.rs index 4b3c2fd..0aba084 100644 --- a/src/dbus/mutter_screen_cast.rs +++ b/src/dbus/mutter_screen_cast.rs @@ -191,7 +191,11 @@ impl Session { ) -> fdo::Result { debug!(connector, ?properties, "record_monitor"); - let Some(output) = self.ipc_outputs.lock().unwrap().get(connector).cloned() else { + let output = { + let ipc_outputs = self.ipc_outputs.lock().unwrap(); + ipc_outputs.values().find(|o| o.name == connector).cloned() + }; + let Some(output) = output else { return Err(fdo::Error::Failed("no such monitor".to_owned())); }; diff --git a/src/ipc/server.rs b/src/ipc/server.rs index 7987ca5..d05131a 100644 --- a/src/ipc/server.rs +++ b/src/ipc/server.rs @@ -143,7 +143,8 @@ async fn process(ctx: &ClientCtx, request: Request) -> Reply { Request::Version => Response::Version(version()), Request::Outputs => { let ipc_outputs = ctx.ipc_outputs.lock().unwrap().clone(); - Response::Outputs(ipc_outputs) + let outputs = ipc_outputs.values().cloned().map(|o| (o.name.clone(), o)); + Response::Outputs(outputs.collect()) } Request::FocusedWindow => { let window = ctx.ipc_focused_window.lock().unwrap().clone(); @@ -183,8 +184,8 @@ async fn process(ctx: &ClientCtx, request: Request) -> Reply { Request::Output { output, action } => { let ipc_outputs = ctx.ipc_outputs.lock().unwrap(); let found = ipc_outputs - .keys() - .any(|name| name.eq_ignore_ascii_case(&output)); + .values() + .any(|o| o.name.eq_ignore_ascii_case(&output)); let response = if found { OutputConfigChanged::Applied } else { @@ -223,7 +224,8 @@ async fn process(ctx: &ClientCtx, request: Request) -> Reply { .ipc_outputs() .lock() .unwrap() - .get(&active_output) + .values() + .find(|o| o.name == active_output) .cloned() }); diff --git a/src/niri.rs b/src/niri.rs index 856f4fc..b017d90 100644 --- a/src/niri.rs +++ b/src/niri.rs @@ -1166,12 +1166,12 @@ impl State { let _span = tracy_client::span!("State::refresh_ipc_outputs"); - for (name, ipc_output) in self.backend.ipc_outputs().lock().unwrap().iter_mut() { + for ipc_output in self.backend.ipc_outputs().lock().unwrap().values_mut() { let logical = self .niri .global_space .outputs() - .find(|output| output.name() == *name) + .find(|output| output.name() == ipc_output.name) .map(logical_output); ipc_output.logical = logical; } From 43df7fad46033692abc6a2b223e6d98f8b7a5aed Mon Sep 17 00:00:00 2001 From: tet Date: Tue, 21 May 2024 22:43:42 +0000 Subject: [PATCH 2/6] Implement wlr-output-management protocol fix: wlr_output_management use WeakOutput --- niri-ipc/src/lib.rs | 4 +- src/handlers/mod.rs | 18 +- src/niri.rs | 16 +- src/protocols/mod.rs | 1 + src/protocols/output_management.rs | 891 +++++++++++++++++++++++++++++ 5 files changed, 926 insertions(+), 4 deletions(-) create mode 100644 src/protocols/output_management.rs diff --git a/niri-ipc/src/lib.rs b/niri-ipc/src/lib.rs index 4db883e..572b4a8 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 845faf0..f288e24 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) { + 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 b017d90..24a328a 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::(&display_handle, |client| { !client.get_data::().unwrap().restricted }); + let mut output_management_state = + OutputManagementManagerState::new::(&display_handle, |client| { + !client.get_data::().unwrap().restricted + }); + output_management_state.on_config_changed(config_.outputs.clone()); let screencopy_state = ScreencopyManagerState::new::(&display_handle, |client| { !client.get_data::().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 d1779b7..a58b48b 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 0000000..2842fce --- /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)); +} From 9dcc9160b3b4be6c44672e8579e1e7107453c8b7 Mon Sep 17 00:00:00 2001 From: Ivan Molodetskikh Date: Thu, 4 Jul 2024 17:51:11 +0400 Subject: [PATCH 3/6] Put Outputs config into a dedicated struct --- niri-config/src/lib.rs | 27 ++++++++++++++++++++++++--- src/backend/tty.rs | 9 +++------ src/handlers/mod.rs | 2 +- src/niri.rs | 28 +++++++--------------------- src/protocols/output_management.rs | 14 ++++++-------- 5 files changed, 41 insertions(+), 39 deletions(-) diff --git a/niri-config/src/lib.rs b/niri-config/src/lib.rs index f1bd0af..1d2bfc6 100644 --- a/niri-config/src/lib.rs +++ b/niri-config/src/lib.rs @@ -23,7 +23,7 @@ pub struct Config { #[knuffel(child, default)] pub input: Input, #[knuffel(children(name = "output"))] - pub outputs: Vec, + pub outputs: Outputs, #[knuffel(children(name = "spawn-at-startup"))] pub spawn_at_startup: Vec, #[knuffel(child, default)] @@ -289,6 +289,9 @@ pub struct Touch { pub map_to_output: Option, } +#[derive(Debug, Default, Clone, PartialEq)] +pub struct Outputs(pub Vec); + #[derive(knuffel::Decode, Debug, Clone, PartialEq)] pub struct Output { #[knuffel(child)] @@ -1514,6 +1517,24 @@ fn expect_only_children( } } +impl FromIterator for Outputs { + fn from_iter>(iter: T) -> Self { + Self(Vec::from_iter(iter)) + } +} + +impl Outputs { + pub fn find(&self, name: &str) -> Option<&Output> { + self.0.iter().find(|o| o.name.eq_ignore_ascii_case(name)) + } + + pub fn find_mut(&mut self, name: &str) -> Option<&mut Output> { + self.0 + .iter_mut() + .find(|o| o.name.eq_ignore_ascii_case(name)) + } +} + impl knuffel::Decode for DefaultColumnWidth where S: knuffel::traits::ErrorSpan, @@ -2635,7 +2656,7 @@ mod tests { focus_follows_mouse: true, workspace_auto_back_and_forth: true, }, - outputs: vec![Output { + outputs: Outputs(vec![Output { off: false, name: "eDP-1".to_owned(), scale: Some(FloatOrInt(2.)), @@ -2647,7 +2668,7 @@ mod tests { refresh: Some(144.), }), variable_refresh_rate: true, - }], + }]), layout: Layout { focus_ring: FocusRing { off: false, diff --git a/src/backend/tty.rs b/src/backend/tty.rs index cb04c71..0654f4f 100644 --- a/src/backend/tty.rs +++ b/src/backend/tty.rs @@ -752,8 +752,7 @@ impl Tty { .config .borrow() .outputs - .iter() - .find(|o| o.name.eq_ignore_ascii_case(&output_name)) + .find(&output_name) .cloned() .unwrap_or_default(); @@ -1633,8 +1632,7 @@ impl Tty { .config .borrow() .outputs - .iter() - .find(|o| o.name.eq_ignore_ascii_case(&surface.name)) + .find(&surface.name) .cloned() .unwrap_or_default(); if config.off { @@ -1763,8 +1761,7 @@ impl Tty { .config .borrow() .outputs - .iter() - .find(|o| o.name.eq_ignore_ascii_case(&output_name)) + .find(&output_name) .cloned() .unwrap_or_default(); diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index f288e24..5c502a1 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -555,7 +555,7 @@ impl OutputManagementHandler for State { &mut self.niri.output_management_state } - fn apply_output_config(&mut self, config: Vec) { + fn apply_output_config(&mut self, config: niri_config::Outputs) { self.niri.config.borrow_mut().outputs = config; self.reload_output_config(); } diff --git a/src/niri.rs b/src/niri.rs index 24a328a..54211bc 100644 --- a/src/niri.rs +++ b/src/niri.rs @@ -156,7 +156,7 @@ pub struct Niri { /// This does not include transient output config changes done via IPC. It is only used when /// reloading the config from disk to determine if the output configuration should be reloaded /// (and transient changes dropped). - pub config_file_output_config: Vec, + pub config_file_output_config: niri_config::Outputs, pub event_loop: LoopHandle<'static, State>, pub scheduler: Scheduler<()>, @@ -1058,10 +1058,7 @@ impl State { for output in self.niri.global_space.outputs() { let name = output.name(); let config = self.niri.config.borrow_mut(); - let config = config - .outputs - .iter() - .find(|o| o.name.eq_ignore_ascii_case(&name)); + let config = config.outputs.find(&name); let scale = config .and_then(|c| c.scale) @@ -1113,18 +1110,14 @@ impl State { pub fn apply_transient_output_config(&mut self, name: &str, action: niri_ipc::OutputAction) { { let mut config = self.niri.config.borrow_mut(); - let config = if let Some(config) = config - .outputs - .iter_mut() - .find(|o| o.name.eq_ignore_ascii_case(name)) - { + let config = if let Some(config) = config.outputs.find_mut(name) { config } else { - config.outputs.push(niri_config::Output { + config.outputs.0.push(niri_config::Output { name: String::from(name), ..Default::default() }); - config.outputs.last_mut().unwrap() + config.outputs.0.last_mut().unwrap() }; match action { @@ -1753,11 +1746,7 @@ impl Niri { for output in self.global_space.outputs().chain(new_output) { let name = output.name(); let position = self.global_space.output_geometry(output).map(|geo| geo.loc); - let config = config - .outputs - .iter() - .find(|o| o.name.eq_ignore_ascii_case(&name)) - .and_then(|c| c.position); + let config = config.outputs.find(&name).and_then(|c| c.position); outputs.push(Data { output: output.clone(), @@ -1862,10 +1851,7 @@ impl Niri { let name = output.name(); let config = self.config.borrow(); - let c = config - .outputs - .iter() - .find(|o| o.name.eq_ignore_ascii_case(&name)); + let c = config.outputs.find(&name); let scale = c.and_then(|c| c.scale).map(|s| s.0).unwrap_or_else(|| { let size_mm = output.physical_properties().size; let resolution = output.current_mode().unwrap().size; diff --git a/src/protocols/output_management.rs b/src/protocols/output_management.rs index 2842fce..23e419b 100644 --- a/src/protocols/output_management.rs +++ b/src/protocols/output_management.rs @@ -38,7 +38,7 @@ pub struct OutputManagementManagerState { serial: u32, clients: HashMap, current_state: HashMap, - current_config: Vec, + current_config: niri_config::Outputs, } pub struct OutputManagementManagerGlobalData { @@ -47,7 +47,7 @@ pub struct OutputManagementManagerGlobalData { pub trait OutputManagementHandler { fn output_management_state(&mut self) -> &mut OutputManagementManagerState; - fn apply_output_config(&mut self, config: Vec); + fn apply_output_config(&mut self, config: niri_config::Outputs); } #[derive(Debug)] @@ -84,11 +84,11 @@ impl OutputManagementManagerState { clients: HashMap::new(), serial: 0, current_state: HashMap::new(), - current_config: Vec::new(), + current_config: Default::default(), } } - pub fn on_config_changed(&mut self, new_config: Vec) { + pub fn on_config_changed(&mut self, new_config: niri_config::Outputs) { self.current_config = new_config; } @@ -405,8 +405,7 @@ where Entry::Vacant(entry) => { let mut config = g_state .current_config - .iter() - .find(|o| o.name.eq_ignore_ascii_case(¤t_config.name)) + .find(¤t_config.name) .cloned() .unwrap_or_else(|| niri_config::Output { name: current_config.name.clone(), @@ -455,8 +454,7 @@ where Entry::Vacant(entry) => { let mut config = g_state .current_config - .iter() - .find(|o| o.name.eq_ignore_ascii_case(¤t_config.name)) + .find(¤t_config.name) .cloned() .unwrap_or_else(|| niri_config::Output { name: current_config.name.clone(), From a56e4ff436cc4f36d7cda89e985d51e37f0b4f78 Mon Sep 17 00:00:00 2001 From: TheAngusMcFire <43189215+TheAngusMcFire@users.noreply.github.com> Date: Fri, 5 Jul 2024 06:55:04 +0200 Subject: [PATCH 4/6] Added Commnads to focus windows or Monitors above/below the active window (#497) * Implement focus-window-up/down-or-monitor calls * Fixed wrong naming of focus-window-or-monitor commands * fix copy pase errors for focusing direction * Fixed wrong behaviour when the current workspace is empty * Cleanup navigation code to reduce complexity * Fix wrong comments and add testcases for FocusWindowOrMonitorUp/Down --------- Co-authored-by: Christian Rieger --- niri-config/src/lib.rs | 4 +++ niri-ipc/src/lib.rs | 4 +++ src/input/mod.rs | 34 ++++++++++++++++++++++++ src/layout/mod.rs | 59 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 101 insertions(+) diff --git a/niri-config/src/lib.rs b/niri-config/src/lib.rs index 1d2bfc6..39d7852 100644 --- a/niri-config/src/lib.rs +++ b/niri-config/src/lib.rs @@ -949,6 +949,8 @@ pub enum Action { FocusColumnLast, FocusColumnRightOrFirst, FocusColumnLeftOrLast, + FocusWindowOrMonitorUp, + FocusWindowOrMonitorDown, FocusColumnOrMonitorLeft, FocusColumnOrMonitorRight, FocusWindowDown, @@ -1027,6 +1029,8 @@ impl From for Action { niri_ipc::Action::FocusColumnLast => Self::FocusColumnLast, niri_ipc::Action::FocusColumnRightOrFirst => Self::FocusColumnRightOrFirst, niri_ipc::Action::FocusColumnLeftOrLast => Self::FocusColumnLeftOrLast, + niri_ipc::Action::FocusWindowOrMonitorUp => Self::FocusWindowOrMonitorUp, + niri_ipc::Action::FocusWindowOrMonitorDown => Self::FocusWindowOrMonitorDown, niri_ipc::Action::FocusColumnOrMonitorLeft => Self::FocusColumnOrMonitorLeft, niri_ipc::Action::FocusColumnOrMonitorRight => Self::FocusColumnOrMonitorRight, niri_ipc::Action::FocusWindowDown => Self::FocusWindowDown, diff --git a/niri-ipc/src/lib.rs b/niri-ipc/src/lib.rs index 572b4a8..931898b 100644 --- a/niri-ipc/src/lib.rs +++ b/niri-ipc/src/lib.rs @@ -120,6 +120,10 @@ pub enum Action { FocusColumnRightOrFirst, /// Focus the next column to the left, looping if at start. FocusColumnLeftOrLast, + /// Focus the window or the monitor above. + FocusWindowOrMonitorUp, + /// Focus the window or the monitor below. + FocusWindowOrMonitorDown, /// Focus the column or the monitor to the left. FocusColumnOrMonitorLeft, /// Focus the column or the monitor to the right. diff --git a/src/input/mod.rs b/src/input/mod.rs index 5e775b0..2a1073f 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -596,6 +596,40 @@ impl State { // FIXME: granular self.niri.queue_redraw_all(); } + Action::FocusWindowOrMonitorUp => { + if let Some(output) = self.niri.output_up() { + if self.niri.layout.focus_window_up_or_output(&output) + && !self.maybe_warp_cursor_to_focus_centered() + { + self.move_cursor_to_output(&output); + } else { + self.maybe_warp_cursor_to_focus(); + } + } else { + self.niri.layout.focus_up(); + self.maybe_warp_cursor_to_focus(); + } + + // FIXME: granular + self.niri.queue_redraw_all(); + } + Action::FocusWindowOrMonitorDown => { + if let Some(output) = self.niri.output_down() { + if self.niri.layout.focus_window_down_or_output(&output) + && !self.maybe_warp_cursor_to_focus_centered() + { + self.move_cursor_to_output(&output); + } else { + self.maybe_warp_cursor_to_focus(); + } + } else { + self.niri.layout.focus_down(); + self.maybe_warp_cursor_to_focus(); + } + + // FIXME: granular + self.niri.queue_redraw_all(); + } Action::FocusColumnOrMonitorLeft => { if let Some(output) = self.niri.output_left() { if self.niri.layout.focus_column_left_or_output(&output) diff --git a/src/layout/mod.rs b/src/layout/mod.rs index 8fabba6..e93a255 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -1264,6 +1264,43 @@ impl Layout { monitor.focus_column_left_or_last(); } + pub fn focus_window_up_or_output(&mut self, output: &Output) -> bool { + if let Some(monitor) = self.active_monitor() { + let workspace = monitor.active_workspace(); + + if !workspace.columns.is_empty() { + let curr_idx = workspace.columns[workspace.active_column_idx].active_tile_idx; + let new_idx = curr_idx.saturating_sub(1); + if curr_idx != new_idx { + workspace.focus_up(); + return false; + } + } + } + + self.focus_output(output); + true + } + + pub fn focus_window_down_or_output(&mut self, output: &Output) -> bool { + if let Some(monitor) = self.active_monitor() { + let workspace = monitor.active_workspace(); + + if !workspace.columns.is_empty() { + let column = &workspace.columns[workspace.active_column_idx]; + let curr_idx = column.active_tile_idx; + let new_idx = min(column.active_tile_idx + 1, column.tiles.len() - 1); + if curr_idx != new_idx { + workspace.focus_down(); + return false; + } + } + } + + self.focus_output(output); + true + } + pub fn focus_column_left_or_output(&mut self, output: &Output) -> bool { if let Some(monitor) = self.active_monitor() { let workspace = monitor.active_workspace(); @@ -2728,6 +2765,8 @@ mod tests { FocusColumnLast, FocusColumnRightOrFirst, FocusColumnLeftOrLast, + FocusWindowOrMonitorUp(#[proptest(strategy = "1..=2u8")] u8), + FocusWindowOrMonitorDown(#[proptest(strategy = "1..=2u8")] u8), FocusColumnOrMonitorLeft(#[proptest(strategy = "1..=2u8")] u8), FocusColumnOrMonitorRight(#[proptest(strategy = "1..=2u8")] u8), FocusWindowDown, @@ -3055,6 +3094,22 @@ mod tests { Op::FocusColumnLast => layout.focus_column_last(), Op::FocusColumnRightOrFirst => layout.focus_column_right_or_first(), Op::FocusColumnLeftOrLast => layout.focus_column_left_or_last(), + Op::FocusWindowOrMonitorUp(id) => { + let name = format!("output{id}"); + let Some(output) = layout.outputs().find(|o| o.name() == name).cloned() else { + return; + }; + + layout.focus_window_up_or_output(&output); + } + Op::FocusWindowOrMonitorDown(id) => { + let name = format!("output{id}"); + let Some(output) = layout.outputs().find(|o| o.name() == name).cloned() else { + return; + }; + + layout.focus_window_down_or_output(&output); + } Op::FocusColumnOrMonitorLeft(id) => { let name = format!("output{id}"); let Some(output) = layout.outputs().find(|o| o.name() == name).cloned() else { @@ -3299,6 +3354,8 @@ mod tests { Op::FocusColumnRight, Op::FocusColumnRightOrFirst, Op::FocusColumnLeftOrLast, + Op::FocusWindowOrMonitorUp(0), + Op::FocusWindowOrMonitorDown(1), Op::FocusColumnOrMonitorLeft(0), Op::FocusColumnOrMonitorRight(1), Op::FocusWindowUp, @@ -3476,6 +3533,8 @@ mod tests { Op::FocusColumnRight, Op::FocusColumnRightOrFirst, Op::FocusColumnLeftOrLast, + Op::FocusWindowOrMonitorUp(0), + Op::FocusWindowOrMonitorDown(1), Op::FocusColumnOrMonitorLeft(0), Op::FocusColumnOrMonitorRight(1), Op::FocusWindowUp, From d3aebdbec4ae2c1ac4199cdd4e95a8d218362b25 Mon Sep 17 00:00:00 2001 From: Salman Farooq Date: Sun, 30 Jun 2024 22:37:44 +0500 Subject: [PATCH 5/6] Implement key repeat for compositor binds --- niri-config/src/lib.rs | 16 ++++++++++++ src/input/mod.rs | 57 ++++++++++++++++++++++++++++++++++++++++-- src/niri.rs | 2 ++ 3 files changed, 73 insertions(+), 2 deletions(-) diff --git a/niri-config/src/lib.rs b/niri-config/src/lib.rs index 39d7852..aaf81bc 100644 --- a/niri-config/src/lib.rs +++ b/niri-config/src/lib.rs @@ -886,6 +886,7 @@ pub struct Binds(pub Vec); pub struct Bind { pub key: Key, pub action: Action, + pub repeat: bool, pub cooldown: Option, pub allow_when_locked: bool, } @@ -2217,11 +2218,15 @@ where .parse::() .map_err(|e| DecodeError::conversion(&node.node_name, e.wrap_err("invalid keybind")))?; + let mut repeat = true; let mut cooldown = None; let mut allow_when_locked = false; let mut allow_when_locked_node = None; for (name, val) in &node.properties { match &***name { + "repeat" => { + repeat = knuffel::traits::DecodeScalar::decode(val, ctx)?; + } "cooldown-ms" => { cooldown = Some(Duration::from_millis( knuffel::traits::DecodeScalar::decode(val, ctx)?, @@ -2249,6 +2254,7 @@ where let dummy = Self { key, action: Action::Spawn(vec![]), + repeat: true, cooldown: None, allow_when_locked: false, }; @@ -2276,6 +2282,7 @@ where Ok(Self { key, action, + repeat, cooldown, allow_when_locked, }) @@ -2844,6 +2851,7 @@ mod tests { modifiers: Modifiers::COMPOSITOR, }, action: Action::Spawn(vec!["alacritty".to_owned()]), + repeat: true, cooldown: None, allow_when_locked: true, }, @@ -2853,6 +2861,7 @@ mod tests { modifiers: Modifiers::COMPOSITOR, }, action: Action::CloseWindow, + repeat: true, cooldown: None, allow_when_locked: false, }, @@ -2862,6 +2871,7 @@ mod tests { modifiers: Modifiers::COMPOSITOR | Modifiers::SHIFT, }, action: Action::FocusMonitorLeft, + repeat: true, cooldown: None, allow_when_locked: false, }, @@ -2871,6 +2881,7 @@ mod tests { modifiers: Modifiers::COMPOSITOR | Modifiers::SHIFT | Modifiers::CTRL, }, action: Action::MoveWindowToMonitorRight, + repeat: true, cooldown: None, allow_when_locked: false, }, @@ -2880,6 +2891,7 @@ mod tests { modifiers: Modifiers::COMPOSITOR, }, action: Action::ConsumeWindowIntoColumn, + repeat: true, cooldown: None, allow_when_locked: false, }, @@ -2889,6 +2901,7 @@ mod tests { modifiers: Modifiers::COMPOSITOR, }, action: Action::FocusWorkspace(WorkspaceReference::Index(1)), + repeat: true, cooldown: None, allow_when_locked: false, }, @@ -2900,6 +2913,7 @@ mod tests { action: Action::FocusWorkspace(WorkspaceReference::Name( "workspace-1".to_string(), )), + repeat: true, cooldown: None, allow_when_locked: false, }, @@ -2909,6 +2923,7 @@ mod tests { modifiers: Modifiers::COMPOSITOR | Modifiers::SHIFT, }, action: Action::Quit(true), + repeat: true, cooldown: None, allow_when_locked: false, }, @@ -2918,6 +2933,7 @@ mod tests { modifiers: Modifiers::COMPOSITOR, }, action: Action::FocusWorkspaceDown, + repeat: true, cooldown: Some(Duration::from_millis(150)), allow_when_locked: false, }, diff --git a/src/input/mod.rs b/src/input/mod.rs index 2a1073f..7fa363c 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -294,6 +294,19 @@ impl State { let time = Event::time_msec(&event); let pressed = event.state() == KeyState::Pressed; + // Stop bind key repeat on any release. This won't work 100% correctly in cases like: + // 1. Press Mod + // 2. Press Left (repeat starts) + // 3. Press PgDown (new repeat starts) + // 4. Release Left (PgDown repeat stops) + // But it's good enough for now. + // FIXME: handle this properly. + if !pressed { + if let Some(token) = self.niri.bind_repeat_timer.take() { + self.niri.event_loop.remove(token); + } + } + let Some(Some(bind)) = self.niri.seat.get_keyboard().unwrap().input( self, event.key_code(), @@ -330,12 +343,44 @@ impl State { return; }; - // Filter actions when the key is released or the session is locked. if !pressed { return; } - self.handle_bind(bind); + self.handle_bind(bind.clone()); + + // Start the key repeat timer if necessary. + if !bind.repeat { + return; + } + + // Stop the previous key repeat if any. + if let Some(token) = self.niri.bind_repeat_timer.take() { + self.niri.event_loop.remove(token); + } + + let config = self.niri.config.borrow(); + let config = &config.input.keyboard; + + let repeat_rate = config.repeat_rate; + if repeat_rate == 0 { + return; + } + let repeat_duration = Duration::from_secs_f64(1. / f64::from(repeat_rate)); + + let repeat_timer = + Timer::from_duration(Duration::from_millis(u64::from(config.repeat_delay))); + + let token = self + .niri + .event_loop + .insert_source(repeat_timer, move |_, _, state| { + state.handle_bind(bind.clone()); + TimeoutAction::ToDuration(repeat_duration) + }) + .unwrap(); + + self.niri.bind_repeat_timer = Some(token); } pub fn handle_bind(&mut self, bind: Bind) { @@ -2132,6 +2177,7 @@ fn should_intercept_key( modifiers: Modifiers::empty(), }, action, + repeat: true, cooldown: None, allow_when_locked: false, }); @@ -2181,6 +2227,7 @@ fn find_bind( modifiers: Modifiers::empty(), }, action, + repeat: true, cooldown: None, allow_when_locked: false, }); @@ -2512,6 +2559,7 @@ mod tests { modifiers: Modifiers::COMPOSITOR | Modifiers::CTRL, }, action: Action::CloseWindow, + repeat: true, cooldown: None, allow_when_locked: false, }]); @@ -2645,6 +2693,7 @@ mod tests { modifiers: Modifiers::COMPOSITOR, }, action: Action::CloseWindow, + repeat: true, cooldown: None, allow_when_locked: false, }, @@ -2654,6 +2703,7 @@ mod tests { modifiers: Modifiers::SUPER, }, action: Action::FocusColumnLeft, + repeat: true, cooldown: None, allow_when_locked: false, }, @@ -2663,6 +2713,7 @@ mod tests { modifiers: Modifiers::empty(), }, action: Action::FocusWindowDown, + repeat: true, cooldown: None, allow_when_locked: false, }, @@ -2672,6 +2723,7 @@ mod tests { modifiers: Modifiers::COMPOSITOR | Modifiers::SUPER, }, action: Action::FocusWindowUp, + repeat: true, cooldown: None, allow_when_locked: false, }, @@ -2681,6 +2733,7 @@ mod tests { modifiers: Modifiers::SUPER | Modifiers::ALT, }, action: Action::FocusColumnRight, + repeat: true, cooldown: None, allow_when_locked: false, }, diff --git a/src/niri.rs b/src/niri.rs index 54211bc..177f576 100644 --- a/src/niri.rs +++ b/src/niri.rs @@ -234,6 +234,7 @@ pub struct Niri { /// Scancodes of the keys to suppress. pub suppressed_keys: HashSet, pub bind_cooldown_timers: HashMap, + pub bind_repeat_timer: Option, pub keyboard_focus: KeyboardFocus, pub idle_inhibiting_surfaces: HashSet, pub is_fdo_idle_inhibited: Arc, @@ -1657,6 +1658,7 @@ impl Niri { popup_grab: None, suppressed_keys: HashSet::new(), bind_cooldown_timers: HashMap::new(), + bind_repeat_timer: Option::default(), presentation_state, security_context_state, gamma_control_manager_state, From c8411e55d9333174114496a14f0a23abf4a36b7d Mon Sep 17 00:00:00 2001 From: Ivan Molodetskikh Date: Fri, 5 Jul 2024 09:10:48 +0400 Subject: [PATCH 6/6] wiki: Mention bind key repeat --- wiki/Configuration:-Key-Bindings.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/wiki/Configuration:-Key-Bindings.md b/wiki/Configuration:-Key-Bindings.md index b303374..231786b 100644 --- a/wiki/Configuration:-Key-Bindings.md +++ b/wiki/Configuration:-Key-Bindings.md @@ -50,6 +50,15 @@ For this reason, most of the default keys use the `Mod` modifier. > Here, look at `sym: Left` and `sym: Right`: these are the key names. > I was pressing the left and the right arrow in this example. +Binds will do key repeat by default (i.e. holding down a bind will make it trigger repeatedly). +You can disable that for specific binds with `repeat=false`: + +``` +binds { + Mod+T repeat=false { spawn "alacritty"; } +} +``` + Binds can also have a cooldown, which will rate-limit the bind and prevent it from repeatedly triggering too quickly. ```