Commit fc167134b65d
tarball2 files changed+69-8
@@ -39,13 +39,18 @@ urgent = "#ebcb8b"
3939
# Variable refresh rate on outputs that support it, off, on, or fullscreen.4040
vrr = "fullscreen"4141
42
-# Per connector. transform and position follow a reload, mode needs a replug.43
-# transform is normal, 90, 180, 270, flipped, flipped-90, flipped-180 or44
-# flipped-270. Outputs without a position line up left to right.42
+# Per connector, or [output.nested] for the nested backend's window. transform,43
+# position and scale follow a reload, mode needs a replug. transform is normal,44
+# 90, 180, 270, flipped, flipped-90, flipped-180 or flipped-270. Outputs without45
+# a position line up left to right. position is in logical pixels, the size of46
+# the output divided by its scale. scale is 0.5 to 4.0 and fractional values47
+# are rounded to the nearest 1/120. X11 clients are drawn at scale 1 and48
+# stretched.4549
# [output.DP-1]4650
# transform = "90"4751
# position = [2560, 0]4852
# mode = "1920x1080@60"53
+# scale = 1.54954
5055
[animation]5156
enabled = true@@ -113,13 +113,14 @@ impl Default for Gaps {
113113
#[serde(default)]114114
pub struct Outputs {115115
pub vrr: Vrr,116
- /// Settings for one connector, by name, like `DP-1`.116
+ /// Settings for one connector, by name, like `DP-1`, or `nested` for the117
+ /// nested backend's window.117118
#[serde(flatten)]118119
pub named: std::collections::BTreeMap<String, OutputConfig>,119120
}120121
121
-/// Transform and position follow a reload. A new mode waits for a restart122
-/// or a replug, since it means a modeset.122
+/// Transform, position and scale follow a reload. A new mode waits for a123
+/// restart or a replug, since it means a modeset.123124
#[derive(Deserialize, Debug, Clone, PartialEq, Default)]124125
#[serde(default, deny_unknown_fields)]125126
pub struct OutputConfig {@@ -130,8 +131,34 @@ pub struct OutputConfig {
130131
/// `WIDTHxHEIGHT` or `WIDTHxHEIGHT@HZ`. Without a rate the highest one131132
/// for that size wins.132133
pub mode: Option<String>,134
+ /// How many physical pixels make a logical one, `0.5` to `4.0` in steps of135
+ /// 1/120. Positions are in logical pixels.136
+ pub scale: Option<f64>,133137
}134138
139
+impl OutputConfig {140
+ pub fn scale(&self) -> f64 {141
+ self.scale.unwrap_or(1.0)142
+ }143
+144
+ fn validate(&mut self, name: &str, w: &mut Vec<String>) {145
+ let Some(s) = self.scale else {146
+ return;147
+ };148
+ if !(MIN_SCALE..=MAX_SCALE).contains(&s) {149
+ w.push(format!(150
+ "{name}.scale: {s} is outside {MIN_SCALE} to {MAX_SCALE}, ignored"151
+ ));152
+ self.scale = None;153
+ } else {154
+ self.scale = Some((s * 120.0).round() / 120.0);155
+ }156
+ }157
+}158
+159
+const MIN_SCALE: f64 = 0.5;160
+const MAX_SCALE: f64 = 4.0;161
+135162
/// Counterclockwise rotation, optionally after flipping horizontally.136163
#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default)]137164
#[serde(rename_all = "kebab-case")]@@ -576,12 +603,19 @@ impl Config {
576603
let animation = section("animation", table.remove("animation"), w);577604
let xwayland = section("xwayland", table.remove("xwayland"), w);578605
let decoration = section("decoration", table.remove("decoration"), w);579
- let output: Outputs = section("output", table.remove("output"), w);580
- for name in output.named.keys().filter(|n| !connector_like(n)) {606
+ let mut output: Outputs = section("output", table.remove("output"), w);607
+ for name in output608
+ .named609
+ .keys()610
+ .filter(|n| *n != "nested" && !connector_like(n))611
+ {581612
w.push(format!(582613
"output.{name}: not a connector name like DP-1 or HDMI-A-1, matches nothing"583614
));584615
}616
+ for (name, config) in &mut output.named {617
+ config.validate(&format!("output.{name}"), w);618
+ }585619
let startup = items("startup", table.remove("startup"), w);586620
let mut binds = Vec::new();587621
if let Some(value) = table.remove("binds") {@@ -876,6 +910,28 @@ mod tests {
876910
}877911
878912
#[test]913
+ fn output_scale_is_snapped_and_range_checked() {914
+ let loaded = Config::parse(915
+ r#"916
+ [output.DP-1]917
+ scale = 1.5918
+ [output.DP-2]919
+ scale = 1.3333920
+ [output.DP-3]921
+ scale = 9.0922
+ [output.nested]923
+ scale = 2.0924
+ "#,925
+ )926
+ .unwrap();927
+ let named = &loaded.config.output.named;928
+ assert_eq!(named["DP-1"].scale(), 1.5);929
+ assert_eq!(named["DP-2"].scale(), 160.0 / 120.0);930
+ assert_eq!(named["DP-3"].scale(), 1.0);931
+ assert_eq!(loaded.warnings.len(), 1, "{:#?}", loaded.warnings);932
+ }933
+934
+ #[test]879935
fn a_bad_option_costs_only_itself() {880936
let loaded = Config::parse(881937
r##"