diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/handlers/xdg_shell.rs | 16 | ||||
| -rw-r--r-- | src/layout/floating.rs | 72 | ||||
| -rw-r--r-- | src/layout/mod.rs | 259 | ||||
| -rw-r--r-- | src/layout/workspace.rs | 4 | ||||
| -rw-r--r-- | src/window/mapped.rs | 4 |
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(¶ms.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(); } |
