Refactor layout to fractional-logical

Lets borders, gaps, and everything else stay pixel-perfect even with
fractional scale. Allows setting fractional border widths, gaps,
struts.

See the new wiki .md for more details.
This commit is contained in:
Ivan Molodetskikh 2024-06-17 09:16:28 +03:00
parent 997119c443
commit 1dae45c58d
35 changed files with 1050 additions and 652 deletions

16
Cargo.lock generated
View File

@ -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",

View File

@ -99,6 +99,7 @@ features = [
]
[dev-dependencies]
approx = "0.5.1"
k9 = "0.12.0"
proptest = "1.4.0"
proptest-derive = "0.4.0"

View File

@ -331,6 +331,10 @@ pub struct Position {
pub y: i32,
}
// MIN and MAX generics are only used during parsing to check the value.
#[derive(Debug, Default, Clone, Copy, PartialEq)]
pub struct FloatOrInt<const MIN: i32, const MAX: i32>(pub f64);
#[derive(knuffel::Decode, Debug, Clone, PartialEq)]
pub struct Layout {
#[knuffel(child, default)]
@ -344,7 +348,7 @@ pub struct Layout {
#[knuffel(child, unwrap(argument), default)]
pub center_focused_column: CenterFocusedColumn,
#[knuffel(child, unwrap(argument), default = Self::default().gaps)]
pub gaps: u16,
pub gaps: FloatOrInt<0, 65535>,
#[knuffel(child, default)]
pub struts: Struts,
}
@ -357,7 +361,7 @@ impl Default for Layout {
preset_column_widths: Default::default(),
default_column_width: Default::default(),
center_focused_column: Default::default(),
gaps: 16,
gaps: FloatOrInt(16.),
struts: Default::default(),
}
}
@ -374,7 +378,7 @@ pub struct FocusRing {
#[knuffel(child)]
pub off: bool,
#[knuffel(child, unwrap(argument), default = Self::default().width)]
pub width: u16,
pub width: FloatOrInt<0, 65535>,
#[knuffel(child, default = Self::default().active_color)]
pub active_color: Color,
#[knuffel(child, default = Self::default().inactive_color)]
@ -389,7 +393,7 @@ impl Default for FocusRing {
fn default() -> Self {
Self {
off: false,
width: 4,
width: FloatOrInt(4.),
active_color: Color::new(127, 200, 255, 255),
inactive_color: Color::new(80, 80, 80, 255),
active_gradient: None,
@ -422,7 +426,7 @@ pub struct Border {
#[knuffel(child)]
pub off: bool,
#[knuffel(child, unwrap(argument), default = Self::default().width)]
pub width: u16,
pub width: FloatOrInt<0, 65535>,
#[knuffel(child, default = Self::default().active_color)]
pub active_color: Color,
#[knuffel(child, default = Self::default().inactive_color)]
@ -437,7 +441,7 @@ impl Default for Border {
fn default() -> Self {
Self {
off: true,
width: 4,
width: FloatOrInt(4.),
active_color: Color::new(255, 200, 127, 255),
inactive_color: Color::new(80, 80, 80, 255),
active_gradient: None,
@ -519,16 +523,16 @@ pub enum PresetWidth {
#[derive(Debug, Clone, PartialEq)]
pub struct DefaultColumnWidth(pub Option<PresetWidth>);
#[derive(knuffel::Decode, Debug, Default, Clone, Copy, PartialEq, Eq)]
#[derive(knuffel::Decode, Debug, Default, Clone, Copy, PartialEq)]
pub struct Struts {
#[knuffel(child, unwrap(argument), default)]
pub left: u16,
pub left: FloatOrInt<0, 65535>,
#[knuffel(child, unwrap(argument), default)]
pub right: u16,
pub right: FloatOrInt<0, 65535>,
#[knuffel(child, unwrap(argument), default)]
pub top: u16,
pub top: FloatOrInt<0, 65535>,
#[knuffel(child, unwrap(argument), default)]
pub bottom: u16,
pub bottom: FloatOrInt<0, 65535>,
}
#[derive(knuffel::Decode, Debug, Default, Clone, Copy, PartialEq, Eq)]
@ -863,7 +867,7 @@ pub struct BorderRule {
#[knuffel(child)]
pub on: bool,
#[knuffel(child, unwrap(argument))]
pub width: Option<u16>,
pub width: Option<FloatOrInt<0, 65535>>,
#[knuffel(child)]
pub active_color: Option<Color>,
#[knuffel(child)]
@ -1144,6 +1148,72 @@ impl<S: knuffel::traits::ErrorSpan> knuffel::DecodeScalar<S> for WorkspaceRefere
}
}
impl<S: knuffel::traits::ErrorSpan, const MIN: i32, const MAX: i32> knuffel::DecodeScalar<S>
for FloatOrInt<MIN, MAX>
{
fn type_check(
type_name: &Option<knuffel::span::Spanned<knuffel::ast::TypeName, S>>,
ctx: &mut knuffel::decode::Context<S>,
) {
if let Some(type_name) = &type_name {
ctx.emit_error(DecodeError::unexpected(
type_name,
"type name",
"no type name expected for this node",
));
}
}
fn raw_decode(
val: &knuffel::span::Spanned<knuffel::ast::Literal, S>,
ctx: &mut knuffel::decode::Context<S>,
) -> Result<Self, DecodeError<S>> {
match &**val {
knuffel::ast::Literal::Int(ref value) => match value.try_into() {
Ok(v) => {
if (MIN..=MAX).contains(&v) {
Ok(FloatOrInt(f64::from(v)))
} else {
ctx.emit_error(DecodeError::conversion(
val,
format!("value must be between {MIN} and {MAX}"),
));
Ok(FloatOrInt::default())
}
}
Err(e) => {
ctx.emit_error(DecodeError::conversion(val, e));
Ok(FloatOrInt::default())
}
},
knuffel::ast::Literal::Decimal(ref value) => match value.try_into() {
Ok(v) => {
if (f64::from(MIN)..=f64::from(MAX)).contains(&v) {
Ok(FloatOrInt(v))
} else {
ctx.emit_error(DecodeError::conversion(
val,
format!("value must be between {MIN} and {MAX}"),
));
Ok(FloatOrInt::default())
}
}
Err(e) => {
ctx.emit_error(DecodeError::conversion(val, e));
Ok(FloatOrInt::default())
}
},
_ => {
ctx.emit_error(DecodeError::unsupported(
val,
"Unsupported value, only numbers are recognized",
));
Ok(FloatOrInt::default())
}
}
}
}
#[derive(knuffel::Decode, Debug, Default, PartialEq)]
pub struct DebugConfig {
#[knuffel(child, unwrap(argument))]
@ -2483,7 +2553,7 @@ mod tests {
border {
on
width 8
width 8.5
}
}
@ -2579,7 +2649,7 @@ mod tests {
layout: Layout {
focus_ring: FocusRing {
off: false,
width: 5,
width: FloatOrInt(5.),
active_color: Color {
r: 0,
g: 100,
@ -2602,7 +2672,7 @@ mod tests {
},
border: Border {
off: false,
width: 3,
width: FloatOrInt(3.),
active_color: Color {
r: 255,
g: 200,
@ -2627,12 +2697,12 @@ mod tests {
default_column_width: Some(DefaultColumnWidth(Some(PresetWidth::Proportion(
0.25,
)))),
gaps: 8,
gaps: FloatOrInt(8.),
struts: Struts {
left: 1,
right: 2,
top: 3,
bottom: 0,
left: FloatOrInt(1.),
right: FloatOrInt(2.),
top: FloatOrInt(3.),
bottom: FloatOrInt(0.),
},
center_focused_column: CenterFocusedColumn::OnOverflow,
},
@ -2716,12 +2786,12 @@ mod tests {
open_fullscreen: Some(false),
focus_ring: BorderRule {
off: true,
width: Some(3),
width: Some(FloatOrInt(3.)),
..Default::default()
},
border: BorderRule {
on: true,
width: Some(8),
width: Some(FloatOrInt(8.5)),
..Default::default()
},
..Default::default()

View File

@ -59,15 +59,15 @@ impl TestCase for GradientAngle {
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
let (a, b) = (size.w / 4, size.h / 4);
let size = (size.w - a * 2, size.h - b * 2);
let area = Rectangle::from_loc_and_size((a, b), size);
let area = Rectangle::from_loc_and_size((a, b), size).to_f64();
[BorderRenderElement::new(
area.size,
Rectangle::from_loc_and_size((0, 0), area.size),
Rectangle::from_loc_and_size((0., 0.), area.size),
[1., 0., 0., 1.],
[0., 1., 0., 1.],
self.angle - FRAC_PI_2,
Rectangle::from_loc_and_size((0, 0), area.size),
Rectangle::from_loc_and_size((0., 0.), area.size),
0.,
CornerRadius::default(),
)

View File

@ -5,10 +5,10 @@ use std::time::Duration;
use niri::animation::ANIMATION_SLOWDOWN;
use niri::layout::focus_ring::FocusRing;
use niri::render_helpers::border::BorderRenderElement;
use niri_config::{Color, CornerRadius};
use niri_config::{Color, CornerRadius, FloatOrInt};
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Logical, Physical, Point, Rectangle, Scale, Size};
use smithay::utils::{Logical, Physical, Point, Rectangle, Size};
use super::TestCase;
@ -22,7 +22,7 @@ impl GradientArea {
pub fn new(_size: Size<i32, Logical>) -> Self {
let border = FocusRing::new(niri_config::FocusRing {
off: false,
width: 1,
width: FloatOrInt(1.),
active_color: Color::new(255, 255, 255, 128),
inactive_color: Color::default(),
active_gradient: None,
@ -75,13 +75,14 @@ impl TestCase for GradientArea {
let (a, b) = (size.w / 4, size.h / 4);
let rect_size = (size.w - a * 2, size.h - b * 2);
let area = Rectangle::from_loc_and_size((a, b), rect_size);
let area = Rectangle::from_loc_and_size((a, b), rect_size).to_f64();
let g_size = Size::from((
(size.w as f32 / 8. + size.w as f32 / 8. * 7. * f).round() as i32,
(size.h as f32 / 8. + size.h as f32 / 8. * 7. * f).round() as i32,
));
let g_loc = ((size.w - g_size.w) / 2, (size.h - g_size.h) / 2);
let g_loc = Point::from(((size.w - g_size.w) / 2, (size.h - g_size.h) / 2)).to_f64();
let g_size = g_size.to_f64();
let mut g_area = Rectangle::from_loc_and_size(g_loc, g_size);
g_area.loc -= area.loc;
@ -91,10 +92,11 @@ impl TestCase for GradientArea {
true,
Rectangle::default(),
CornerRadius::default(),
1.,
);
rv.extend(
self.border
.render(renderer, Point::from(g_loc), Scale::from(1.))
.render(renderer, g_loc)
.map(|elem| Box::new(elem) as _),
);
@ -105,7 +107,7 @@ impl TestCase for GradientArea {
[1., 0., 0., 1.],
[0., 1., 0., 1.],
FRAC_PI_4,
Rectangle::from_loc_and_size((0, 0), rect_size),
Rectangle::from_loc_and_size((0, 0), rect_size).to_f64(),
0.,
CornerRadius::default(),
)

View File

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

View File

@ -3,7 +3,7 @@ use std::time::Duration;
use niri::layout::Options;
use niri::render_helpers::RenderTarget;
use niri_config::Color;
use niri_config::{Color, FloatOrInt};
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Logical, Physical, Point, Rectangle, Scale, Size};
@ -20,7 +20,7 @@ impl Tile {
pub fn freeform(size: Size<i32, Logical>) -> Self {
let window = TestWindow::freeform(0);
let mut rv = Self::with_window(window);
rv.tile.request_tile_size(size, false);
rv.tile.request_tile_size(size.to_f64(), false);
rv.window.communicate();
rv
}
@ -28,7 +28,7 @@ impl Tile {
pub fn fixed_size(size: Size<i32, Logical>) -> Self {
let window = TestWindow::fixed_size(0);
let mut rv = Self::with_window(window);
rv.tile.request_tile_size(size, false);
rv.tile.request_tile_size(size.to_f64(), false);
rv.window.communicate();
rv
}
@ -37,7 +37,7 @@ impl Tile {
let window = TestWindow::fixed_size(0);
window.set_csd_shadow_width(64);
let mut rv = Self::with_window(window);
rv.tile.request_tile_size(size, false);
rv.tile.request_tile_size(size.to_f64(), false);
rv.window.communicate();
rv
}
@ -71,13 +71,13 @@ impl Tile {
},
border: niri_config::Border {
off: false,
width: 32,
width: FloatOrInt(32.),
active_color: Color::new(255, 163, 72, 255),
..Default::default()
},
..Default::default()
};
let tile = niri::layout::tile::Tile::new(window.clone(), Rc::new(options));
let tile = niri::layout::tile::Tile::new(window.clone(), 1., Rc::new(options));
Self { window, tile }
}
}
@ -85,7 +85,7 @@ impl Tile {
impl TestCase for Tile {
fn resize(&mut self, width: i32, height: i32) {
self.tile
.request_tile_size(Size::from((width, height)), false);
.request_tile_size(Size::from((width, height)).to_f64(), false);
self.window.communicate();
}
@ -102,12 +102,13 @@ impl TestCase for Tile {
renderer: &mut GlesRenderer,
size: Size<i32, Physical>,
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
let tile_size = self.tile.tile_size().to_physical(1);
let location = Point::from(((size.w - tile_size.w) / 2, (size.h - tile_size.h) / 2));
let size = size.to_f64();
let tile_size = self.tile.tile_size().to_physical(1.);
let location = Point::from((size.w - tile_size.w, size.h - tile_size.h)).downscale(2.);
self.tile.update(
true,
Rectangle::from_loc_and_size((-location.x, -location.y), size.to_logical(1)),
Rectangle::from_loc_and_size((-location.x, -location.y), size.to_logical(1.)),
);
self.tile
.render(

View File

@ -47,7 +47,9 @@ impl TestCase for Window {
size: Size<i32, Physical>,
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
let win_size = self.window.size().to_physical(1);
let location = Point::from(((size.w - win_size.w) / 2, (size.h - win_size.h) / 2));
let location = Point::from((size.w - win_size.w, size.h - win_size.h))
.to_f64()
.downscale(2.);
self.window
.render(

View File

@ -6,9 +6,9 @@ use niri::layout::{
InteractiveResizeData, LayoutElement, LayoutElementRenderElement, LayoutElementRenderSnapshot,
};
use niri::render_helpers::renderer::NiriRenderer;
use niri::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement};
use niri::render_helpers::{RenderTarget, SplitElements};
use niri::window::ResolvedWindowRules;
use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement};
use smithay::backend::renderer::element::{Id, Kind};
use smithay::output::{self, Output};
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
@ -37,7 +37,7 @@ impl TestWindow {
let size = Size::from((100, 200));
let min_size = Size::from((0, 0));
let max_size = Size::from((0, 0));
let buffer = SolidColorBuffer::new(size, [0.15, 0.64, 0.41, 1.]);
let buffer = SolidColorBuffer::new(size.to_f64(), [0.15, 0.64, 0.41, 1.]);
Self {
id,
@ -49,7 +49,7 @@ impl TestWindow {
buffer,
pending_fullscreen: false,
csd_shadow_width: 0,
csd_shadow_buffer: SolidColorBuffer::new((0, 0), [0., 0., 0., 0.3]),
csd_shadow_buffer: SolidColorBuffer::new((0., 0.), [0., 0., 0., 0.3]),
})),
}
}
@ -112,14 +112,14 @@ impl TestWindow {
if inner.size != new_size {
inner.size = new_size;
inner.buffer.resize(new_size);
inner.buffer.resize(new_size.to_f64());
rv = true;
}
let mut csd_shadow_size = new_size;
csd_shadow_size.w += inner.csd_shadow_width * 2;
csd_shadow_size.h += inner.csd_shadow_width * 2;
inner.csd_shadow_buffer.resize(csd_shadow_size);
inner.csd_shadow_buffer.resize(csd_shadow_size.to_f64());
rv
}
@ -147,8 +147,8 @@ impl LayoutElement for TestWindow {
fn render<R: NiriRenderer>(
&self,
_renderer: &mut R,
location: Point<i32, Logical>,
scale: Scale<f64>,
location: Point<f64, Logical>,
_scale: Scale<f64>,
alpha: f32,
_target: RenderTarget,
) -> SplitElements<LayoutElementRenderElement<R>> {
@ -158,17 +158,15 @@ impl LayoutElement for TestWindow {
normal: vec![
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,
)

View File

@ -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<i32, Logical>) {
fn position_popup_within_rect(&self, popup: &PopupKind, target: Rectangle<f64, Logical>) {
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<i32, Logical>,
target: Rectangle<f64, Logical>,
) -> Rectangle<i32, Logical> {
// 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 {

View File

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

View File

@ -35,7 +35,7 @@ impl PointerGrab<State> for ResizeGrab {
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<i32, Logical>)>,
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<f64, Logical>)>,
event: &MotionEvent,
) {
// While the grab is active, no client has pointer focus.
@ -60,7 +60,7 @@ impl PointerGrab<State> for ResizeGrab {
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<i32, Logical>)>,
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<f64, Logical>)>,
event: &RelativeMotionEvent,
) {
// While the grab is active, no client has pointer focus.

View File

@ -46,7 +46,7 @@ impl PointerGrab<State> for ViewOffsetGrab {
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<i32, Logical>)>,
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<f64, Logical>)>,
event: &MotionEvent,
) {
// While the grab is active, no client has pointer focus.
@ -74,7 +74,7 @@ impl PointerGrab<State> for ViewOffsetGrab {
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<i32, Logical>)>,
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<f64, Logical>)>,
event: &RelativeMotionEvent,
) {
// While the grab is active, no client has pointer focus.

View File

@ -34,10 +34,10 @@ pub struct ClosingWindow {
block_out_from: Option<BlockOutFrom>,
/// Size of the window geometry.
geo_size: Size<i32, Logical>,
geo_size: Size<f64, Logical>,
/// Position in the workspace.
pos: Point<i32, Logical>,
pos: Point<f64, Logical>,
/// How much the texture should be offset.
buffer_offset: Point<f64, Logical>,
@ -64,8 +64,8 @@ impl ClosingWindow {
renderer: &mut GlesRenderer,
snapshot: RenderSnapshot<E, E>,
scale: Scale<f64>,
geo_size: Size<i32, Logical>,
pos: Point<i32, Logical>,
geo_size: Size<f64, Logical>,
pos: Point<f64, Logical>,
anim: Animation,
) -> anyhow::Result<Self> {
let _span = tracy_client::span!("ClosingWindow::new");
@ -123,7 +123,7 @@ impl ClosingWindow {
pub fn render(
&self,
renderer: &mut GlesRenderer,
view_rect: Rectangle<i32, Logical>,
view_rect: Rectangle<f64, Logical>,
scale: Scale<f64>,
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),

View File

@ -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<i32, Logical>; 8],
sizes: [Size<i32, Logical>; 8],
locations: [Point<f64, Logical>; 8],
sizes: [Size<f64, Logical>; 8],
borders: [BorderRenderElement; 8],
full_size: Size<i32, Logical>,
full_size: Size<f64, Logical>,
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<i32, Logical>,
win_size: Size<f64, Logical>,
is_active: bool,
is_border: bool,
view_rect: Rectangle<i32, Logical>,
view_rect: Rectangle<f64, Logical>,
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<i32, Logical>,
scale: Scale<f64>,
location: Point<f64, Logical>,
) -> impl Iterator<Item = FocusRingRenderElement> {
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<i32, Logical>| {
let mut push = |buffer, border: &BorderRenderElement, location: Point<f64, Logical>| {
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 {

View File

@ -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<R> => {
@ -110,7 +110,7 @@ pub trait LayoutElement {
fn render<R: NiriRenderer>(
&self,
renderer: &mut R,
location: Point<i32, Logical>,
location: Point<f64, Logical>,
scale: Scale<f64>,
alpha: f32,
target: RenderTarget,
@ -120,7 +120,7 @@ pub trait LayoutElement {
fn render_normal<R: NiriRenderer>(
&self,
renderer: &mut R,
location: Point<i32, Logical>,
location: Point<f64, Logical>,
scale: Scale<f64>,
alpha: f32,
target: RenderTarget,
@ -132,7 +132,7 @@ pub trait LayoutElement {
fn render_popups<R: NiriRenderer>(
&self,
renderer: &mut R,
location: Point<i32, Logical>,
location: Point<f64, Logical>,
scale: Scale<f64>,
alpha: f32,
target: RenderTarget,
@ -206,10 +206,10 @@ enum MonitorSet<W: LayoutElement> {
},
}
#[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<W: LayoutElement> Layout<W> {
@ -486,12 +496,12 @@ impl<W: LayoutElement> Layout<W> {
width: Option<ColumnWidth>,
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<W: LayoutElement> Layout<W> {
width: Option<ColumnWidth>,
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<W: LayoutElement> Layout<W> {
width: Option<ColumnWidth>,
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<W: LayoutElement> Layout<W> {
width: Option<ColumnWidth>,
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<W: LayoutElement> Layout<W> {
None
}
pub fn window_loc(&self, window: &W::Id) -> Option<Point<i32, Logical>> {
pub fn window_loc(&self, window: &W::Id) -> Option<Point<f64, Logical>> {
match &self.monitor_set {
MonitorSet::Normal { monitors, .. } => {
for mon in monitors {
@ -1440,7 +1450,7 @@ impl<W: LayoutElement> Layout<W> {
&self,
output: &Output,
pos_within_output: Point<f64, Logical>,
) -> Option<(&W, Option<Point<i32, Logical>>)> {
) -> Option<(&W, Option<Point<f64, Logical>>)> {
let MonitorSet::Normal { monitors, .. } = &self.monitor_set else {
return None;
};
@ -1485,8 +1495,15 @@ impl<W: LayoutElement> Layout<W> {
);
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<W: LayoutElement> Layout<W> {
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<W: LayoutElement> Default for MonitorSet<W> {
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<W: LayoutElement> Default for Layout<W> {
fn default() -> Self {
@ -2459,7 +2484,7 @@ mod tests {
fn render<R: NiriRenderer>(
&self,
_renderer: &mut R,
_location: Point<i32, Logical>,
_location: Point<f64, Logical>,
_scale: Scale<f64>,
_alpha: f32,
_target: RenderTarget,
@ -2595,9 +2620,19 @@ mod tests {
]
}
fn arbitrary_scale() -> impl Strategy<Value = f64> {
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<Value = u16> {
#[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<Value = f64> {
// 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<Value = Struts> {
@ -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,

View File

@ -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<W: LayoutElement> Monitor<W> {
/// 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<Rectangle<i32, Logical>> {
pub fn active_tile_visual_rectangle(&self) -> Option<Rectangle<f64, Logical>> {
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<W: LayoutElement> Monitor<W> {
pub fn window_under(
&self,
pos_within_output: Point<f64, Logical>,
) -> Option<(&W, Option<Point<i32, Logical>>)> {
) -> Option<(&W, Option<Point<f64, Logical>>)> {
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<W: LayoutElement> Monitor<W> {
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<W: LayoutElement> Monitor<W> {
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<W: LayoutElement> Monitor<W> {
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<W: LayoutElement> Monitor<W> {
) -> Vec<MonitorRenderElement<R>> {
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<W: LayoutElement> Monitor<W> {
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<W: LayoutElement> Monitor<W> {
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<W: LayoutElement> Monitor<W> {
(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<W: LayoutElement> Monitor<W> {
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<W: LayoutElement> Monitor<W> {
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.

View File

@ -55,8 +55,8 @@ impl OpenAnimation {
&self,
renderer: &mut GlesRenderer,
elements: &[impl RenderElement<GlesRenderer>],
geo_size: Size<i32, Logical>,
location: Point<i32, Logical>,
geo_size: Size<f64, Logical>,
location: Point<f64, Logical>,
scale: Scale<f64>,
) -> anyhow::Result<OpeningWindowRenderElement> {
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,
);

View File

@ -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<W: LayoutElement> {
fullscreen_backdrop: SolidColorBuffer,
/// The size we were requested to fullscreen into.
fullscreen_size: Size<i32, Logical>,
fullscreen_size: Size<f64, Logical>,
/// The animation upon opening a window.
open_animation: Option<OpenAnimation>,
@ -70,6 +69,9 @@ pub struct Tile<W: LayoutElement> {
/// 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<Options>,
}
@ -93,18 +95,18 @@ type TileRenderSnapshot =
#[derive(Debug)]
struct ResizeAnimation {
anim: Animation,
size_from: Size<i32, Logical>,
size_from: Size<f64, Logical>,
snapshot: LayoutElementRenderSnapshot,
}
#[derive(Debug)]
struct MoveAnimation {
anim: Animation,
from: i32,
from: f64,
}
impl<W: LayoutElement> Tile<W> {
pub fn new(window: W, options: Rc<Options>) -> Self {
pub fn new(window: W, scale: f64, options: Rc<Options>) -> 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<W: LayoutElement> Tile<W> {
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<W: LayoutElement> Tile<W> {
move_y_animation: None,
unmap_snapshot: None,
rounded_corner_damage: Default::default(),
scale,
options,
}
}
pub fn update_config(&mut self, options: Rc<Options>) {
pub fn update_config(&mut self, scale: f64, options: Rc<Options>) {
self.scale = scale;
self.options = options;
let rules = self.window.rules();
@ -147,7 +151,7 @@ impl<W: LayoutElement> Tile<W> {
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<W: LayoutElement> Tile<W> {
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<W: LayoutElement> Tile<W> {
|| self.move_y_animation.is_some()
}
pub fn update(&mut self, is_active: bool, view_rect: Rectangle<i32, Logical>) {
pub fn update(&mut self, is_active: bool, view_rect: Rectangle<f64, Logical>) {
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<W: LayoutElement> Tile<W> {
view_rect.size,
),
radius,
self.scale,
);
let draw_focus_ring_with_background = if self.effective_border_width().is_some() {
@ -281,20 +286,25 @@ impl<W: LayoutElement> Tile<W> {
!draw_focus_ring_with_background,
view_rect,
radius,
self.scale,
);
}
pub fn render_offset(&self) -> Point<i32, Logical> {
pub fn scale(&self) -> f64 {
self.scale
}
pub fn render_offset(&self) -> Point<f64, Logical> {
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<W: LayoutElement> Tile<W> {
self.resize_animation.as_ref().map(|resize| &resize.anim)
}
pub fn animate_move_from(&mut self, from: Point<i32, Logical>) {
pub fn animate_move_from(&mut self, from: Point<f64, Logical>) {
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<W: LayoutElement> Tile<W> {
});
}
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<W: LayoutElement> Tile<W> {
}
/// Returns `None` if the border is hidden and `Some(width)` if it should be shown.
fn effective_border_width(&self) -> Option<i32> {
fn effective_border_width(&self) -> Option<f64> {
if self.is_fullscreen {
return None;
}
@ -383,22 +393,27 @@ impl<W: LayoutElement> Tile<W> {
}
/// Returns the location of the window's visual geometry within this Tile.
pub fn window_loc(&self) -> Point<i32, Logical> {
let mut loc = Point::from((0, 0));
pub fn window_loc(&self) -> Point<f64, Logical> {
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<W: LayoutElement> Tile<W> {
loc
}
pub fn tile_size(&self) -> Size<i32, Logical> {
let mut size = self.window.size();
pub fn tile_size(&self) -> Size<f64, Logical> {
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<i32, Logical> {
self.window.size()
pub fn window_size(&self) -> Size<f64, Logical> {
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<i32, Logical> {
let mut size = self.window.size();
fn animated_window_size(&self) -> Size<f64, Logical> {
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<i32, Logical> {
fn animated_tile_size(&self) -> Size<f64, Logical> {
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<i32, Logical> {
let mut loc = Point::from((0, 0));
pub fn buf_loc(&self) -> Point<f64, Logical> {
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<W: LayoutElement> Tile<W> {
}
pub fn is_in_activation_region(&self, point: Point<f64, Logical>) -> 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<i32, Logical>, animate: bool) {
pub fn request_tile_size(&mut self, mut size: Size<f64, Logical>, 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<i32, Logical>) {
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<f64, Logical>) {
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<i32, Logical> {
let mut size = self.window.min_size();
pub fn min_size(&self) -> Size<f64, Logical> {
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<i32, Logical> {
let mut size = self.window.max_size();
pub fn max_size(&self) -> Size<f64, Logical> {
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<W: LayoutElement> Tile<W> {
fn render_inner<R: NiriRenderer>(
&self,
renderer: &mut R,
location: Point<i32, Logical>,
location: Point<f64, Logical>,
scale: Scale<f64>,
focus_ring: bool,
target: RenderTarget,
@ -581,7 +612,7 @@ impl<W: LayoutElement> Tile<W> {
};
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<W: LayoutElement> Tile<W> {
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<W: LayoutElement> Tile<W> {
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<W: LayoutElement> Tile<W> {
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<W: LayoutElement> Tile<W> {
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<W: LayoutElement> Tile<W> {
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<R: NiriRenderer>(
&self,
renderer: &mut R,
location: Point<i32, Logical>,
location: Point<f64, Logical>,
scale: Scale<f64>,
focus_ring: bool,
target: RenderTarget,
@ -798,7 +823,7 @@ impl<W: LayoutElement> Tile<W> {
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::<Vec<TileRenderElement<_>>>();
match open.render(renderer, &elements, self.tile_size(), location, scale) {
Ok(elem) => {
@ -843,7 +868,7 @@ impl<W: LayoutElement> Tile<W> {
let contents = self.render(
renderer,
Point::from((0, 0)),
Point::from((0., 0.)),
scale,
false,
RenderTarget::Output,
@ -852,7 +877,7 @@ impl<W: LayoutElement> Tile<W> {
// 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,

File diff suppressed because it is too large Load Diff

View File

@ -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<Output>,
// Surface under pointer and its location in global coordinate space.
pub surface: Option<(WlSurface, Point<i32, Logical>)>,
pub surface: Option<(WlSurface, Point<f64, Logical>)>,
// If surface belongs to a window, this is that window.
pub window: Option<Window>,
}
@ -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;
}
}

View File

@ -26,12 +26,12 @@ pub struct BorderRenderElement {
#[derive(Debug, Clone, Copy, PartialEq)]
struct Parameters {
size: Size<i32, Logical>,
gradient_area: Rectangle<i32, Logical>,
size: Size<f64, Logical>,
gradient_area: Rectangle<f64, Logical>,
color_from: [f32; 4],
color_to: [f32; 4],
angle: f32,
geometry: Rectangle<i32, Logical>,
geometry: Rectangle<f64, Logical>,
border_width: f32,
corner_radius: CornerRadius,
}
@ -39,12 +39,12 @@ struct Parameters {
impl BorderRenderElement {
#[allow(clippy::too_many_arguments)]
pub fn new(
size: Size<i32, Logical>,
gradient_area: Rectangle<i32, Logical>,
size: Size<f64, Logical>,
gradient_area: Rectangle<f64, Logical>,
color_from: [f32; 4],
color_to: [f32; 4],
angle: f32,
geometry: Rectangle<i32, Logical>,
geometry: Rectangle<f64, Logical>,
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<i32, Logical>,
gradient_area: Rectangle<i32, Logical>,
size: Size<f64, Logical>,
gradient_area: Rectangle<f64, Logical>,
color_from: [f32; 4],
color_to: [f32; 4],
angle: f32,
geometry: Rectangle<i32, Logical>,
geometry: Rectangle<f64, Logical>,
border_width: f32,
corner_radius: CornerRadius,
) {
@ -172,7 +172,7 @@ impl BorderRenderElement {
);
}
pub fn with_location(mut self, location: Point<i32, Logical>) -> Self {
pub fn with_location(mut self, location: Point<f64, Logical>) -> Self {
self.inner = self.inner.with_location(location);
self
}

View File

@ -18,7 +18,7 @@ pub struct ClippedSurfaceRenderElement<R: NiriRenderer> {
inner: WaylandSurfaceRenderElement<R>,
program: GlesTexProgram,
corner_radius: CornerRadius,
geometry: Rectangle<i32, Logical>,
geometry: Rectangle<f64, Logical>,
input_to_geo: Mat3,
}
@ -32,7 +32,7 @@ impl<R: NiriRenderer> ClippedSurfaceRenderElement<R> {
pub fn new(
elem: WaylandSurfaceRenderElement<R>,
scale: Scale<f64>,
geometry: Rectangle<i32, Logical>,
geometry: Rectangle<f64, Logical>,
program: GlesTexProgram,
corner_radius: CornerRadius,
) -> Self {
@ -86,7 +86,7 @@ impl<R: NiriRenderer> ClippedSurfaceRenderElement<R> {
pub fn will_clip(
elem: &WaylandSurfaceRenderElement<R>,
scale: Scale<f64>,
geometry: Rectangle<i32, Logical>,
geometry: Rectangle<f64, Logical>,
corner_radius: CornerRadius,
) -> bool {
let elem_geo = elem.geometry(scale);
@ -95,10 +95,10 @@ impl<R: NiriRenderer> ClippedSurfaceRenderElement<R> {
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<R: NiriRenderer> Element for ClippedSurfaceRenderElement<R> {
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<TtyRenderer<'render>>
}
impl RoundedCornerDamage {
pub fn set_size(&mut self, size: Size<i32, Logical>) {
pub fn set_size(&mut self, size: Size<f64, Logical>) {
self.damage.set_size(size);
}

View File

@ -7,7 +7,7 @@ use smithay::utils::{Buffer, Logical, Physical, Point, Rectangle, Scale, Size};
pub struct ExtraDamage {
id: Id,
commit: CommitCounter,
geometry: Rectangle<i32, Logical>,
geometry: Rectangle<f64, Logical>,
}
impl ExtraDamage {
@ -19,7 +19,7 @@ impl ExtraDamage {
}
}
pub fn set_size(&mut self, size: Size<i32, Logical>) {
pub fn set_size(&mut self, size: Size<f64, Logical>) {
if self.geometry.size == size {
return;
}
@ -32,7 +32,7 @@ impl ExtraDamage {
self.commit.increment();
}
pub fn with_location(mut self, location: Point<i32, Logical>) -> Self {
pub fn with_location(mut self, location: Point<f64, Logical>) -> Self {
self.geometry.loc = location;
self
}
@ -58,7 +58,7 @@ impl Element for ExtraDamage {
}
fn geometry(&self, scale: Scale<f64>) -> Rectangle<i32, Physical> {
self.geometry.to_physical_precise_round(scale)
self.geometry.to_physical_precise_up(scale)
}
}

View File

@ -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<B> {
pub buffer: B,
pub location: Point<i32, Logical>,
pub location: Point<f64, Logical>,
pub src: Option<Rectangle<f64, Logical>>,
pub dst: Option<Size<i32, Logical>>,
}
@ -67,7 +67,7 @@ pub trait ToRenderElement {
fn to_render_element(
&self,
location: Point<i32, Logical>,
location: Point<f64, Logical>,
scale: Scale<f64>,
alpha: f32,
kind: Kind,
@ -119,14 +119,14 @@ impl ToRenderElement for BakedBuffer<TextureBuffer<GlesTexture>> {
fn to_render_element(
&self,
location: Point<i32, Logical>,
location: Point<f64, Logical>,
_scale: Scale<f64>,
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<SolidColorBuffer> {
fn to_render_element(
&self,
location: Point<i32, Logical>,
scale: Scale<f64>,
location: Point<f64, Logical>,
_scale: Scale<f64>,
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)
}
}

View File

@ -18,12 +18,12 @@ pub struct ResizeRenderElement(ShaderRenderElement);
impl ResizeRenderElement {
#[allow(clippy::too_many_arguments)]
pub fn new(
area: Rectangle<i32, Logical>,
area: Rectangle<f64, Logical>,
scale: Scale<f64>,
texture_prev: (GlesTexture, Rectangle<i32, Physical>),
size_prev: Size<i32, Logical>,
size_prev: Size<f64, Logical>,
texture_next: (GlesTexture, Rectangle<i32, Physical>),
size_next: Size<i32, Logical>,
size_next: Size<f64, Logical>,
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.

View File

@ -23,8 +23,8 @@ pub struct ShaderRenderElement {
program: ProgramType,
id: Id,
commit_counter: CommitCounter,
area: Rectangle<i32, Logical>,
opaque_regions: Vec<Rectangle<i32, Logical>>,
area: Rectangle<f64, Logical>,
opaque_regions: Vec<Rectangle<f64, Logical>>,
alpha: f32,
additional_uniforms: Vec<Uniform<'static>>,
textures: HashMap<String, GlesTexture>,
@ -198,8 +198,8 @@ impl ShaderRenderElement {
#[allow(clippy::too_many_arguments)]
pub fn new(
program: ProgramType,
size: Size<i32, Logical>,
opaque_regions: Option<Vec<Rectangle<i32, Logical>>>,
size: Size<f64, Logical>,
opaque_regions: Option<Vec<Rectangle<f64, Logical>>>,
alpha: f32,
uniforms: Vec<Uniform<'_>>,
textures: HashMap<String, GlesTexture>,
@ -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<i32, Logical>,
opaque_regions: Option<Vec<Rectangle<i32, Logical>>>,
size: Size<f64, Logical>,
opaque_regions: Option<Vec<Rectangle<f64, Logical>>>,
uniforms: Vec<Uniform<'_>>,
textures: HashMap<String, GlesTexture>,
) {
@ -251,7 +251,7 @@ impl ShaderRenderElement {
self.commit_counter.increment();
}
pub fn with_location(mut self, location: Point<i32, Logical>) -> Self {
pub fn with_location(mut self, location: Point<f64, Logical>) -> Self {
self.area.loc = location;
self
}
@ -277,7 +277,7 @@ impl Element for ShaderRenderElement {
fn opaque_regions(&self, scale: Scale<f64>) -> OpaqueRegions<i32, Physical> {
self.opaque_regions
.iter()
.map(|region| region.to_physical_precise_round(scale))
.map(|region| region.to_physical_precise_down(scale))
.collect()
}

View File

@ -25,7 +25,7 @@ pub struct RenderSnapshot<C, B> {
pub block_out_from: Option<BlockOutFrom>,
/// Visual size of the element at the point of the snapshot.
pub size: Size<i32, Logical>,
pub size: Size<f64, Logical>,
/// Contents rendered into a texture (lazily).
pub texture: OnceCell<Option<(GlesTexture, Rectangle<i32, Physical>)>>,
@ -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();

View File

@ -12,7 +12,7 @@ use super::BakedBuffer;
pub fn render_snapshot_from_surface_tree(
renderer: &mut GlesRenderer,
surface: &WlSurface,
location: Point<i32, Logical>,
location: Point<f64, Logical>,
storage: &mut Vec<BakedBuffer<TextureBuffer<GlesTexture>>>,
) {
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;
}

View File

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

View File

@ -95,16 +95,24 @@ pub fn to_physical_precise_round<N: Coordinate>(scale: f64, logical: impl Coordi
N::from_f64((logical.to_f64() * scale).round())
}
pub fn output_size(output: &Output) -> Size<i32, Logical> {
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<f64, Logical> {
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 {

View File

@ -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<R: NiriRenderer>(
&self,
renderer: &mut R,
location: Point<i32, Logical>,
location: Point<f64, Logical>,
scale: Scale<f64>,
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<R: NiriRenderer>(
&self,
renderer: &mut R,
location: Point<i32, Logical>,
location: Point<f64, Logical>,
scale: Scale<f64>,
alpha: f32,
target: RenderTarget,
) -> Vec<LayoutElementRenderElement<R>> {
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<R: NiriRenderer>(
&self,
renderer: &mut R,
location: Point<i32, Logical>,
location: Point<f64, Logical>,
scale: Scale<f64>,
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,

View File

@ -48,6 +48,10 @@ layout {
Set gaps around (inside and outside) windows in logical pixels.
<sup>Since: 0.1.7</sup> 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.
<sup>Since: 0.1.7</sup> 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.
<sup>Since: 0.1.7</sup> 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 {

34
wiki/Fractional-Layout.md Normal file
View File

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

View File

@ -24,3 +24,4 @@
## Development
* [Design Principles](./Design-Principles.md)
* [Developing niri](./Developing-niri.md)
* [Fractional Layout](./Fractional-Layout.md)