From aac54d0ea1a5c95aba698aed583ee3fa9670f18b Mon Sep 17 00:00:00 2001 From: Ivan Molodetskikh Date: Fri, 13 Dec 2024 10:28:25 +0300 Subject: Implement floating child stacking above parents --- src/layout/floating.rs | 72 ++++++++++++-- src/layout/mod.rs | 259 +++++++++++++++++++++++++++++++++++++++++++++++- src/layout/workspace.rs | 4 + 3 files changed, 326 insertions(+), 9 deletions(-) (limited to 'src/layout') 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 FloatingSpace { fn add_tile_at( &mut self, - idx: usize, + mut idx: usize, mut tile: Tile, pos: Option>, activate: bool, @@ -353,6 +353,14 @@ impl FloatingSpace { 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 FloatingSpace { 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) { @@ -382,6 +392,33 @@ impl FloatingSpace { 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 = 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> { let id = self.active_window_id.clone()?; Some(self.remove_tile(&id)) @@ -434,15 +471,22 @@ impl FloatingSpace { 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 FloatingSpace { 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) -> bool { let Some(tile_idx) = self.idx_of(id) else { return false; @@ -769,7 +822,7 @@ impl FloatingSpace { 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 FloatingSpace { 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>; + fn is_child_of(&self, parent: &Self) -> bool; + fn rules(&self) -> &ResolvedWindowRules; /// Runs periodic clean-up tasks. @@ -1113,6 +1115,16 @@ impl Layout { 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) { 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>, bbox: Cell>, initial_bbox: Rectangle, requested_size: Cell>>, @@ -3884,6 +3897,8 @@ mod tests { struct TestWindowParams { #[proptest(strategy = "1..=5usize")] id: usize, + #[proptest(strategy = "arbitrary_parent_id()")] + parent_id: Option, is_floating: bool, #[proptest(strategy = "arbitrary_bbox()")] bbox: Rectangle, @@ -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> { + 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, }, SwitchFocusFloatingTiling, + SetParent { + #[proptest(strategy = "1..=5usize")] + id: usize, + #[proptest(strategy = "prop::option::of(1..=5usize)")] + new_parent_id: Option, + }, 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, 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 { // 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 Workspace { }) } + 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) { if !self.floating.update_window(window, serial) { self.scrolling.update_window(window, serial); -- cgit