Commit 1c7e3075e57f
tarball5 files changed+166-16
@@ -72,6 +72,9 @@ pub struct Window {
7272
pub sticky: bool,7373
pub scratchpad: Option<usize>,7474
pub urgent: bool,75
+ /// Its client took the buffer away, as when hiding to a tray. The entry76
+ /// stays for when the client maps it again, but nothing shows or focuses it.77
+ pub hidden: bool,7578
evicted_from: Option<String>,7679
}7780
@@ -121,7 +124,8 @@ pub struct Arrangement {
121124
/// Visible floating windows and shown scratchpads, bottom to top.122125
pub floating: Vec<(WindowId, Rect)>,123126
pub fullscreen: Option<WindowId>,124
- /// Floating transients of the fullscreen window, which stay above it.127
+ /// Floating transients of the fullscreen window and shown scratchpads,128
+ /// which stay above it.125129
pub over: Vec<WindowId>,126130
}127131
@@ -321,7 +325,7 @@ impl Layout {
321325
let Some(win) = self.windows.get(w) else {322326
return false;323327
};324
- let Some(o) = win.output else {328
+ let Some(o) = win.output.filter(|_| !win.hidden) else {325329
return false;326330
};327331
match win.scratchpad {@@ -360,6 +364,7 @@ impl Layout {
360364
sticky: false,361365
scratchpad: None,362366
urgent: false,367
+ hidden: false,363368
evicted_from: None,364369
});365370
if let Some(o) = output {@@ -457,7 +462,8 @@ impl Layout {
457462
}458463
459464
/// Focuses `w`, or the deepest modal dialog over it, which is returned.460
- /// The windows between stay below it in the history, so closing the465
+ /// The windows between stay below it in the history, and a modal dialog466
+ /// focused on its own brings its parent up beneath it, so closing the461467
/// dialog hands focus back to its parent.462468
pub fn focus(&mut self, mut w: WindowId) -> Option<WindowId> {463469
loop {@@ -470,8 +476,13 @@ impl Layout {
470476
stack.push(w);471477
}472478
}479
+ let parent = win.parent.filter(|_| win.modal);473480
self.history.retain(|&h| h != w);474481
self.history.push(w);482
+ if let Some(p) = parent {483
+ self.history.retain(|&h| h != p);484
+ self.history.insert(self.history.len() - 1, p);485
+ }475486
self.windows[w].urgent = false;476487
match self.modal_child(w) {477488
Some(child) => w = child,@@ -716,7 +727,7 @@ impl Layout {
716727
/// Flags a window that wants attention unless it already has the focus,717728
/// and its tag too when the window is out of sight.718729
pub fn mark_urgent(&mut self, w: WindowId) {719
- if !self.windows.contains_key(w) {730
+ if self.windows.get(w).is_none_or(|win| win.hidden) {720731
return;721732
}722733
if self.focused() != Some(w) {@@ -805,6 +816,14 @@ impl Layout {
805816
}806817
}807818
819
+ /// Hides `w` or shows it again, keeping everything else about it.820
+ pub fn set_hidden(&mut self, w: WindowId, hidden: bool) {821
+ if let Some(win) = self.windows.get_mut(w) {822
+ win.hidden = hidden;823
+ win.urgent &= !hidden;824
+ }825
+ }826
+808827
/// Target geometry for everything on `o`, in output-local logical809828
/// coordinates offset by the output's area.810829
pub fn arrange(&self, o: OutputId) -> Arrangement {@@ -840,23 +859,28 @@ impl Layout {
840859
for sp in &self.scratchpads {841860
if let Some(i) = sp.shown842861
&& sp.output == Some(o)862
+ && !self.windows[sp.windows[i]].hidden843863
&& let Some(r) = self.windows[sp.windows[i]].float_rect844864
{845865
floating.push((sp.windows[i], centre_in(out.usable(), r.w, r.h)));846866
}847867
}868
+ let is_fullscreen = |w: WindowId| self.windows[w].fullscreen && vis(w);848869
let fullscreen = self849870
.history850871
.iter()851872
.rev()852873
.copied()853
- .find(|&w| self.windows[w].fullscreen && vis(w));874
+ .find(|&w| is_fullscreen(w))875
+ .or_else(|| self.windows.keys().find(|&w| is_fullscreen(w)));854876
let over = fullscreen855877
.map(|f| {856878
floating857879
.iter()858880
.map(|&(w, _)| w)859
- .filter(|&w| w != f && self.descends(w, f))881
+ .filter(|&w| {882
+ w != f && (self.descends(w, f) || self.windows[w].scratchpad.is_some())883
+ })860884
.collect()861885
})862886
.unwrap_or_default();@@ -903,9 +927,20 @@ impl Layout {
903927
}904928
}905929
930
+ /// Focuses `w` unless it is out of sight or a fullscreen window hides it,931
+ /// flagging it urgent then. Returns whether it took the focus.932
+ pub fn focus_unless_covered(&mut self, w: WindowId) -> bool {933
+ if self.visible(w) && !self.covered(w) {934
+ self.focus(w);935
+ return true;936
+ }937
+ self.mark_urgent(w);938
+ false939
+ }940
+906941
/// Whether a fullscreen window on `w`'s output hides it. The fullscreen907
- /// window's transients are not hidden, and a window that is itself908
- /// fullscreen takes over.942
+ /// window's transients and shown scratchpads are not hidden, and a window943
+ /// that is itself fullscreen takes over.909944
pub fn covered(&self, w: WindowId) -> bool {910945
let Some(win) = self.windows.get(w) else {911946
return false;@@ -39,10 +39,10 @@ impl Layout {
3939
let s = self.scratchpads.iter().position(|sp| sp.name == name)?;4040
let o = self.active?;4141
let sp = &mut self.scratchpads[s];42
- let next = match sp.shown {43
- None => 0,44
- Some(i) => i + 1,45
- };42
+ let mut next = sp.shown.map_or(0, |i| i + 1);43
+ while sp.windows.get(next).is_some_and(|&w| self.windows[w].hidden) {44
+ next += 1;45
+ }4646
if next >= sp.windows.len() {4747
sp.shown = None;4848
return None;@@ -63,7 +63,7 @@ impl Layout {
6363
win.output = Some(o);6464
}6565
win.float_rect = win.float_rect.map(|r| centre_in(usable, r.w, r.h));66
- self.focus(w);66
+ self.focus_unless_covered(w);6767
Some(w)6868
}6969
@@ -769,3 +769,118 @@ fn modal_dialogs_keep_focus_over_their_parent() {
769769
assert_eq!(l.focused(), Some(parent));770770
l.check();771771
}772
+773
+/// A fullscreen window nobody ever focused, as a rule with `focus = false`774
+/// makes, still takes the output once its tag is in view.775
+#[test]776
+fn fullscreen_does_not_need_focus_history() {777
+ let mut l = Layout::default();778
+ let o = l.add_output("a", AREA);779
+ let (main, other) = (l.tag("main"), l.tag("other"));780
+ l.view(o, main);781
+ let shown = l.add_window(None, Some(main), None);782
+ let player = l.add_window(None, Some(main), None);783
+ l.toggle_fullscreen(player);784
+ assert_eq!(l.arrange(o).fullscreen, Some(player));785
+ assert!(l.covered(shown));786
+787
+ let hidden = l.add_window(None, Some(other), None);788
+ l.toggle_fullscreen(hidden);789
+ assert_eq!(l.arrange(o).fullscreen, Some(player));790
+ l.focus(shown);791
+ l.view(o, other);792
+ assert_eq!(l.arrange(o).fullscreen, Some(hidden));793
+}794
+795
+/// A request to focus a covered window flags it instead, but its own796
+/// fullscreen window's transients and a summoned scratchpad take the focus.797
+#[test]798
+fn covered_windows_are_flagged_not_focused() {799
+ let mut l = Layout::default();800
+ let o = l.add_output("a", AREA);801
+ let game = l.add_window(None, None, None);802
+ l.focus(game);803
+ l.toggle_fullscreen(game);804
+ let stranger = l.add_window(None, None, None);805
+ assert!(!l.focus_unless_covered(stranger));806
+ assert_eq!(l.focused(), Some(game));807
+ assert!(l.window(stranger).unwrap().urgent);808
+809
+ let dialog = l.add_window(None, None, Some(Rect::new(300.0, 200.0, 200.0, 100.0)));810
+ l.set_parent(dialog, Some(game));811
+ assert!(l.focus_unless_covered(dialog));812
+ assert_eq!(l.focused(), Some(dialog));813
+814
+ let pad = l.add_window(None, None, None);815
+ l.scratchpad_add(pad, "s");816
+ assert_eq!(l.summon("s"), Some(pad));817
+ assert_eq!(l.focused(), Some(pad));818
+ assert!(l.arrange(o).over.contains(&pad));819
+ l.check();820
+}821
+822
+/// A window its client hid keeps its place, floating state, parent links and823
+/// scratchpad, and comes back as it was when shown again.824
+#[test]825
+fn hidden_windows_come_back_as_they_were() {826
+ let mut l = Layout::default();827
+ let o = l.add_output("a", AREA);828
+ let tag = l.tag("work");829
+ l.view(o, tag);830
+ let tray = l.add_window(None, Some(tag), Some(Rect::new(40.0, 50.0, 300.0, 200.0)));831
+ let child = l.add_window(None, Some(tag), Some(Rect::new(60.0, 70.0, 100.0, 80.0)));832
+ let tiled = l.add_window(None, Some(tag), None);833
+ l.set_parent(child, Some(tray));834
+ l.toggle_sticky(tray);835
+ l.focus(tray);836
+ l.focus(tiled);837
+838
+ l.set_hidden(tray, true);839
+ assert!(!l.visible(tray));840
+ assert!(l.arrange(o).floating.iter().all(|f| f.0 != tray));841
+ assert!(!l.focus_unless_covered(tray));842
+ assert!(!l.window(tray).unwrap().urgent);843
+ assert_eq!(l.focused(), Some(tiled));844
+ assert_eq!(l.window(child).and_then(|w| w.parent), Some(tray));845
+846
+ l.set_hidden(tray, false);847
+ let w = l.window(tray).unwrap();848
+ assert!(l.visible(tray));849
+ assert!(w.floating && w.sticky && w.tag == tag);850
+ assert_eq!(w.float_rect, Some(Rect::new(40.0, 50.0, 300.0, 200.0)));851
+ assert!(l.arrange(o).floating.iter().any(|f| f.0 == tray));852
+ l.check();853
+}854
+855
+/// Summoning skips a scratchpad window that is hidden to a tray.856
+#[test]857
+fn summoning_skips_hidden_scratchpad_windows() {858
+ let mut l = Layout::default();859
+ let o = l.add_output("a", AREA);860
+ let a = l.add_window(None, None, None);861
+ let b = l.add_window(None, None, None);862
+ l.scratchpad_add(a, "s");863
+ l.scratchpad_add(b, "s");864
+ l.set_hidden(a, true);865
+ assert_eq!(l.summon("s"), Some(b));866
+ assert!(l.arrange(o).floating.iter().all(|f| f.0 != a));867
+ assert_eq!(l.summon("s"), None);868
+}869
+870
+/// A modal dialog that takes focus on mapping puts its parent just beneath871
+/// it, so closing it does not land on whatever was focused before the parent.872
+#[test]873
+fn a_modal_brings_its_parent_up_beneath_it() {874
+ let mut l = Layout::default();875
+ l.add_output("a", AREA);876
+ let parent = l.add_window(None, None, None);877
+ let other = l.add_window(None, None, None);878
+ l.focus(parent);879
+ l.focus(other);880
+ let dialog = l.add_window(None, None, Some(Rect::new(300.0, 200.0, 200.0, 100.0)));881
+ l.set_parent(dialog, Some(parent));882
+ l.set_modal(dialog, true);883
+ l.focus(dialog);884
+ l.remove_window(dialog);885
+ assert_eq!(l.focused(), Some(parent));886
+}@@ -63,8 +63,8 @@ impl XdgActivationHandler for State {
6363
let refused = self.wm.get(id).is_some_and(|w| w.rule.focus == Some(false));6464
let may_focus =6565
ctx.is_some_and(|c| c.may_focus) && data.timestamp.elapsed() < FRESH && !refused;66
- if may_focus && self.wm.layout.visible(id) {67
- self.wm.layout.focus(id);66
+ if may_focus {67
+ self.wm.layout.focus_unless_covered(id);6868
} else {6969
self.wm.layout.mark_urgent(id);7070
}@@ -33,7 +33,7 @@ impl ForeignToplevelHandler for State {
3333
let tag = w.tag;3434
self.wm.layout.toggle_view(o, tag);3535
}36
- self.wm.layout.focus(id);36
+ self.wm.layout.focus_unless_covered(id);3737
}3838
Action::Close(k) => {3939
if let Some(window) = id_of(k).and_then(|id| self.wm.window(id)) {