Merge branch 'YaLTeR:main' into main

This commit is contained in:
Kiko 2024-07-05 07:51:45 +02:00 committed by GitHub
commit d128c7c837
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 1210 additions and 54 deletions

View File

@ -23,7 +23,7 @@ pub struct Config {
#[knuffel(child, default)]
pub input: Input,
#[knuffel(children(name = "output"))]
pub outputs: Vec<Output>,
pub outputs: Outputs,
#[knuffel(children(name = "spawn-at-startup"))]
pub spawn_at_startup: Vec<SpawnAtStartup>,
#[knuffel(child, default)]
@ -289,6 +289,9 @@ pub struct Touch {
pub map_to_output: Option<String>,
}
#[derive(Debug, Default, Clone, PartialEq)]
pub struct Outputs(pub Vec<Output>);
#[derive(knuffel::Decode, Debug, Clone, PartialEq)]
pub struct Output {
#[knuffel(child)]
@ -894,6 +897,7 @@ pub struct Binds(pub Vec<Bind>);
pub struct Bind {
pub key: Key,
pub action: Action,
pub repeat: bool,
pub cooldown: Option<Duration>,
pub allow_when_locked: bool,
}
@ -957,6 +961,8 @@ pub enum Action {
FocusColumnLast,
FocusColumnRightOrFirst,
FocusColumnLeftOrLast,
FocusWindowOrMonitorUp,
FocusWindowOrMonitorDown,
FocusColumnOrMonitorLeft,
FocusColumnOrMonitorRight,
FocusWindowDown,
@ -1035,6 +1041,8 @@ impl From<niri_ipc::Action> 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,
@ -1525,6 +1533,24 @@ fn expect_only_children<S>(
}
}
impl FromIterator<Output> for Outputs {
fn from_iter<T: IntoIterator<Item = Output>>(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<S> knuffel::Decode<S> for DefaultColumnWidth
where
S: knuffel::traits::ErrorSpan,
@ -2203,11 +2229,15 @@ where
.parse::<Key>()
.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)?,
@ -2235,6 +2265,7 @@ where
let dummy = Self {
key,
action: Action::Spawn(vec![]),
repeat: true,
cooldown: None,
allow_when_locked: false,
};
@ -2262,6 +2293,7 @@ where
Ok(Self {
key,
action,
repeat,
cooldown,
allow_when_locked,
})
@ -2646,7 +2678,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.)),
@ -2658,7 +2690,7 @@ mod tests {
refresh: Some(144.),
}),
variable_refresh_rate: true,
}],
}]),
layout: Layout {
focus_ring: FocusRing {
off: false,
@ -2830,6 +2862,7 @@ mod tests {
modifiers: Modifiers::COMPOSITOR,
},
action: Action::Spawn(vec!["alacritty".to_owned()]),
repeat: true,
cooldown: None,
allow_when_locked: true,
},
@ -2839,6 +2872,7 @@ mod tests {
modifiers: Modifiers::COMPOSITOR,
},
action: Action::CloseWindow,
repeat: true,
cooldown: None,
allow_when_locked: false,
},
@ -2848,6 +2882,7 @@ mod tests {
modifiers: Modifiers::COMPOSITOR | Modifiers::SHIFT,
},
action: Action::FocusMonitorLeft,
repeat: true,
cooldown: None,
allow_when_locked: false,
},
@ -2857,6 +2892,7 @@ mod tests {
modifiers: Modifiers::COMPOSITOR | Modifiers::SHIFT | Modifiers::CTRL,
},
action: Action::MoveWindowToMonitorRight,
repeat: true,
cooldown: None,
allow_when_locked: false,
},
@ -2866,6 +2902,7 @@ mod tests {
modifiers: Modifiers::COMPOSITOR,
},
action: Action::ConsumeWindowIntoColumn,
repeat: true,
cooldown: None,
allow_when_locked: false,
},
@ -2875,6 +2912,7 @@ mod tests {
modifiers: Modifiers::COMPOSITOR,
},
action: Action::FocusWorkspace(WorkspaceReference::Index(1)),
repeat: true,
cooldown: None,
allow_when_locked: false,
},
@ -2886,6 +2924,7 @@ mod tests {
action: Action::FocusWorkspace(WorkspaceReference::Name(
"workspace-1".to_string(),
)),
repeat: true,
cooldown: None,
allow_when_locked: false,
},
@ -2895,6 +2934,7 @@ mod tests {
modifiers: Modifiers::COMPOSITOR | Modifiers::SHIFT,
},
action: Action::Quit(true),
repeat: true,
cooldown: None,
allow_when_locked: false,
},
@ -2904,6 +2944,7 @@ mod tests {
modifiers: Modifiers::COMPOSITOR,
},
action: Action::FocusWorkspaceDown,
repeat: true,
cooldown: Some(Duration::from_millis(150)),
allow_when_locked: false,
},

View File

@ -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.
@ -433,7 +437,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 +450,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,

View File

