diff --git a/Cargo.lock b/Cargo.lock index 8026862..54412c8 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 cb15c2c..dd0f6e0 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 4fbb96e..a9285fa 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 f9871a9..203f31b 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 b674157..e76820f 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 d787217..2e730db 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 301fc19..9526187 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 f19ec5b..be6150d 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 1ce770b..dbba519 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 c1e29ea..3e6234f 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 acb08e1..d0e1c55 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 38483ca..535bae3 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 4e2d278..b4f9f96 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 8945d2a..06b5927 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 b198e83..e7c0388 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 135f694..ffc5e0e 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 b166879..eb72242 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() { return None; } - (after_idx, Point::from((0, -size.h + offset))) + (after_idx, Point::from((0., -size.h + offset))) }; let ws = &self.workspaces[idx]; - ws.resize_edges_under(pos_within_output + ws_offset.to_f64()) + ws.resize_edges_under(pos_within_output + ws_offset) } None => { let ws = &self.workspaces[self.active_workspace_idx]; @@ -880,10 +880,8 @@ impl Monitor { ) -> Vec> { let _span = tracy_client::span!("Monitor::render_elements"); - let output_scale = Scale::from(self.output.current_scale().fractional_scale()); - let output_transform = self.output.current_transform(); - let output_mode = self.output.current_mode().unwrap(); - let size = output_transform.transform_size(output_mode.size); + let scale = self.output.current_scale().fractional_scale(); + let size = output_size(&self.output); match &self.workspace_switch { Some(switch) => { @@ -891,7 +889,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 vec![]; @@ -904,7 +902,7 @@ impl Monitor { Some(RelocateRenderElement::from_element( CropRenderElement::from_element( elem, - output_scale, + scale, // HACK: crop to infinite bounds for all sides except the side // where the workspaces join, // otherwise it will cut pixel shaders and mess up @@ -914,7 +912,7 @@ impl Monitor { (i32::MAX / 2, i32::MAX / 2), ), )?, - (0, -offset + size.h), + Point::from((0., -offset + size.h)).to_physical_precise_round(scale), Relocate::Relative, )) }); @@ -934,13 +932,13 @@ impl Monitor { Some(RelocateRenderElement::from_element( CropRenderElement::from_element( elem, - output_scale, + scale, Rectangle::from_extemities( (-i32::MAX / 2, -i32::MAX / 2), - (i32::MAX / 2, size.h), + (i32::MAX / 2, to_physical_precise_round(scale, size.h)), ), )?, - (0, -offset), + Point::from((0., -offset)).to_physical_precise_round(scale), Relocate::Relative, )) }); @@ -955,7 +953,7 @@ impl Monitor { Some(RelocateRenderElement::from_element( CropRenderElement::from_element( elem, - output_scale, + scale, // HACK: set infinite crop bounds due to a damage tracking bug // which causes glitched rendering for maximized GTK windows. // FIXME: use proper bounds after fixing the Crop element. diff --git a/src/layout/opening_window.rs b/src/layout/opening_window.rs index 38eb89a..d265c46 100644 --- a/src/layout/opening_window.rs +++ b/src/layout/opening_window.rs @@ -55,8 +55,8 @@ impl OpenAnimation { &self, renderer: &mut GlesRenderer, elements: &[impl RenderElement], - geo_size: Size, - location: Point, + geo_size: Size, + location: Point, scale: Scale, ) -> anyhow::Result { let progress = self.anim.value(); @@ -75,17 +75,17 @@ impl OpenAnimation { let texture_size = geo.size.to_f64().to_logical(scale); if Shaders::get(renderer).program(ProgramType::Open).is_some() { - let mut area = Rectangle::from_loc_and_size(location.to_f64() + offset, texture_size); + let mut area = Rectangle::from_loc_and_size(location + offset, texture_size); // Expand the area a bit to allow for more varied effects. let mut target_size = area.size.upscale(1.5); target_size.w = f64::max(area.size.w + 1000., target_size.w); target_size.h = f64::max(area.size.h + 1000., target_size.h); - let diff = target_size.to_point() - area.size.to_point(); - area.loc -= diff.downscale(2.); - area.size += diff.to_size(); + let diff = (target_size.to_point() - area.size.to_point()).downscale(2.); + let diff = diff.to_physical_precise_round(scale).to_logical(scale); + area.loc -= diff; + area.size += diff.upscale(2.).to_size(); - let area = area.to_i32_up(); let area_loc = Vec2::new(area.loc.x as f32, area.loc.y as f32); let area_size = Vec2::new(area.size.w as f32, area.size.h as f32); @@ -135,7 +135,7 @@ impl OpenAnimation { let elem = PrimaryGpuTextureRenderElement(elem); - let center = geo_size.to_point().to_f64().downscale(2.); + let center = geo_size.to_point().downscale(2.); let elem = RescaleRenderElement::from_element( elem, (center - offset).to_physical_precise_round(scale), @@ -144,7 +144,7 @@ impl OpenAnimation { let elem = RelocateRenderElement::from_element( elem, - (location.to_f64() + offset).to_physical_precise_round(scale), + (location + offset).to_physical_precise_round(scale), Relocate::Relative, ); diff --git a/src/layout/tile.rs b/src/layout/tile.rs index 719f12b..943a957 100644 --- a/src/layout/tile.rs +++ b/src/layout/tile.rs @@ -1,10 +1,8 @@ -use std::cmp::max; use std::rc::Rc; use std::time::Duration; use niri_config::CornerRadius; use smithay::backend::allocator::Fourcc; -use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement}; use smithay::backend::renderer::element::{Element, Kind}; use smithay::backend::renderer::gles::GlesRenderer; use smithay::utils::{Logical, Point, Rectangle, Scale, Size, Transform}; @@ -23,6 +21,7 @@ use crate::render_helpers::damage::ExtraDamage; use crate::render_helpers::renderer::NiriRenderer; use crate::render_helpers::resize::ResizeRenderElement; use crate::render_helpers::snapshot::RenderSnapshot; +use crate::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement}; use crate::render_helpers::{render_to_encompassing_texture, RenderTarget}; /// Toplevel window with decorations. @@ -50,7 +49,7 @@ pub struct Tile { fullscreen_backdrop: SolidColorBuffer, /// The size we were requested to fullscreen into. - fullscreen_size: Size, + fullscreen_size: Size, /// The animation upon opening a window. open_animation: Option, @@ -70,6 +69,9 @@ pub struct Tile { /// Extra damage for clipped surface corner radius changes. rounded_corner_damage: RoundedCornerDamage, + /// Scale of the output the tile is on (and rounds its sizes to). + scale: f64, + /// Configurable properties of the layout. pub options: Rc, } @@ -93,18 +95,18 @@ type TileRenderSnapshot = #[derive(Debug)] struct ResizeAnimation { anim: Animation, - size_from: Size, + size_from: Size, snapshot: LayoutElementRenderSnapshot, } #[derive(Debug)] struct MoveAnimation { anim: Animation, - from: i32, + from: f64, } impl Tile { - pub fn new(window: W, options: Rc) -> Self { + pub fn new(window: W, scale: f64, options: Rc) -> Self { let rules = window.rules(); let border_config = rules.border.resolve_against(options.border); let focus_ring_config = rules.focus_ring.resolve_against(options.focus_ring.into()); @@ -114,7 +116,7 @@ impl Tile { border: FocusRing::new(border_config.into()), focus_ring: FocusRing::new(focus_ring_config.into()), is_fullscreen: false, // FIXME: up-to-date fullscreen right away, but we need size. - fullscreen_backdrop: SolidColorBuffer::new((0, 0), [0., 0., 0., 1.]), + fullscreen_backdrop: SolidColorBuffer::new((0., 0.), [0., 0., 0., 1.]), fullscreen_size: Default::default(), open_animation: None, resize_animation: None, @@ -122,11 +124,13 @@ impl Tile { move_y_animation: None, unmap_snapshot: None, rounded_corner_damage: Default::default(), + scale, options, } } - pub fn update_config(&mut self, options: Rc) { + pub fn update_config(&mut self, scale: f64, options: Rc) { + self.scale = scale; self.options = options; let rules = self.window.rules(); @@ -147,7 +151,7 @@ impl Tile { pub fn update_window(&mut self) { // FIXME: remove when we can get a fullscreen size right away. - if self.fullscreen_size != Size::from((0, 0)) { + if self.fullscreen_size != Size::from((0., 0.)) { self.is_fullscreen = self.window.is_fullscreen(); } @@ -160,16 +164,16 @@ impl Tile { let val = resize.anim.value(); let size_from = resize.size_from; - size.w = (size_from.w as f64 + (size.w - size_from.w) as f64 * val).round() as i32; - size.h = (size_from.h as f64 + (size.h - size_from.h) as f64 * val).round() as i32; + size.w = size_from.w + (size.w - size_from.w) * val; + size.h = size_from.h + (size.h - size_from.h) * val; size } else { animate_from.size }; - let change = self.window.size().to_point() - size_from.to_point(); - let change = max(change.x.abs(), change.y.abs()); + let change = self.window.size().to_f64().to_point() - size_from.to_point(); + let change = f64::max(change.x.abs(), change.y.abs()); if change > RESIZE_ANIMATION_THRESHOLD { let anim = Animation::new(0., 1., 0., self.options.animations.window_resize.anim); self.resize_animation = Some(ResizeAnimation { @@ -235,13 +239,13 @@ impl Tile { || self.move_y_animation.is_some() } - pub fn update(&mut self, is_active: bool, view_rect: Rectangle) { + pub fn update(&mut self, is_active: bool, view_rect: Rectangle) { let rules = self.window.rules(); let draw_border_with_background = rules .draw_border_with_background .unwrap_or_else(|| !self.window.has_ssd()); - let border_width = self.effective_border_width().unwrap_or(0); + let border_width = self.effective_border_width().unwrap_or(0.); let radius = if self.is_fullscreen { CornerRadius::default() } else { @@ -260,6 +264,7 @@ impl Tile { view_rect.size, ), radius, + self.scale, ); let draw_focus_ring_with_background = if self.effective_border_width().is_some() { @@ -281,20 +286,25 @@ impl Tile { !draw_focus_ring_with_background, view_rect, radius, + self.scale, ); } - pub fn render_offset(&self) -> Point { + pub fn scale(&self) -> f64 { + self.scale + } + + pub fn render_offset(&self) -> Point { let mut offset = Point::from((0., 0.)); if let Some(move_) = &self.move_x_animation { - offset.x += f64::from(move_.from) * move_.anim.value(); + offset.x += move_.from * move_.anim.value(); } if let Some(move_) = &self.move_y_animation { - offset.y += f64::from(move_.from) * move_.anim.value(); + offset.y += move_.from * move_.anim.value(); } - offset.to_i32_round() + offset } pub fn start_open_animation(&mut self) { @@ -310,16 +320,16 @@ impl Tile { self.resize_animation.as_ref().map(|resize| &resize.anim) } - pub fn animate_move_from(&mut self, from: Point) { + pub fn animate_move_from(&mut self, from: Point) { self.animate_move_x_from(from.x); self.animate_move_y_from(from.y); } - pub fn animate_move_x_from(&mut self, from: i32) { + pub fn animate_move_x_from(&mut self, from: f64) { self.animate_move_x_from_with_config(from, self.options.animations.window_movement.0); } - pub fn animate_move_x_from_with_config(&mut self, from: i32, config: niri_config::Animation) { + pub fn animate_move_x_from_with_config(&mut self, from: f64, config: niri_config::Animation) { let current_offset = self.render_offset().x; // Preserve the previous config if ongoing. @@ -334,11 +344,11 @@ impl Tile { }); } - pub fn animate_move_y_from(&mut self, from: i32) { + pub fn animate_move_y_from(&mut self, from: f64) { self.animate_move_y_from_with_config(from, self.options.animations.window_movement.0); } - pub fn animate_move_y_from_with_config(&mut self, from: i32, config: niri_config::Animation) { + pub fn animate_move_y_from_with_config(&mut self, from: f64, config: niri_config::Animation) { let current_offset = self.render_offset().y; // Preserve the previous config if ongoing. @@ -370,7 +380,7 @@ impl Tile { } /// Returns `None` if the border is hidden and `Some(width)` if it should be shown. - fn effective_border_width(&self) -> Option { + fn effective_border_width(&self) -> Option { if self.is_fullscreen { return None; } @@ -383,22 +393,27 @@ impl Tile { } /// Returns the location of the window's visual geometry within this Tile. - pub fn window_loc(&self) -> Point { - let mut loc = Point::from((0, 0)); + pub fn window_loc(&self) -> Point { + let mut loc = Point::from((0., 0.)); // In fullscreen, center the window in the given size. if self.is_fullscreen { - let window_size = self.window.size(); + let window_size = self.window_size(); let target_size = self.fullscreen_size; // Windows aren't supposed to be larger than the fullscreen size, but in case we get // one, leave it at the top-left as usual. if window_size.w < target_size.w { - loc.x += (target_size.w - window_size.w) / 2; + loc.x += (target_size.w - window_size.w) / 2.; } if window_size.h < target_size.h { - loc.y += (target_size.h - window_size.h) / 2; + loc.y += (target_size.h - window_size.h) / 2.; } + + // Round to physical pixels. + loc = loc + .to_physical_precise_round(self.scale) + .to_logical(self.scale); } if let Some(width) = self.effective_border_width() { @@ -408,68 +423,73 @@ impl Tile { loc } - pub fn tile_size(&self) -> Size { - let mut size = self.window.size(); + pub fn tile_size(&self) -> Size { + let mut size = self.window_size(); if self.is_fullscreen { // Normally we'd just return the fullscreen size here, but this makes things a bit // nicer if a fullscreen window is bigger than the fullscreen size for some reason. - size.w = max(size.w, self.fullscreen_size.w); - size.h = max(size.h, self.fullscreen_size.h); + size.w = f64::max(size.w, self.fullscreen_size.w); + size.h = f64::max(size.h, self.fullscreen_size.h); return size; } if let Some(width) = self.effective_border_width() { - size.w = size.w.saturating_add(width * 2); - size.h = size.h.saturating_add(width * 2); + size.w += width * 2.; + size.h += width * 2.; } size } - pub fn window_size(&self) -> Size { - self.window.size() + pub fn window_size(&self) -> Size { + let mut size = self.window.size().to_f64(); + size = size + .to_physical_precise_round(self.scale) + .to_logical(self.scale); + size } - fn animated_window_size(&self) -> Size { - let mut size = self.window.size(); + fn animated_window_size(&self) -> Size { + let mut size = self.window_size(); if let Some(resize) = &self.resize_animation { let val = resize.anim.value(); - let size_from = resize.size_from; + let size_from = resize.size_from.to_f64(); - size.w = (size_from.w as f64 + (size.w - size_from.w) as f64 * val).round() as i32; - size.w = max(1, size.w); - size.h = (size_from.h as f64 + (size.h - size_from.h) as f64 * val).round() as i32; - size.h = max(1, size.h); + size.w = f64::max(1., size_from.w + (size.w - size_from.w) * val); + size.h = f64::max(1., size_from.h + (size.h - size_from.h) * val); + size = size + .to_physical_precise_round(self.scale) + .to_logical(self.scale); } size } - fn animated_tile_size(&self) -> Size { + fn animated_tile_size(&self) -> Size { let mut size = self.animated_window_size(); if self.is_fullscreen { // Normally we'd just return the fullscreen size here, but this makes things a bit // nicer if a fullscreen window is bigger than the fullscreen size for some reason. - size.w = max(size.w, self.fullscreen_size.w); - size.h = max(size.h, self.fullscreen_size.h); + size.w = f64::max(size.w, self.fullscreen_size.w); + size.h = f64::max(size.h, self.fullscreen_size.h); return size; } if let Some(width) = self.effective_border_width() { - size.w = size.w.saturating_add(width * 2); - size.h = size.h.saturating_add(width * 2); + size.w += width * 2.; + size.h += width * 2.; } size } - pub fn buf_loc(&self) -> Point { - let mut loc = Point::from((0, 0)); + pub fn buf_loc(&self) -> Point { + let mut loc = Point::from((0., 0.)); loc += self.window_loc(); - loc += self.window.buf_loc(); + loc += self.window.buf_loc().to_f64(); loc } @@ -479,74 +499,85 @@ impl Tile { } pub fn is_in_activation_region(&self, point: Point) -> bool { - let activation_region = Rectangle::from_loc_and_size((0, 0), self.tile_size()); - activation_region.to_f64().contains(point) + let activation_region = Rectangle::from_loc_and_size((0., 0.), self.tile_size()); + activation_region.contains(point) } - pub fn request_tile_size(&mut self, mut size: Size, animate: bool) { + pub fn request_tile_size(&mut self, mut size: Size, animate: bool) { // Can't go through effective_border_width() because we might be fullscreen. if !self.border.is_off() { let width = self.border.width(); - size.w = max(1, size.w - width * 2); - size.h = max(1, size.h - width * 2); + size.w = f64::max(1., size.w - width * 2.); + size.h = f64::max(1., size.h - width * 2.); } - self.window.request_size(size, animate); + // The size request has to be i32 unfortunately, due to Wayland. We floor here instead of + // round to avoid situations where proportionally-sized columns don't fit on the screen + // exactly. + self.window.request_size(size.to_i32_floor(), animate); } - pub fn tile_width_for_window_width(&self, size: i32) -> i32 { + pub fn tile_width_for_window_width(&self, size: f64) -> f64 { if self.border.is_off() { size } else { - size.saturating_add(self.border.width() * 2) + size + self.border.width() * 2. } } - pub fn tile_height_for_window_height(&self, size: i32) -> i32 { + pub fn tile_height_for_window_height(&self, size: f64) -> f64 { if self.border.is_off() { size } else { - size.saturating_add(self.border.width() * 2) + size + self.border.width() * 2. } } - pub fn window_height_for_tile_height(&self, size: i32) -> i32 { + pub fn window_width_for_tile_width(&self, size: f64) -> f64 { if self.border.is_off() { size } else { - size.saturating_sub(self.border.width() * 2) + size - self.border.width() * 2. } } - pub fn request_fullscreen(&mut self, size: Size) { + pub fn window_height_for_tile_height(&self, size: f64) -> f64 { + if self.border.is_off() { + size + } else { + size - self.border.width() * 2. + } + } + + pub fn request_fullscreen(&mut self, size: Size) { self.fullscreen_backdrop.resize(size); self.fullscreen_size = size; - self.window.request_fullscreen(size); + self.window.request_fullscreen(size.to_i32_round()); } - pub fn min_size(&self) -> Size { - let mut size = self.window.min_size(); + pub fn min_size(&self) -> Size { + let mut size = self.window.min_size().to_f64(); if let Some(width) = self.effective_border_width() { - size.w = max(1, size.w); - size.h = max(1, size.h); + size.w = f64::max(1., size.w); + size.h = f64::max(1., size.h); - size.w = size.w.saturating_add(width * 2); - size.h = size.h.saturating_add(width * 2); + size.w += width * 2.; + size.h += width * 2.; } size } - pub fn max_size(&self) -> Size { - let mut size = self.window.max_size(); + pub fn max_size(&self) -> Size { + let mut size = self.window.max_size().to_f64(); if let Some(width) = self.effective_border_width() { - if size.w > 0 { - size.w = size.w.saturating_add(width * 2); + if size.w > 0. { + size.w += width * 2.; } - if size.h > 0 { - size.h = size.h.saturating_add(width * 2); + if size.h > 0. { + size.h += width * 2.; } } @@ -567,7 +598,7 @@ impl Tile { fn render_inner( &self, renderer: &mut R, - location: Point, + location: Point, scale: Scale, focus_ring: bool, target: RenderTarget, @@ -581,7 +612,7 @@ impl Tile { }; let window_loc = self.window_loc(); - let window_size = self.window_size(); + let window_size = self.window_size().to_f64(); let animated_window_size = self.animated_window_size(); let window_render_loc = location + window_loc; let area = Rectangle::from_loc_and_size(window_render_loc, animated_window_size); @@ -609,7 +640,7 @@ impl Tile { if let Some(texture_from) = resize.snapshot.texture(gles_renderer, scale, target) { let window_elements = self.window.render_normal( gles_renderer, - Point::from((0, 0)), + Point::from((0., 0.)), scale, 1., target, @@ -664,8 +695,7 @@ impl Tile { resize_fallback = Some( SolidColorRenderElement::from_buffer( &fallback_buffer, - area.loc.to_physical_precise_round(scale), - scale, + area.loc, alpha, Kind::Unspecified, ) @@ -726,11 +756,11 @@ impl Tile { if radius != CornerRadius::default() && has_border_shader { return BorderRenderElement::new( geo.size, - Rectangle::from_loc_and_size((0, 0), geo.size), + Rectangle::from_loc_and_size((0., 0.), geo.size), elem.color(), elem.color(), 0., - Rectangle::from_loc_and_size((0, 0), geo.size), + Rectangle::from_loc_and_size((0., 0.), geo.size), 0., radius, ) @@ -758,8 +788,7 @@ impl Tile { let elem = self.is_fullscreen.then(|| { SolidColorRenderElement::from_buffer( &self.fullscreen_backdrop, - location.to_physical_precise_round(scale), - scale, + location, 1., Kind::Unspecified, ) @@ -769,23 +798,19 @@ impl Tile { let elem = self.effective_border_width().map(|width| { self.border - .render(renderer, location + Point::from((width, width)), scale) + .render(renderer, location + Point::from((width, width))) .map(Into::into) }); let rv = rv.chain(elem.into_iter().flatten()); - let elem = focus_ring.then(|| { - self.focus_ring - .render(renderer, location, scale) - .map(Into::into) - }); + let elem = focus_ring.then(|| self.focus_ring.render(renderer, location).map(Into::into)); rv.chain(elem.into_iter().flatten()) } pub fn render( &self, renderer: &mut R, - location: Point, + location: Point, scale: Scale, focus_ring: bool, target: RenderTarget, @@ -798,7 +823,7 @@ impl Tile { if let Some(open) = &self.open_animation { let renderer = renderer.as_gles_renderer(); let elements = - self.render_inner(renderer, Point::from((0, 0)), scale, focus_ring, target); + self.render_inner(renderer, Point::from((0., 0.)), scale, focus_ring, target); let elements = elements.collect::>>(); match open.render(renderer, &elements, self.tile_size(), location, scale) { Ok(elem) => { @@ -843,7 +868,7 @@ impl Tile { let contents = self.render( renderer, - Point::from((0, 0)), + Point::from((0., 0.)), scale, false, RenderTarget::Output, @@ -852,7 +877,7 @@ impl Tile { // A bit of a hack to render blocked out as for screencast, but I think it's fine here. let blocked_out_contents = self.render( renderer, - Point::from((0, 0)), + Point::from((0., 0.)), scale, false, RenderTarget::Screencast, diff --git a/src/layout/workspace.rs b/src/layout/workspace.rs index cf0a83e..e9f6d6a 100644 --- a/src/layout/workspace.rs +++ b/src/layout/workspace.rs @@ -5,6 +5,7 @@ use std::time::Duration; use niri_config::{CenterFocusedColumn, PresetWidth, Struts, Workspace as WorkspaceConfig}; use niri_ipc::SizeChange; +use ordered_float::NotNan; use smithay::backend::renderer::gles::GlesRenderer; use smithay::desktop::{layer_map_for_output, Window}; use smithay::output::Output; @@ -54,13 +55,13 @@ pub struct Workspace { /// /// This should be computed from the current workspace output size, or, if all outputs have /// been disconnected, preserved until a new output is connected. - view_size: Size, + view_size: Size, /// Latest known working area for this workspace. /// /// This is similar to view size, but takes into account things like layer shell exclusive /// zones. - working_area: Rectangle, + working_area: Rectangle, /// Columns of windows on this workspace. pub columns: Vec>, @@ -79,7 +80,7 @@ pub struct Workspace { /// Any gaps, including left padding from work area left exclusive zone, is handled /// with this view offset (rather than added as a constant elsewhere in the code). This allows /// for natural handling of fullscreen windows, which must ignore work area padding. - view_offset: i32, + view_offset: f64, /// Adjustment of the view offset, if one is currently ongoing. view_offset_adj: Option, @@ -94,15 +95,18 @@ pub struct Workspace { /// index of the previous column to activate. /// /// The value is the view offset that the previous column had before, to restore it. - activate_prev_column_on_removal: Option, + activate_prev_column_on_removal: Option, /// View offset to restore after unfullscreening. - view_offset_before_fullscreen: Option, + view_offset_before_fullscreen: Option, /// Windows in the closing animation. closing_windows: Vec, - /// Configurable properties of the layout. + /// Configurable properties of the layout as received from the parent monitor. + pub base_options: Rc, + + /// Configurable properties of the layout with logical sizes adjusted for the current `scale`. pub options: Rc, /// Optional name of this workspace. @@ -137,7 +141,7 @@ niri_render_elements! { #[derive(Debug, Clone, Copy, PartialEq)] struct ColumnData { /// Cached actual column width. - width: i32, + width: f64, } #[derive(Debug)] @@ -152,7 +156,7 @@ struct ViewGesture { tracker: SwipeTracker, delta_from_tracker: f64, // The view offset we'll use if needed for activate_prev_column_on_removal. - static_view_offset: i32, + static_view_offset: f64, /// Whether the gesture is controlled by the touchpad. is_touchpad: bool, } @@ -160,7 +164,7 @@ struct ViewGesture { #[derive(Debug)] struct InteractiveResize { window: W::Id, - original_window_size: Size, + original_window_size: Size, data: InteractiveResizeData, } @@ -175,7 +179,7 @@ pub enum ColumnWidth { /// proportions. Preset(usize), /// Fixed width in logical pixels. - Fixed(i32), + Fixed(f64), } /// Height of a window in a column. @@ -190,12 +194,12 @@ pub enum ColumnWidth { /// This does not preclude the usual set of binds to set or resize a window proportionally. Just, /// they are converted to, and stored as fixed height right away, so that once you resize a window /// to fit the desired content, it can never become smaller than that when moving between monitors. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq)] pub enum WindowHeight { /// Automatically computed height, evenly distributed across the column. Auto, /// Fixed height in logical pixels. - Fixed(i32), + Fixed(f64), } #[derive(Debug)] @@ -229,10 +233,13 @@ pub struct Column { move_animation: Option, /// Latest known view size for this column's workspace. - view_size: Size, + view_size: Size, /// Latest known working area for this column's workspace. - working_area: Rectangle, + working_area: Rectangle, + + /// Scale of the output the column is on (and rounds its sizes to). + scale: f64, /// Configurable properties of the layout. options: Rc, @@ -247,7 +254,7 @@ struct TileData { height: WindowHeight, /// Cached actual size of the tile. - size: Size, + size: Size, /// Cached whether the tile is being interactively resized by its left edge. interactively_resizing_by_left_edge: bool, @@ -274,7 +281,7 @@ impl ViewOffsetAdjustment { impl ColumnData { pub fn new(column: &Column) -> Self { - let mut rv = Self { width: 0 }; + let mut rv = Self { width: 0. }; rv.update(column); rv } @@ -285,10 +292,10 @@ impl ColumnData { } impl ColumnWidth { - fn resolve(self, options: &Options, view_width: i32) -> i32 { + fn resolve(self, options: &Options, view_width: f64) -> f64 { match self { ColumnWidth::Proportion(proportion) => { - ((view_width - options.gaps) as f64 * proportion).floor() as i32 - options.gaps + (view_width - options.gaps) * proportion - options.gaps } ColumnWidth::Preset(idx) => options.preset_widths[idx].resolve(options, view_width), ColumnWidth::Fixed(width) => width, @@ -300,7 +307,7 @@ impl From for ColumnWidth { fn from(value: PresetWidth) -> Self { match value { PresetWidth::Proportion(p) => Self::Proportion(p.clamp(0., 10000.)), - PresetWidth::Fixed(f) => Self::Fixed(f.clamp(1, 100000)), + PresetWidth::Fixed(f) => Self::Fixed(f64::from(f.clamp(1, 100000))), } } } @@ -333,7 +340,7 @@ impl Workspace { pub fn new_with_config( output: Output, config: Option, - options: Rc, + base_options: Rc, ) -> Self { let original_output = config .as_ref() @@ -341,10 +348,15 @@ impl Workspace { .map(OutputId) .unwrap_or(OutputId::new(&output)); + let scale = output.current_scale(); + let options = + Rc::new(Options::clone(&base_options).adjusted_for_scale(scale.fractional_scale())); + let working_area = compute_working_area(&output, options.struts); + Self { original_output, - scale: output.current_scale(), + scale, transform: output.current_transform(), view_size: output_size(&output), working_area, @@ -353,11 +365,12 @@ impl Workspace { data: vec![], active_column_idx: 0, interactive_resize: None, - view_offset: 0, + view_offset: 0., view_offset_adj: None, activate_prev_column_on_removal: None, view_offset_before_fullscreen: None, closing_windows: vec![], + base_options, options, name: config.map(|c| c.name.0), id: WorkspaceId::next(), @@ -366,7 +379,7 @@ impl Workspace { pub fn new_with_config_no_outputs( config: Option, - options: Rc, + base_options: Rc, ) -> Self { let original_output = OutputId( config @@ -374,22 +387,28 @@ impl Workspace { .and_then(|c| c.open_on_output) .unwrap_or_default(), ); + + let scale = smithay::output::Scale::Integer(1); + let options = + Rc::new(Options::clone(&base_options).adjusted_for_scale(scale.fractional_scale())); + Self { output: None, - scale: smithay::output::Scale::Integer(1), + scale, transform: Transform::Normal, original_output, - view_size: Size::from((1280, 720)), - working_area: Rectangle::from_loc_and_size((0, 0), (1280, 720)), + view_size: Size::from((1280., 720.)), + working_area: Rectangle::from_loc_and_size((0., 0.), (1280., 720.)), columns: vec![], data: vec![], active_column_idx: 0, interactive_resize: None, - view_offset: 0, + view_offset: 0., view_offset_adj: None, activate_prev_column_on_removal: None, view_offset_before_fullscreen: None, closing_windows: vec![], + base_options, options, name: config.map(|c| c.name.0), id: WorkspaceId::next(), @@ -408,15 +427,19 @@ impl Workspace { self.name = None; } + pub fn scale(&self) -> smithay::output::Scale { + self.scale + } + pub fn advance_animations(&mut self, current_time: Duration) { if let Some(ViewOffsetAdjustment::Animation(anim)) = &mut self.view_offset_adj { anim.set_current_time(current_time); - self.view_offset = anim.value().round() as i32; + self.view_offset = anim.value(); if anim.is_done() { self.view_offset_adj = None; } } else if let Some(ViewOffsetAdjustment::Gesture(gesture)) = &self.view_offset_adj { - self.view_offset = gesture.current_view_offset.round() as i32; + self.view_offset = gesture.current_view_offset; } for col in &mut self.columns { @@ -444,24 +467,28 @@ impl Workspace { } pub fn update_render_elements(&mut self, is_active: bool) { - let view_pos = Point::from((self.view_pos(), 0)); + let view_pos = Point::from((self.view_pos(), 0.)); let view_size = self.view_size(); let active_idx = self.active_column_idx; for (col_idx, (col, col_x)) in self.columns_mut().enumerate() { let is_active = is_active && col_idx == active_idx; - let col_off = Point::from((col_x, 0)); + let col_off = Point::from((col_x, 0.)); let col_pos = view_pos - col_off - col.render_offset(); let view_rect = Rectangle::from_loc_and_size(col_pos, view_size); col.update_render_elements(is_active, view_rect); } } - pub fn update_config(&mut self, options: Rc) { + pub fn update_config(&mut self, base_options: Rc) { + let scale = self.scale.fractional_scale(); + let options = Rc::new(Options::clone(&base_options).adjusted_for_scale(scale)); + for (column, data) in zip(&mut self.columns, &mut self.data) { - column.update_config(options.clone()); + column.update_config(scale, options.clone()); data.update(column); } + self.base_options = base_options; self.options = options; } @@ -527,8 +554,8 @@ impl Workspace { &mut self, scale: smithay::output::Scale, transform: Transform, - size: Size, - working_area: Rectangle, + size: Size, + working_area: Rectangle, ) { let scale_transform_changed = self.transform != transform || self.scale.integer_scale() != scale.integer_scale() @@ -537,11 +564,18 @@ impl Workspace { return; } + let fractional_scale_changed = self.scale.fractional_scale() != scale.fractional_scale(); + self.scale = scale; self.transform = transform; self.view_size = size; self.working_area = working_area; + if fractional_scale_changed { + // Options need to be recomputed for the new scale. + self.update_config(self.base_options.clone()); + } + for col in &mut self.columns { col.set_view_size(self.view_size, self.working_area); } @@ -553,7 +587,7 @@ impl Workspace { } } - pub fn view_size(&self) -> Size { + pub fn view_size(&self) -> Size { self.view_size } @@ -586,20 +620,20 @@ impl Workspace { let mut width = width.resolve(&self.options, self.working_area.size.w); if !is_fixed && !border.off { - width -= border.width as i32 * 2; + width -= border.width.0 * 2.; } - max(1, width) + max(1, width.floor() as i32) } else { 0 }; - let mut height = self.working_area.size.h - self.options.gaps * 2; + let mut height = self.working_area.size.h - self.options.gaps * 2.; if !border.off { - height -= border.width as i32 * 2; + height -= border.width.0 * 2.; } - Size::from((width, max(height, 1))) + Size::from((width, max(height.floor() as i32, 1))) } pub fn configure_new_window( @@ -617,7 +651,7 @@ impl Workspace { .expect("no x11 support") .with_pending_state(|state| { if state.states.contains(xdg_toplevel::State::Fullscreen) { - state.size = Some(self.view_size); + state.size = Some(self.view_size.to_i32_round()); } else { state.size = Some(self.new_window_size(width, rules)); } @@ -626,15 +660,15 @@ impl Workspace { }); } - fn compute_new_view_offset_for_column(&self, current_x: i32, idx: usize) -> i32 { + fn compute_new_view_offset_for_column(&self, current_x: f64, idx: usize) -> f64 { if self.columns[idx].is_fullscreen { - return 0; + return 0.; } let new_col_x = self.column_x(idx); let final_x = if let Some(ViewOffsetAdjustment::Animation(anim)) = &self.view_offset_adj { - current_x - self.view_offset + anim.to().round() as i32 + current_x - self.view_offset + anim.to() } else { current_x }; @@ -651,7 +685,7 @@ impl Workspace { new_offset - self.working_area.loc.x } - fn animate_view_offset(&mut self, current_x: i32, idx: usize, new_view_offset: i32) { + fn animate_view_offset(&mut self, current_x: f64, idx: usize, new_view_offset: f64) { self.animate_view_offset_with_config( current_x, idx, @@ -662,9 +696,9 @@ impl Workspace { fn animate_view_offset_with_config( &mut self, - current_x: i32, + current_x: f64, idx: usize, - new_view_offset: i32, + new_view_offset: f64, config: niri_config::Animation, ) { let new_col_x = self.column_x(idx); @@ -673,9 +707,8 @@ impl Workspace { // If we're already animating towards that, don't restart it. if let Some(ViewOffsetAdjustment::Animation(anim)) = &self.view_offset_adj { - if anim.value().round() as i32 == self.view_offset - && anim.to().round() as i32 == new_view_offset - { + let pixel = 1. / self.scale.fractional_scale(); + if (anim.value() - self.view_offset).abs() < pixel && anim.to() == new_view_offset { return; } } @@ -688,8 +721,8 @@ impl Workspace { // FIXME: also compute and use current velocity. self.view_offset_adj = Some(ViewOffsetAdjustment::Animation(Animation::new( - self.view_offset as f64, - new_view_offset as f64, + self.view_offset, + new_view_offset, 0., config, ))); @@ -697,7 +730,7 @@ impl Workspace { fn animate_view_offset_to_column_fit( &mut self, - current_x: i32, + current_x: f64, idx: usize, config: niri_config::Animation, ) { @@ -707,7 +740,7 @@ impl Workspace { fn animate_view_offset_to_column_centered( &mut self, - current_x: i32, + current_x: f64, idx: usize, config: niri_config::Animation, ) { @@ -731,14 +764,14 @@ impl Workspace { return; } - let new_view_offset = -(self.working_area.size.w - width) / 2 - self.working_area.loc.x; + let new_view_offset = -(self.working_area.size.w - width) / 2. - self.working_area.loc.x; self.animate_view_offset_with_config(current_x, idx, new_view_offset, config); } fn animate_view_offset_to_column( &mut self, - current_x: i32, + current_x: f64, idx: usize, prev_idx: Option, ) { @@ -752,7 +785,7 @@ impl Workspace { fn animate_view_offset_to_column_with_config( &mut self, - current_x: i32, + current_x: f64, idx: usize, prev_idx: Option, config: niri_config::Animation, @@ -786,7 +819,7 @@ impl Workspace { } else { // Source is right from target. source_x - target_x + source_width - } + self.options.gaps * 2; + } + self.options.gaps * 2.; // If it fits together, do a normal animation, otherwise center the new column. if total_width <= self.working_area.size.w { @@ -853,7 +886,7 @@ impl Workspace { width: ColumnWidth, is_full_width: bool, ) { - let tile = Tile::new(window, self.options.clone()); + let tile = Tile::new(window, self.scale.fractional_scale(), self.options.clone()); self.add_tile_at(col_idx, tile, activate, width, is_full_width, None); } @@ -874,6 +907,7 @@ impl Workspace { tile, self.view_size, self.working_area, + self.scale.fractional_scale(), self.options.clone(), width, is_full_width, @@ -889,7 +923,7 @@ impl Workspace { if was_empty { if self.options.center_focused_column == CenterFocusedColumn::Always { self.view_offset = - -(self.working_area.size.w - width) / 2 - self.working_area.loc.x; + -(self.working_area.size.w - width) / 2. - self.working_area.loc.x; } else { // Try to make the code produce a left-aligned offset, even in presence of left // exclusive zones. @@ -974,6 +1008,7 @@ impl Workspace { window, self.view_size, self.working_area, + self.scale.fractional_scale(), self.options.clone(), width, is_full_width, @@ -1017,6 +1052,7 @@ impl Workspace { self.active_column_idx + 1 }; + column.update_config(self.scale.fractional_scale(), self.options.clone()); column.set_view_size(self.view_size, self.working_area); let width = column.width(); self.data.insert(idx, ColumnData::new(&column)); @@ -1028,7 +1064,7 @@ impl Workspace { if was_empty { if self.options.center_focused_column == CenterFocusedColumn::Always { self.view_offset = - -(self.working_area.size.w - width) / 2 - self.working_area.loc.x; + -(self.working_area.size.w - width) / 2. - self.working_area.loc.x; } else { // Try to make the code produce a left-aligned offset, even in presence of left // exclusive zones. @@ -1294,7 +1330,7 @@ impl Workspace { // Move other columns in tandem with resizing. let started_resize_anim = - column.tiles[tile_idx].resize_animation().is_some() && offset != 0; + column.tiles[tile_idx].resize_animation().is_some() && offset != 0.; if started_resize_anim { if self.active_column_idx <= col_idx { for col in &mut self.columns[col_idx + 1..] { @@ -1317,7 +1353,7 @@ impl Workspace { // If offset == 0, then don't mess with the view or the gesture. Some clients (Firefox, // Chromium, Electron) currently don't commit after the ack of a configure that drops // the Resizing state, which can trigger this code path for a while. - let resize = if offset != 0 { resize } else { None }; + let resize = if offset != 0. { resize } else { None }; if let Some(resize) = resize { // If this is an interactive resize commit of an active window, then we need to // either preserve the view offset or adjust it accordingly. @@ -1327,17 +1363,17 @@ impl Workspace { let offset = if centered { // FIXME: when view_offset becomes fractional, this can be made additive too. let new_offset = - -(self.working_area.size.w - width) / 2 - self.working_area.loc.x; + -(self.working_area.size.w - width) / 2. - self.working_area.loc.x; new_offset - self.view_offset } else if resize.edges.contains(ResizeEdge::LEFT) { -offset } else { - 0 + 0. }; self.view_offset += offset; if let Some(ViewOffsetAdjustment::Animation(anim)) = &mut self.view_offset_adj { - anim.offset(offset as f64); + anim.offset(offset); } else { // Don't bother with the gesture. self.view_offset_adj = None; @@ -1388,7 +1424,7 @@ impl Workspace { pub fn store_unmap_snapshot_if_empty(&mut self, renderer: &mut GlesRenderer, window: &W::Id) { let output_scale = Scale::from(self.scale.fractional_scale()); let view_size = self.view_size(); - for (tile, tile_pos) in self.tiles_with_render_positions_mut() { + for (tile, tile_pos) in self.tiles_with_render_positions_mut(false) { if tile.window().id() == window { let view_pos = Point::from((-tile_pos.x, -tile_pos.y)); let view_rect = Rectangle::from_loc_and_size(view_pos, view_size); @@ -1418,7 +1454,7 @@ impl Workspace { let output_scale = Scale::from(self.scale.fractional_scale()); let (tile, mut tile_pos) = self - .tiles_with_render_positions_mut() + .tiles_with_render_positions_mut(false) .find(|(tile, _)| tile.window().id() == window) .unwrap(); @@ -1454,8 +1490,11 @@ impl Workspace { .data .iter() .enumerate() - .filter_map(|(idx, data)| (idx != tile_idx).then_some(data.size.w)) + .filter_map(|(idx, data)| { + (idx != tile_idx).then_some(NotNan::new(data.size.w).unwrap()) + }) .max() + .map(NotNan::into_inner) .unwrap() }; tile_pos.x -= offset; @@ -1476,10 +1515,13 @@ impl Workspace { #[cfg(test)] pub fn verify_invariants(&self) { - assert!(self.view_size.w > 0); - assert!(self.view_size.h > 0); - assert!(self.scale.fractional_scale() > 0.); - assert!(self.scale.fractional_scale().is_finite()); + use approx::assert_abs_diff_eq; + + let scale = self.scale.fractional_scale(); + assert!(self.view_size.w > 0.); + assert!(self.view_size.h > 0.); + assert!(scale > 0.); + assert!(scale.is_finite()); assert_eq!(self.columns.len(), self.data.len()); if !self.columns.is_empty() { @@ -1487,6 +1529,7 @@ impl Workspace { for (column, data) in zip(&self.columns, &self.data) { assert!(Rc::ptr_eq(&self.options, &column.options)); + assert_eq!(self.scale.fractional_scale(), column.scale); column.verify_invariants(); let mut data2 = *data; @@ -1506,6 +1549,14 @@ impl Workspace { }) ); } + + for (_, tile_pos) in self.tiles_with_render_positions() { + let rounded_pos = tile_pos.to_physical_precise_round(scale).to_logical(scale); + + // Tile positions must be rounded to physical pixels. + assert_abs_diff_eq!(tile_pos.x, rounded_pos.x, epsilon = 1e-5); + assert_abs_diff_eq!(tile_pos.y, rounded_pos.y, epsilon = 1e-5); + } } if let Some(resize) = &self.interactive_resize { @@ -1599,7 +1650,7 @@ impl Workspace { let view_offset_delta = -self.column_x(self.active_column_idx) + current_col_x; self.view_offset += view_offset_delta; if let Some(ViewOffsetAdjustment::Animation(anim)) = &mut self.view_offset_adj { - anim.offset(view_offset_delta as f64); + anim.offset(view_offset_delta); } // The column we just moved is offset by the difference between its new and old position. @@ -1679,7 +1730,7 @@ impl Workspace { } let offset = self.column_x(source_col_idx) - self.column_x(source_col_idx - 1); - let mut offset = Point::from((offset, 0)); + let mut offset = Point::from((offset, 0.)); // Move into adjacent column. let target_column_idx = source_col_idx - 1; @@ -1720,7 +1771,7 @@ impl Workspace { let width = source_column.width; let is_full_width = source_column.is_full_width; - let mut offset = Point::from((source_column.render_offset().x, 0)); + let mut offset = Point::from((source_column.render_offset().x, 0.)); let tile = self.remove_tile_by_idx(source_col_idx, source_column.active_tile_idx, None); @@ -1749,7 +1800,7 @@ impl Workspace { let source_col_idx = self.active_column_idx; let offset = self.column_x(source_col_idx) - self.column_x(source_col_idx + 1); - let mut offset = Point::from((offset, 0)); + let mut offset = Point::from((offset, 0.)); let source_column = &self.columns[source_col_idx]; offset.x += source_column.render_offset().x; @@ -1827,7 +1878,7 @@ impl Workspace { let offset = self.column_x(source_column_idx) + self.columns[source_column_idx].render_offset().x - self.column_x(self.active_column_idx); - let mut offset = Point::from((offset, 0)); + let mut offset = Point::from((offset, 0.)); let prev_off = self.columns[source_column_idx].tile_offset(0); let tile = self.remove_tile_by_idx(source_column_idx, 0, None); @@ -1867,7 +1918,7 @@ impl Workspace { let offset = self.column_x(self.active_column_idx) - self.column_x(self.active_column_idx + 1); - let mut offset = Point::from((offset, 0)); + let mut offset = Point::from((offset, 0.)); let source_column = &self.columns[self.active_column_idx]; if source_column.tiles.len() == 1 { @@ -1909,17 +1960,17 @@ impl Workspace { } } - fn view_pos(&self) -> i32 { + fn view_pos(&self) -> f64 { self.column_x(self.active_column_idx) + self.view_offset } /// Returns a view offset value suitable for saving and later restoration. /// /// This means that it shouldn't return an in-progress animation or gesture value. - fn static_view_offset(&self) -> i32 { + fn static_view_offset(&self) -> f64 { match &self.view_offset_adj { // For animations we can return the final value. - Some(ViewOffsetAdjustment::Animation(anim)) => anim.to().round() as i32, + Some(ViewOffsetAdjustment::Animation(anim)) => anim.to(), Some(ViewOffsetAdjustment::Gesture(gesture)) => gesture.static_view_offset, _ => self.view_offset, } @@ -1927,12 +1978,12 @@ impl Workspace { // HACK: pass a self.data iterator in manually as a workaround for the lack of method partial // borrowing. Note that this method's return value does not borrow the entire &Self! - fn column_xs(&self, data: impl Iterator) -> impl Iterator { + fn column_xs(&self, data: impl Iterator) -> impl Iterator { let gaps = self.options.gaps; - let mut x = 0; + let mut x = 0.; // Chain with a dummy value to be able to get one past all columns' X. - let dummy = ColumnData { width: 0 }; + let dummy = ColumnData { width: 0. }; let data = data.chain(iter::once(dummy)); data.map(move |data| { @@ -1942,7 +1993,7 @@ impl Workspace { }) } - fn column_x(&self, column_idx: usize) -> i32 { + fn column_x(&self, column_idx: usize) -> f64 { self.column_xs(self.data.iter().copied()) .nth(column_idx) .unwrap() @@ -1951,7 +2002,7 @@ impl Workspace { fn column_xs_in_render_order( &self, data: impl Iterator, - ) -> impl Iterator { + ) -> impl Iterator { let active_idx = self.active_column_idx; let active_pos = self.column_x(active_idx); let offsets = self @@ -1961,12 +2012,12 @@ impl Workspace { iter::once(active_pos).chain(offsets) } - fn columns_mut(&mut self) -> impl Iterator, i32)> + '_ { + fn columns_mut(&mut self) -> impl Iterator, f64)> + '_ { let offsets = self.column_xs(self.data.iter().copied()); zip(&mut self.columns, offsets) } - fn columns_in_render_order(&self) -> impl Iterator, i32)> + '_ { + fn columns_in_render_order(&self) -> impl Iterator, f64)> + '_ { let offsets = self.column_xs_in_render_order(self.data.iter().copied()); let (first, rest) = self.columns.split_at(self.active_column_idx); @@ -1976,7 +2027,7 @@ impl Workspace { zip(tiles, offsets) } - fn columns_in_render_order_mut(&mut self) -> impl Iterator, i32)> + '_ { + fn columns_in_render_order_mut(&mut self) -> impl Iterator, f64)> + '_ { let offsets = self.column_xs_in_render_order(self.data.iter().copied()); let (first, rest) = self.columns.split_at_mut(self.active_column_idx); @@ -1986,14 +2037,17 @@ impl Workspace { zip(tiles, offsets) } - fn tiles_with_render_positions(&self) -> impl Iterator, Point)> { - let view_off = Point::from((-self.view_pos(), 0)); + fn tiles_with_render_positions(&self) -> impl Iterator, Point)> { + let scale = self.scale.fractional_scale(); + let view_off = Point::from((-self.view_pos(), 0.)); self.columns_in_render_order() .flat_map(move |(col, col_x)| { - let col_off = Point::from((col_x, 0)); + let col_off = Point::from((col_x, 0.)); let col_render_off = col.render_offset(); col.tiles_in_render_order().map(move |(tile, tile_off)| { let pos = view_off + col_off + col_render_off + tile_off + tile.render_offset(); + // Round to physical pixels. + let pos = pos.to_physical_precise_round(scale).to_logical(scale); (tile, pos) }) }) @@ -2001,16 +2055,22 @@ impl Workspace { fn tiles_with_render_positions_mut( &mut self, - ) -> impl Iterator, Point)> { - let view_off = Point::from((-self.view_pos(), 0)); + round: bool, + ) -> impl Iterator, Point)> { + let scale = self.scale.fractional_scale(); + let view_off = Point::from((-self.view_pos(), 0.)); self.columns_in_render_order_mut() .flat_map(move |(col, col_x)| { - let col_off = Point::from((col_x, 0)); + let col_off = Point::from((col_x, 0.)); let col_render_off = col.render_offset(); col.tiles_in_render_order_mut() .map(move |(tile, tile_off)| { - let pos = + let mut pos = view_off + col_off + col_render_off + tile_off + tile.render_offset(); + // Round to physical pixels. + if round { + pos = pos.to_physical_precise_round(scale).to_logical(scale); + } (tile, pos) }) }) @@ -2019,14 +2079,14 @@ impl Workspace { /// Returns the geometry of the active tile relative to and clamped to the view. /// /// During animations, assumes the final view position. - pub fn active_tile_visual_rectangle(&self) -> Option> { + pub fn active_tile_visual_rectangle(&self) -> Option> { let col = self.columns.get(self.active_column_idx)?; let final_view_offset = self .view_offset_adj .as_ref() - .map_or(self.view_offset, |adj| adj.target_view_offset() as i32); - let view_off = Point::from((-final_view_offset, 0)); + .map_or(self.view_offset, |adj| adj.target_view_offset()); + let view_off = Point::from((-final_view_offset, 0.)); let (tile, tile_off) = col.tiles().nth(col.active_tile_idx).unwrap(); @@ -2034,21 +2094,21 @@ impl Workspace { let tile_size = tile.tile_size(); let tile_rect = Rectangle::from_loc_and_size(tile_pos, tile_size); - let view = Rectangle::from_loc_and_size((0, 0), self.view_size); + let view = Rectangle::from_loc_and_size((0., 0.), self.view_size); view.intersection(tile_rect) } pub fn window_under( &self, pos: Point, - ) -> Option<(&W, Option>)> { + ) -> Option<(&W, Option>)> { if self.columns.is_empty() { return None; } self.tiles_with_render_positions() .find_map(|(tile, tile_pos)| { - let pos_within_tile = pos - tile_pos.to_f64(); + let pos_within_tile = pos - tile_pos; if tile.is_in_input_region(pos_within_tile) { let pos_within_surface = tile_pos + tile.buf_loc(); @@ -2068,7 +2128,7 @@ impl Workspace { self.tiles_with_render_positions() .find_map(|(tile, tile_pos)| { - let pos_within_tile = pos - tile_pos.to_f64(); + let pos_within_tile = pos - tile_pos; // This logic should be consistent with window_under() in when it returns Some vs. // None. @@ -2190,6 +2250,7 @@ impl Workspace { window, self.view_size, self.working_area, + self.scale.fractional_scale(), self.options.clone(), width, is_full_width, @@ -2256,7 +2317,7 @@ impl Workspace { let mut rv = vec![]; // Draw the closing windows on top. - let view_rect = Rectangle::from_loc_and_size((self.view_pos(), 0), self.view_size); + let view_rect = Rectangle::from_loc_and_size((self.view_pos(), 0.), self.view_size); for closing in self.closing_windows.iter().rev() { let elem = closing.render(renderer.as_gles_renderer(), view_rect, output_scale, target); rv.push(elem.into()); @@ -2291,9 +2352,9 @@ impl Workspace { } let gesture = ViewGesture { - current_view_offset: self.view_offset as f64, + current_view_offset: self.view_offset, tracker: SwipeTracker::new(), - delta_from_tracker: self.view_offset as f64, + delta_from_tracker: self.view_offset, static_view_offset: self.static_view_offset(), is_touchpad, }; @@ -2317,7 +2378,7 @@ impl Workspace { gesture.tracker.push(delta_x, timestamp); let norm_factor = if gesture.is_touchpad { - self.working_area.size.w as f64 / VIEW_GESTURE_WORKING_AREA_MOVEMENT + self.working_area.size.w / VIEW_GESTURE_WORKING_AREA_MOVEMENT } else { 1. }; @@ -2343,7 +2404,7 @@ impl Workspace { // effort and bug potential. let norm_factor = if gesture.is_touchpad { - self.working_area.size.w as f64 / VIEW_GESTURE_WORKING_AREA_MOVEMENT + self.working_area.size.w / VIEW_GESTURE_WORKING_AREA_MOVEMENT } else { 1. }; @@ -2352,7 +2413,7 @@ impl Workspace { let current_view_offset = pos + gesture.delta_from_tracker; if self.columns.is_empty() { - self.view_offset = current_view_offset.round() as i32; + self.view_offset = current_view_offset; self.view_offset_adj = None; return true; } @@ -2365,7 +2426,7 @@ impl Workspace { // either side. struct Snap { // View position relative to x = 0 (the first column). - view_pos: i32, + view_pos: f64, // Column to activate for this snapping point. col_idx: usize, } @@ -2376,7 +2437,7 @@ impl Workspace { let right_strut = self.view_size.w - self.working_area.size.w - self.working_area.loc.x; if self.options.center_focused_column == CenterFocusedColumn::Always { - let mut col_x = 0; + let mut col_x = 0.; for (col_idx, col) in self.columns.iter().enumerate() { let col_w = col.width(); @@ -2385,7 +2446,7 @@ impl Workspace { } else if self.working_area.size.w <= col_w { col_x - left_strut } else { - col_x - (self.working_area.size.w - col_w) / 2 - left_strut + col_x - (self.working_area.size.w - col_w) / 2. - left_strut }; snapping_points.push(Snap { view_pos, col_idx }); @@ -2404,7 +2465,7 @@ impl Workspace { }); }; - let mut col_x = 0; + let mut col_x = 0.; for (col_idx, col) in self.columns.iter().enumerate() { let col_w = col.width(); @@ -2417,7 +2478,7 @@ impl Workspace { } else { // Logic from compute_new_view_offset. let padding = - ((self.working_area.size.w - col_w) / 2).clamp(0, self.options.gaps); + ((self.working_area.size.w - col_w) / 2.).clamp(0., self.options.gaps); let left = col_x - padding - left_strut; let right = col_x + col_w + padding + right_strut; push(col_idx, left, right); @@ -2428,13 +2489,13 @@ impl Workspace { } // Find the closest snapping point. - snapping_points.sort_by_key(|snap| snap.view_pos); + snapping_points.sort_by_key(|snap| NotNan::new(snap.view_pos).unwrap()); let active_col_x = self.column_x(self.active_column_idx); - let target_view_pos = (active_col_x as f64 + target_view_offset).round() as i32; + let target_view_pos = active_col_x + target_view_offset; let target_snap = snapping_points .iter() - .min_by_key(|snap| snap.view_pos.abs_diff(target_view_pos)) + .min_by_key(|snap| NotNan::new((snap.view_pos - target_view_pos).abs()).unwrap()) .unwrap(); let mut new_col_idx = target_snap.col_idx; @@ -2453,7 +2514,7 @@ impl Workspace { } } else { let padding = - ((self.working_area.size.w - col_w) / 2).clamp(0, self.options.gaps); + ((self.working_area.size.w - col_w) / 2.).clamp(0., self.options.gaps); if target_snap.view_pos + left_strut + self.working_area.size.w < col_x + col_w + padding { @@ -2475,7 +2536,7 @@ impl Workspace { } } else { let padding = - ((self.working_area.size.w - col_w) / 2).clamp(0, self.options.gaps); + ((self.working_area.size.w - col_w) / 2.).clamp(0., self.options.gaps); if col_x - padding < target_snap.view_pos + left_strut { break; } @@ -2487,8 +2548,8 @@ impl Workspace { } let new_col_x = self.column_x(new_col_idx); - let delta = (active_col_x - new_col_x) as f64; - self.view_offset = (current_view_offset + delta).round() as i32; + let delta = active_col_x - new_col_x; + self.view_offset = current_view_offset + delta; if self.active_column_idx != new_col_idx { self.view_offset_before_fullscreen = None; @@ -2500,7 +2561,7 @@ impl Workspace { self.view_offset_adj = Some(ViewOffsetAdjustment::Animation(Animation::new( current_view_offset + delta, - target_view_offset as f64, + target_view_offset, velocity, self.options.animations.horizontal_view_movement.0, ))); @@ -2578,7 +2639,7 @@ impl Workspace { dx *= 2.; } - let window_width = (f64::from(resize.original_window_size.w) + dx).round() as i32; + let window_width = (resize.original_window_size.w + dx).round() as i32; col.set_column_width(SizeChange::SetFixed(window_width), Some(tile_idx), false); } @@ -2594,7 +2655,7 @@ impl Workspace { // FIXME: some smarter height distribution would be nice here so that vertical // resizes work as expected in more cases. - let window_height = (f64::from(resize.original_window_size.h) + dy).round() as i32; + let window_height = (resize.original_window_size.h + dy).round() as i32; col.set_window_height(SizeChange::SetFixed(window_height), Some(tile_idx), false); } } @@ -2657,20 +2718,23 @@ impl Workspace { } impl Column { + #[allow(clippy::too_many_arguments)] fn new( window: W, - view_size: Size, - working_area: Rectangle, + view_size: Size, + working_area: Rectangle, + scale: f64, options: Rc, width: ColumnWidth, is_full_width: bool, animate_resize: bool, ) -> Self { - let tile = Tile::new(window, options.clone()); + let tile = Tile::new(window, scale, options.clone()); Self::new_with_tile( tile, view_size, working_area, + scale, options, width, is_full_width, @@ -2678,10 +2742,12 @@ impl Column { ) } + #[allow(clippy::too_many_arguments)] fn new_with_tile( tile: Tile, - view_size: Size, - working_area: Rectangle, + view_size: Size, + working_area: Rectangle, + scale: f64, options: Rc, width: ColumnWidth, is_full_width: bool, @@ -2697,6 +2763,7 @@ impl Column { move_animation: None, view_size, working_area, + scale, options, }; @@ -2711,7 +2778,7 @@ impl Column { rv } - fn set_view_size(&mut self, size: Size, working_area: Rectangle) { + fn set_view_size(&mut self, size: Size, working_area: Rectangle) { if self.view_size == size && self.working_area == working_area { return; } @@ -2722,7 +2789,7 @@ impl Column { self.update_tile_sizes(false); } - fn update_config(&mut self, options: Rc) { + fn update_config(&mut self, scale: f64, options: Rc) { let mut update_sizes = false; // If preset widths changed, make our width non-preset. @@ -2743,10 +2810,11 @@ impl Column { } for (tile, data) in zip(&mut self.tiles, &mut self.data) { - tile.update_config(options.clone()); + tile.update_config(scale, options.clone()); data.update(tile); } + self.scale = scale; self.options = options; if update_sizes { @@ -2780,7 +2848,7 @@ impl Column { self.move_animation.is_some() || self.tiles.iter().any(Tile::are_animations_ongoing) } - pub fn update_render_elements(&mut self, is_active: bool, view_rect: Rectangle) { + pub fn update_render_elements(&mut self, is_active: bool, view_rect: Rectangle) { let active_idx = self.active_tile_idx; for (tile_idx, (tile, tile_off)) in self.tiles_mut().enumerate() { let is_active = is_active && tile_idx == active_idx; @@ -2791,17 +2859,17 @@ impl Column { } } - pub fn render_offset(&self) -> Point { + pub fn render_offset(&self) -> Point { let mut offset = Point::from((0., 0.)); if let Some(anim) = &self.move_animation { offset.x += anim.value(); } - offset.to_i32_round() + offset } - pub fn animate_move_from(&mut self, from_x_offset: i32) { + pub fn animate_move_from(&mut self, from_x_offset: f64) { self.animate_move_from_with_config( from_x_offset, self.options.animations.window_movement.0, @@ -2810,13 +2878,13 @@ impl Column { pub fn animate_move_from_with_config( &mut self, - from_x_offset: i32, + from_x_offset: f64, config: niri_config::Animation, ) { let current_offset = self.move_animation.as_ref().map_or(0., Animation::value); self.move_animation = Some(Animation::new( - f64::from(from_x_offset) + current_offset, + from_x_offset + current_offset, 0., 0., config, @@ -2857,17 +2925,17 @@ impl Column { .find(|(_, tile)| tile.window().id() == window) .unwrap(); - let height = tile.window().size().h; + let height = f64::from(tile.window().size().h); let offset = tile .window() .animation_snapshot() - .map_or(0, |from| from.size.h - height); + .map_or(0., |from| from.size.h - height); tile.update_window(); self.data[tile_idx].update(tile); // Move windows below in tandem with resizing. - if tile.resize_animation().is_some() && offset != 0 { + if tile.resize_animation().is_some() && offset != 0. { for tile in &mut self.tiles[tile_idx + 1..] { tile.animate_move_y_from_with_config( offset, @@ -2891,27 +2959,29 @@ impl Column { .iter() .filter_map(|size| { let w = size.w; - if w == 0 { + if w == 0. { None } else { - Some(w) + Some(NotNan::new(w).unwrap()) } }) .max() - .unwrap_or(1); + .map(NotNan::into_inner) + .unwrap_or(1.); let max_width = max_size .iter() .filter_map(|size| { let w = size.w; - if w == 0 { + if w == 0. { None } else { - Some(w) + Some(NotNan::new(w).unwrap()) } }) .min() - .unwrap_or(i32::MAX); - let max_width = max(max_width, min_width); + .map(NotNan::into_inner) + .unwrap_or(f64::from(i32::MAX)); + let max_width = f64::max(max_width, min_width); let width = if self.is_full_width { ColumnWidth::Proportion(1.) @@ -2920,7 +2990,7 @@ impl Column { }; let width = width.resolve(&self.options, self.working_area.size.w); - let width = max(min(width, max_width), min_width); + let width = f64::max(f64::min(width, max_width), min_width); // Compute the tile heights. Start by converting window heights to tile heights. let mut heights = zip(&self.tiles, &self.data) @@ -2937,18 +3007,18 @@ impl Column { // Subtract all fixed-height tiles. for (h, (min_size, max_size)) in zip(&mut heights, zip(&min_size, &max_size)) { // Check if the tile has an exact height constraint. - if min_size.h > 0 && min_size.h == max_size.h { + if min_size.h > 0. && min_size.h == max_size.h { *h = WindowHeight::Fixed(min_size.h); } if let WindowHeight::Fixed(h) = h { - if max_size.h > 0 { - *h = min(*h, max_size.h); + if max_size.h > 0. { + *h = f64::min(*h, max_size.h); } - if min_size.h > 0 { - *h = max(*h, min_size.h); + if min_size.h > 0. { + *h = f64::max(*h, min_size.h); } - *h = max(*h, 1); + *h = f64::max(*h, 1.); height_left -= *h + self.options.gaps; auto_tiles_left -= 1; @@ -2968,13 +3038,15 @@ impl Column { // This case is separately handled above. while auto_tiles_left > 0 { // Compute the current auto height. - let auto_height = height_left / auto_tiles_left as i32 - self.options.gaps; - let auto_height = max(auto_height, 1); + let auto_height = height_left / auto_tiles_left as f64 - self.options.gaps; + let auto_height = f64::max(auto_height, 1.); // Integer division above can result in imperfect height distribution. We will make some // tiles 1 px taller to account for this. - let mut ones_left = height_left - .saturating_sub((auto_height + self.options.gaps) * auto_tiles_left as i32); + let mut ones_left = f64::max( + 0., + height_left - (auto_height + self.options.gaps) * auto_tiles_left as f64, + ) as i32; let mut unsatisfied_min = false; let mut ones_left_2 = ones_left; @@ -2985,12 +3057,12 @@ impl Column { let mut auto = auto_height; if ones_left_2 > 0 { - auto += 1; + auto += 1.; ones_left_2 -= 1; } // Check if the auto height satisfies the min height. - if min_size.h > 0 && min_size.h > auto { + if min_size.h > 0. && min_size.h > auto { *h = WindowHeight::Fixed(min_size.h); height_left -= min_size.h + self.options.gaps; auto_tiles_left -= 1; @@ -3013,7 +3085,7 @@ impl Column { let mut auto = auto_height; if ones_left > 0 { - auto += 1; + auto += 1.; ones_left -= 1; } @@ -3034,8 +3106,13 @@ impl Column { } } - fn width(&self) -> i32 { - self.data.iter().map(|data| data.size.w).max().unwrap() + fn width(&self) -> f64 { + self.data + .iter() + .map(|data| NotNan::new(data.size.w).unwrap()) + .max() + .map(NotNan::into_inner) + .unwrap() } fn focus_up(&mut self) { @@ -3094,6 +3171,8 @@ impl Column { #[cfg(test)] fn verify_invariants(&self) { + use approx::assert_abs_diff_eq; + assert!(!self.tiles.is_empty(), "columns can't be empty"); assert!(self.active_tile_idx < self.tiles.len()); assert_eq!(self.tiles.len(), self.data.len()); @@ -3104,11 +3183,18 @@ impl Column { for (tile, data) in zip(&self.tiles, &self.data) { assert!(Rc::ptr_eq(&self.options, &tile.options)); + assert_eq!(self.scale, tile.scale()); assert_eq!(self.is_fullscreen, tile.window().is_pending_fullscreen()); let mut data2 = *data; data2.update(tile); assert_eq!(data, &data2, "tile data must be up to date"); + + let scale = tile.scale(); + let size = tile.tile_size(); + let rounded = size.to_physical_precise_round(scale).to_logical(scale); + assert_abs_diff_eq!(size.w, rounded.w, epsilon = 1e-5); + assert_abs_diff_eq!(size.h, rounded.h, epsilon = 1e-5); } } @@ -3127,7 +3213,9 @@ impl Column { .preset_widths .iter() .position(|prop| { - prop.resolve(&self.options, self.working_area.size.w) > current + let resolved = prop.resolve(&self.options, self.working_area.size.w); + // Some allowance for fractional scaling purposes. + current + 1. < resolved }) .unwrap_or(0) } @@ -3156,7 +3244,7 @@ impl Column { }; // FIXME: fix overflows then remove limits. - const MAX_PX: i32 = 100000; + const MAX_PX: f64 = 100000.; const MAX_F: f64 = 10000.; let width = match (current, change) { @@ -3166,13 +3254,16 @@ impl Column { // intention behind the ability to set a fixed size. let tile_idx = tile_idx.unwrap_or(self.active_tile_idx); let tile = &self.tiles[tile_idx]; - ColumnWidth::Fixed(tile.tile_width_for_window_width(fixed).clamp(1, MAX_PX)) + ColumnWidth::Fixed( + tile.tile_width_for_window_width(f64::from(fixed)) + .clamp(1., MAX_PX), + ) } (_, SizeChange::SetProportion(proportion)) => { ColumnWidth::Proportion((proportion / 100.).clamp(0., MAX_F)) } (_, SizeChange::AdjustFixed(delta)) => { - let width = current_px.saturating_add(delta).clamp(1, MAX_PX); + let width = (current_px + f64::from(delta)).clamp(1., MAX_PX); ColumnWidth::Fixed(width) } (ColumnWidth::Proportion(current), SizeChange::AdjustProportion(delta)) => { @@ -3180,8 +3271,12 @@ impl Column { ColumnWidth::Proportion(proportion) } (ColumnWidth::Fixed(_), SizeChange::AdjustProportion(delta)) => { - let current = (current_px + self.options.gaps) as f64 - / (self.working_area.size.w - self.options.gaps) as f64; + let full = self.working_area.size.w - self.options.gaps; + let current = if full == 0. { + 1. + } else { + (current_px + self.options.gaps) / full + }; let proportion = (current + delta / 100.).clamp(0., MAX_F); ColumnWidth::Proportion(proportion) } @@ -3200,28 +3295,29 @@ impl Column { WindowHeight::Fixed(height) => height, }; let current_tile_px = tile.tile_height_for_window_height(current_window_px); - let current_prop = (current_tile_px + self.options.gaps) as f64 - / (self.working_area.size.h - self.options.gaps) as f64; + + let full = self.working_area.size.h - self.options.gaps; + let current_prop = if full == 0. { + 1. + } else { + (current_tile_px + self.options.gaps) / (full) + }; // FIXME: fix overflows then remove limits. - const MAX_PX: i32 = 100000; + const MAX_PX: f64 = 100000.; let mut window_height = match change { - SizeChange::SetFixed(fixed) => fixed, + SizeChange::SetFixed(fixed) => f64::from(fixed), SizeChange::SetProportion(proportion) => { - let tile_height = ((self.working_area.size.h - self.options.gaps) as f64 - * proportion - - self.options.gaps as f64) - .round() as i32; + let tile_height = + (self.working_area.size.h - self.options.gaps) * proportion - self.options.gaps; tile.window_height_for_tile_height(tile_height) } - SizeChange::AdjustFixed(delta) => current_window_px.saturating_add(delta), + SizeChange::AdjustFixed(delta) => current_window_px + f64::from(delta), SizeChange::AdjustProportion(delta) => { let proportion = current_prop + delta / 100.; - let tile_height = ((self.working_area.size.h - self.options.gaps) as f64 - * proportion - - self.options.gaps as f64) - .round() as i32; + let tile_height = + (self.working_area.size.h - self.options.gaps) * proportion - self.options.gaps; tile.window_height_for_tile_height(tile_height) } }; @@ -3232,13 +3328,13 @@ impl Column { let max_h = win.max_size().h; if max_h > 0 { - window_height = window_height.min(max_h); + window_height = f64::min(window_height, f64::from(max_h)); } if min_h > 0 { - window_height = window_height.max(min_h); + window_height = f64::max(window_height, f64::from(min_h)); } - self.data[tile_idx].height = WindowHeight::Fixed(window_height.clamp(1, MAX_PX)); + self.data[tile_idx].height = WindowHeight::Fixed(window_height.clamp(1., MAX_PX)); self.update_tile_sizes(animate); } @@ -3259,7 +3355,7 @@ impl Column { } /// Returns the static window location, not taking the render offset into account. - pub fn window_loc(&self, tile_idx: usize) -> Point { + pub fn window_loc(&self, tile_idx: usize) -> Point { let (tile, pos) = self.tiles().nth(tile_idx).unwrap(); pos + tile.window_loc() } @@ -3269,11 +3365,11 @@ impl Column { fn tile_offsets_iter( &self, data: impl Iterator, - ) -> impl Iterator> { + ) -> impl Iterator> { let center = self.options.center_focused_column == CenterFocusedColumn::Always; let gaps = self.options.gaps; let col_width = self.width(); - let mut y = 0; + let mut y = 0.; if !self.is_fullscreen { y = self.working_area.loc.y + self.options.gaps; @@ -3288,10 +3384,10 @@ impl Column { let data = data.chain(iter::once(dummy)); data.map(move |data| { - let mut pos = Point::from((0, y)); + let mut pos = Point::from((0., y)); if center { - pos.x = (col_width - data.size.w) / 2; + pos.x = (col_width - data.size.w) / 2.; } else if data.interactively_resizing_by_left_edge { pos.x = col_width - data.size.w; } @@ -3301,18 +3397,18 @@ impl Column { }) } - fn tile_offsets(&self) -> impl Iterator> + '_ { + fn tile_offsets(&self) -> impl Iterator> + '_ { self.tile_offsets_iter(self.data.iter().copied()) } - fn tile_offset(&self, tile_idx: usize) -> Point { + fn tile_offset(&self, tile_idx: usize) -> Point { self.tile_offsets().nth(tile_idx).unwrap() } fn tile_offsets_in_render_order( &self, data: impl Iterator, - ) -> impl Iterator> { + ) -> impl Iterator> { let active_idx = self.active_tile_idx; let active_pos = self.tile_offset(active_idx); let offsets = self @@ -3322,17 +3418,17 @@ impl Column { iter::once(active_pos).chain(offsets) } - fn tiles(&self) -> impl Iterator, Point)> + '_ { + fn tiles(&self) -> impl Iterator, Point)> + '_ { let offsets = self.tile_offsets_iter(self.data.iter().copied()); zip(&self.tiles, offsets) } - fn tiles_mut(&mut self) -> impl Iterator, Point)> + '_ { + fn tiles_mut(&mut self) -> impl Iterator, Point)> + '_ { let offsets = self.tile_offsets_iter(self.data.iter().copied()); zip(&mut self.tiles, offsets) } - fn tiles_in_render_order(&self) -> impl Iterator, Point)> + '_ { + fn tiles_in_render_order(&self) -> impl Iterator, Point)> + '_ { let offsets = self.tile_offsets_in_render_order(self.data.iter().copied()); let (first, rest) = self.tiles.split_at(self.active_tile_idx); @@ -3344,7 +3440,7 @@ impl Column { fn tiles_in_render_order_mut( &mut self, - ) -> impl Iterator, Point)> + '_ { + ) -> impl Iterator, Point)> + '_ { let offsets = self.tile_offsets_in_render_order(self.data.iter().copied()); let (first, rest) = self.tiles.split_at_mut(self.active_tile_idx); @@ -3356,19 +3452,19 @@ impl Column { } fn compute_new_view_offset( - cur_x: i32, - view_width: i32, - new_col_x: i32, - new_col_width: i32, - gaps: i32, -) -> i32 { + cur_x: f64, + view_width: f64, + new_col_x: f64, + new_col_width: f64, + gaps: f64, +) -> f64 { // If the column is wider than the view, always left-align it. if view_width <= new_col_width { - return 0; + return 0.; } // Compute the padding in case it needs to be smaller due to large tile width. - let padding = ((view_width - new_col_width) / 2).clamp(0, gaps); + let padding = ((view_width - new_col_width) / 2.).clamp(0., gaps); // Compute the desired new X with padding. let new_x = new_col_x - padding; @@ -3380,8 +3476,8 @@ fn compute_new_view_offset( } // Otherwise, prefer the alignment that results in less motion from the current position. - let dist_to_left = cur_x.abs_diff(new_x); - let dist_to_right = (cur_x + view_width).abs_diff(new_right_x); + let dist_to_left = (cur_x - new_x).abs(); + let dist_to_right = ((cur_x + view_width) - new_right_x).abs(); if dist_to_left <= dist_to_right { -padding } else { @@ -3389,41 +3485,49 @@ fn compute_new_view_offset( } } -pub fn compute_working_area(output: &Output, struts: Struts) -> Rectangle { +pub fn compute_working_area(output: &Output, struts: Struts) -> Rectangle { // Start with the layer-shell non-exclusive zone. - let mut working_area = layer_map_for_output(output).non_exclusive_zone(); + let mut working_area = layer_map_for_output(output).non_exclusive_zone().to_f64(); // Add struts. - let w = working_area.size.w; - let h = working_area.size.h; + working_area.size.w = f64::max(0., working_area.size.w - struts.left.0 - struts.right.0); + working_area.loc.x += struts.left.0; - working_area.size.w = w - .saturating_sub(struts.left.into()) - .saturating_sub(struts.right.into()); - working_area.loc.x += struts.left as i32; + working_area.size.h = f64::max(0., working_area.size.h - struts.top.0 - struts.bottom.0); + working_area.loc.y += struts.top.0; - working_area.size.h = h - .saturating_sub(struts.top.into()) - .saturating_sub(struts.bottom.into()); - working_area.loc.y += struts.top as i32; + // Round location to start at a physical pixel. + let scale = output.current_scale().fractional_scale(); + let loc = working_area + .loc + .to_physical_precise_ceil(scale) + .to_logical(scale); + + let mut size_diff = (loc - working_area.loc).to_size(); + size_diff.w = f64::min(working_area.size.w, size_diff.w); + size_diff.h = f64::min(working_area.size.h, size_diff.h); + + working_area.size -= size_diff; + working_area.loc = loc; working_area } fn compute_toplevel_bounds( border_config: niri_config::Border, - working_area_size: Size, - gaps: i32, + working_area_size: Size, + gaps: f64, ) -> Size { - let mut border = 0; + let mut border = 0.; if !border_config.off { - border = border_config.width as i32 * 2; + border = border_config.width.0 * 2.; } Size::from(( - max(working_area_size.w - gaps * 2 - border, 1), - max(working_area_size.h - gaps * 2 - border, 1), + f64::max(working_area_size.w - gaps * 2. - border, 1.), + f64::max(working_area_size.h - gaps * 2. - border, 1.), )) + .to_i32_floor() } fn cancel_resize_for_column( diff --git a/src/niri.rs b/src/niri.rs index b4f3a23..b8309f6 100644 --- a/src/niri.rs +++ b/src/niri.rs @@ -346,12 +346,12 @@ pub enum KeyboardFocus { ScreenshotUi, } -#[derive(Default, Clone, PartialEq, Eq)] +#[derive(Default, Clone, PartialEq)] pub struct PointerFocus { // Output under pointer. pub output: Option, // Surface under pointer and its location in global coordinate space. - pub surface: Option<(WlSurface, Point)>, + pub surface: Option<(WlSurface, Point)>, // If surface belongs to a window, this is that window. pub window: Option, } @@ -588,8 +588,8 @@ impl State { if let Some(rect) = rect { let output_geo = self.niri.global_space.output_geometry(&output).unwrap(); let mut rect = rect; - rect.loc += output_geo.loc; - rv = self.move_cursor_to_rect(rect.to_f64(), mode); + rect.loc += output_geo.loc.to_f64(); + rv = self.move_cursor_to_rect(rect, mode); } rv @@ -1659,7 +1659,7 @@ impl Niri { config, } = data; - let size = output_size(&output); + let size = output_size(&output).to_i32_round(); let new_position = config .map(|pos| Point::from((pos.x, pos.y))) @@ -1763,7 +1763,7 @@ impl Niri { LockRenderState::Unlocked }; - let size = output_size(&output); + let size = output_size(&output).to_i32_round(); let state = OutputState { global, redraw_state: RedrawState::Idle, @@ -1853,7 +1853,7 @@ impl Niri { } pub fn output_resized(&mut self, output: &Output) { - let output_size = output_size(output); + let output_size = output_size(output).to_i32_round(); let is_locked = self.is_locked(); layer_map_for_output(output).arrange(); @@ -1990,7 +1990,10 @@ impl Niri { WindowSurfaceType::ALL, ) .map(|(surface, pos_within_output)| { - (surface, pos_within_output + output_pos_in_global_space) + ( + surface, + (pos_within_output + output_pos_in_global_space).to_f64(), + ) }); return rv; @@ -2005,14 +2008,15 @@ impl Niri { layers .layer_under(layer, pos_within_output) .and_then(|layer| { - let layer_pos_within_output = layers.layer_geometry(layer).unwrap().loc; + let layer_pos_within_output = + layers.layer_geometry(layer).unwrap().loc.to_f64(); layer .surface_under( - pos_within_output - layer_pos_within_output.to_f64(), + pos_within_output - layer_pos_within_output, WindowSurfaceType::ALL, ) .map(|(surface, pos_within_layer)| { - (surface, pos_within_layer + layer_pos_within_output) + (surface, pos_within_layer.to_f64() + layer_pos_within_output) }) }) .map(|s| (s, None)) @@ -2026,11 +2030,11 @@ impl Niri { let window = &mapped.window; window .surface_under( - pos_within_output - win_pos_within_output.to_f64(), + pos_within_output - win_pos_within_output, WindowSurfaceType::ALL, ) .map(|(s, pos_within_window)| { - (s, pos_within_window + win_pos_within_output) + (s, pos_within_window.to_f64() + win_pos_within_output) }) .map(|s| (s, Some(window.clone()))) }) @@ -2057,7 +2061,8 @@ impl Niri { return rv; }; - let surface_loc_in_global_space = surface_pos_within_output + output_pos_in_global_space; + let surface_loc_in_global_space = + surface_pos_within_output + output_pos_in_global_space.to_f64(); rv.surface = Some((surface, surface_loc_in_global_space)); rv.window = window; @@ -3533,7 +3538,7 @@ impl Niri { // FIXME: pointer. let elements = mapped.render( renderer, - mapped.window.geometry().loc, + mapped.window.geometry().loc.to_f64(), scale, alpha, RenderTarget::ScreenCapture, @@ -3784,8 +3789,8 @@ impl Niri { // Constraint does not apply if not within region. if let Some(region) = constraint.region() { - let new_pos_within_surface = new_pos.to_i32_round() - *surface_loc; - if !region.contains(new_pos_within_surface) { + let new_pos_within_surface = new_pos - *surface_loc; + if !region.contains(new_pos_within_surface.to_i32_round()) { return; } } diff --git a/src/render_helpers/border.rs b/src/render_helpers/border.rs index 8e7d4b1..5a3d4e1 100644 --- a/src/render_helpers/border.rs +++ b/src/render_helpers/border.rs @@ -26,12 +26,12 @@ pub struct BorderRenderElement { #[derive(Debug, Clone, Copy, PartialEq)] struct Parameters { - size: Size, - gradient_area: Rectangle, + size: Size, + gradient_area: Rectangle, color_from: [f32; 4], color_to: [f32; 4], angle: f32, - geometry: Rectangle, + geometry: Rectangle, border_width: f32, corner_radius: CornerRadius, } @@ -39,12 +39,12 @@ struct Parameters { impl BorderRenderElement { #[allow(clippy::too_many_arguments)] pub fn new( - size: Size, - gradient_area: Rectangle, + size: Size, + gradient_area: Rectangle, color_from: [f32; 4], color_to: [f32; 4], angle: f32, - geometry: Rectangle, + geometry: Rectangle, border_width: f32, corner_radius: CornerRadius, ) -> Self { @@ -90,12 +90,12 @@ impl BorderRenderElement { #[allow(clippy::too_many_arguments)] pub fn update( &mut self, - size: Size, - gradient_area: Rectangle, + size: Size, + gradient_area: Rectangle, color_from: [f32; 4], color_to: [f32; 4], angle: f32, - geometry: Rectangle, + geometry: Rectangle, border_width: f32, corner_radius: CornerRadius, ) { @@ -172,7 +172,7 @@ impl BorderRenderElement { ); } - pub fn with_location(mut self, location: Point) -> Self { + pub fn with_location(mut self, location: Point) -> Self { self.inner = self.inner.with_location(location); self } diff --git a/src/render_helpers/clipped_surface.rs b/src/render_helpers/clipped_surface.rs index 7d52e41..8e5259e 100644 --- a/src/render_helpers/clipped_surface.rs +++ b/src/render_helpers/clipped_surface.rs @@ -18,7 +18,7 @@ pub struct ClippedSurfaceRenderElement { inner: WaylandSurfaceRenderElement, program: GlesTexProgram, corner_radius: CornerRadius, - geometry: Rectangle, + geometry: Rectangle, input_to_geo: Mat3, } @@ -32,7 +32,7 @@ impl ClippedSurfaceRenderElement { pub fn new( elem: WaylandSurfaceRenderElement, scale: Scale, - geometry: Rectangle, + geometry: Rectangle, program: GlesTexProgram, corner_radius: CornerRadius, ) -> Self { @@ -86,7 +86,7 @@ impl ClippedSurfaceRenderElement { pub fn will_clip( elem: &WaylandSurfaceRenderElement, scale: Scale, - geometry: Rectangle, + geometry: Rectangle, corner_radius: CornerRadius, ) -> bool { let elem_geo = elem.geometry(scale); @@ -95,10 +95,10 @@ impl ClippedSurfaceRenderElement { if corner_radius == CornerRadius::default() { !geo.contains_rect(elem_geo) } else { - let corners = Self::rounded_corners(geometry.to_f64(), corner_radius); + let corners = Self::rounded_corners(geometry, corner_radius); let corners = corners .into_iter() - .map(|rect| rect.to_physical_precise_round(scale)); + .map(|rect| rect.to_physical_precise_up(scale)); let geo = Rectangle::subtract_rects_many([geo], corners); !Rectangle::subtract_rects_many([elem_geo], geo).is_empty() } @@ -186,11 +186,11 @@ impl Element for ClippedSurfaceRenderElement { if self.corner_radius == CornerRadius::default() { regions.collect() } else { - let corners = Self::rounded_corners(self.geometry.to_f64(), self.corner_radius); + let corners = Self::rounded_corners(self.geometry, self.corner_radius); let elem_loc = self.geometry(scale).loc; let corners = corners.into_iter().map(|rect| { - let mut rect = rect.to_physical_precise_round(scale); + let mut rect = rect.to_physical_precise_up(scale); rect.loc -= elem_loc; rect }); @@ -278,7 +278,7 @@ impl<'render> RenderElement> } impl RoundedCornerDamage { - pub fn set_size(&mut self, size: Size) { + pub fn set_size(&mut self, size: Size) { self.damage.set_size(size); } diff --git a/src/render_helpers/damage.rs b/src/render_helpers/damage.rs index f8cd8c2..e05ee64 100644 --- a/src/render_helpers/damage.rs +++ b/src/render_helpers/damage.rs @@ -7,7 +7,7 @@ use smithay::utils::{Buffer, Logical, Physical, Point, Rectangle, Scale, Size}; pub struct ExtraDamage { id: Id, commit: CommitCounter, - geometry: Rectangle, + geometry: Rectangle, } impl ExtraDamage { @@ -19,7 +19,7 @@ impl ExtraDamage { } } - pub fn set_size(&mut self, size: Size) { + pub fn set_size(&mut self, size: Size) { if self.geometry.size == size { return; } @@ -32,7 +32,7 @@ impl ExtraDamage { self.commit.increment(); } - pub fn with_location(mut self, location: Point) -> Self { + pub fn with_location(mut self, location: Point) -> Self { self.geometry.loc = location; self } @@ -58,7 +58,7 @@ impl Element for ExtraDamage { } fn geometry(&self, scale: Scale) -> Rectangle { - self.geometry.to_physical_precise_round(scale) + self.geometry.to_physical_precise_up(scale) } } diff --git a/src/render_helpers/mod.rs b/src/render_helpers/mod.rs index 2eb1d6d..12ebd02 100644 --- a/src/render_helpers/mod.rs +++ b/src/render_helpers/mod.rs @@ -3,7 +3,6 @@ use std::ptr; use anyhow::{ensure, Context}; use niri_config::BlockOutFrom; use smithay::backend::allocator::Fourcc; -use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement}; use smithay::backend::renderer::element::utils::{Relocate, RelocateRenderElement}; use smithay::backend::renderer::element::{Kind, RenderElement}; use smithay::backend::renderer::gles::{GlesMapping, GlesRenderer, GlesTexture}; @@ -13,6 +12,7 @@ use smithay::reexports::wayland_server::protocol::wl_buffer::WlBuffer; use smithay::reexports::wayland_server::protocol::wl_shm; use smithay::utils::{Logical, Physical, Point, Rectangle, Scale, Size, Transform}; use smithay::wayland::shm; +use solid_color::{SolidColorBuffer, SolidColorRenderElement}; use self::primary_gpu_texture::PrimaryGpuTextureRenderElement; use self::texture::{TextureBuffer, TextureRenderElement}; @@ -50,7 +50,7 @@ pub enum RenderTarget { #[derive(Debug)] pub struct BakedBuffer { pub buffer: B, - pub location: Point, + pub location: Point, pub src: Option>, pub dst: Option>, } @@ -67,7 +67,7 @@ pub trait ToRenderElement { fn to_render_element( &self, - location: Point, + location: Point, scale: Scale, alpha: f32, kind: Kind, @@ -119,14 +119,14 @@ impl ToRenderElement for BakedBuffer> { fn to_render_element( &self, - location: Point, + location: Point, _scale: Scale, alpha: f32, kind: Kind, ) -> Self::RenderElement { let elem = TextureRenderElement::from_texture_buffer( self.buffer.clone(), - (location + self.location).to_f64(), + location + self.location, alpha, self.src, self.dst.map(|dst| dst.to_f64()), @@ -141,20 +141,12 @@ impl ToRenderElement for BakedBuffer { fn to_render_element( &self, - location: Point, - scale: Scale, + location: Point, + _scale: Scale, alpha: f32, kind: Kind, ) -> Self::RenderElement { - SolidColorRenderElement::from_buffer( - &self.buffer, - (location + self.location) - .to_physical_precise_round(scale) - .to_i32_round(), - scale, - alpha, - kind, - ) + SolidColorRenderElement::from_buffer(&self.buffer, location + self.location, alpha, kind) } } diff --git a/src/render_helpers/resize.rs b/src/render_helpers/resize.rs index b0fdda9..4361938 100644 --- a/src/render_helpers/resize.rs +++ b/src/render_helpers/resize.rs @@ -18,12 +18,12 @@ pub struct ResizeRenderElement(ShaderRenderElement); impl ResizeRenderElement { #[allow(clippy::too_many_arguments)] pub fn new( - area: Rectangle, + area: Rectangle, scale: Scale, texture_prev: (GlesTexture, Rectangle), - size_prev: Size, + size_prev: Size, texture_next: (GlesTexture, Rectangle), - size_next: Size, + size_next: Size, progress: f32, clamped_progress: f32, corner_radius: CornerRadius, @@ -35,17 +35,17 @@ impl ResizeRenderElement { let (texture_prev, tex_prev_geo) = texture_prev; let (texture_next, tex_next_geo) = texture_next; - let scale_prev = area.size.to_f64() / size_prev.to_f64(); - let scale_next = area.size.to_f64() / size_next.to_f64(); + let scale_prev = area.size / size_prev; + let scale_next = area.size / size_next; // Compute the area necessary to fit a crossfade. let tex_prev_geo_scaled = tex_prev_geo.to_f64().upscale(scale_prev); let tex_next_geo_scaled = tex_next_geo.to_f64().upscale(scale_next); - let combined_geo = tex_prev_geo_scaled.merge(tex_next_geo_scaled); + let combined_geo = tex_prev_geo_scaled.merge(tex_next_geo_scaled).to_i32_up(); let area = Rectangle::from_loc_and_size( - area.loc + combined_geo.loc.to_logical(scale).to_i32_round(), - combined_geo.size.to_logical(scale).to_i32_round(), + area.loc + combined_geo.loc.to_logical(scale), + combined_geo.size.to_logical(scale), ); // Convert Smithay types into glam types. diff --git a/src/render_helpers/shader_element.rs b/src/render_helpers/shader_element.rs index a432ca3..ebd9b1e 100644 --- a/src/render_helpers/shader_element.rs +++ b/src/render_helpers/shader_element.rs @@ -23,8 +23,8 @@ pub struct ShaderRenderElement { program: ProgramType, id: Id, commit_counter: CommitCounter, - area: Rectangle, - opaque_regions: Vec>, + area: Rectangle, + opaque_regions: Vec>, alpha: f32, additional_uniforms: Vec>, textures: HashMap, @@ -198,8 +198,8 @@ impl ShaderRenderElement { #[allow(clippy::too_many_arguments)] pub fn new( program: ProgramType, - size: Size, - opaque_regions: Option>>, + size: Size, + opaque_regions: Option>>, alpha: f32, uniforms: Vec>, textures: HashMap, @@ -209,7 +209,7 @@ impl ShaderRenderElement { program, id: Id::new(), commit_counter: CommitCounter::default(), - area: Rectangle::from_loc_and_size((0, 0), size), + area: Rectangle::from_loc_and_size((0., 0.), size), opaque_regions: opaque_regions.unwrap_or_default(), alpha, additional_uniforms: uniforms.into_iter().map(|u| u.into_owned()).collect(), @@ -238,8 +238,8 @@ impl ShaderRenderElement { pub fn update( &mut self, - size: Size, - opaque_regions: Option>>, + size: Size, + opaque_regions: Option>>, uniforms: Vec>, textures: HashMap, ) { @@ -251,7 +251,7 @@ impl ShaderRenderElement { self.commit_counter.increment(); } - pub fn with_location(mut self, location: Point) -> Self { + pub fn with_location(mut self, location: Point) -> Self { self.area.loc = location; self } @@ -277,7 +277,7 @@ impl Element for ShaderRenderElement { fn opaque_regions(&self, scale: Scale) -> OpaqueRegions { self.opaque_regions .iter() - .map(|region| region.to_physical_precise_round(scale)) + .map(|region| region.to_physical_precise_down(scale)) .collect() } diff --git a/src/render_helpers/snapshot.rs b/src/render_helpers/snapshot.rs index 62416c4..e050682 100644 --- a/src/render_helpers/snapshot.rs +++ b/src/render_helpers/snapshot.rs @@ -25,7 +25,7 @@ pub struct RenderSnapshot { pub block_out_from: Option, /// Visual size of the element at the point of the snapshot. - pub size: Size, + pub size: Size, /// Contents rendered into a texture (lazily). pub texture: OnceCell)>>, @@ -55,7 +55,7 @@ where .blocked_out_contents .iter() .map(|baked| { - baked.to_render_element(Point::from((0, 0)), scale, 1., Kind::Unspecified) + baked.to_render_element(Point::from((0., 0.)), scale, 1., Kind::Unspecified) }) .collect(); @@ -81,7 +81,7 @@ where .contents .iter() .map(|baked| { - baked.to_render_element(Point::from((0, 0)), scale, 1., Kind::Unspecified) + baked.to_render_element(Point::from((0., 0.)), scale, 1., Kind::Unspecified) }) .collect(); diff --git a/src/render_helpers/surface.rs b/src/render_helpers/surface.rs index 859be85..9373867 100644 --- a/src/render_helpers/surface.rs +++ b/src/render_helpers/surface.rs @@ -12,7 +12,7 @@ use super::BakedBuffer; pub fn render_snapshot_from_surface_tree( renderer: &mut GlesRenderer, surface: &WlSurface, - location: Point, + location: Point, storage: &mut Vec>>, ) { let _span = tracy_client::span!("render_snapshot_from_surface_tree"); @@ -28,7 +28,7 @@ pub fn render_snapshot_from_surface_tree( let data = &*data.borrow(); if let Some(view) = data.view() { - location += view.offset; + location += view.offset.to_f64(); TraversalAction::DoChildren(location) } else { TraversalAction::SkipChildren @@ -43,7 +43,7 @@ pub fn render_snapshot_from_surface_tree( if let Some(data) = data { if let Some(view) = data.borrow().view() { - location += view.offset; + location += view.offset.to_f64(); } else { return; } diff --git a/src/ui/config_error_notification.rs b/src/ui/config_error_notification.rs index 2def4b2..b120f49 100644 --- a/src/ui/config_error_notification.rs +++ b/src/ui/config_error_notification.rs @@ -144,7 +144,7 @@ impl ConfigErrorNotification { let size = buffer.logical_size(); let y_range = size.h + f64::from(PADDING) * 2.; - let x = (f64::from(output_size.w) - size.w).max(0.) / 2.; + let x = (output_size.w - size.w).max(0.) / 2.; let y = match &self.state { State::Hidden => unreachable!(), State::Showing(anim) | State::Hiding(anim) => -size.h + anim.value() * y_range, diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 3c6a36e..0365487 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -95,16 +95,24 @@ pub fn to_physical_precise_round(scale: f64, logical: impl Coordi N::from_f64((logical.to_f64() * scale).round()) } -pub fn output_size(output: &Output) -> Size { +pub fn round_logical_in_physical(scale: f64, logical: f64) -> f64 { + (logical * scale).round() / scale +} + +pub fn round_logical_in_physical_max1(scale: f64, logical: f64) -> f64 { + if logical == 0. { + return 0.; + } + + (logical * scale).max(1.).round() / scale +} + +pub fn output_size(output: &Output) -> Size { let output_scale = output.current_scale().fractional_scale(); let output_transform = output.current_transform(); let output_mode = output.current_mode().unwrap(); - - // Like in LayerMap::arrange(). - // - // FIXME: return fractional logical size. let logical_size = output_mode.size.to_f64().to_logical(output_scale); - output_transform.transform_size(logical_size.to_i32_round()) + output_transform.transform_size(logical_size) } pub fn logical_output(output: &Output) -> niri_ipc::LogicalOutput { diff --git a/src/window/mapped.rs b/src/window/mapped.rs index 8e495e7..6db13ad 100644 --- a/src/window/mapped.rs +++ b/src/window/mapped.rs @@ -3,7 +3,6 @@ use std::cmp::{max, min}; use std::time::Duration; use niri_config::WindowRule; -use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement}; use smithay::backend::renderer::element::surface::render_elements_from_surface_tree; use smithay::backend::renderer::element::{Id, Kind}; use smithay::backend::renderer::gles::GlesRenderer; @@ -24,6 +23,7 @@ use crate::layout::{ use crate::niri::WindowOffscreenId; use crate::render_helpers::renderer::NiriRenderer; use crate::render_helpers::snapshot::RenderSnapshot; +use crate::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement}; use crate::render_helpers::surface::render_snapshot_from_surface_tree; use crate::render_helpers::{BakedBuffer, RenderTarget, SplitElements}; use crate::utils::{send_scale_transform, ResizeEdge}; @@ -104,7 +104,7 @@ impl Mapped { need_to_recompute_rules: false, is_focused: false, is_active_in_column: false, - block_out_buffer: RefCell::new(SolidColorBuffer::new((0, 0), [0., 0., 0., 1.])), + block_out_buffer: RefCell::new(SolidColorBuffer::new((0., 0.), [0., 0., 0., 1.])), animate_next_configure: false, animate_serials: Vec::new(), animation_snapshot: None, @@ -158,18 +158,18 @@ impl Mapped { fn render_snapshot(&self, renderer: &mut GlesRenderer) -> LayoutElementRenderSnapshot { let _span = tracy_client::span!("Mapped::render_snapshot"); - let size = self.size(); + let size = self.size().to_f64(); let mut buffer = self.block_out_buffer.borrow_mut(); buffer.resize(size); let blocked_out_contents = vec![BakedBuffer { buffer: buffer.clone(), - location: Point::from((0, 0)), + location: Point::from((0., 0.)), src: None, dst: None, }]; - let buf_pos = self.window.geometry().loc.upscale(-1); + let buf_pos = self.window.geometry().loc.upscale(-1).to_f64(); let mut contents = vec![]; @@ -180,7 +180,7 @@ impl Mapped { render_snapshot_from_surface_tree( renderer, popup.wl_surface(), - buf_pos + offset, + buf_pos + offset.to_f64(), &mut contents, ); } @@ -248,7 +248,7 @@ impl LayoutElement for Mapped { fn render( &self, renderer: &mut R, - location: Point, + location: Point, scale: Scale, alpha: f32, target: RenderTarget, @@ -257,17 +257,12 @@ impl LayoutElement for Mapped { if target.should_block_out(self.rules.block_out_from) { let mut buffer = self.block_out_buffer.borrow_mut(); - buffer.resize(self.window.geometry().size); - let elem = SolidColorRenderElement::from_buffer( - &buffer, - location.to_physical_precise_round(scale), - scale, - alpha, - Kind::Unspecified, - ); + buffer.resize(self.window.geometry().size.to_f64()); + let elem = + SolidColorRenderElement::from_buffer(&buffer, location, alpha, Kind::Unspecified); rv.normal.push(elem.into()); } else { - let buf_pos = location - self.window.geometry().loc; + let buf_pos = location - self.window.geometry().loc.to_f64(); let surface = self.toplevel().wl_surface(); for (popup, popup_offset) in PopupManager::popups_for_surface(surface) { @@ -276,7 +271,7 @@ impl LayoutElement for Mapped { rv.popups.extend(render_elements_from_surface_tree( renderer, popup.wl_surface(), - (buf_pos + offset).to_physical_precise_round(scale), + (buf_pos + offset.to_f64()).to_physical_precise_round(scale), scale, alpha, Kind::Unspecified, @@ -299,24 +294,19 @@ impl LayoutElement for Mapped { fn render_normal( &self, renderer: &mut R, - location: Point, + location: Point, scale: Scale, alpha: f32, target: RenderTarget, ) -> Vec> { if target.should_block_out(self.rules.block_out_from) { let mut buffer = self.block_out_buffer.borrow_mut(); - buffer.resize(self.window.geometry().size); - let elem = SolidColorRenderElement::from_buffer( - &buffer, - location.to_physical_precise_round(scale), - scale, - alpha, - Kind::Unspecified, - ); + buffer.resize(self.window.geometry().size.to_f64()); + let elem = + SolidColorRenderElement::from_buffer(&buffer, location, alpha, Kind::Unspecified); vec![elem.into()] } else { - let buf_pos = location - self.window.geometry().loc; + let buf_pos = location - self.window.geometry().loc.to_f64(); let surface = self.toplevel().wl_surface(); render_elements_from_surface_tree( renderer, @@ -332,7 +322,7 @@ impl LayoutElement for Mapped { fn render_popups( &self, renderer: &mut R, - location: Point, + location: Point, scale: Scale, alpha: f32, target: RenderTarget, @@ -342,7 +332,7 @@ impl LayoutElement for Mapped { } else { let mut rv = vec![]; - let buf_pos = location - self.window.geometry().loc; + let buf_pos = location - self.window.geometry().loc.to_f64(); let surface = self.toplevel().wl_surface(); for (popup, popup_offset) in PopupManager::popups_for_surface(surface) { let offset = self.window.geometry().loc + popup_offset - popup.geometry().loc; @@ -350,7 +340,7 @@ impl LayoutElement for Mapped { rv.extend(render_elements_from_surface_tree( renderer, popup.wl_surface(), - (buf_pos + offset).to_physical_precise_round(scale), + (buf_pos + offset.to_f64()).to_physical_precise_round(scale), scale, alpha, Kind::Unspecified, diff --git a/wiki/Configuration:-Layout.md b/wiki/Configuration:-Layout.md index a16dcb7..e27d0ef 100644 --- a/wiki/Configuration:-Layout.md +++ b/wiki/Configuration:-Layout.md @@ -48,6 +48,10 @@ layout { Set gaps around (inside and outside) windows in logical pixels. +Since: 0.1.7 You can use fractional values. +The value will be rounded to physical pixels according to the scale factor of every output. +For example, `gaps 0.5` on an output with `scale 2` will result in one physical-pixel wide gaps. + ``` layout { gaps 16 @@ -170,6 +174,22 @@ layout { } ``` +#### Width + +Set the thickness of the border in logical pixels. + +Since: 0.1.7 You can use fractional values. +The value will be rounded to physical pixels according to the scale factor of every output. +For example, `width 0.5` on an output with `scale 2` will result in one physical-pixel thick borders. + +``` +layout { + border { + width 2 + } +} +``` + #### Colors Colors can be set in a variety of ways: @@ -227,6 +247,10 @@ They are set in logical pixels. Left and right struts will cause the next window to the side to always peek out slightly. Top and bottom struts will simply add outer gaps in addition to the area occupied by layer-shell panels and regular gaps. +Since: 0.1.7 You can use fractional values. +The value will be rounded to physical pixels according to the scale factor of every output. +For example, `top 0.5` on an output with `scale 2` will result in one physical-pixel wide top strut. + ``` layout { struts { diff --git a/wiki/Fractional-Layout.md b/wiki/Fractional-Layout.md new file mode 100644 index 0000000..220dc85 --- /dev/null +++ b/wiki/Fractional-Layout.md @@ -0,0 +1,34 @@ +There are two main coordinate spaces in niri: physical (pixels of every individual output) and logical (shared among all outputs, takes into account the scale of every output). +Wayland clients mostly work in the logical space, and it's the most convenient space to do all the layout in, since it bakes in the output scaling factor. + +However, many things need to be sized or positioned at integer physical coordinates. +For example, Wayland toplevel buffers are assumed to be placed at an integer physical pixel on an output (and `WaylandSurfaceRenderElement` will do that for you). +Borders and focus rings should also have a width equal to an integer number of physical pixels to stay crisp (not to mention that `SolidColorRenderElement` does not anti-alias lines at fractional pixel positions). + +Integer physical coordinates do not necessarily correspond to integer logical coordinates though. +Even with an integer scale = 2, a physical pixel at (1, 1) will be at the logical position of (0.5, 0.5). +This problem becomes much worse with fractional scale factors where most integer logical coordinates will fall on fractional physical coordinates. + +Thus, niri uses fractional logical coordinates for most of its layout. +However, one needs to be very careful to keep things aligned to the physical grid to avoid artifacts like: + +* Border width alternating 1 px thicker/thinner +* Border showing 1 px off from the window at certain positions +* 1 px gaps around rounded corners +* Slightly blurry window contents during resizes +* And so on... + +The way it's handled in niri is: + +1. All relevant sizes on a workspace are rounded to an integer physical coordinate according to the current output scale. Things like struts, gaps, border widths, working area location. + + It's important to understand that they remain fractional numbers in the logical space, but these numbers correspond to an integer number of pixels in the physical space. + The rounding looks something like: `(logical_size * scale).round() / scale`. + Whenever a workspace moves to an output with a different scale (or the output scale changes), all sizes are re-rounded from their original configured values to align with the new physical space. +2. The view offset and individual column/tile render offsets are *not* rounded to physical pixels, but: +3. `tiles_with_render_positions()` rounds tile positions to physical pixels as it returns them, +4. Custom shaders like opening, closing and resizing windows, are also careful to keep positions and sizes rounded to the physical pixels. + +The idea is that every tile can assume that it is rendered at an integer physical coordinate, therefore when shifting the position by, say, border width (also rounded to integer physical coordinates), the new position will stay rounded to integer physical coordinates. +The same logic works for the rest of the layout thanks to gaps, struts and working area being similarly rounded. +This way, the entire layout is always aligned, as long as it is positioned at an integer physical coordinate (which rounding the tile positions effectively achieves). diff --git a/wiki/_Sidebar.md b/wiki/_Sidebar.md index 5e99919..0e4a8c3 100644 --- a/wiki/_Sidebar.md +++ b/wiki/_Sidebar.md @@ -24,3 +24,4 @@ ## Development * [Design Principles](./Design-Principles.md) * [Developing niri](./Developing-niri.md) +* [Fractional Layout](./Fractional-Layout.md)