mirror of
https://github.com/niri-wm/niri.git
synced 2026-06-22 02:01:55 +07:00
Implement floating child stacking above parents
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user