From 1dae45c58d7eabeda21ef490d712915890bf6cff Mon Sep 17 00:00:00 2001 From: Ivan Molodetskikh Date: Mon, 17 Jun 2024 09:16:28 +0300 Subject: 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. --- Cargo.lock | 16 +- Cargo.toml | 1 + niri-config/src/lib.rs | 114 +++++- niri-visual-tests/src/cases/gradient_angle.rs | 6 +- niri-visual-tests/src/cases/gradient_area.rs | 16 +- niri-visual-tests/src/cases/layout.rs | 4 +- niri-visual-tests/src/cases/tile.rs | 21 +- niri-visual-tests/src/cases/window.rs | 4 +- niri-visual-tests/src/test_window.rs | 22 +- src/handlers/xdg_shell.rs | 41 +- src/input/mod.rs | 8 +- src/input/resize_grab.rs | 4 +- src/input/view_offset_grab.rs | 4 +- src/layout/closing_window.rs | 25 +- src/layout/focus_ring.rs | 73 ++-- src/layout/mod.rs | 204 ++++++++-- src/layout/monitor.rs | 56 ++- src/layout/opening_window.rs | 18 +- src/layout/tile.rs | 221 +++++----- src/layout/workspace.rs | 560 +++++++++++++++----------- src/niri.rs | 39 +- src/render_helpers/border.rs | 20 +- src/render_helpers/clipped_surface.rs | 16 +- src/render_helpers/damage.rs | 8 +- src/render_helpers/mod.rs | 24 +- src/render_helpers/resize.rs | 16 +- src/render_helpers/shader_element.rs | 18 +- src/render_helpers/snapshot.rs | 6 +- src/render_helpers/surface.rs | 6 +- src/ui/config_error_notification.rs | 2 +- src/utils/mod.rs | 20 +- src/window/mapped.rs | 50 +-- wiki/Configuration:-Layout.md | 24 ++ wiki/Fractional-Layout.md | 34 ++ wiki/_Sidebar.md | 1 + 35 files changed, 1050 insertions(+), 652 deletions(-) create mode 100644 wiki/Fractional-Layout.md diff --git a/Cargo.lock b/Cargo.lock index 8026862b..54412c8c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -143,6 +143,15 @@ dependencies = [ "num-traits", ] +[[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" @@ -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", diff --git a/Cargo.toml b/Cargo.toml index cb15c2cf..dd0f6e05 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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(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); -#[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, + pub width: Option>, #[knuffel(child)] pub active_color: Option, #[knuffel(child)] @@ -1144,6 +1148,72 @@ impl knuffel::DecodeScalar for WorkspaceRefere } } +impl knuffel::DecodeScalar + for FloatOrInt +{ + fn type_check( + type_name: &Option>, + ctx: &mut knuffel::decode::Context, + ) { + 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, + ctx: &mut knuffel::decode::Context, + ) -> Result> { + 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>> { 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) -> 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) -> 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) -> 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, ) -> Vec>> { - 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, ) -> Vec>> { 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( &self, _renderer: &mut R, - location: Point, - scale: Scale, + location: Point, + _scale: Scale, alpha: f32, _target: RenderTarget, ) -> SplitElements> { @@ -158,17 +158,15 @@ impl LayoutElement for TestWindow { normal: vec![ SolidColorRenderElement::from_buffer( &inner.buffer, - location.to_physical_precise_round(scale), - scale, + location, alpha, Kind::Unspecified, ) .into(), SolidColorRenderElement::from_buffer( &inner.csd_shadow_buffer, - (location - Point::from((inner.csd_shadow_width, inner.csd_shadow_width))) - .to_physical_precise_round(scale), - scale, + location + - Point::from((inner.csd_shadow_width, inner.csd_shadow_width)).to_f64(), alpha, Kind::Unspecified, ) diff --git a/src/handlers/xdg_shell.rs b/src/handlers/xdg_shell.rs index c1e29ea4..3e6234fa 100644 --- a/src/handlers/xdg_shell.rs +++ b/src/handlers/xdg_shell.rs @@ -788,9 +788,9 @@ impl State { // window can be scrolled to both edges of the screen), but within the whole monitor's // height. let mut target = - Rectangle::from_loc_and_size((0, 0), (window_geo.size.w, output_geo.size.h)); + Rectangle::from_loc_and_size((0, 0), (window_geo.size.w, output_geo.size.h)).to_f64(); target.loc -= self.niri.layout.window_loc(window).unwrap(); - target.loc -= get_popup_toplevel_coords(popup); + target.loc -= get_popup_toplevel_coords(popup).to_f64(); self.position_popup_within_rect(popup, target); } @@ -813,10 +813,10 @@ impl State { target.loc -= layer_geo.loc; target.loc -= get_popup_toplevel_coords(popup); - self.position_popup_within_rect(popup, target); + self.position_popup_within_rect(popup, target.to_f64()); } - fn position_popup_within_rect(&self, popup: &PopupKind, target: Rectangle) { + fn position_popup_within_rect(&self, popup: &PopupKind, target: Rectangle) { match popup { PopupKind::Xdg(popup) => { popup.with_pending_state(|state| { @@ -826,28 +826,29 @@ impl State { PopupKind::InputMethod(popup) => { let text_input_rectangle = popup.text_input_rectangle(); let mut bbox = - utils::bbox_from_surface_tree(popup.wl_surface(), text_input_rectangle.loc); + utils::bbox_from_surface_tree(popup.wl_surface(), text_input_rectangle.loc) + .to_f64(); // Position bbox horizontally first. let overflow_x = (bbox.loc.x + bbox.size.w) - (target.loc.x + target.size.w); - if overflow_x > 0 { + if overflow_x > 0. { bbox.loc.x -= overflow_x; } // Ensure that the popup starts within the window. - bbox.loc.x = bbox.loc.x.max(target.loc.x); + bbox.loc.x = f64::max(bbox.loc.x, target.loc.x); // Try to position IME popup below the text input rectangle. let mut below = bbox; - below.loc.y += text_input_rectangle.size.h; + below.loc.y += f64::from(text_input_rectangle.size.h); let mut above = bbox; above.loc.y -= bbox.size.h; if target.loc.y + target.size.h >= below.loc.y + below.size.h { - popup.set_location(below.loc); + popup.set_location(below.loc.to_i32_round()); } else { - popup.set_location(above.loc); + popup.set_location(above.loc.to_i32_round()); } } } @@ -907,25 +908,25 @@ impl State { fn unconstrain_with_padding( positioner: PositionerState, - target: Rectangle, + target: Rectangle, ) -> Rectangle { // Try unconstraining with a small padding first which looks nicer, then if it doesn't fit try // unconstraining without padding. - const PADDING: i32 = 8; + const PADDING: f64 = 8.; let mut padded = target; - if PADDING * 2 < padded.size.w { + if PADDING * 2. < padded.size.w { padded.loc.x += PADDING; - padded.size.w -= PADDING * 2; + padded.size.w -= PADDING * 2.; } - if PADDING * 2 < padded.size.h { + if PADDING * 2. < padded.size.h { padded.loc.y += PADDING; - padded.size.h -= PADDING * 2; + padded.size.h -= PADDING * 2.; } // No padding, so just unconstrain with the original target. if padded == target { - return positioner.get_unconstrained_geometry(target); + return positioner.get_unconstrained_geometry(target.to_i32_round()); } // Do not try to resize to fit the padded target rectangle. @@ -937,13 +938,13 @@ fn unconstrain_with_padding( .constraint_adjustment .remove(ConstraintAdjustment::ResizeY); - let geo = no_resize.get_unconstrained_geometry(padded); - if padded.contains_rect(geo) { + let geo = no_resize.get_unconstrained_geometry(padded.to_i32_round()); + if padded.contains_rect(geo.to_f64()) { return geo; } // Could not unconstrain into the padded target, so resort to the regular one. - positioner.get_unconstrained_geometry(target) + positioner.get_unconstrained_geometry(target.to_i32_round()) } pub fn add_mapped_toplevel_pre_commit_hook(toplevel: &ToplevelSurface) -> HookId { diff --git a/src/input/mod.rs b/src/input/mod.rs index acb08e1c..d0e1c55b 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -935,7 +935,7 @@ impl State { // Check if we have an active pointer constraint. let mut pointer_confined = None; if let Some(focus) = &self.niri.pointer_focus.surface { - let pos_within_surface = pos.to_i32_round() - focus.1; + let pos_within_surface = pos - focus.1; let mut pointer_locked = false; with_pointer_constraint(&focus.0, &pointer, |constraint| { @@ -946,7 +946,7 @@ impl State { // Constraint does not apply if not within region. if let Some(region) = constraint.region() { - if !region.contains(pos_within_surface) { + if !region.contains(pos_within_surface.to_i32_round()) { return; } } @@ -1036,8 +1036,8 @@ impl State { // Prevent the pointer from leaving the confine region, if any. if let Some(region) = region { - let new_pos_within_surface = new_pos.to_i32_round() - focus_surface.1; - if !region.contains(new_pos_within_surface) { + let new_pos_within_surface = new_pos - focus_surface.1; + if !region.contains(new_pos_within_surface.to_i32_round()) { prevent = true; } } diff --git a/src/input/resize_grab.rs b/src/input/resize_grab.rs index 38483ca6..535bae32 100644 --- a/src/input/resize_grab.rs +++ b/src/input/resize_grab.rs @@ -35,7 +35,7 @@ impl PointerGrab for ResizeGrab { &mut self, data: &mut State, handle: &mut PointerInnerHandle<'_, State>, - _focus: Option<(::PointerFocus, Point)>, + _focus: Option<(::PointerFocus, Point)>, event: &MotionEvent, ) { // While the grab is active, no client has pointer focus. @@ -60,7 +60,7 @@ impl PointerGrab for ResizeGrab { &mut self, data: &mut State, handle: &mut PointerInnerHandle<'_, State>, - _focus: Option<(::PointerFocus, Point)>, + _focus: Option<(::PointerFocus, Point)>, event: &RelativeMotionEvent, ) { // While the grab is active, no client has pointer focus. diff --git a/src/input/view_offset_grab.rs b/src/input/view_offset_grab.rs index 4e2d2785..b4f9f96c 100644 --- a/src/input/view_offset_grab.rs +++ b/src/input/view_offset_grab.rs @@ -46,7 +46,7 @@ impl PointerGrab for ViewOffsetGrab { &mut self, data: &mut State, handle: &mut PointerInnerHandle<'_, State>, - _focus: Option<(::PointerFocus, Point)>, + _focus: Option<(::PointerFocus, Point)>, event: &MotionEvent, ) { // While the grab is active, no client has pointer focus. @@ -74,7 +74,7 @@ impl PointerGrab for ViewOffsetGrab { &mut self, data: &mut State, handle: &mut PointerInnerHandle<'_, State>, - _focus: Option<(::PointerFocus, Point)>, + _focus: Option<(::PointerFocus, Point)>, event: &RelativeMotionEvent, ) { // While the grab is active, no client has pointer focus. diff --git a/src/layout/closing_window.rs b/src/layout/closing_window.rs index 8945d2aa..06b5927c 100644 --- a/src/layout/closing_window.rs +++ b/src/layout/closing_window.rs @@ -34,10 +34,10 @@ pub struct ClosingWindow { block_out_from: Option, /// Size of the window geometry. - geo_size: Size, + geo_size: Size, /// Position in the workspace. - pos: Point, + pos: Point, /// How much the texture should be offset. buffer_offset: Point, @@ -64,8 +64,8 @@ impl ClosingWindow { renderer: &mut GlesRenderer, snapshot: RenderSnapshot, scale: Scale, - geo_size: Size, - pos: Point, + geo_size: Size, + pos: Point, anim: Animation, ) -> anyhow::Result { let _span = tracy_client::span!("ClosingWindow::new"); @@ -123,7 +123,7 @@ impl ClosingWindow { pub fn render( &self, renderer: &mut GlesRenderer, - view_rect: Rectangle, + view_rect: Rectangle, scale: Scale, target: RenderTarget, ) -> ClosingWindowRenderElement { @@ -140,7 +140,12 @@ impl ClosingWindow { let area_loc = Vec2::new(view_rect.loc.x as f32, view_rect.loc.y as f32); let area_size = Vec2::new(view_rect.size.w as f32, view_rect.size.h as f32); - let geo_loc = Vec2::new(self.pos.x as f32, self.pos.y as f32); + // Round to physical pixels relative to the view position. This is similar to what + // happens when rendering normal windows. + let relative = self.pos - view_rect.loc; + let pos = view_rect.loc + relative.to_physical_precise_round(scale).to_logical(scale); + + let geo_loc = Vec2::new(pos.x as f32, pos.y as f32); let geo_size = Vec2::new(self.geo_size.w as f32, self.geo_size.h as f32); let input_to_geo = Mat3::from_scale(area_size / geo_size) @@ -171,7 +176,7 @@ impl ClosingWindow { HashMap::from([(String::from("niri_tex"), buffer.texture().clone())]), Kind::Unspecified, ) - .with_location(Point::from((0, 0))) + .with_location(Point::from((0., 0.))) .into(); } @@ -186,15 +191,15 @@ impl ClosingWindow { let elem = PrimaryGpuTextureRenderElement(elem); - let center = self.geo_size.to_point().to_f64().downscale(2.); + let center = self.geo_size.to_point().downscale(2.); let elem = RescaleRenderElement::from_element( elem, (center - offset).to_physical_precise_round(scale), ((1. - clamped_progress) / 5. + 0.8).max(0.), ); - let mut location = self.pos.to_f64() + offset; - location.x -= view_rect.loc.x as f64; + let mut location = self.pos + offset; + location.x -= view_rect.loc.x; let elem = RelocateRenderElement::from_element( elem, location.to_physical_precise_round(scale), diff --git a/src/layout/focus_ring.rs b/src/layout/focus_ring.rs index b198e838..e7c0388b 100644 --- a/src/layout/focus_ring.rs +++ b/src/layout/focus_ring.rs @@ -1,23 +1,22 @@ -use std::cmp::{max, min}; use std::iter::zip; use arrayvec::ArrayVec; use niri_config::{CornerRadius, Gradient, GradientRelativeTo}; -use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement}; use smithay::backend::renderer::element::Kind; -use smithay::utils::{Logical, Point, Rectangle, Scale, Size}; +use smithay::utils::{Logical, Point, Rectangle, Size}; use crate::niri_render_elements; use crate::render_helpers::border::BorderRenderElement; use crate::render_helpers::renderer::NiriRenderer; +use crate::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement}; #[derive(Debug)] pub struct FocusRing { buffers: [SolidColorBuffer; 8], - locations: [Point; 8], - sizes: [Size; 8], + locations: [Point; 8], + sizes: [Size; 8], borders: [BorderRenderElement; 8], - full_size: Size, + full_size: Size, is_border: bool, use_border_shader: bool, config: niri_config::FocusRing, @@ -56,14 +55,15 @@ impl FocusRing { pub fn update_render_elements( &mut self, - win_size: Size, + win_size: Size, is_active: bool, is_border: bool, - view_rect: Rectangle, + view_rect: Rectangle, radius: CornerRadius, + scale: f64, ) { - let width = i32::from(self.config.width); - self.full_size = win_size + Size::from((width * 2, width * 2)); + let width = self.config.width.0; + self.full_size = win_size + Size::from((width, width)).upscale(2.); let color = if is_active { self.config.active_color @@ -107,39 +107,48 @@ impl FocusRing { 0. }; + let ceil = |logical: f64| (logical * scale).ceil() / scale; + + // All of this stuff should end up aligned to physical pixels because: + // * Window size and border width are rounded to physical pixels before being passed to this + // function. + // * We will ceil the corner radii below. + // * We do not divide anything, only add, subtract and multiply by integers. + // * At rendering time, tile positions are rounded to physical pixels. + if is_border { - let top_left = max(width, radius.top_left.ceil() as i32); - let top_right = min( + let top_left = f64::max(width, ceil(f64::from(radius.top_left))); + let top_right = f64::min( self.full_size.w - top_left, - max(width, radius.top_right.ceil() as i32), + f64::max(width, ceil(f64::from(radius.top_right))), ); - let bottom_left = min( + let bottom_left = f64::min( self.full_size.h - top_left, - max(width, radius.bottom_left.ceil() as i32), + f64::max(width, ceil(f64::from(radius.bottom_left))), ); - let bottom_right = min( + let bottom_right = f64::min( self.full_size.h - top_right, - min( + f64::min( self.full_size.w - bottom_left, - max(width, radius.bottom_right.ceil() as i32), + f64::max(width, ceil(f64::from(radius.bottom_right))), ), ); // Top edge. - self.sizes[0] = Size::from((win_size.w + width * 2 - top_left - top_right, width)); + self.sizes[0] = Size::from((win_size.w + width * 2. - top_left - top_right, width)); self.locations[0] = Point::from((-width + top_left, -width)); // Bottom edge. self.sizes[1] = - Size::from((win_size.w + width * 2 - bottom_left - bottom_right, width)); + Size::from((win_size.w + width * 2. - bottom_left - bottom_right, width)); self.locations[1] = Point::from((-width + bottom_left, win_size.h)); // Left edge. - self.sizes[2] = Size::from((width, win_size.h + width * 2 - top_left - bottom_left)); + self.sizes[2] = Size::from((width, win_size.h + width * 2. - top_left - bottom_left)); self.locations[2] = Point::from((-width, -width + top_left)); // Right edge. - self.sizes[3] = Size::from((width, win_size.h + width * 2 - top_right - bottom_right)); + self.sizes[3] = Size::from((width, win_size.h + width * 2. - top_right - bottom_right)); self.locations[3] = Point::from((win_size.w, -width + top_right)); // Top-left corner. @@ -203,8 +212,7 @@ impl FocusRing { pub fn render( &self, renderer: &mut impl NiriRenderer, - location: Point, - scale: Scale, + location: Point, ) -> impl Iterator { let mut rv = ArrayVec::<_, 8>::new(); @@ -215,24 +223,17 @@ impl FocusRing { let border_width = -self.locations[0].y; // If drawing as a border with width = 0, then there's nothing to draw. - if self.is_border && border_width == 0 { + if self.is_border && border_width == 0. { return rv.into_iter(); } let has_border_shader = BorderRenderElement::has_shader(renderer); - let mut push = |buffer, border: &BorderRenderElement, location: Point| { + let mut push = |buffer, border: &BorderRenderElement, location: Point| { let elem = if self.use_border_shader && has_border_shader { border.clone().with_location(location).into() } else { - SolidColorRenderElement::from_buffer( - buffer, - location.to_physical_precise_round(scale), - scale, - 1., - Kind::Unspecified, - ) - .into() + SolidColorRenderElement::from_buffer(buffer, location, 1., Kind::Unspecified).into() }; rv.push(elem); }; @@ -252,8 +253,8 @@ impl FocusRing { rv.into_iter() } - pub fn width(&self) -> i32 { - self.config.width.into() + pub fn width(&self) -> f64 { + self.config.width.0 } pub fn is_off(&self) -> bool { diff --git a/src/layout/mod.rs b/src/layout/mod.rs index 135f694f..ffc5e0e8 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -34,9 +34,8 @@ use std::mem; use std::rc::Rc; use std::time::Duration; -use niri_config::{CenterFocusedColumn, Config, Struts, Workspace as WorkspaceConfig}; +use niri_config::{CenterFocusedColumn, Config, FloatOrInt, Struts, Workspace as WorkspaceConfig}; use niri_ipc::SizeChange; -use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement}; use smithay::backend::renderer::element::surface::WaylandSurfaceRenderElement; use smithay::backend::renderer::element::Id; use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture}; @@ -50,9 +49,10 @@ use self::workspace::{compute_working_area, Column, ColumnWidth, OutputId, Works use crate::niri_render_elements; use crate::render_helpers::renderer::NiriRenderer; use crate::render_helpers::snapshot::RenderSnapshot; +use crate::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement}; use crate::render_helpers::texture::TextureBuffer; use crate::render_helpers::{BakedBuffer, RenderTarget, SplitElements}; -use crate::utils::{output_size, ResizeEdge}; +use crate::utils::{output_size, round_logical_in_physical_max1, ResizeEdge}; use crate::window::ResolvedWindowRules; pub mod closing_window; @@ -63,7 +63,7 @@ pub mod tile; pub mod workspace; /// Size changes up to this many pixels don't animate. -pub const RESIZE_ANIMATION_THRESHOLD: i32 = 10; +pub const RESIZE_ANIMATION_THRESHOLD: f64 = 10.; niri_render_elements! { LayoutElementRenderElement => { @@ -110,7 +110,7 @@ pub trait LayoutElement { fn render( &self, renderer: &mut R, - location: Point, + location: Point, scale: Scale, alpha: f32, target: RenderTarget, @@ -120,7 +120,7 @@ pub trait LayoutElement { fn render_normal( &self, renderer: &mut R, - location: Point, + location: Point, scale: Scale, alpha: f32, target: RenderTarget, @@ -132,7 +132,7 @@ pub trait LayoutElement { fn render_popups( &self, renderer: &mut R, - location: Point, + location: Point, scale: Scale, alpha: f32, target: RenderTarget, @@ -206,10 +206,10 @@ enum MonitorSet { }, } -#[derive(Debug, PartialEq)] +#[derive(Debug, Clone, PartialEq)] pub struct Options { /// Padding around windows in logical pixels. - pub gaps: i32, + pub gaps: f64, /// Extra padding around the working area in logical pixels. pub struts: Struts, pub focus_ring: niri_config::FocusRing, @@ -225,7 +225,7 @@ pub struct Options { impl Default for Options { fn default() -> Self { Self { - gaps: 16, + gaps: 16., struts: Default::default(), focus_ring: Default::default(), border: Default::default(), @@ -265,7 +265,7 @@ impl Options { .unwrap_or(Some(ColumnWidth::Proportion(0.5))); Self { - gaps: layout.gaps.into(), + gaps: layout.gaps.0, struts: layout.struts, focus_ring: layout.focus_ring, border: layout.border, @@ -275,6 +275,16 @@ impl Options { animations: config.animations.clone(), } } + + fn adjusted_for_scale(mut self, scale: f64) -> Self { + let round = |logical: f64| round_logical_in_physical_max1(scale, logical); + + self.gaps = round(self.gaps); + self.focus_ring.width = FloatOrInt(round(self.focus_ring.width.0)); + self.border.width = FloatOrInt(round(self.border.width.0)); + + self + } } impl Layout { @@ -486,12 +496,12 @@ impl Layout { width: Option, is_full_width: bool, ) -> Option<&Output> { - let mut width = width.unwrap_or_else(|| ColumnWidth::Fixed(window.size().w)); + let mut width = width.unwrap_or_else(|| ColumnWidth::Fixed(f64::from(window.size().w))); if let ColumnWidth::Fixed(w) = &mut width { let rules = window.rules(); let border_config = rules.border.resolve_against(self.options.border); if !border_config.off { - *w += border_config.width as i32 * 2; + *w += border_config.width.0 * 2.; } } @@ -575,12 +585,12 @@ impl Layout { width: Option, is_full_width: bool, ) -> Option<&Output> { - let mut width = width.unwrap_or_else(|| ColumnWidth::Fixed(window.size().w)); + let mut width = width.unwrap_or_else(|| ColumnWidth::Fixed(f64::from(window.size().w))); if let ColumnWidth::Fixed(w) = &mut width { let rules = window.rules(); let border_config = rules.border.resolve_against(self.options.border); if !border_config.off { - *w += border_config.width as i32 * 2; + *w += border_config.width.0 * 2.; } } @@ -633,12 +643,12 @@ impl Layout { width: Option, is_full_width: bool, ) -> Option<&Output> { - let mut width = width.unwrap_or_else(|| ColumnWidth::Fixed(window.size().w)); + let mut width = width.unwrap_or_else(|| ColumnWidth::Fixed(f64::from(window.size().w))); if let ColumnWidth::Fixed(w) = &mut width { let rules = window.rules(); let border_config = rules.border.resolve_against(self.options.border); if !border_config.off { - *w += border_config.width as i32 * 2; + *w += border_config.width.0 * 2.; } } @@ -671,12 +681,12 @@ impl Layout { width: Option, is_full_width: bool, ) { - let mut width = width.unwrap_or_else(|| ColumnWidth::Fixed(window.size().w)); + let mut width = width.unwrap_or_else(|| ColumnWidth::Fixed(f64::from(window.size().w))); if let ColumnWidth::Fixed(w) = &mut width { let rules = window.rules(); let border_config = rules.border.resolve_against(self.options.border); if !border_config.off { - *w += border_config.width as i32 * 2; + *w += border_config.width.0 * 2.; } } @@ -887,7 +897,7 @@ impl Layout { None } - pub fn window_loc(&self, window: &W::Id) -> Option> { + pub fn window_loc(&self, window: &W::Id) -> Option> { match &self.monitor_set { MonitorSet::Normal { monitors, .. } => { for mon in monitors { @@ -1440,7 +1450,7 @@ impl Layout { &self, output: &Output, pos_within_output: Point, - ) -> Option<(&W, Option>)> { + ) -> Option<(&W, Option>)> { let MonitorSet::Normal { monitors, .. } = &self.monitor_set else { return None; }; @@ -1485,8 +1495,15 @@ impl Layout { ); assert_eq!( - workspace.options, self.options, - "workspace options must be synchronized with layout" + workspace.base_options, self.options, + "workspace base options must be synchronized with layout" + ); + + let options = Options::clone(&workspace.base_options) + .adjusted_for_scale(workspace.scale().fractional_scale()); + assert_eq!( + &*workspace.options, &options, + "workspace options must be base options adjusted for workspace scale" ); assert!( @@ -1589,10 +1606,17 @@ impl Layout { for workspace in &monitor.workspaces { assert_eq!( - workspace.options, self.options, + workspace.base_options, self.options, "workspace options must be synchronized with layout" ); + let options = Options::clone(&workspace.base_options) + .adjusted_for_scale(workspace.scale().fractional_scale()); + assert_eq!( + &*workspace.options, &options, + "workspace options must be base options adjusted for workspace scale" + ); + assert!( seen_workspace_id.insert(workspace.id()), "workspace id must be unique" @@ -2368,13 +2392,14 @@ impl Default for MonitorSet { mod tests { use std::cell::Cell; - use niri_config::WorkspaceName; + use niri_config::{FloatOrInt, WorkspaceName}; use proptest::prelude::*; use proptest_derive::Arbitrary; use smithay::output::{Mode, PhysicalProperties, Subpixel}; use smithay::utils::Rectangle; use super::*; + use crate::utils::round_logical_in_physical; impl Default for Layout { fn default() -> Self { @@ -2459,7 +2484,7 @@ mod tests { fn render( &self, _renderer: &mut R, - _location: Point, + _location: Point, _scale: Scale, _alpha: f32, _target: RenderTarget, @@ -2595,9 +2620,19 @@ mod tests { ] } + fn arbitrary_scale() -> impl Strategy { + prop_oneof![Just(1.), Just(1.5), Just(2.),] + } + #[derive(Debug, Clone, Copy, Arbitrary)] enum Op { AddOutput(#[proptest(strategy = "1..=5usize")] usize), + AddScaledOutput { + #[proptest(strategy = "1..=5usize")] + id: usize, + #[proptest(strategy = "arbitrary_scale()")] + scale: f64, + }, RemoveOutput(#[proptest(strategy = "1..=5usize")] usize), FocusOutput(#[proptest(strategy = "1..=5usize")] usize), AddNamedWorkspace { @@ -2769,6 +2804,32 @@ mod tests { ); layout.add_output(output.clone()); } + Op::AddScaledOutput { id, scale } => { + let name = format!("output{id}"); + if layout.outputs().any(|o| o.name() == name) { + return; + } + + let output = Output::new( + name, + PhysicalProperties { + size: Size::from((1280, 720)), + subpixel: Subpixel::Unknown, + make: String::new(), + model: String::new(), + }, + ); + output.change_current_state( + Some(Mode { + size: Size::from((1280, 720)), + refresh: 60000, + }), + None, + Some(smithay::output::Scale::Fractional(scale)), + None, + ); + layout.add_output(output.clone()); + } Op::RemoveOutput(id) => { let name = format!("output{id}"); let Some(output) = layout.outputs().find(|o| o.name() == name).cloned() else { @@ -3560,7 +3621,7 @@ mod tests { let mut options = Options::default(); options.border.off = false; - options.border.width = 1; + options.border.width = FloatOrInt(1.); check_ops_with_options(options, &ops); } @@ -3578,7 +3639,7 @@ mod tests { let mut options = Options::default(); options.border.off = false; - options.border.width = 1; + options.border.width = FloatOrInt(1.); check_ops_with_options(options, &ops); } @@ -3916,7 +3977,7 @@ mod tests { fn config_change_updates_cached_sizes() { let mut config = Config::default(); config.layout.border.off = false; - config.layout.border.width = 2; + config.layout.border.width = FloatOrInt(2.); let mut layout = Layout::new(&config); @@ -3927,18 +3988,83 @@ mod tests { } .apply(&mut layout); - config.layout.border.width = 4; + config.layout.border.width = FloatOrInt(4.); layout.update_config(&config); layout.verify_invariants(); } - fn arbitrary_spacing() -> impl Strategy { + #[test] + fn working_area_starts_at_physical_pixel() { + let struts = Struts { + left: FloatOrInt(0.5), + right: FloatOrInt(1.), + top: FloatOrInt(0.75), + bottom: FloatOrInt(1.), + }; + + let output = Output::new( + String::from("output"), + PhysicalProperties { + size: Size::from((1280, 720)), + subpixel: Subpixel::Unknown, + make: String::new(), + model: String::new(), + }, + ); + output.change_current_state( + Some(Mode { + size: Size::from((1280, 720)), + refresh: 60000, + }), + None, + None, + None, + ); + + let area = compute_working_area(&output, struts); + + assert_eq!(round_logical_in_physical(1., area.loc.x), area.loc.x); + assert_eq!(round_logical_in_physical(1., area.loc.y), area.loc.y); + } + + #[test] + fn large_fractional_strut() { + let struts = Struts { + left: FloatOrInt(0.), + right: FloatOrInt(0.), + top: FloatOrInt(50000.5), + bottom: FloatOrInt(0.), + }; + + let output = Output::new( + String::from("output"), + PhysicalProperties { + size: Size::from((1280, 720)), + subpixel: Subpixel::Unknown, + make: String::new(), + model: String::new(), + }, + ); + output.change_current_state( + Some(Mode { + size: Size::from((1280, 720)), + refresh: 60000, + }), + None, + None, + None, + ); + + compute_working_area(&output, struts); + } + + fn arbitrary_spacing() -> impl Strategy { // Give equal weight to: // - 0: the element is disabled // - 4: some reasonable value // - random value, likely unreasonably big - prop_oneof![Just(0), Just(4), (1..=u16::MAX)] + prop_oneof![Just(0.), Just(4.), ((1.)..=65535.)] } fn arbitrary_struts() -> impl Strategy { @@ -3949,10 +4075,10 @@ mod tests { arbitrary_spacing(), ) .prop_map(|(left, right, top, bottom)| Struts { - left, - right, - top, - bottom, + left: FloatOrInt(left), + right: FloatOrInt(right), + top: FloatOrInt(top), + bottom: FloatOrInt(bottom), }) } @@ -3971,7 +4097,7 @@ mod tests { ) -> niri_config::FocusRing { niri_config::FocusRing { off, - width, + width: FloatOrInt(width), ..Default::default() } } @@ -3984,7 +4110,7 @@ mod tests { ) -> niri_config::Border { niri_config::Border { off, - width, + width: FloatOrInt(width), ..Default::default() } } @@ -3999,7 +4125,7 @@ mod tests { center_focused_column in arbitrary_center_focused_column(), ) -> Options { Options { - gaps: gaps.into(), + gaps, struts, center_focused_column, focus_ring, diff --git a/src/layout/monitor.rs b/src/layout/monitor.rs index b166879b..eb72242f 100644 --- a/src/layout/monitor.rs +++ b/src/layout/monitor.rs @@ -7,7 +7,7 @@ use smithay::backend::renderer::element::utils::{ CropRenderElement, Relocate, RelocateRenderElement, }; use smithay::output::Output; -use smithay::utils::{Logical, Point, Rectangle, Scale}; +use smithay::utils::{Logical, Point, Rectangle}; use super::workspace::{ compute_working_area, Column, ColumnWidth, OutputId, Workspace, WorkspaceId, @@ -19,7 +19,7 @@ use crate::input::swipe_tracker::SwipeTracker; use crate::render_helpers::renderer::NiriRenderer; use crate::render_helpers::RenderTarget; use crate::rubber_band::RubberBand; -use crate::utils::{output_size, ResizeEdge}; +use crate::utils::{output_size, to_physical_precise_round, ResizeEdge}; /// Amount of touchpad movement to scroll the height of one workspace. const WORKSPACE_GESTURE_MOVEMENT: f64 = 300.; @@ -761,16 +761,16 @@ impl Monitor { /// Returns the geometry of the active tile relative to and clamped to the output. /// /// During animations, assumes the final view position. - pub fn active_tile_visual_rectangle(&self) -> Option> { + pub fn active_tile_visual_rectangle(&self) -> Option> { let mut rect = self.active_workspace_ref().active_tile_visual_rectangle()?; if let Some(switch) = &self.workspace_switch { - let size = output_size(&self.output); + let size = output_size(&self.output).to_f64(); let offset = switch.target_idx() - self.active_workspace_idx as f64; - let offset = (offset * size.h as f64).round() as i32; + let offset = offset * size.h; - let clip_rect = Rectangle::from_loc_and_size((0, -offset), size); + let clip_rect = Rectangle::from_loc_and_size((0., -offset), size); rect = rect.intersection(clip_rect)?; } @@ -780,16 +780,16 @@ impl Monitor { pub fn window_under( &self, pos_within_output: Point, - ) -> Option<(&W, Option>)> { + ) -> Option<(&W, Option>)> { match &self.workspace_switch { Some(switch) => { - let size = output_size(&self.output); + let size = output_size(&self.output).to_f64(); let render_idx = switch.current_idx(); let before_idx = render_idx.floor(); let after_idx = render_idx.ceil(); - let offset = ((render_idx - before_idx) * size.h as f64).round() as i32; + let offset = (render_idx - before_idx) * size.h; if after_idx < 0. || before_idx as usize >= self.workspaces.len() { return None; @@ -797,22 +797,22 @@ impl Monitor { let after_idx = after_idx as usize; - let (idx, ws_offset) = if pos_within_output.y < (size.h - offset) as f64 { + let (idx, ws_offset) = if pos_within_output.y < size.h - offset { if before_idx < 0. { return None; } - (before_idx as usize, Point::from((0, offset))) + (before_idx as usize, Point::from((0., offset))) } else { if after_idx >= self.workspaces.len() { return None; } - (after_idx, Point::from((0, -size.h + offset))) + (after_idx, Point::from((0., -size.h + offset))) }; let ws = &self.workspaces[idx]; - let (win, win_pos) = ws.window_under(pos_within_output + ws_offset.to_f64())?; + let (win, win_pos) = ws.window_under(pos_within_output + ws_offset)?; Some((win, win_pos.map(|p| p - ws_offset))) } None => { @@ -831,7 +831,7 @@ impl Monitor { let before_idx = render_idx.floor(); let after_idx = render_idx.ceil(); - let offset = ((render_idx - before_idx) * size.h as f64).round() as i32; + let offset = (render_idx - before_idx) * size.h; if after_idx < 0. || before_idx as usize >= self.workspaces.len() { return None; @@ -839,22 +839,22 @@ impl Monitor { let after_idx = after_idx as usize; - let (idx, ws_offset) = if pos_within_output.y < (size.h - offset) as f64 { + let (idx, ws_offset) = if pos_within_output.y < size.h - offset { if before_idx < 0. { return None; } - (before_idx as usize, Point::from((0, offset))) + (before_idx as usize, Point::from((0., offset))) } else { if after_idx >= self.workspaces.len() {