aboutsummaryrefslogtreecommitdiff
path: root/src/layout
diff options
context:
space:
mode:
authorIvan Molodetskikh <yalterz@gmail.com>2025-09-20 09:37:52 +0300
committerIvan Molodetskikh <yalterz@gmail.com>2025-10-02 09:33:08 +0300
commitd5f4e79e4c35d4f5a7a4a64e85e72fb1545ef2db (patch)
tree4f5d4a299c1e8305aa147f326ac2b5c98de2873e /src/layout
parentd015c7e55bf455698cc4115af39549d9c8e20efc (diff)
downloadniri-d5f4e79e4c35d4f5a7a4a64e85e72fb1545ef2db.tar.gz
niri-d5f4e79e4c35d4f5a7a4a64e85e72fb1545ef2db.tar.bz2
niri-d5f4e79e4c35d4f5a7a4a64e85e72fb1545ef2db.zip
Add per-workspace layout config
Per-workspace background-color doesn't work yet.
Diffstat (limited to 'src/layout')
-rw-r--r--src/layout/mod.rs82
-rw-r--r--src/layout/tests.rs89
-rw-r--r--src/layout/workspace.rs50
3 files changed, 194 insertions, 27 deletions
diff --git a/src/layout/mod.rs b/src/layout/mod.rs
index b5b137ea..e6dacfde 100644
--- a/src/layout/mod.rs
+++ b/src/layout/mod.rs
@@ -380,6 +380,12 @@ struct InteractiveMoveData<W: LayoutElement> {
///
/// This helps the pointer remain inside the window as it resizes.
pub(self) pointer_ratio_within_window: (f64, f64),
+ /// Config overrides for the workspace where the window is currently located.
+ ///
+ /// To avoid sudden window changes when starting an interactive move, it will remember the
+ /// config overrides for the workspace where the move originated from. As soon as the window
+ /// moves over some different workspace though, this override will reset.
+ pub(self) workspace_config: Option<(WorkspaceId, niri_config::LayoutPart)>,
}
#[derive(Debug)]
@@ -576,6 +582,13 @@ impl Options {
}
}
+ fn with_merged_layout(mut self, part: Option<&niri_config::LayoutPart>) -> Self {
+ if let Some(part) = part {
+ self.layout.merge_with(part);
+ }
+ self
+ }
+
fn adjusted_for_scale(mut self, scale: f64) -> Self {
let round = |logical: f64| round_logical_in_physical_max1(scale, logical);
@@ -2296,7 +2309,9 @@ impl<W: LayoutElement> Layout<W> {
move_.tile.verify_invariants();
let scale = move_.output.current_scale().fractional_scale();
- let options = Options::clone(&self.options).adjusted_for_scale(scale);
+ let options = Options::clone(&self.options)
+ .with_merged_layout(move_.workspace_config.as_ref().map(|(_, c)| c))
+ .adjusted_for_scale(scale);
assert_eq!(
&*move_.tile.options, &options,
"interactive moved tile options must be \
@@ -2830,6 +2845,14 @@ impl<W: LayoutElement> Layout<W> {
}
pub fn update_config(&mut self, config: &Config) {
+ // Update workspace-specific config for all named workspaces.
+ for ws in self.workspaces_mut() {
+ let Some(name) = ws.name() else { continue };
+ if let Some(config) = config.workspaces.iter().find(|w| &w.name.0 == name) {
+ ws.update_layout_config(config.layout.clone().map(|x| x.0));
+ }
+ }
+
self.update_options(Options::from_config(config));
}
@@ -2839,11 +2862,10 @@ impl<W: LayoutElement> Layout<W> {
if let Some(InteractiveMoveState::Moving(move_)) = &mut self.interactive_move {
let view_size = output_size(&move_.output);
let scale = move_.output.current_scale().fractional_scale();
- move_.tile.update_config(
- view_size,
- scale,
- Rc::new(Options::clone(&options).adjusted_for_scale(scale)),
- );
+ let options = Options::clone(&options)
+ .with_merged_layout(move_.workspace_config.as_ref().map(|(_, c)| c))
+ .adjusted_for_scale(scale);
+ move_.tile.update_config(view_size, scale, Rc::new(options));
}
match &mut self.monitor_set {
@@ -3737,15 +3759,17 @@ impl<W: LayoutElement> Layout<W> {
}
.band(sq_dist / INTERACTIVE_MOVE_START_THRESHOLD);
- let (is_floating, tile) = self
+ let (is_floating, tile, workspace_config) = self
.workspaces_mut()
.find(|ws| ws.has_window(&window_id))
.map(|ws| {
+ let workspace_config = ws.layout_config().cloned().map(|c| (ws.id(), c));
(
ws.is_floating(&window_id),
ws.tiles_mut()
.find(|tile| *tile.window().id() == window_id)
.unwrap(),
+ workspace_config,
)
})
.unwrap();
@@ -3807,11 +3831,10 @@ impl<W: LayoutElement> Layout<W> {
let view_size = output_size(&output);
let scale = output.current_scale().fractional_scale();
- tile.update_config(
- view_size,
- scale,
- Rc::new(Options::clone(&self.options).adjusted_for_scale(scale)),
- );
+ let options = Options::clone(&self.options)
+ .with_merged_layout(workspace_config.as_ref().map(|(_, c)| c))
+ .adjusted_for_scale(scale);
+ tile.update_config(view_size, scale, Rc::new(options));
// Unfullscreen.
let floating_size = tile.floating_window_size;
@@ -3864,6 +3887,7 @@ impl<W: LayoutElement> Layout<W> {
is_full_width,
is_floating,
pointer_ratio_within_window,
+ workspace_config,
};
if let Some((tile_pos, zoom)) = tile_pos {
@@ -3880,6 +3904,23 @@ impl<W: LayoutElement> Layout<W> {
return false;
}
+ let mut ws_id = None;
+ if let Some(mon) = self.monitor_for_output(&output) {
+ let (insert_ws, _) = mon.insert_position(move_.pointer_pos_within_output);
+ if let InsertWorkspace::Existing(id) = insert_ws {
+ ws_id = Some(id);
+ }
+ }
+
+ // If moved over a different workspace, reset the config override.
+ let mut update_config = false;
+ if let Some((id, _)) = &move_.workspace_config {
+ if Some(*id) != ws_id {
+ move_.workspace_config = None;
+ update_config = true;
+ }
+ }
+
if output != move_.output {
move_.tile.window().output_leave(&move_.output);
move_.tile.window().output_enter(&output);
@@ -3887,15 +3928,18 @@ impl<W: LayoutElement> Layout<W> {
output.current_scale(),
output.current_transform(),
);
- let view_size = output_size(&output);
- let scale = output.current_scale().fractional_scale();
- move_.tile.update_config(
- view_size,
- scale,
- Rc::new(Options::clone(&self.options).adjusted_for_scale(scale)),
- );
move_.output = output.clone();
self.focus_output(&output);
+ update_config = true;
+ }
+
+ if update_config {
+ let view_size = output_size(&output);
+ let scale = output.current_scale().fractional_scale();
+ let options = Options::clone(&self.options)
+ .with_merged_layout(move_.workspace_config.as_ref().map(|(_, c)| c))
+ .adjusted_for_scale(scale);
+ move_.tile.update_config(view_size, scale, Rc::new(options));
}
move_.pointer_pos_within_output = pointer_pos_within_output;
diff --git a/src/layout/tests.rs b/src/layout/tests.rs
index 6f425047..7d51b751 100644
--- a/src/layout/tests.rs
+++ b/src/layout/tests.rs
@@ -414,11 +414,19 @@ enum Op {
ws_name: usize,
#[proptest(strategy = "prop::option::of(1..=5usize)")]
output_name: Option<usize>,
+ #[proptest(strategy = "prop::option::of(arbitrary_layout_part().prop_map(Box::new))")]
+ layout_config: Option<Box<niri_config::LayoutPart>>,
},
UnnameWorkspace {
#[proptest(strategy = "1..=5usize")]
ws_name: usize,
},
+ UpdateWorkspaceLayoutConfig {
+ #[proptest(strategy = "1..=5usize")]
+ ws_name: usize,
+ #[proptest(strategy = "prop::option::of(arbitrary_layout_part().prop_map(Box::new))")]
+ layout_config: Option<Box<niri_config::LayoutPart>>,
+ },
AddWindow {
params: TestWindowParams,
},
@@ -817,15 +825,31 @@ impl Op {
Op::AddNamedWorkspace {
ws_name,
output_name,
+ layout_config,
} => {
layout.ensure_named_workspace(&WorkspaceConfig {
name: WorkspaceName(format!("ws{ws_name}")),
open_on_output: output_name.map(|name| format!("output{name}")),
+ layout: layout_config.map(|x| niri_config::WorkspaceLayoutPart(*x)),
});
}
Op::UnnameWorkspace { ws_name } => {
layout.unname_workspace(&format!("ws{ws_name}"));
}
+ Op::UpdateWorkspaceLayoutConfig {
+ ws_name,
+ layout_config,
+ } => {
+ let ws_name = format!("ws{ws_name}");
+ let Some(ws) = layout
+ .workspaces_mut()
+ .find(|ws| ws.name() == Some(&ws_name))
+ else {
+ return;
+ };
+
+ ws.update_layout_config(layout_config.map(|x| *x));
+ }
Op::SetWorkspaceName {
new_ws_name,
ws_name,
@@ -1607,6 +1631,7 @@ fn operations_dont_panic() {
Op::AddNamedWorkspace {
ws_name: 1,
output_name: Some(1),
+ layout_config: None,
},
Op::UnnameWorkspace { ws_name: 1 },
Op::AddWindow {
@@ -1754,6 +1779,7 @@ fn operations_from_starting_state_dont_panic() {
Op::AddNamedWorkspace {
ws_name: 1,
output_name: Some(1),
+ layout_config: None,
},
Op::UnnameWorkspace { ws_name: 1 },
Op::AddWindow {
@@ -2289,10 +2315,12 @@ fn removing_all_outputs_preserves_empty_named_workspaces() {
Op::AddNamedWorkspace {
ws_name: 1,
output_name: None,
+ layout_config: None,
},
Op::AddNamedWorkspace {
ws_name: 2,
output_name: None,
+ layout_config: None,
},
Op::RemoveOutput(1),
];
@@ -2655,6 +2683,7 @@ fn named_workspace_to_output() {
Op::AddNamedWorkspace {
ws_name: 1,
output_name: None,
+ layout_config: None,
},
Op::AddOutput(1),
Op::MoveWorkspaceToOutput(1),
@@ -2670,6 +2699,7 @@ fn named_workspace_to_output_ewaf() {
Op::AddNamedWorkspace {
ws_name: 1,
output_name: Some(2),
+ layout_config: None,
},
Op::AddOutput(1),
Op::AddOutput(2),
@@ -2898,6 +2928,65 @@ fn interactive_move_toggle_floating_ends_dnd_gesture() {
}
#[test]
+fn interactive_move_from_workspace_with_layout_config() {
+ let ops = [
+ Op::AddNamedWorkspace {
+ ws_name: 1,
+ output_name: Some(2),
+ layout_config: Some(Box::new(niri_config::LayoutPart {
+ border: Some(niri_config::BorderRule {
+ on: true,
+ ..Default::default()
+ }),
+ ..Default::default()
+ })),
+ },
+ Op::AddOutput(1),
+ Op::AddWindow {
+ params: TestWindowParams::new(2),
+ },
+ Op::InteractiveMoveBegin {
+ window: 2,
+ output_idx: 1,
+ px: 0.0,
+ py: 0.0,
+ },
+ Op::InteractiveMoveUpdate {
+ window: 2,
+ dx: 0.0,
+ dy: 3586.692842955048,
+ output_idx: 1,
+ px: 0.0,
+ py: 0.0,
+ },
+ // Now remove and add the output. It will have the same workspace.
+ Op::RemoveOutput(1),
+ Op::AddOutput(1),
+ Op::InteractiveMoveUpdate {
+ window: 2,
+ dx: 0.0,
+ dy: 0.0,
+ output_idx: 1,
+ px: 0.0,
+ py: 0.0,
+ },
+ // Now move onto a different workspace.
+ Op::FocusWorkspaceDown,
+ Op::CompleteAnimations,
+ Op::InteractiveMoveUpdate {
+ window: 2,
+ dx: 0.0,
+ dy: 0.0,
+ output_idx: 1,
+ px: 0.0,
+ py: 0.0,
+ },
+ ];
+
+ check_ops(ops);
+}
+
+#[test]
fn set_width_fixed_negative() {
let ops = [
Op::AddOutput(3),
diff --git a/src/layout/workspace.rs b/src/layout/workspace.rs
index 2d71ad1b..06d3009d 100644
--- a/src/layout/workspace.rs
+++ b/src/layout/workspace.rs
@@ -99,6 +99,9 @@ pub struct Workspace<W: LayoutElement> {
/// Optional name of this workspace.
pub(super) name: Option<String>,
+ /// Layout config overrides for this workspace.
+ layout_config: Option<niri_config::LayoutPart>,
+
/// Unique ID of this workspace.
id: WorkspaceId,
}
@@ -202,7 +205,7 @@ impl<W: LayoutElement> Workspace<W> {
pub fn new_with_config(
output: Output,
- config: Option<WorkspaceConfig>,
+ mut config: Option<WorkspaceConfig>,
clock: Clock,
base_options: Rc<Options>,
) -> Self {
@@ -212,9 +215,14 @@ impl<W: LayoutElement> Workspace<W> {
.map(OutputId)
.unwrap_or(OutputId::new(&output));
+ let layout_config = config.as_mut().and_then(|c| c.layout.take().map(|x| x.0));
+
let scale = output.current_scale();
- let options =
- Rc::new(Options::clone(&base_options).adjusted_for_scale(scale.fractional_scale()));
+ let options = Rc::new(
+ Options::clone(&base_options)
+ .with_merged_layout(layout_config.as_ref())
+ .adjusted_for_scale(scale.fractional_scale()),
+ );
let view_size = output_size(&output);
let working_area = compute_working_area(&output);
@@ -253,12 +261,13 @@ impl<W: LayoutElement> Workspace<W> {
base_options,
options,
name: config.map(|c| c.name.0),
+ layout_config,
id: WorkspaceId::next(),
}
}
pub fn new_with_config_no_outputs(
- config: Option<WorkspaceConfig>,
+ mut config: Option<WorkspaceConfig>,
clock: Clock,
base_options: Rc<Options>,
) -> Self {
@@ -269,9 +278,14 @@ impl<W: LayoutElement> Workspace<W> {
.unwrap_or_default(),
);
+ let layout_config = config.as_mut().and_then(|c| c.layout.take().map(|x| x.0));
+
let scale = smithay::output::Scale::Integer(1);
- let options =
- Rc::new(Options::clone(&base_options).adjusted_for_scale(scale.fractional_scale()));
+ let options = Rc::new(
+ Options::clone(&base_options)
+ .with_merged_layout(layout_config.as_ref())
+ .adjusted_for_scale(scale.fractional_scale()),
+ );
let view_size = Size::from((1280., 720.));
let working_area = Rectangle::from_size(Size::from((1280., 720.)));
@@ -310,6 +324,7 @@ impl<W: LayoutElement> Workspace<W> {
base_options,
options,
name: config.map(|c| c.name.0),
+ layout_config,
id: WorkspaceId::next(),
}
}
@@ -370,7 +385,11 @@ impl<W: LayoutElement> Workspace<W> {
pub fn update_config(&mut self, base_options: Rc<Options>) {
let scale = self.scale.fractional_scale();
- let options = Rc::new(Options::clone(&base_options).adjusted_for_scale(scale));
+ let options = Rc::new(
+ Options::clone(&base_options)
+ .with_merged_layout(self.layout_config.as_ref())
+ .adjusted_for_scale(scale),
+ );
self.scrolling.update_config(
self.view_size,
@@ -394,6 +413,15 @@ impl<W: LayoutElement> Workspace<W> {
self.options = options;
}
+ pub fn update_layout_config(&mut self, layout_config: Option<niri_config::LayoutPart>) {
+ if self.layout_config == layout_config {
+ return;
+ }
+
+ self.layout_config = layout_config;
+ self.update_config(self.base_options.clone());
+ }
+
pub fn update_shaders(&mut self) {
self.scrolling.update_shaders();
self.floating.update_shaders();
@@ -1770,6 +1798,10 @@ impl<W: LayoutElement> Workspace<W> {
self.working_area
}
+ pub fn layout_config(&self) -> Option<&niri_config::LayoutPart> {
+ self.layout_config.as_ref()
+ }
+
#[cfg(test)]
pub fn scrolling(&self) -> &ScrollingSpace<W> {
&self.scrolling
@@ -1788,7 +1820,9 @@ impl<W: LayoutElement> Workspace<W> {
assert!(scale > 0.);
assert!(scale.is_finite());
- let options = Options::clone(&self.base_options).adjusted_for_scale(scale);
+ let options = Options::clone(&self.base_options)
+ .with_merged_layout(self.layout_config.as_ref())
+ .adjusted_for_scale(scale);
assert_eq!(
&*self.options, &options,
"options must be base options adjusted for scale"