aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/handlers/xdg_shell.rs16
-rw-r--r--src/layout/floating.rs72
-rw-r--r--src/layout/mod.rs259
-rw-r--r--src/layout/workspace.rs4
-rw-r--r--src/window/mapped.rs4
5 files changed, 346 insertions, 9 deletions
diff --git a/src/handlers/xdg_shell.rs b/src/handlers/xdg_shell.rs
index 869492a2..5cb515ce 100644
--- a/src/handlers/xdg_shell.rs
+++ b/src/handlers/xdg_shell.rs
@@ -647,6 +647,22 @@ impl XdgShellHandler for State {
fn title_changed(&mut self, toplevel: ToplevelSurface) {
self.update_window_rules(&toplevel);
}
+
+ fn parent_changed(&mut self, toplevel: ToplevelSurface) {
+ let Some(parent) = toplevel.parent() else {
+ return;
+ };
+
+ if let Some((mapped, output)) = self.niri.layout.find_window_and_output_mut(&parent) {
+ let output = output.cloned();
+ let window = mapped.window.clone();
+ if self.niri.layout.descendants_added(&window) {
+ if let Some(output) = output {
+ self.niri.queue_redraw(&output);
+ }
+ }
+ }
+ }
}
delegate_xdg_shell!(State);
diff --git a/src/layout/floating.rs b/src/layout/floating.rs
index 64446c61..342a8d3c 100644
--- a/src/layout/floating.rs
+++ b/src/layout/floating.rs
@@ -325,7 +325,7 @@ impl<W: LayoutElement> FloatingSpace<W> {
fn add_tile_at(
&mut self,
- idx: usize,
+ mut idx: usize,
mut tile: Tile<W>,
pos: Option<Point<f64, Logical>>,
activate: bool,
@@ -353,6 +353,14 @@ impl<W: LayoutElement> FloatingSpace<W> {
self.active_window_id = Some(win.id().clone());
}
+ // Make sure the tile isn't inserted below its parent.
+ for (i, tile_above) in self.tiles.iter().enumerate().take(idx) {
+ if win.is_child_of(tile_above.window()) {
+ idx = i;
+ break;
+ }
+ }
+
let mut pos = pos.unwrap_or_else(|| {
let area_size = self.working_area.size.to_point();
let tile_size = tile.tile_size().to_point();
@@ -367,6 +375,8 @@ impl<W: LayoutElement> FloatingSpace<W> {
let data = Data::new(self.working_area, &tile, pos);
self.data.insert(idx, data);
self.tiles.insert(idx, tile);
+
+ self.bring_up_descendants_of(idx);
}
pub fn add_tile_above(&mut self, above: &W::Id, tile: Tile<W>) {
@@ -382,6 +392,33 @@ impl<W: LayoutElement> FloatingSpace<W> {
self.add_tile_at(idx, tile, Some(pos), activate);
}
+ fn bring_up_descendants_of(&mut self, idx: usize) {
+ let tile = &self.tiles[idx];
+ let win = tile.window();
+
+ // We always maintain the correct stacking order, so walking descendants back to front
+ // should give us all of them.
+ let mut descendants: Vec<usize> = Vec::new();
+ for (i, tile_below) in self.tiles.iter().enumerate().skip(idx + 1).rev() {
+ let win_below = tile_below.window();
+ if win_below.is_child_of(win)
+ || descendants
+ .iter()
+ .any(|idx| win_below.is_child_of(self.tiles[*idx].window()))
+ {
+ descendants.push(i);
+ }
+ }
+
+ // Now, descendants is in back-to-front order, and repositioning them in the front-to-back
+ // order will preserve the subsequent indices and work out right.
+ let mut idx = idx;
+ for descendant_idx in descendants.into_iter().rev() {
+ self.raise_window(descendant_idx, idx);
+ idx += 1;
+ }
+ }
+
pub fn remove_active_tile(&mut self) -> Option<RemovedTile<W>> {
let id = self.active_window_id.clone()?;
Some(self.remove_tile(&id))
@@ -434,15 +471,22 @@ impl<W: LayoutElement> FloatingSpace<W> {
return false;
};
- let tile = self.tiles.remove(idx);
- let data = self.data.remove(idx);
- self.tiles.insert(0, tile);
- self.data.insert(0, data);
+ self.raise_window(idx, 0);
self.active_window_id = Some(id.clone());
+ self.bring_up_descendants_of(0);
true
}
+ fn raise_window(&mut self, from_idx: usize, to_idx: usize) {
+ assert!(to_idx <= from_idx);
+
+ let tile = self.tiles.remove(from_idx);
+ let data = self.data.remove(from_idx);
+ self.tiles.insert(to_idx, tile);
+ self.data.insert(to_idx, data);
+ }
+
pub fn start_close_animation_for_window(
&mut self,
renderer: &mut GlesRenderer,
@@ -561,6 +605,15 @@ impl<W: LayoutElement> FloatingSpace<W> {
win.request_size(win_size, animate, None);
}
+ pub fn descendants_added(&mut self, id: &W::Id) -> bool {
+ let Some(idx) = self.idx_of(id) else {
+ return false;
+ };
+
+ self.bring_up_descendants_of(idx);
+ true
+ }
+
pub fn update_window(&mut self, id: &W::Id, serial: Option<Serial>) -> bool {
let Some(tile_idx) = self.idx_of(id) else {
return false;
@@ -769,7 +822,7 @@ impl<W: LayoutElement> FloatingSpace<W> {
assert!(self.scale.is_finite());
assert_eq!(self.tiles.len(), self.data.len());
- for (tile, data) in zip(&self.tiles, &self.data) {
+ for (i, (tile, data)) in zip(&self.tiles, &self.data).enumerate() {
assert!(Rc::ptr_eq(&self.options, &tile.options));
assert_eq!(self.clock, tile.clock);
assert_eq!(self.scale, tile.scale());
@@ -786,6 +839,13 @@ impl<W: LayoutElement> FloatingSpace<W> {
data2.update(tile);
data2.update_config(self.working_area);
assert_eq!(data, &data2, "tile data must be up to date");
+
+ for tile_below in &self.tiles[i + 1..] {
+ assert!(
+ !tile_below.window().is_child_of(tile.window()),
+ "children must be stacked above parents"
+ );
+ }
}
if let Some(id) = &self.active_window_id {
diff --git a/src/layout/mod.rs b/src/layout/mod.rs
index a0592546..5068f43a 100644
--- a/src/layout/mod.rs
+++ b/src/layout/mod.rs
@@ -186,6 +186,8 @@ pub trait LayoutElement {
/// Size previously requested through [`LayoutElement::request_size()`].
fn requested_size(&self) -> Option<Size<i32, Logical>>;
+ fn is_child_of(&self, parent: &Self) -> bool;
+
fn rules(&self) -> &ResolvedWindowRules;
/// Runs periodic clean-up tasks.
@@ -1113,6 +1115,16 @@ impl<W: LayoutElement> Layout<W> {
None
}
+ pub fn descendants_added(&mut self, id: &W::Id) -> bool {
+ for ws in self.workspaces_mut() {
+ if ws.descendants_added(id) {
+ return true;
+ }
+ }
+
+ false
+ }
+
pub fn update_window(&mut self, window: &W::Id, serial: Option<Serial>) {
if let Some(InteractiveMoveState::Moving(move_)) = &mut self.interactive_move {
if move_.tile.window().id() == window {
@@ -3868,6 +3880,7 @@ mod tests {
#[derive(Debug)]
struct TestWindowInner {
id: usize,
+ parent_id: Cell<Option<usize>>,
bbox: Cell<Rectangle<i32, Logical>>,
initial_bbox: Rectangle<i32, Logical>,
requested_size: Cell<Option<Size<i32, Logical>>>,
@@ -3884,6 +3897,8 @@ mod tests {
struct TestWindowParams {
#[proptest(strategy = "1..=5usize")]
id: usize,
+ #[proptest(strategy = "arbitrary_parent_id()")]
+ parent_id: Option<usize>,
is_floating: bool,
#[proptest(strategy = "arbitrary_bbox()")]
bbox: Rectangle<i32, Logical>,
@@ -3895,6 +3910,7 @@ mod tests {
pub fn new(id: usize) -> Self {
Self {
id,
+ parent_id: None,
is_floating: false,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
min_max_size: Default::default(),
@@ -3906,6 +3922,7 @@ mod tests {
fn new(params: TestWindowParams) -> Self {
Self(Rc::new(TestWindowInner {
id: params.id,
+ parent_id: Cell::new(params.parent_id),
bbox: Cell::new(params.bbox),
initial_bbox: params.bbox,
requested_size: Cell::new(None),
@@ -4033,6 +4050,10 @@ mod tests {
self.0.requested_size.get()
}
+ fn is_child_of(&self, parent: &Self) -> bool {
+ self.0.parent_id.get() == Some(parent.0.id)
+ }
+
fn refresh(&self) {}
fn rules(&self) -> &ResolvedWindowRules {
@@ -4136,6 +4157,13 @@ mod tests {
]
}
+ fn arbitrary_parent_id() -> impl Strategy<Value = Option<usize>> {
+ prop_oneof![
+ 5 => Just(None),
+ 1 => prop::option::of(1..=5usize),
+ ]
+ }
+
#[derive(Debug, Clone, Copy, Arbitrary)]
enum Op {
AddOutput(#[proptest(strategy = "1..=5usize")] usize),
@@ -4195,6 +4223,7 @@ mod tests {
FocusWindowUpOrColumnRight,
FocusWindowOrWorkspaceDown,
FocusWindowOrWorkspaceUp,
+ FocusWindow(#[proptest(strategy = "1..=5usize")] usize),
MoveColumnLeft,
MoveColumnRight,
MoveColumnToFirst,
@@ -4265,6 +4294,12 @@ mod tests {
id: Option<usize>,
},
SwitchFocusFloatingTiling,
+ SetParent {
+ #[proptest(strategy = "1..=5usize")]
+ id: usize,
+ #[proptest(strategy = "prop::option::of(1..=5usize)")]
+ new_parent_id: Option<usize>,
+ },
Communicate(#[proptest(strategy = "1..=5usize")] usize),
Refresh {
is_active: bool,
@@ -4446,10 +4481,15 @@ mod tests {
Op::UnnameWorkspace { ws_name } => {
layout.unname_workspace(&format!("ws{ws_name}"));
}
- Op::AddWindow { params } => {
+ Op::AddWindow { mut params } => {
if layout.has_window(&params.id) {
return;
}
+ if let Some(parent_id) = params.parent_id {
+ if parent_id_causes_loop(layout, params.id, parent_id) {
+ params.parent_id = None;
+ }
+ }
let win = TestWindow::new(params);
layout.add_window(
@@ -4461,7 +4501,7 @@ mod tests {
);
}
Op::AddWindowRightOf {
- params,
+ mut params,
right_of_id,
} => {
let mut found_right_of = false;
@@ -4511,10 +4551,19 @@ mod tests {
return;
}
+ if let Some(parent_id) = params.parent_id {
+ if parent_id_causes_loop(layout, params.id, parent_id) {
+ params.parent_id = None;
+ }
+ }
+
let win = TestWindow::new(params);
layout.add_window_right_of(&right_of_id, win, None, false, params.is_floating);
}
- Op::AddWindowToNamedWorkspace { params, ws_name } => {
+ Op::AddWindowToNamedWorkspace {
+ mut params,
+ ws_name,
+ } => {
let ws_name = format!("ws{ws_name}");
let mut found_workspace = false;
@@ -4567,6 +4616,12 @@ mod tests {
return;
}
+ if let Some(parent_id) = params.parent_id {
+ if parent_id_causes_loop(layout, params.id, parent_id) {
+ params.parent_id = None;
+ }
+ }
+
let win = TestWindow::new(params);
layout.add_window_to_named_workspace(
&ws_name,
@@ -4635,6 +4690,7 @@ mod tests {
Op::FocusWindowUpOrColumnRight => layout.focus_up_or_right(),
Op::FocusWindowOrWorkspaceDown => layout.focus_window_or_workspace_down(),
Op::FocusWindowOrWorkspaceUp => layout.focus_window_or_workspace_up(),
+ Op::FocusWindow(id) => layout.activate_window(&id),
Op::MoveColumnLeft => layout.move_left(),
Op::MoveColumnRight => layout.move_right(),
Op::MoveColumnToFirst => layout.move_column_to_first(),
@@ -4740,6 +4796,62 @@ mod tests {
Op::SwitchFocusFloatingTiling => {
layout.switch_focus_floating_tiling();
}
+ Op::SetParent {
+ id,
+ mut new_parent_id,
+ } => {
+ if !layout.has_window(&id) {
+ return;
+ }
+
+ if let Some(parent_id) = new_parent_id {
+ if parent_id_causes_loop(layout, id, parent_id) {
+ new_parent_id = None;
+ }
+ }
+
+ let mut update = false;
+
+ if let Some(InteractiveMoveState::Moving(move_)) = &layout.interactive_move {
+ if move_.tile.window().0.id == id {
+ move_.tile.window().0.parent_id.set(new_parent_id);
+ update = true;
+ }
+ }
+
+ match &mut layout.monitor_set {
+ MonitorSet::Normal { monitors, .. } => {
+ 'outer: for mon in monitors {
+ for ws in &mut mon.workspaces {
+ for win in ws.windows() {
+ if win.0.id == id {
+ win.0.parent_id.set(new_parent_id);
+ update = true;
+ break 'outer;
+ }
+ }
+ }
+ }
+ }
+ MonitorSet::NoOutputs { workspaces, .. } => {
+ 'outer: for ws in workspaces {
+ for win in ws.windows() {
+ if win.0.id == id {
+ win.0.parent_id.set(new_parent_id);
+ update = true;
+ break 'outer;
+ }
+ }
+ }
+ }
+ }
+
+ if update {
+ if let Some(new_parent_id) = new_parent_id {
+ layout.descendants_added(&new_parent_id);
+ }
+ }
+ }
Op::Communicate(id) => {
let mut update = false;
@@ -6270,6 +6382,147 @@ mod tests {
assert!(win.0.pending_activated.get());
}
+ #[test]
+ fn stacking_add_parent_brings_up_child() {
+ let ops = [
+ Op::AddOutput(0),
+ Op::AddWindow {
+ params: TestWindowParams {
+ is_floating: true,
+ parent_id: Some(1),
+ ..TestWindowParams::new(0)
+ },
+ },
+ Op::AddWindow {
+ params: TestWindowParams {
+ is_floating: true,
+ ..TestWindowParams::new(1)
+ },
+ },
+ ];
+
+ check_ops(&ops);
+ }
+
+ #[test]
+ fn stacking_add_parent_brings_up_descendants() {
+ let ops = [
+ Op::AddOutput(0),
+ Op::AddWindow {
+ params: TestWindowParams {
+ is_floating: true,
+ parent_id: Some(2),
+ ..TestWindowParams::new(0)
+ },
+ },
+ Op::AddWindow {
+ params: TestWindowParams {
+ is_floating: true,
+ parent_id: Some(0),
+ ..TestWindowParams::new(1)
+ },
+ },
+ Op::AddWindow {
+ params: TestWindowParams {
+ is_floating: true,
+ ..TestWindowParams::new(2)
+ },
+ },
+ ];
+
+ check_ops(&ops);
+ }
+
+ #[test]
+ fn stacking_activate_brings_up_descendants() {
+ let ops = [
+ Op::AddOutput(0),
+ Op::AddWindow {
+ params: TestWindowParams {
+ is_floating: true,
+ ..TestWindowParams::new(0)
+ },
+ },
+ Op::AddWindow {
+ params: TestWindowParams {
+ is_floating: true,
+ parent_id: Some(0),
+ ..TestWindowParams::new(1)
+ },
+ },
+ Op::AddWindow {
+ params: TestWindowParams {
+ is_floating: true,
+ parent_id: Some(1),
+ ..TestWindowParams::new(2)
+ },
+ },
+ Op::AddWindow {
+ params: TestWindowParams {
+ is_floating: true,
+ ..TestWindowParams::new(3)
+ },
+ },
+ Op::FocusWindow(0),
+ ];
+
+ check_ops(&ops);
+ }
+
+ #[test]
+ fn stacking_set_parent_brings_up_child() {
+ let ops = [
+ Op::AddOutput(0),
+ Op::AddWindow {
+ params: TestWindowParams {
+ is_floating: true,
+ ..TestWindowParams::new(0)
+ },
+ },
+ Op::AddWindow {
+ params: TestWindowParams {
+ is_floating: true,
+ ..TestWindowParams::new(1)
+ },
+ },
+ Op::SetParent {
+ id: 0,
+ new_parent_id: Some(1),
+ },
+ ];
+
+ check_ops(&ops);
+ }
+
+ fn parent_id_causes_loop(layout: &Layout<TestWindow>, id: usize, mut parent_id: usize) -> bool {
+ if parent_id == id {
+ return true;
+ }
+
+ 'outer: loop {
+ for (_, win) in layout.windows() {
+ if win.0.id == parent_id {
+ match win.0.parent_id.get() {
+ Some(new_parent_id) => {
+ if new_parent_id == id {
+ // Found a loop.
+ return true;
+ }
+
+ parent_id = new_parent_id;
+ continue 'outer;
+ }
+ // Reached window with no parent.
+ None => return false,
+ }
+ }
+ }
+
+ // Parent is not in the layout.
+ return false;
+ }
+ }
+
fn arbitrary_spacing() -> impl Strategy<Value = f64> {
// Give equal weight to:
// - 0: the element is disabled
diff --git a/src/layout/workspace.rs b/src/layout/workspace.rs
index 1cc38ff6..65ff7863 100644
--- a/src/layout/workspace.rs
+++ b/src/layout/workspace.rs
@@ -1127,6 +1127,10 @@ impl<W: LayoutElement> Workspace<W> {
})
}
+ pub fn descendants_added(&mut self, id: &W::Id) -> bool {
+ self.floating.descendants_added(id)
+ }
+
pub fn update_window(&mut self, window: &W::Id, serial: Option<Serial>) {
if !self.floating.update_window(window, serial) {
self.scrolling.update_window(window, serial);
diff --git a/src/window/mapped.rs b/src/window/mapped.rs
index 299e0acc..adfab2d4 100644
--- a/src/window/mapped.rs
+++ b/src/window/mapped.rs
@@ -720,6 +720,10 @@ impl LayoutElement for Mapped {
self.toplevel().with_pending_state(|state| state.size)
}
+ fn is_child_of(&self, parent: &Self) -> bool {
+ self.toplevel().parent().as_ref() == Some(parent.toplevel().wl_surface())
+ }
+
fn refresh(&self) {
self.window.refresh();
}