From 962e159db61dc6c7822aa899b1d9dc86fb6a0de5 Mon Sep 17 00:00:00 2001 From: axtloss Date: Sun, 28 Jan 2024 14:25:40 +0100 Subject: Add option to rotate outputs --- niri-config/src/lib.rs | 54 ++++++++++++++++++++++++ resources/default-config.kdl | 4 ++ src/niri.rs | 97 +++++++++++++++++++++++++++++--------------- src/pw_utils.rs | 2 + src/screenshot_ui.rs | 11 +++-- 5 files changed, 131 insertions(+), 37 deletions(-) diff --git a/niri-config/src/lib.rs b/niri-config/src/lib.rs index 03b39390..98d2097e 100644 --- a/niri-config/src/lib.rs +++ b/niri-config/src/lib.rs @@ -187,6 +187,8 @@ pub struct Output { pub name: String, #[knuffel(child, unwrap(argument), default = 1.)] pub scale: f64, + #[knuffel(child, unwrap(argument, str), default = Transform::Normal)] + pub transform: Transform, #[knuffel(child)] pub position: Option, #[knuffel(child, unwrap(argument, str))] @@ -199,12 +201,62 @@ impl Default for Output { off: false, name: String::new(), scale: 1., + transform: Transform::Normal, position: None, mode: None, } } } +/// Output transform, which goes counter-clockwise. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Transform { + Normal, + _90, + _180, + _270, + Flipped, + Flipped90, + Flipped180, + Flipped270, +} + +impl FromStr for Transform { + type Err = miette::Error; + + fn from_str(s: &str) -> Result { + match s { + "normal" => Ok(Self::Normal), + "90" => Ok(Self::_90), + "180" => Ok(Self::_180), + "270" => Ok(Self::_270), + "flipped" => Ok(Self::Flipped), + "flipped-90" => Ok(Self::Flipped90), + "flipped-180" => Ok(Self::Flipped180), + "flipped-270" => Ok(Self::Flipped270), + _ => Err(miette!(concat!( + r#"invalid transform, can be "90", "180", "270", "#, + r#""flipped", "flipped-90", "flipped-180" or "flipped-270""# + ))), + } + } +} + +impl From for smithay::utils::Transform { + fn from(value: Transform) -> Self { + match value { + Transform::Normal => Self::Normal, + Transform::_90 => Self::_90, + Transform::_180 => Self::_180, + Transform::_270 => Self::_270, + Transform::Flipped => Self::Flipped, + Transform::Flipped90 => Self::Flipped90, + Transform::Flipped180 => Self::Flipped180, + Transform::Flipped270 => Self::Flipped270, + } + } +} + #[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq, Eq)] pub struct Position { #[knuffel(property)] @@ -726,6 +778,7 @@ mod tests { output "eDP-1" { scale 2.0 + transform "flipped-90" position x=10 y=20 mode "1920x1080@144" } @@ -826,6 +879,7 @@ mod tests { off: false, name: "eDP-1".to_owned(), scale: 2., + transform: Transform::Flipped90, position: Some(Position { x: 10, y: 20 }), mode: Some(Mode { width: 1920, diff --git a/resources/default-config.kdl b/resources/default-config.kdl index 19d69d4b..4aae7cc4 100644 --- a/resources/default-config.kdl +++ b/resources/default-config.kdl @@ -65,6 +65,10 @@ input { // Scale is a floating-point number, but at the moment only integer values work. scale 2.0 + // Transform allows to rotate the output counter-clockwise, valid values are: + // normal, 90, 180, 270, flipped, flipped-90, flipped-180 and flipped-270. + transform "normal" + // Resolution and, optionally, refresh rate of the output. // The format is "x" or "x@". // If the refresh rate is omitted, niri will pick the highest refresh rate diff --git a/src/niri.rs b/src/niri.rs index 268dd3f4..2ed62c99 100644 --- a/src/niri.rs +++ b/src/niri.rs @@ -648,20 +648,22 @@ impl State { let mut resized_outputs = vec![]; for output in self.niri.global_space.outputs() { let name = output.name(); - let scale = self - .niri - .config - .borrow() - .outputs - .iter() - .find(|o| o.name == name) - .map(|c| c.scale) - .unwrap_or(1.); + let config = self.niri.config.borrow_mut(); + let config = config.outputs.iter().find(|o| o.name == name); + + let scale = config.map(|c| c.scale).unwrap_or(1.); let scale = scale.clamp(1., 10.).ceil() as i32; - if output.current_scale().integer_scale() != scale { + + let transform = config + .map(|c| c.transform.into()) + .unwrap_or(Transform::Normal); + + if output.current_scale().integer_scale() != scale + || output.current_transform() != transform + { output.change_current_state( None, - None, + Some(transform), Some(output::Scale::Integer(scale)), None, ); @@ -1172,18 +1174,21 @@ impl Niri { let global = output.create_global::(&self.display_handle); let name = output.name(); - let scale = self - .config - .borrow() - .outputs - .iter() - .find(|o| o.name == name) - .map(|c| c.scale) - .unwrap_or(1.); + + let config = self.config.borrow(); + let c = config.outputs.iter().find(|o| o.name == name); + let scale = c.map(|c| c.scale).unwrap_or(1.); let scale = scale.clamp(1., 10.).ceil() as i32; + let transform = c.map(|c| c.transform.into()).unwrap_or(Transform::Normal); + drop(config); - // Set scale before adding to the layout since that will read the output size. - output.change_current_state(None, None, Some(output::Scale::Integer(scale)), None); + // Set scale and transform before adding to the layout since that will read the output size. + output.change_current_state( + None, + Some(transform), + Some(output::Scale::Integer(scale)), + None, + ); self.layout.add_output(output.clone()); @@ -1300,14 +1305,16 @@ impl Niri { } // If the output size changed with an open screenshot UI, close the screenshot UI. - if let Some((old_size, old_scale)) = self.screenshot_ui.output_size(&output) { - let output_transform = output.current_transform(); + if let Some((old_size, old_scale, old_transform)) = self.screenshot_ui.output_size(&output) + { + let transform = output.current_transform(); let output_mode = output.current_mode().unwrap(); - let size = output_transform.transform_size(output_mode.size); + let size = transform.transform_size(output_mode.size); let scale = output.current_scale().integer_scale(); - // FIXME: scale changes shouldn't matter but they currently do since I haven't quite - // figured out how to draw the screenshot textures in physical coordinates. - if old_size != size || old_scale != scale { + // FIXME: scale changes and transform flips shouldn't matter but they currently do since + // I haven't quite figured out how to draw the screenshot textures in + // physical coordinates. + if old_size != size || old_scale != scale || old_transform != transform { self.screenshot_ui.close(); self.cursor_manager .set_cursor_image(CursorImageStatus::default_named()); @@ -1747,7 +1754,9 @@ impl Niri { // FIXME we basically need to pick the largest scale factor across the overlapping // outputs, this is how it's usually done in clients as well. let mut cursor_scale = 1; + let mut cursor_transform = Transform::Normal; let mut dnd_scale = 1; + let mut dnd_transform = Transform::Normal; for output in self.global_space.outputs() { let geo = self.global_space.output_geometry(output).unwrap(); @@ -1755,6 +1764,9 @@ impl Niri { if let Some(mut overlap) = geo.intersection(bbox) { overlap.loc -= surface_pos; cursor_scale = cursor_scale.max(output.current_scale().integer_scale()); + // FIXME: using the largest overlapping or "primary" output transform would + // make more sense here. + cursor_transform = output.current_transform(); output_update(output, Some(overlap), surface); } else { output_update(output, None, surface); @@ -1765,6 +1777,9 @@ impl Niri { if let Some(mut overlap) = geo.intersection(bbox) { overlap.loc -= surface_pos; dnd_scale = dnd_scale.max(output.current_scale().integer_scale()); + // FIXME: using the largest overlapping or "primary" output transform + // would make more sense here. + dnd_transform = output.current_transform(); output_update(output, Some(overlap), surface); } else { output_update(output, None, surface); @@ -1773,11 +1788,11 @@ impl Niri { } with_states(surface, |data| { - send_surface_state(surface, data, cursor_scale, Transform::Normal); + send_surface_state(surface, data, cursor_scale, cursor_transform); }); if let Some((surface, _)) = dnd { with_states(surface, |data| { - send_surface_state(surface, data, dnd_scale, Transform::Normal); + send_surface_state(surface, data, dnd_scale, dnd_transform); }); } } @@ -1794,6 +1809,7 @@ impl Niri { }; let mut dnd_scale = 1; + let mut dnd_transform = Transform::Normal; for output in self.global_space.outputs() { let geo = self.global_space.output_geometry(output).unwrap(); @@ -1815,15 +1831,18 @@ impl Niri { if let Some(mut overlap) = geo.intersection(bbox) { overlap.loc -= surface_pos; dnd_scale = dnd_scale.max(output.current_scale().integer_scale()); + // FIXME: using the largest overlapping or "primary" output transform would + // make more sense here. + dnd_transform = output.current_transform(); output_update(output, Some(overlap), surface); } else { output_update(output, None, surface); } - - with_states(surface, |data| { - send_surface_state(surface, data, dnd_scale, Transform::Normal); - }); } + + with_states(surface, |data| { + send_surface_state(surface, data, dnd_scale, dnd_transform); + }); } } } @@ -2409,6 +2428,9 @@ impl Niri { let _span = tracy_client::span!("Niri::render_for_screen_cast"); let size = output.current_mode().unwrap().size; + let transform = output.current_transform(); + let size = transform.transform_size(size); + let scale = Scale::from(output.current_scale().fractional_scale()); let mut elements = None; @@ -2531,6 +2553,9 @@ impl Niri { .cloned() .filter_map(|output| { let size = output.current_mode().unwrap().size; + let transform = output.current_transform(); + let size = transform.transform_size(size); + let scale = Scale::from(output.current_scale().fractional_scale()); let elements = self.render::(renderer, &output, true); @@ -2558,6 +2583,9 @@ impl Niri { let _span = tracy_client::span!("Niri::screenshot"); let size = output.current_mode().unwrap().size; + let transform = output.current_transform(); + let size = transform.transform_size(size); + let scale = Scale::from(output.current_scale().fractional_scale()); let elements = self.render::(renderer, output, true); let pixels = render_to_vec(renderer, size, scale, Fourcc::Abgr8888, &elements)?; @@ -2679,6 +2707,9 @@ impl Niri { let geom = geom.to_physical(output_scale); let size = geom.size; + let transform = output.current_transform(); + let size = transform.transform_size(size); + let elements = self.render::(renderer, &output, include_pointer); let pixels = render_to_vec( renderer, diff --git a/src/pw_utils.rs b/src/pw_utils.rs index 097cfdd7..45ccc559 100644 --- a/src/pw_utils.rs +++ b/src/pw_utils.rs @@ -112,6 +112,8 @@ impl PipeWire { let mode = output.current_mode().unwrap(); let size = mode.size; + let transform = output.current_transform(); + let size = transform.transform_size(size); let refresh = mode.refresh; let stream = Stream::new(&self.core, "niri-screen-cast-src", Properties::new()) diff --git a/src/screenshot_ui.rs b/src/screenshot_ui.rs index 2d097a24..f50e84d6 100644 --- a/src/screenshot_ui.rs +++ b/src/screenshot_ui.rs @@ -42,6 +42,7 @@ pub enum ScreenshotUi { pub struct OutputData { size: Size, scale: i32, + transform: Transform, texture: GlesTexture, texture_buffer: TextureBuffer, buffers: [SolidColorBuffer; 8], @@ -94,6 +95,7 @@ impl ScreenshotUi { ) } }; + let scale = selection.0.current_scale().integer_scale(); let selection = ( selection.0, @@ -104,9 +106,9 @@ impl ScreenshotUi { let output_data = screenshots .into_iter() .map(|(output, texture)| { - let output_transform = output.current_transform(); + let transform = output.current_transform(); let output_mode = output.current_mode().unwrap(); - let size = output_transform.transform_size(output_mode.size); + let size = transform.transform_size(output_mode.size); let scale = output.current_scale().integer_scale(); let texture_buffer = TextureBuffer::from_texture( renderer, @@ -129,6 +131,7 @@ impl ScreenshotUi { let data = OutputData { size, scale, + transform, texture, texture_buffer, buffers, @@ -333,10 +336,10 @@ impl ScreenshotUi { } } - pub fn output_size(&self, output: &Output) -> Option<(Size, i32)> { + pub fn output_size(&self, output: &Output) -> Option<(Size, i32, Transform)> { if let Self::Open { output_data, .. } = self { let data = output_data.get(output)?; - Some((data.size, data.scale)) + Some((data.size, data.scale, data.transform)) } else { None } -- cgit