Commit ecb0c8c1f31d
tarball6 files changed+160-26
@@ -67,6 +67,8 @@ pub struct Window {
6767
pub fullscreen: bool,6868
/// The window this one is a dialog or other transient of.6969
pub parent: Option<WindowId>,70
+ /// Focus aimed at the parent goes to this window while it is mapped.71
+ pub modal: bool,7072
pub sticky: bool,7173
pub scratchpad: Option<usize>,7274
pub urgent: bool,@@ -140,6 +142,8 @@ pub struct Layout {
140142
active: Option<OutputId>,141143
/// Focus history, most recent last.142144
history: Vec<WindowId>,145
+ /// How many windows are modal, so focus lookups skip the scan for one.146
+ modals: usize,143147
scratchpads: Vec<Scratchpad>,144148
last_scratchpad: Option<usize>,145149
parked: HashMap<String, Parked>,@@ -162,6 +166,7 @@ impl Default for Layout {
162166
outputs: SlotMap::with_key(),163167
active: None,164168
history: Vec::new(),169
+ modals: 0,165170
scratchpads: Vec::new(),166171
last_scratchpad: None,167172
parked: HashMap::new(),@@ -351,6 +356,7 @@ impl Layout {
351356
float_default: false,352357
fullscreen: false,353358
parent: None,359
+ modal: false,354360
sticky: false,355361
scratchpad: None,356362
urgent: false,@@ -371,6 +377,7 @@ impl Layout {
371377
}372378
self.unplace(w);373379
self.history.retain(|&h| h != w);380
+ self.modals -= usize::from(self.windows[w].modal);374381
if let Some(s) = self.windows[w].scratchpad {375382
self.leave_scratchpad(s, w);376383
}@@ -449,21 +456,71 @@ impl Layout {
449456
out.floating.retain(|&f| f != w);450457
}451458
452
- pub fn focus(&mut self, w: WindowId) {453
- let Some(win) = self.windows.get(w) else {454
- return;455
- };456
- self.active = win.output.or(self.active);457
- if let Some(o) = win.output {458
- let stack = &mut self.outputs[o].floating;459
- if let Some(i) = stack.iter().position(|&f| f == w) {460
- stack.remove(i);461
- stack.push(w);459
+ /// Focuses `w`, or the deepest modal dialog over it, which is returned.460
+ /// The windows between stay below it in the history, so closing the461
+ /// dialog hands focus back to its parent.462
+ pub fn focus(&mut self, mut w: WindowId) -> Option<WindowId> {463
+ loop {464
+ let win = self.windows.get(w)?;465
+ self.active = win.output.or(self.active);466
+ if let Some(o) = win.output {467
+ let stack = &mut self.outputs[o].floating;468
+ if let Some(i) = stack.iter().position(|&f| f == w) {469
+ stack.remove(i);470
+ stack.push(w);471
+ }472
+ }473
+ self.history.retain(|&h| h != w);474
+ self.history.push(w);475
+ self.windows[w].urgent = false;476
+ match self.modal_child(w) {477
+ Some(child) => w = child,478
+ None => return Some(w),462479
}463480
}464
- self.history.retain(|&h| h != w);465
- self.history.push(w);466
- self.windows[w].urgent = false;481
+ }482
+483
+ /// Marks `w` as a modal dialog of its parent, or not.484
+ pub fn set_modal(&mut self, w: WindowId, modal: bool) {485
+ if let Some(win) = self.windows.get_mut(w)486
+ && std::mem::replace(&mut win.modal, modal) != modal487
+ {488
+ self.modals = if modal {489
+ self.modals + 1490
+ } else {491
+ self.modals - 1492
+ };493
+ }494
+ }495
+496
+ /// The visible modal dialog of `w` on its output, the most recently497
+ /// focused if it has several.498
+ pub fn modal_child(&self, w: WindowId) -> Option<WindowId> {499
+ if self.modals == 0 {500
+ return None;501
+ }502
+ let output = self.windows.get(w)?.output;503
+ self.windows504
+ .iter()505
+ .filter(|&(c, win)| {506
+ win.modal && win.parent == Some(w) && win.output == output && self.visible(c)507
+ })508
+ .max_by_key(|&(c, _)| self.history.iter().rposition(|&h| h == c))509
+ .map(|(c, _)| c)510
+ }511
+512
+ /// The window that takes focus aimed at `w`, the deepest modal dialog513
+ /// over it or `w` itself.514
+ pub fn modal_leaf(&self, mut w: WindowId) -> WindowId {515
+ while let Some(child) = self.modal_child(w) {516
+ w = child;517
+ }518
+ w519
+ }520
+521
+ /// Whether focus aimed at `w` would change nothing.522
+ pub fn is_focused(&self, w: WindowId) -> bool {523
+ self.focused() == Some(self.modal_leaf(w))467524
}468525
469526
/// The most recently focused visible window on the active output. Focus@@ -480,13 +537,15 @@ impl Layout {
480537
.rev()481538
.copied()482539
.find(|&w| self.windows[w].output == Some(o) && self.visible(w))540
+ .map(|w| self.modal_leaf(w))483541
}484542
485543
/// Focuses the nearest visible window in `dir`, crossing to the next486544
/// output when there is none on this one.487545
pub fn focus_dir(&mut self, dir: Dir) -> Option<WindowId> {488546
let o = self.active?;489
- let from = self.focused_on(o).and_then(|f| self.rect_of(f));547
+ let current = self.focused_on(o);548
+ let from = current.and_then(|f| self.rect_of(f));490549
let target = from.and_then(|from| {491550
let arr = self.arrange(o);492551
let reachable = |w| {@@ -499,12 +558,11 @@ impl Layout {
499558
.filter(|p| p.visible)500559
.map(|p| (p.window, p.rect))501560
.chain(arr.floating.iter().copied())502
- .filter(|&(w, _)| Some(w) != self.focused_on(o) && reachable(w));561
+ .filter(|&(w, _)| Some(self.modal_leaf(w)) != current && reachable(w));503562
nearest(from, dir, candidates)504563
});505564
if let Some(w) = target {506
- self.focus(w);507
- return Some(w);565
+ return self.focus(w);508566
}509567
let next = self.output_towards(o, dir)?;510568
self.active = Some(next);@@ -729,3 +729,43 @@ fn scratchpads_stay_centred_whatever_their_rect() {
729729
assert_eq!(r.x + r.w / 2.0, area.x + area.w / 2.0);730730
assert_eq!(r.y, area.y);731731
}732
+733
+/// Focus aimed at a window with a modal dialog lands on the deepest dialog734
+/// from every path, and falls back to the parent once the dialog is gone.735
+#[test]736
+fn modal_dialogs_keep_focus_over_their_parent() {737
+ let mut l = Layout::default();738
+ l.add_output("a", AREA);739
+ let parent = l.add_window(None, None, None);740
+ let other = l.add_window(None, None, None);741
+ l.focus(parent);742
+ let dialog = l.add_window(None, None, Some(Rect::new(300.0, 200.0, 200.0, 100.0)));743
+ let nested = l.add_window(None, None, Some(Rect::new(350.0, 250.0, 100.0, 50.0)));744
+ l.set_parent(dialog, Some(parent));745
+ l.set_parent(nested, Some(dialog));746
+ l.set_modal(dialog, true);747
+ assert_eq!(l.focused(), Some(dialog));748
+ l.set_modal(nested, true);749
+ assert_eq!(l.focused(), Some(nested));750
+751
+ l.focus(other);752
+ assert_eq!(l.focused(), Some(other));753
+ assert_eq!(l.focus(parent), Some(nested));754
+ assert_eq!(l.focused(), Some(nested));755
+ assert!(l.is_focused(parent));756
+757
+ l.focus(other);758
+ for dir in [Dir::Left, Dir::Right, Dir::Up, Dir::Down] {759
+ l.focus(other);760
+ if let Some(w) = l.focus_dir(dir) {761
+ assert_ne!(w, parent, "directional focus stopped on a blocked parent");762
+ }763
+ }764
+765
+ l.focus(parent);766
+ l.remove_window(nested);767
+ assert_eq!(l.focused(), Some(dialog));768
+ l.remove_window(dialog);769
+ assert_eq!(l.focused(), Some(parent));770
+ l.check();771
+}@@ -261,10 +261,9 @@ fn focus_under_pointer(state: &mut State) {
261261
}262262
let id = window_at(state, pos);263263
if let Some(id) = id264
- && state.wm.layout.focused() != Some(id)264
+ && !state.wm.layout.is_focused(id)265265
{266
- state.input.followed = Some(id);267
- state.wm.layout.focus(id);266
+ state.input.followed = state.wm.layout.focus(id);268267
state.apply_layout();269268
}270269
}@@ -297,12 +296,11 @@ fn focus_on_hover(state: &mut State, pos: Point<f64, Logical>, warped: bool) {
297296
let Some(id) = under else {298297
return;299298
};300
- if state.wm.layout.focused() == Some(id) {299
+ if state.wm.layout.is_focused(id) {301300
return;302301
}303302
state.wm.layer_focus = None;304
- state.input.followed = Some(id);305
- state.wm.layout.focus(id);303
+ state.input.followed = state.wm.layout.focus(id);306304
state.apply_layout();307305
}308306
@@ -561,10 +559,9 @@ fn start_drag(state: &mut State, button: u32) -> bool {
561559
let Some((id, edges)) = state.edge_at(pos, over).filter(|_| button == BTN_LEFT) else {562560
return false;563561
};564
- if state.wm.layout.focused() != Some(id) {562
+ if !state.wm.layout.is_focused(id) {565563
state.wm.layer_focus = None;566
- state.input.followed = Some(id);567
- state.wm.layout.focus(id);564
+ state.input.followed = state.wm.layout.focus(id);568565
state.apply_layout();569566
}570567
return begin_drag(state, id, Some(edges), None);@@ -52,6 +52,7 @@ impl State {
5252
.add_window(output, tag, float.filter(|_| floating));5353
let parent = self.parent_of(&window);5454
self.wm.layout.set_parent(id, parent);55
+ self.wm.layout.set_modal(id, is_modal(&window));5556
if floating != self.wm.layout.window(id).is_some_and(|w| w.floating) {5657
self.wm.layout.toggle_floating(id);5758
}@@ -100,6 +101,13 @@ impl State {
100101
self.window_id(&parent)101102
}102103
104
+ /// Feeds the window's modal state to the layout and lets focus follow.105
+ pub(crate) fn sync_modal(&mut self, id: WindowId) {106
+ let modal = self.wm.window(id).is_some_and(is_modal);107
+ self.wm.layout.set_modal(id, modal);108
+ self.apply_layout();109
+ }110
+103111
/// Holds a new window on its seed until its first buffer lands, so the104112
/// spawn animation never plays over an empty slot. Clients that are slow105113
/// to draw get it anyway after a while.@@ -547,6 +555,13 @@ impl State {
547555
548556
/// Whether the client has acknowledged the configure with `serial` or a later549557
/// one.558
+fn is_modal(window: &Window) -> bool {559
+ match window.x11_surface() {560
+ Some(x) => x.is_modal(),561
+ None => window.toplevel().is_some_and(crate::xdg::is_modal),562
+ }563
+}564
+550565
fn acked(window: &Window, serial: Serial) -> bool {551566
let Some(t) = window.toplevel() else {552567
return true;@@ -35,6 +35,14 @@ pub fn is_dialog(toplevel: &ToplevelSurface) -> bool {
3535
toplevel.parent().is_some() || marked3636
}3737
38
+/// A toplevel that asked to be modal through xdg_dialog_v1.39
+pub fn is_modal(toplevel: &ToplevelSurface) -> bool {40
+ with_states(toplevel.wl_surface(), |s| {41
+ let role = s.data_map.get::<XdgToplevelSurfaceData>();42
+ role.is_some_and(|r| r.lock().unwrap().dialog_hint == ToplevelDialogHint::Modal)43
+ })44
+}45
+3846
/// The size of a window that asks for exactly one.3947
pub fn fixed_size(toplevel: &ToplevelSurface) -> Option<Size<i32, Logical>> {4048
let (min, max) = size_hints(toplevel);@@ -142,6 +150,7 @@ impl XdgDialogHandler for State {
142150
fn dialog_hint_changed(&mut self, surface: ToplevelSurface, _hint: ToplevelDialogHint) {143151
if let Some(id) = self.window_id(surface.wl_surface()) {144152
self.rematch(id);153
+ self.sync_modal(id);145154
}146155
}147156
}@@ -399,6 +399,14 @@ impl XwmHandler for State {
399399
}400400
}401401
402
+ fn modal_request(&mut self, _xwm: XwmId, window: X11Surface) {403
+ self.x11_modal(&window, true);404
+ }405
+406
+ fn unmodal_request(&mut self, _xwm: XwmId, window: X11Surface) {407
+ self.x11_modal(&window, false);408
+ }409
+402410
fn allow_selection_access(&mut self, xwm: XwmId, _selection: SelectionTarget) -> bool {403411
self.settings.config.xwayland.selection_access == SelectionAccess::Any404412
|| matches!(@@ -547,6 +555,13 @@ impl State {
547555
self.dirty.redraw = true;548556
}549557
558
+ fn x11_modal(&mut self, window: &X11Surface, on: bool) {559
+ logged("modal", window.set_modal(on));560
+ if let Some(id) = self.x11_window_id(window) {561
+ self.sync_modal(id);562
+ }563
+ }564
+550565
fn x11_fullscreen(&mut self, window: &X11Surface, on: bool) {551566
if let Some(id) = self.x11_window_id(window)552567
&& self