mirror of
https://github.com/YaLTeR/niri.git
synced 2025-10-05 16:12:47 +02:00
Add per-workspace layout config
Per-workspace background-color doesn't work yet.
This commit is contained in:
@@ -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,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
@@ -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>>,
|
||||
|
@@ -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;
|
||||
|
@@ -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),
|
||||
@@ -2897,6 +2927,65 @@ fn interactive_move_toggle_floating_ends_dnd_gesture() {
|
||||
check_ops(ops);
|
||||
}
|
||||
|
||||
#[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 = [
|
||||
|
@@ -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"
|
||||
|
Reference in New Issue
Block a user