diff options
| -rw-r--r-- | niri-config/src/lib.rs | 5 | ||||
| -rw-r--r-- | niri-config/src/workspace.rs | 36 | ||||
| -rw-r--r-- | src/layout/mod.rs | 82 | ||||
| -rw-r--r-- | src/layout/tests.rs | 89 | ||||
| -rw-r--r-- | src/layout/workspace.rs | 50 |
5 files changed, 233 insertions, 29 deletions
diff --git a/niri-config/src/lib.rs b/niri-config/src/lib.rs index 6dd32620..e969b250 100644 --- a/niri-config/src/lib.rs +++ b/niri-config/src/lib.rs @@ -38,7 +38,7 @@ pub use crate::output::{Output, OutputName, Outputs, Position, Vrr}; pub use crate::utils::FloatOrInt; use crate::utils::MergeWith as _; pub use crate::window_rule::{FloatingPosition, RelativeTo, WindowRule}; -pub use crate::workspace::Workspace; +pub use crate::workspace::{Workspace, WorkspaceLayoutPart}; #[derive(knuffel::Decode, Debug, PartialEq)] pub struct Config { @@ -1795,18 +1795,21 @@ mod tests { open_on_output: Some( "eDP-1", ), + layout: None, }, Workspace { name: WorkspaceName( "workspace-2", ), open_on_output: None, + layout: None, }, Workspace { name: WorkspaceName( "workspace-3", ), open_on_output: None, + layout: None, }, ], } diff --git a/niri-config/src/workspace.rs b/niri-config/src/workspace.rs index e502bdb7..d34dcf46 100644 --- a/niri-config/src/workspace.rs +++ b/niri-config/src/workspace.rs @@ -1,16 +1,50 @@ use knuffel::errors::DecodeError; -#[derive(knuffel::Decode, Debug, Clone, PartialEq, Eq)] +use crate::LayoutPart; + +#[derive(knuffel::Decode, Debug, Clone, PartialEq)] pub struct Workspace { #[knuffel(argument)] pub name: WorkspaceName, #[knuffel(child, unwrap(argument))] pub open_on_output: Option<String>, + #[knuffel(child)] + pub layout: Option<WorkspaceLayoutPart>, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct WorkspaceName(pub String); +#[derive(Debug, Clone, PartialEq)] +pub struct WorkspaceLayoutPart(pub LayoutPart); + +impl<S: knuffel::traits::ErrorSpan> knuffel::Decode<S> for WorkspaceLayoutPart { + fn decode_node( + node: &knuffel::ast::SpannedNode<S>, + ctx: &mut knuffel::decode::Context<S>, + ) -> Result<Self, DecodeError<S>> { + for child in node.children() { + let name = &**child.node_name; + + // Check for disallowed properties. + // + // - empty-workspace-above-first is a monitor-level concept. + // - insert-hint customization could make sense for workspaces, however currently it is + // also handled at the monitor level (since insert hints in-between workspaces are a + // monitor-level concept), so for now this config option would do nothing. + if matches!(name, "empty-workspace-above-first" | "insert-hint") { + ctx.emit_error(DecodeError::unexpected( + child, + "node", + format!("node `{name}` is not allowed inside `workspace.layout`"), + )); + } + } + + LayoutPart::decode_node(node, ctx).map(Self) + } +} + impl<S: knuffel::traits::ErrorSpan> knuffel::DecodeScalar<S> for WorkspaceName { fn type_check( type_name: &Option<knuffel::span::Spanned<knuffel::ast::TypeName, S>>, 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" |
