diff options
| author | Ivan Molodetskikh <yalterz@gmail.com> | 2024-06-17 09:16:28 +0300 |
|---|---|---|
| committer | Ivan Molodetskikh <yalterz@gmail.com> | 2024-06-18 14:01:28 +0300 |
| commit | 1dae45c58d7eabeda21ef490d712915890bf6cff (patch) | |
| tree | 62c473ab1662a1161ed522517ea57b7bd8db340c | |
| parent | 997119c44338ad96a40b4a1d6e958f77062a37ef (diff) | |
| download | niri-1dae45c58d7eabeda21ef490d712915890bf6cff.tar.gz niri-1dae45c58d7eabeda21ef490d712915890bf6cff.tar.bz2 niri-1dae45c58d7eabeda21ef490d712915890bf6cff.zip | |
Refactor layout to fractional-logical
Lets borders, gaps, and everything else stay pixel-perfect even with
fractional scale. Allows setting fractional border widths, gaps,
struts.
See the new wiki .md for more details.
35 files changed, 1050 insertions, 652 deletions
@@ -144,6 +144,15 @@ dependencies = [ ] [[package]] +name = "approx" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", +] + +[[package]] name = "arrayvec" version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -604,7 +613,7 @@ version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a98d30140e3296250832bbaaff83b27dcd6fa3cc70fb6f1f3e5c9c0023b5317" dependencies = [ - "approx", + "approx 0.4.0", "num-traits", ] @@ -2163,6 +2172,7 @@ name = "niri" version = "0.1.6" dependencies = [ "anyhow", + "approx 0.5.1", "arrayvec", "async-channel", "async-io 1.13.0", @@ -3162,7 +3172,7 @@ checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "smithay" version = "0.3.0" -source = "git+https://github.com/Smithay/smithay.git#6169b213fb663d85d2e139d3bbe44dfae1ec9328" +source = "git+https://github.com/Smithay/smithay.git#b4f8120be0fb9b7f038d041efa7f6549e26cd2bc" dependencies = [ "appendlist", "bitflags 2.5.0", @@ -3234,7 +3244,7 @@ dependencies = [ [[package]] name = "smithay-drm-extras" version = "0.1.0" -source = "git+https://github.com/Smithay/smithay.git#6169b213fb663d85d2e139d3bbe44dfae1ec9328" +source = "git+https://github.com/Smithay/smithay.git#b4f8120be0fb9b7f038d041efa7f6549e26cd2bc" dependencies = [ "drm", "edid-rs", @@ -99,6 +99,7 @@ features = [ ] [dev-dependencies] +approx = "0.5.1" k9 = "0.12.0" proptest = "1.4.0" proptest-derive = "0.4.0" diff --git a/niri-config/src/lib.rs b/niri-config/src/lib.rs index 4fbb96e1..a9285fad 100644 --- a/niri-config/src/lib.rs +++ b/niri-config/src/lib.rs @@ -331,6 +331,10 @@ pub struct Position { pub y: i32, } +// MIN and MAX generics are only used during parsing to check the value. +#[derive(Debug, Default, Clone, Copy, PartialEq)] +pub struct FloatOrInt<const MIN: i32, const MAX: i32>(pub f64); + #[derive(knuffel::Decode, Debug, Clone, PartialEq)] pub struct Layout { #[knuffel(child, default)] @@ -344,7 +348,7 @@ pub struct Layout { #[knuffel(child, unwrap(argument), default)] pub center_focused_column: CenterFocusedColumn, #[knuffel(child, unwrap(argument), default = Self::default().gaps)] - pub gaps: u16, + pub gaps: FloatOrInt<0, 65535>, #[knuffel(child, default)] pub struts: Struts, } @@ -357,7 +361,7 @@ impl Default for Layout { preset_column_widths: Default::default(), default_column_width: Default::default(), center_focused_column: Default::default(), - gaps: 16, + gaps: FloatOrInt(16.), struts: Default::default(), } } @@ -374,7 +378,7 @@ pub struct FocusRing { #[knuffel(child)] pub off: bool, #[knuffel(child, unwrap(argument), default = Self::default().width)] - pub width: u16, + pub width: FloatOrInt<0, 65535>, #[knuffel(child, default = Self::default().active_color)] pub active_color: Color, #[knuffel(child, default = Self::default().inactive_color)] @@ -389,7 +393,7 @@ impl Default for FocusRing { fn default() -> Self { Self { off: false, - width: 4, + width: FloatOrInt(4.), active_color: Color::new(127, 200, 255, 255), inactive_color: Color::new(80, 80, 80, 255), active_gradient: None, @@ -422,7 +426,7 @@ pub struct Border { #[knuffel(child)] pub off: bool, #[knuffel(child, unwrap(argument), default = Self::default().width)] - pub width: u16, + pub width: FloatOrInt<0, 65535>, #[knuffel(child, default = Self::default().active_color)] pub active_color: Color, #[knuffel(child, default = Self::default().inactive_color)] @@ -437,7 +441,7 @@ impl Default for Border { fn default() -> Self { Self { off: true, - width: 4, + width: FloatOrInt(4.), active_color: Color::new(255, 200, 127, 255), inactive_color: Color::new(80, 80, 80, 255), active_gradient: None, @@ -519,16 +523,16 @@ pub enum PresetWidth { #[derive(Debug, Clone, PartialEq)] pub struct DefaultColumnWidth(pub Option<PresetWidth>); -#[derive(knuffel::Decode, Debug, Default, Clone, Copy, PartialEq, Eq)] +#[derive(knuffel::Decode, Debug, Default, Clone, Copy, PartialEq)] pub struct Struts { #[knuffel(child, unwrap(argument), default)] - pub left: u16, + pub left: FloatOrInt<0, 65535>, #[knuffel(child, unwrap(argument), default)] - pub right: u16, + pub right: FloatOrInt<0, 65535>, #[knuffel(child, unwrap(argument), default)] - pub top: u16, + pub top: FloatOrInt<0, 65535>, #[knuffel(child, unwrap(argument), default)] - pub bottom: u16, + pub bottom: FloatOrInt<0, 65535>, } #[derive(knuffel::Decode, Debug, Default, Clone, Copy, PartialEq, Eq)] @@ -863,7 +867,7 @@ pub struct BorderRule { #[knuffel(child)] pub on: bool, #[knuffel(child, unwrap(argument))] - pub width: Option<u16>, + pub width: Option<FloatOrInt<0, 65535>>, #[knuffel(child)] pub active_color: Option<Color>, #[knuffel(child)] @@ -1144,6 +1148,72 @@ impl<S: knuffel::traits::ErrorSpan> knuffel::DecodeScalar<S> for WorkspaceRefere } } +impl<S: knuffel::traits::ErrorSpan, const MIN: i32, const MAX: i32> knuffel::DecodeScalar<S> + for FloatOrInt<MIN, MAX> +{ + fn type_check( + type_name: &Option<knuffel::span::Spanned<knuffel::ast::TypeName, S>>, + ctx: &mut knuffel::decode::Context<S>, + ) { + if let Some(type_name) = &type_name { + ctx.emit_error(DecodeError::unexpected( + type_name, + "type name", + "no type name expected for this node", + )); + } + } + + fn raw_decode( + val: &knuffel::span::Spanned<knuffel::ast::Literal, S>, + ctx: &mut knuffel::decode::Context<S>, + ) -> Result<Self, DecodeError<S>> { + match &**val { + knuffel::ast::Literal::Int(ref value) => match value.try_into() { + Ok(v) => { + if (MIN..=MAX).contains(&v) { + Ok(FloatOrInt(f64::from(v))) + } else { + ctx.emit_error(DecodeError::conversion( + val, + format!("value must be between {MIN} and {MAX}"), + )); + Ok(FloatOrInt::default()) + } + } + Err(e) => { + ctx.emit_error(DecodeError::conversion(val, e)); + Ok(FloatOrInt::default()) + } + }, + knuffel::ast::Literal::Decimal(ref value) => match value.try_into() { + Ok(v) => { + if (f64::from(MIN)..=f64::from(MAX)).contains(&v) { + Ok(FloatOrInt(v)) + } else { + ctx.emit_error(DecodeError::conversion( + val, + format!("value must be between {MIN} and {MAX}"), + )); + Ok(FloatOrInt::default()) + } + } + Err(e) => { + ctx.emit_error(DecodeError::conversion(val, e)); + Ok(FloatOrInt::default()) + } + }, + _ => { + ctx.emit_error(DecodeError::unsupported( + val, + "Unsupported value, only numbers are recognized", + )); + Ok(FloatOrInt::default()) + } + } + } +} + #[derive(knuffel::Decode, Debug, Default, PartialEq)] pub struct DebugConfig { #[knuffel(child, unwrap(argument))] @@ -2483,7 +2553,7 @@ mod tests { border { on - width 8 + width 8.5 } } @@ -2579,7 +2649,7 @@ mod tests { layout: Layout { focus_ring: FocusRing { off: false, - width: 5, + width: FloatOrInt(5.), active_color: Color { r: 0, g: 100, @@ -2602,7 +2672,7 @@ mod tests { }, border: Border { off: false, - width: 3, + width: FloatOrInt(3.), active_color: Color { r: 255, g: 200, @@ -2627,12 +2697,12 @@ mod tests { default_column_width: Some(DefaultColumnWidth(Some(PresetWidth::Proportion( 0.25, )))), - gaps: 8, + gaps: FloatOrInt(8.), struts: Struts { - left: 1, - right: 2, - top: 3, - bottom: 0, + left: FloatOrInt(1.), + right: FloatOrInt(2.), + top: FloatOrInt(3.), + bottom: FloatOrInt(0.), }, center_focused_column: CenterFocusedColumn::OnOverflow, }, @@ -2716,12 +2786,12 @@ mod tests { open_fullscreen: Some(false), focus_ring: BorderRule { off: true, - width: Some(3), + width: Some(FloatOrInt(3.)), ..Default::default() }, border: BorderRule { on: true, - width: Some(8), + width: Some(FloatOrInt(8.5)), ..Default::default() }, ..Default::default() diff --git a/niri-visual-tests/src/cases/gradient_angle.rs b/niri-visual-tests/src/cases/gradient_angle.rs index f9871a93..203f31b1 100644 --- a/niri-visual-tests/src/cases/gradient_angle.rs +++ b/niri-visual-tests/src/cases/gradient_angle.rs @@ -59,15 +59,15 @@ impl TestCase for GradientAngle { ) -> Vec<Box<dyn RenderElement<GlesRenderer>>> { let (a, b) = (size.w / 4, size.h / 4); let size = (size.w - a * 2, size.h - b * 2); - let area = Rectangle::from_loc_and_size((a, b), size); + let area = Rectangle::from_loc_and_size((a, b), size).to_f64(); [BorderRenderElement::new( area.size, - Rectangle::from_loc_and_size((0, 0), area.size), + Rectangle::from_loc_and_size((0., 0.), area.size), [1., 0., 0., 1.], [0., 1., 0., 1.], self.angle - FRAC_PI_2, - Rectangle::from_loc_and_size((0, 0), area.size), + Rectangle::from_loc_and_size((0., 0.), area.size), 0., CornerRadius::default(), ) diff --git a/niri-visual-tests/src/cases/gradient_area.rs b/niri-visual-tests/src/cases/gradient_area.rs index b6741575..e76820fd 100644 --- a/niri-visual-tests/src/cases/gradient_area.rs +++ b/niri-visual-tests/src/cases/gradient_area.rs @@ -5,10 +5,10 @@ use std::time::Duration; use niri::animation::ANIMATION_SLOWDOWN; use niri::layout::focus_ring::FocusRing; use niri::render_helpers::border::BorderRenderElement; -use niri_config::{Color, CornerRadius}; +use niri_config::{Color, CornerRadius, FloatOrInt}; use smithay::backend::renderer::element::RenderElement; use smithay::backend::renderer::gles::GlesRenderer; -use smithay::utils::{Logical, Physical, Point, Rectangle, Scale, Size}; +use smithay::utils::{Logical, Physical, Point, Rectangle, Size}; use super::TestCase; @@ -22,7 +22,7 @@ impl GradientArea { pub fn new(_size: Size<i32, Logical>) -> Self { let border = FocusRing::new(niri_config::FocusRing { off: false, - width: 1, + width: FloatOrInt(1.), active_color: Color::new(255, 255, 255, 128), inactive_color: Color::default(), active_gradient: None, @@ -75,13 +75,14 @@ impl TestCase for GradientArea { let (a, b) = (size.w / 4, size.h / 4); let rect_size = (size.w - a * 2, size.h - b * 2); - let area = Rectangle::from_loc_and_size((a, b), rect_size); + let area = Rectangle::from_loc_and_size((a, b), rect_size).to_f64(); let g_size = Size::from(( (size.w as f32 / 8. + size.w as f32 / 8. * 7. * f).round() as i32, (size.h as f32 / 8. + size.h as f32 / 8. * 7. * f).round() as i32, )); - let g_loc = ((size.w - g_size.w) / 2, (size.h - g_size.h) / 2); + let g_loc = Point::from(((size.w - g_size.w) / 2, (size.h - g_size.h) / 2)).to_f64(); + let g_size = g_size.to_f64(); let mut g_area = Rectangle::from_loc_and_size(g_loc, g_size); g_area.loc -= area.loc; @@ -91,10 +92,11 @@ impl TestCase for GradientArea { true, Rectangle::default(), CornerRadius::default(), + 1., ); rv.extend( self.border - .render(renderer, Point::from(g_loc), Scale::from(1.)) + .render(renderer, g_loc) .map(|elem| Box::new(elem) as _), ); @@ -105,7 +107,7 @@ impl TestCase for GradientArea { [1., 0., 0., 1.], [0., 1., 0., 1.], FRAC_PI_4, - Rectangle::from_loc_and_size((0, 0), rect_size), + Rectangle::from_loc_and_size((0, 0), rect_size).to_f64(), 0., CornerRadius::default(), ) diff --git a/niri-visual-tests/src/cases/layout.rs b/niri-visual-tests/src/cases/layout.rs index d7872173..2e730dbe 100644 --- a/niri-visual-tests/src/cases/layout.rs +++ b/niri-visual-tests/src/cases/layout.rs @@ -5,7 +5,7 @@ use niri::layout::workspace::ColumnWidth; use niri::layout::{LayoutElement as _, Options}; use niri::render_helpers::RenderTarget; use niri::utils::get_monotonic_time; -use niri_config::Color; +use niri_config::{Color, FloatOrInt}; use smithay::backend::renderer::element::RenderElement; use smithay::backend::renderer::gles::GlesRenderer; use smithay::desktop::layer_map_for_output; @@ -49,7 +49,7 @@ impl Layout { }, border: niri_config::Border { off: false, - width: 4, + width: FloatOrInt(4.), active_color: Color::new(255, 163, 72, 255), inactive_color: Color::new(50, 50, 50, 255), active_gradient: None, diff --git a/niri-visual-tests/src/cases/tile.rs b/niri-visual-tests/src/cases/tile.rs index 301fc19f..95261877 100644 --- a/niri-visual-tests/src/cases/tile.rs +++ b/niri-visual-tests/src/cases/tile.rs @@ -3,7 +3,7 @@ use std::time::Duration; use niri::layout::Options; use niri::render_helpers::RenderTarget; -use niri_config::Color; +use niri_config::{Color, FloatOrInt}; use smithay::backend::renderer::element::RenderElement; use smithay::backend::renderer::gles::GlesRenderer; use smithay::utils::{Logical, Physical, Point, Rectangle, Scale, Size}; @@ -20,7 +20,7 @@ impl Tile { pub fn freeform(size: Size<i32, Logical>) -> Self { let window = TestWindow::freeform(0); let mut rv = Self::with_window(window); - rv.tile.request_tile_size(size, false); + rv.tile.request_tile_size(size.to_f64(), false); rv.window.communicate(); rv } @@ -28,7 +28,7 @@ impl Tile { pub fn fixed_size(size: Size<i32, Logical>) -> Self { let window = TestWindow::fixed_size(0); let mut rv = Self::with_window(window); - rv.tile.request_tile_size(size, false); + rv.tile.request_tile_size(size.to_f64(), false); rv.window.communicate(); rv } @@ -37,7 +37,7 @@ impl Tile { let window = TestWindow::fixed_size(0); window.set_csd_shadow_width(64); let mut rv = Self::with_window(window); - rv.tile.request_tile_size(size, false); + rv.tile.request_tile_size(size.to_f64(), false); rv.window.communicate(); rv } @@ -71,13 +71,13 @@ impl Tile { }, border: niri_config::Border { off: false, - width: 32, + width: FloatOrInt(32.), active_color: Color::new(255, 163, 72, 255), ..Default::default() }, ..Default::default() }; - let tile = niri::layout::tile::Tile::new(window.clone(), Rc::new(options)); + let tile = niri::layout::tile::Tile::new(window.clone(), 1., Rc::new(options)); Self { window, tile } } } @@ -85,7 +85,7 @@ impl Tile { impl TestCase for Tile { fn resize(&mut self, width: i32, height: i32) { self.tile - .request_tile_size(Size::from((width, height)), false); + .request_tile_size(Size::from((width, height)).to_f64(), false); self.window.communicate(); } @@ -102,12 +102,13 @@ impl TestCase for Tile { renderer: &mut GlesRenderer, size: Size<i32, Physical>, ) -> Vec<Box<dyn RenderElement<GlesRenderer>>> { - let tile_size = self.tile.tile_size().to_physical(1); - let location = Point::from(((size.w - tile_size.w) / 2, (size.h - tile_size.h) / 2)); + let size = size.to_f64(); + let tile_size = self.tile.tile_size().to_physical(1.); + let location = Point::from((size.w - tile_size.w, size.h - tile_size.h)).downscale(2.); self.tile.update( true, - Rectangle::from_loc_and_size((-location.x, -location.y), size.to_logical(1)), + Rectangle::from_loc_and_size((-location.x, -location.y), size.to_logical(1.)), ); self.tile .render( diff --git a/niri-visual-tests/src/cases/window.rs b/niri-visual-tests/src/cases/window.rs index f19ec5ba..be6150d4 100644 --- a/niri-visual-tests/src/cases/window.rs +++ b/niri-visual-tests/src/cases/window.rs @@ -47,7 +47,9 @@ impl TestCase for Window { size: Size<i32, Physical>, ) -> Vec<Box<dyn RenderElement<GlesRenderer>>> { let win_size = self.window.size().to_physical(1); - let location = Point::from(((size.w - win_size.w) / 2, (size.h - win_size.h) / 2)); + let location = Point::from((size.w - win_size.w, size.h - win_size.h)) + .to_f64() + .downscale(2.); self.window .render( diff --git a/niri-visual-tests/src/test_window.rs b/niri-visual-tests/src/test_window.rs index 1ce770bd..dbba5192 100644 --- a/niri-visual-tests/src/test_window.rs +++ b/niri-visual-tests/src/test_window.rs @@ -6,9 +6,9 @@ use niri::layout::{ InteractiveResizeData, LayoutElement, LayoutElementRenderElement, LayoutElementRenderSnapshot, }; use niri::render_helpers::renderer::NiriRenderer; +use niri::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement}; use niri::render_helpers::{RenderTarget, SplitElements}; use niri::window::ResolvedWindowRules; -use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement}; use smithay::backend::renderer::element::{Id, Kind}; use smithay::output::{self, Output}; use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; @@ -37,7 +37,7 @@ impl TestWindow { let size = Size::from((100, 200)); let min_size = Size::from((0, 0)); let max_size = Size::from((0, 0)); - let buffer = SolidColorBuffer::new(size, [0.15, 0.64, 0.41, 1.]); + let buffer = SolidColorBuffer::new(size.to_f64(), [0.15, 0.64, 0.41, 1.]); Self { id, @@ -49,7 +49,7 @@ impl TestWindow { buffer, pending_fullscreen: false, csd_shadow_width: 0, - csd_shadow_buffer: SolidColorBuffer::new((0, 0), [0., 0., 0., 0.3]), + csd_shadow_buffer: SolidColorBuffer::new((0., 0.), [0., 0., 0., 0.3]), })), } } @@ -112,14 +112,14 @@ impl TestWindow { if inner.size != new_size { inner.size = new_size; - inner.buffer.resize(new_size); + inner.buffer.resize(new_size.to_f64()); rv = true; } let mut csd_shadow_size = new_size; csd_shadow_size.w += inner.csd_shadow_width * 2; csd_shadow_size.h += inner.csd_shadow_width * 2; - inner.csd_shadow_buffer.resize(csd_shadow_size); + inner.csd_shadow_buffer.resize(csd_shadow_size.to_f64()); rv } @@ -147,8 +147,8 @@ impl LayoutElement for TestWindow { fn render<R: NiriRenderer>( &self, _renderer: &mut R, - location: Point<i32, Logical>, - scale: Scale<f64>, + location: Point<f64, Logical>, + _scale: Scale<f64>, alpha: f32, _target: RenderTarget, ) -> SplitElements<LayoutElementRenderElement<R>> { @@ -158,17 +158,15 @@ impl LayoutElement for TestWindow { normal: vec