Commit b44dde08d7ac
tarball6 files changed+130-13
@@ -65,6 +65,8 @@ pub struct Window {
6565
/// user chose.6666
pub float_default: bool,6767
pub fullscreen: bool,68
+ /// The window this one is a dialog or other transient of.69
+ pub parent: Option<WindowId>,6870
pub sticky: bool,6971
pub scratchpad: Option<usize>,7072
evicted_from: Option<String>,@@ -103,6 +105,8 @@ pub struct Arrangement {
103105
/// Visible floating windows and shown scratchpads, bottom to top.104106
pub floating: Vec<(WindowId, Rect)>,105107
pub fullscreen: Option<WindowId>,108
+ /// Floating transients of the fullscreen window, which stay above it.109
+ pub over: Vec<WindowId>,106110
}107111
108112
#[derive(Clone, Copy, Debug)]@@ -330,6 +334,7 @@ impl Layout {
330334
float_rect: floating,331335
float_default: false,332336
fullscreen: false,337
+ parent: None,333338
sticky: false,334339
scratchpad: None,335340
evicted_from: None,@@ -352,6 +357,11 @@ impl Layout {
352357
if let Some(s) = self.windows[w].scratchpad {353358
self.leave_scratchpad(s, w);354359
}360
+ for win in self.windows.values_mut() {361
+ if win.parent == Some(w) {362
+ win.parent = None;363
+ }364
+ }355365
self.windows.remove(w);356366
}357367
@@ -440,13 +450,17 @@ impl Layout {
440450
let from = self.focused_on(o).and_then(|f| self.rect_of(f));441451
let target = from.and_then(|from| {442452
let arr = self.arrange(o);453
+ let reachable = |w| {454
+ arr.fullscreen455
+ .is_none_or(|f| w == f || arr.over.contains(&w))456
+ };443457
let candidates = arr444458
.tiled445459
.iter()446460
.filter(|p| p.visible)447461
.map(|p| (p.window, p.rect))448462
.chain(arr.floating.iter().copied())449
- .filter(|&(w, _)| Some(w) != self.focused_on(o));463
+ .filter(|&(w, _)| Some(w) != self.focused_on(o) && reachable(w));450464
nearest(from, dir, candidates)451465
});452466
if let Some(w) = target {@@ -703,11 +717,56 @@ impl Layout {
703717
.rev()704718
.copied()705719
.find(|&w| self.windows[w].fullscreen && vis(w));720
+ let over = fullscreen721
+ .map(|f| {722
+ floating723
+ .iter()724
+ .map(|&(w, _)| w)725
+ .filter(|&w| w != f && self.descends(w, f))726
+ .collect()727
+ })728
+ .unwrap_or_default();706729
Arrangement {707730
tiled,708731
floating,709732
fullscreen,733
+ over,734
+ }735
+ }736
+737
+ /// Records that `w` is a transient of `parent`.738
+ pub fn set_parent(&mut self, w: WindowId, parent: Option<WindowId>) {739
+ if parent.is_some_and(|p| p == w || self.descends(p, w)) {740
+ return;741
+ }742
+ if let Some(win) = self.windows.get_mut(w) {743
+ win.parent = parent;744
+ }745
+ }746
+747
+ fn descends(&self, mut w: WindowId, of: WindowId) -> bool {748
+ for _ in 0..self.windows.len() {749
+ match self.windows.get(w).and_then(|x| x.parent) {750
+ Some(p) if p == of => return true,751
+ Some(p) => w = p,752
+ None => return false,753
+ }710754
}755
+ false756
+ }757
+758
+ /// Whether a fullscreen window on `w`'s output hides it. The fullscreen759
+ /// window's transients are not hidden, and a window that is itself760
+ /// fullscreen takes over.761
+ pub fn covered(&self, w: WindowId) -> bool {762
+ let Some(win) = self.windows.get(w) else {763
+ return false;764
+ };765
+ let Some(o) = win.output else {766
+ return false;767
+ };768
+ let arr = self.arrange(o);769
+ !win.fullscreen && arr.fullscreen.is_some_and(|f| f != w) && !arr.over.contains(&w)711770
}712771
713772
#[cfg(test)]@@ -470,3 +470,35 @@ fn moves_are_seen_or_undone() {
470470
}471471
}472472
}473
+474
+/// A fullscreen window hides everything but its own transients, which stay475
+/// reachable by focus and are not hidden when they map.476
+#[test]477
+fn fullscreen_leaves_its_transients_reachable() {478
+ let mut l = Layout::default();479
+ let o = l.add_output("a", AREA);480
+ let beside = l.add_window(None, None, None);481
+ l.focus(beside);482
+ let game = l.add_window(None, None, None);483
+ l.focus(game);484
+ l.toggle_fullscreen(game);485
+ let stranger = l.add_window(None, None, None);486
+ assert!(l.covered(stranger));487
+ let dialog = l.add_window(None, None, Some(Rect::new(300.0, 200.0, 200.0, 100.0)));488
+ l.set_parent(dialog, Some(game));489
+ assert!(!l.covered(dialog));490
+ l.focus(dialog);491
+492
+ let arr = l.arrange(o);493
+ assert_eq!(arr.fullscreen, Some(game));494
+ assert_eq!(arr.over, vec![dialog]);495
+ for dir in [Dir::Left, Dir::Right, Dir::Up, Dir::Down] {496
+ if let Some(w) = l.focus_dir(dir) {497
+ assert!(w == game || w == dialog, "focus walked to a covered window");498
+ }499
+ }500
+501
+ l.remove_window(game);502
+ assert!(!l.covered(stranger));503
+ assert_eq!(l.window(dialog).and_then(|w| w.parent), None);504
+}@@ -77,14 +77,14 @@ pub fn send_frame_callbacks(
7777
crate::lock::send_frames(lock, output, elapsed);7878
return;7979
}80
- let covering = wm81
- .layout82
- .arrange(o)80
+ let arr = wm.layout.arrange(o);81
+ let covering = arr8382
.fullscreen8483
.filter(|&w| wm.anim.gliding(w, now).is_none())8584
.and_then(|w| wm.window(w));85
+ let over: Vec<_> = arr.over.iter().filter_map(|&w| wm.window(w)).collect();8686
for window in wm.space.elements_for_output(output) {87
- if covering.is_some_and(|c| c != window) {87
+ if covering.is_some_and(|c| c != window && !over.contains(&window)) {8888
continue;8989
}9090
window.send_frame(output, elapsed, Some(Duration::ZERO), |_, _| {@@ -3,7 +3,7 @@
33
44
use crate::wm::{LastFrame, Wm};55
use monolith_config::Decoration;6
-use monolith_layout::{OutputId, Rect};6
+use monolith_layout::{OutputId, Rect, WindowId};77
use monolith_render_vk::{Clip, Effect, VulkanFrame, VulkanRenderer, VulkanTexture};88
use smithay::backend::renderer::element::memory::MemoryRenderBufferRenderElement;99
use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement};@@ -89,14 +89,18 @@ pub fn build(
8989
}9090
}9191
}92
- for &(w, r) in &arr.floating {92
+ let float = |&(w, r): &(WindowId, Rect)| {9393
let glide = anim.gliding(w, now);94
- above.push((w, local(glide.unwrap_or(r)), true, glide.is_some()));95
- }94
+ (w, local(glide.unwrap_or(r)), true, glide.is_some())95
+ };96
+ let (over, under): (Vec<_>, Vec<_>) =97
+ arr.floating.iter().partition(|f| arr.over.contains(&f.0));98
+ above.extend(under.into_iter().map(float));9699
if let Some(w) = arr.fullscreen {97100
let glide = anim.gliding(w, now);98101
above.push((w, local(glide.unwrap_or(area)), false, glide.is_some()));99102
}103
+ above.extend(over.into_iter().map(float));100104
let focused = layout.focused();101105
let mut out = Vec::new();102106
for x in unmanaged.iter().rev() {@@ -251,6 +251,7 @@ impl State {
251251
let now = now_ns();252252
let mut shown = Vec::new();253253
let mut tracks = Vec::new();254
+ let mut over = Vec::new();254255
let outputs: Vec<OutputId> = self.wm.layout.outputs().map(|(o, _)| o).collect();255256
for o in outputs {256257
let arr = self.wm.layout.arrange(o);@@ -268,6 +269,7 @@ impl State {
268269
if let Some(w) = arr.fullscreen {269270
shown.push((w, area, Kind::Fullscreen, o));270271
}272
+ over.extend(arr.over.iter().copied());271273
let held: Vec<WindowId> = arr272274
.tiled273275
.iter()@@ -384,7 +386,7 @@ impl State {
384386
for window in hidden {385387
self.wm.space.unmap_elem(&window);386388
}387
- for id in mapped {389
+ for id in mapped.into_iter().chain(over) {388390
if let Some(window) = self.wm.window(id).cloned() {389391
self.wm.space.raise_element(&window, false);390392
}@@ -62,6 +62,8 @@ impl State {
6262
.wm6363
.layout6464
.add_window(output, tag, float.filter(|_| floating));65
+ let parent = self.parent_of(&window);66
+ self.wm.layout.set_parent(id, parent);6567
if floating != self.wm.layout.window(id).is_some_and(|w| w.floating) {6668
self.wm.layout.toggle_floating(id);6769
}@@ -84,12 +86,30 @@ impl State {
8486
s.reclassify = reclassify;8587
}8688
self.await_first_buffer(id);87
- if self.wm.layout.visible(id) {89
+ if self.wm.layout.visible(id) && !self.wm.layout.covered(id) {8890
self.wm.layout.focus(id);8991
}9092
self.apply_layout();9193
}9294
95
+ /// The mapped window `window` is a dialog or other transient of.96
+ fn parent_of(&self, window: &Window) -> Option<WindowId> {97
+ if let Some(x) = window.x11_surface() {98
+ let parent = x.is_transient_for()?;99
+ return self100
+ .wm101
+ .iter()102
+ .find(|(_, w)| {103
+ w.window104
+ .x11_surface()105
+ .is_some_and(|p| p.window_id() == parent)106
+ })107
+ .map(|(id, _)| id);108
+ }109
+ let parent = window.toplevel()?.parent()?;110
+ self.window_id(&parent)111
+ }112
+93113
/// Holds a new window on its seed until its first buffer lands, so the94114
/// spawn animation never plays over an empty slot. Clients that are slow95115
/// to draw get it anyway after a while.@@ -264,9 +284,9 @@ impl State {
264284
fn float_at_natural_size(&mut self, id: WindowId, w: f64, h: f64) {265285
let parent = self266286
.wm287
+ .layout267288
.window(id)268
- .and_then(|win| win.toplevel()?.parent())269
- .and_then(|p| self.window_id(&p))289
+ .and_then(|win| win.parent)270290
.and_then(|p| self.wm.get(p)?.placed)271291
.map(|(_, r, _)| r);272292
let output = self