Implement floating child stacking above parents

This commit is contained in:
Ivan Molodetskikh
2024-12-13 10:28:25 +03:00
parent 4fe718581b
commit aac54d0ea1
6 changed files with 350 additions and 9 deletions
+4
View File
@@ -240,6 +240,10 @@ impl LayoutElement for TestWindow {
self.inner.borrow().requested_size
}
fn is_child_of(&self, _parent: &Self) -> bool {
false
}
fn refresh(&self) {}
fn rules(&self) -> &ResolvedWindowRules {
+16
View File
@@ -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);
+66 -6
View File
@@ -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 {
+256 -3
View File
@@ -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
+4
View File
@@ -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);
+4
View File
@@ -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();
}