aboutsummaryrefslogtreecommitdiff
path: root/src/layout/mod.rs
diff options
context:
space:
mode:
authorIvan Molodetskikh <yalterz@gmail.com>2024-06-17 09:16:28 +0300
committerIvan Molodetskikh <yalterz@gmail.com>2024-06-18 14:01:28 +0300
commit1dae45c58d7eabeda21ef490d712915890bf6cff (patch)
tree62c473ab1662a1161ed522517ea57b7bd8db340c /src/layout/mod.rs
parent997119c44338ad96a40b4a1d6e958f77062a37ef (diff)
downloadniri-1dae45c58d7eabeda21ef490d712915890bf6cff.tar.gz
niri-1dae45c58d7eabeda21ef490d712915890bf6cff.tar.bz2
niri-1dae45c58d7eabeda21ef490d712915890bf6cff.zip
Refactor layout to fractional-logical
Lets borders, gaps, and everything else stay pixel-perfect even with fractional scale. Allows setting fractional border widths, gaps, struts. See the new wiki .md for more details.
Diffstat (limited to 'src/layout/mod.rs')
-rw-r--r--src/layout/mod.rs204
1 files changed, 165 insertions, 39 deletions
diff --git a/src/layout/mod.rs b/src/layout/mod.rs
index 135f694f..ffc5e0e8 100644
--- a/src/layout/mod.rs
+++ b/src/layout/mod.rs
@@ -34,9 +34,8 @@ use std::mem;
use std::rc::Rc;
use std::time::Duration;
-use niri_config::{CenterFocusedColumn, Config, Struts, Workspace as WorkspaceConfig};
+use niri_config::{CenterFocusedColumn, Config, FloatOrInt, Struts, Workspace as WorkspaceConfig};
use niri_ipc::SizeChange;
-use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement};
use smithay::backend::renderer::element::surface::WaylandSurfaceRenderElement;
use smithay::backend::renderer::element::Id;
use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture};
@@ -50,9 +49,10 @@ use self::workspace::{compute_working_area, Column, ColumnWidth, OutputId, Works
use crate::niri_render_elements;
use crate::render_helpers::renderer::NiriRenderer;
use crate::render_helpers::snapshot::RenderSnapshot;
+use crate::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement};
use crate::render_helpers::texture::TextureBuffer;
use crate::render_helpers::{BakedBuffer, RenderTarget, SplitElements};
-use crate::utils::{output_size, ResizeEdge};
+use crate::utils::{output_size, round_logical_in_physical_max1, ResizeEdge};
use crate::window::ResolvedWindowRules;
pub mod closing_window;
@@ -63,7 +63,7 @@ pub mod tile;
pub mod workspace;
/// Size changes up to this many pixels don't animate.
-pub const RESIZE_ANIMATION_THRESHOLD: i32 = 10;
+pub const RESIZE_ANIMATION_THRESHOLD: f64 = 10.;
niri_render_elements! {
LayoutElementRenderElement<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,