@ -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<String, niri_ipc::Output>;
pub type IpcOutputMap = HashMap<OutputId, niri_ipc::Output>;
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) {

View File

@ -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<crtc::Handle, Surface>,
output_ids: HashMap<crtc::Handle, OutputId>,
// 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,12 +744,15 @@ 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()
.outputs
.iter()
.find(|o| o.name.eq_ignore_ascii_case(&output_name))
.find(&output_name)
.cloned()
.unwrap_or_default();
@ -1464,7 +1487,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 +1550,7 @@ impl Tty {
.map(logical_output);
let ipc_output = niri_ipc::Output {
name: connector_name.clone(),
name,
make,
model,
physical_size,
@ -1538,7 +1561,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);
}
}
@ -1605,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 {
@ -1735,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();

View File

@ -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;

View File

@ -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.

View File

@ -191,7 +191,11 @@ impl Session {
) -> fdo::Result<OwnedObjectPath> {
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()));
};

View File

@ -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: niri_config::Outputs) {
self.niri.config.borrow_mut().outputs = config;
self.reload_output_config();
}
}
delegate_output_management!(State);

View File

@ -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) {
@ -596,6 +641,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)
@ -2098,6 +2177,7 @@ fn should_intercept_key(
modifiers: Modifiers::empty(),
},
action,
repeat: true,
cooldown: None,
allow_when_locked: false,
});
@ -2147,6 +2227,7 @@ fn find_bind(
modifiers: Modifiers::empty(),
},
action,
repeat: true,
cooldown: None,
allow_when_locked: false,
});
@ -2478,6 +2559,7 @@ mod tests {
modifiers: Modifiers::COMPOSITOR | Modifiers::CTRL,
},
action: Action::CloseWindow,
repeat: true,
cooldown: None,
allow_when_locked: false,
}]);
@ -2611,6 +2693,7 @@ mod tests {
modifiers: Modifiers::COMPOSITOR,
},
action: Action::CloseWindow,
repeat: true,
cooldown: None,
allow_when_locked: false,
},
@ -2620,6 +2703,7 @@ mod tests {
modifiers: Modifiers::SUPER,
},
action: Action::FocusColumnLeft,
repeat: true,
cooldown: None,
allow_when_locked: false,
},
@ -2629,6 +2713,7 @@ mod tests {
modifiers: Modifiers::empty(),
},
action: Action::FocusWindowDown,
repeat: true,
cooldown: None,
allow_when_locked: false,
},
@ -2638,6 +2723,7 @@ mod tests {
modifiers: Modifiers::COMPOSITOR | Modifiers::SUPER,
},
action: Action::FocusWindowUp,
repeat: true,
cooldown: None,
allow_when_locked: false,
},
@ -2647,6 +2733,7 @@ mod tests {
modifiers: Modifiers::SUPER | Modifiers::ALT,
},
action: Action::FocusColumnRight,
repeat: true,
cooldown: None,
allow_when_locked: false,
},

View File

@ -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()
});

View File

@ -1264,6 +1264,43 @@ impl<W: LayoutElement> Layout<W> {
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,

View File

@ -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")]
@ -155,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<niri_config::Output>,
pub config_file_output_config: niri_config::Outputs,
pub event_loop: LoopHandle<'static, State>,
pub scheduler: Scheduler<()>,
@ -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,
@ -232,6 +234,7 @@ pub struct Niri {
/// Scancodes of the keys to suppress.
pub suppressed_keys: HashSet<u32>,
pub bind_cooldown_timers: HashMap<Key, RegistrationToken>,
pub bind_repeat_timer: Option<RegistrationToken>,
pub keyboard_focus: KeyboardFocus,
pub idle_inhibiting_surfaces: HashSet<WlSurface>,
pub is_fdo_idle_inhibited: Arc<AtomicBool>,
@ -1051,15 +1054,12 @@ 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();
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)
@ -1103,23 +1103,22 @@ 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) {
{
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 {
@ -1166,18 +1165,21 @@ 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;
}
#[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 +1484,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 +1633,7 @@ impl Niri {
layer_shell_state,
session_lock_state,
foreign_toplevel_state,
output_management_state,
screencopy_state,
viewporter_state,
xdg_foreign_state,
@ -1650,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,
@ -1739,11 +1748,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(),
@ -1848,10 +1853,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;

View File

@ -1,3 +1,4 @@
pub mod foreign_toplevel;
pub mod gamma_control;
pub mod output_management;
pub mod screencopy;

View File

@ -0,0 +1,889 @@
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: niri_config::Outputs,
}
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: niri_config::Outputs);
}
#[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: Default::default(),
}
}
pub fn on_config_changed(&mut self, new_config: niri_config::Outputs) {
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
.find(&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
.find(&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: &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_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<ClientId, ClientData>, 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::<State>(display, &client, data, *output, conf);
}
}
}
fn send_new_head<D>(
display: &DisplayHandle,
client: &Client,
client_data: &mut ClientData,
output: OutputId,
conf: &niri_ipc::Output,
) where
D: GlobalDispatch<ZwlrOutputManagerV1, OutputManagementManagerGlobalData>,
D: Dispatch<ZwlrOutputManagerV1, ()>,
D: Dispatch<ZwlrOutputConfigurationV1, u32>,
D: Dispatch<ZwlrOutputConfigurationHeadV1, OutputConfigurationHeadState>,
D: Dispatch<ZwlrOutputModeV1, ()>,
D: OutputManagementHandler,
D: 'static,
D: Dispatch<ZwlrOutputHeadV1, OutputId>,
D: Dispatch<ZwlrOutputModeV1, ()>,
D: 'static,
{
let new_head = client
.create_resource::<ZwlrOutputHeadV1, _, D>(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::<ZwlrOutputModeV1, _, D>(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));
}

View File

@ -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.
```