atagen

monolith

public
Star0
Watch0
0f99c9f9166b1c1e481d2afb9978abd96d82ce37
1
ssh://brewery@pls.lobotomise.me:33/atagen/monolith.git
crates/layout/src/lib.rsRust890 lines26.8 KB
1
//! Window organisation without smithay. Tags with per-output views, one
2
//! tiling tree per output filtered by those views, floating windows,
3
//! scratchpads, focus, and restoring windows when an output comes back.
4
5
mod outputs;
6
mod scratchpad;
7
mod tree;
8
9
#[cfg(test)]
10
mod tests;
11
12
pub use monolith_planner::Rect;
13
pub use tree::{Axis, Dir, Tree};
14
15
use outputs::Parked;
16
use slotmap::{SlotMap, new_key_type};
17
use std::collections::HashMap;
18
19
new_key_type! {
20
    pub struct WindowId;
21
    pub struct TagId;
22
    pub struct OutputId;
23
}
24
25
pub const HOMELESS: &str = "homeless";
26
27
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
28
pub enum ExpandScope {
29
    Window,
30
    Container,
31
    All,
32
}
33
34
/// Space taken from an output's edges by bars and other panels.
35
#[derive(Clone, Copy, Debug, PartialEq, Default)]
36
pub struct Insets {
37
    pub top: f64,
38
    pub right: f64,
39
    pub bottom: f64,
40
    pub left: f64,
41
}
42
43
#[derive(Clone, Copy, Debug, PartialEq)]
44
pub struct Gaps {
45
    pub inner: f64,
46
    pub outer: f64,
47
}
48
49
impl Default for Gaps {
50
    fn default() -> Self {
51
        Self {
52
            inner: 8.0,
53
            outer: 8.0,
54
        }
55
    }
56
}
57
58
#[derive(Clone, Debug)]
59
pub struct Window {
60
    pub output: Option<OutputId>,
61
    pub tag: TagId,
62
    pub floating: bool,
63
    pub float_rect: Option<Rect>,
64
    /// `float_rect` is the layout's default, not a size the window or the
65
    /// user chose.
66
    pub float_default: bool,
67
    pub fullscreen: bool,
68
    /// The window this one is a dialog or other transient of.
69
    pub parent: Option<WindowId>,
70
    pub sticky: bool,
71
    pub scratchpad: Option<usize>,
72
    evicted_from: Option<String>,
73
}
74
75
#[derive(Clone, Debug)]
76
pub struct Tag {
77
    pub name: String,
78
    pub urgent: bool,
79
}
80
81
#[derive(Clone, Debug)]
82
pub struct Output {
83
    pub name: String,
84
    pub area: Rect,
85
    pub reserved: Insets,
86
    pub tree: Tree<WindowId>,
87
    /// Viewed tags besides the homeless tag, which is always viewed.
88
    pub viewed: Vec<TagId>,
89
    /// Floating windows, bottom to top.
90
    pub floating: Vec<WindowId>,
91
}
92
93
#[derive(Clone, Debug)]
94
pub struct Scratchpad {
95
    pub name: String,
96
    pub windows: Vec<WindowId>,
97
    pub shown: Option<usize>,
98
    pub output: Option<OutputId>,
99
}
100
101
#[derive(Clone, Debug, Default)]
102
pub struct Arrangement {
103
    /// Every tiled window on the output. Hidden ones are zero-size seeds.
104
    pub tiled: Vec<Placed>,
105
    /// Visible floating windows and shown scratchpads, bottom to top.
106
    pub floating: Vec<(WindowId, Rect)>,
107
    pub fullscreen: Option<WindowId>,
108
    /// Floating transients of the fullscreen window, which stay above it.
109
    pub over: Vec<WindowId>,
110
}
111
112
#[derive(Clone, Copy, Debug)]
113
pub struct Placed {
114
    pub window: WindowId,
115
    pub rect: Rect,
116
    pub visible: bool,
117
}
118
119
#[derive(Clone, Debug)]
120
pub struct Layout {
121
    windows: SlotMap<WindowId, Window>,
122
    tags: SlotMap<TagId, Tag>,
123
    tag_order: Vec<TagId>,
124
    homeless: TagId,
125
    outputs: SlotMap<OutputId, Output>,
126
    active: Option<OutputId>,
127
    /// Focus history, most recent last.
128
    history: Vec<WindowId>,
129
    scratchpads: Vec<Scratchpad>,
130
    last_scratchpad: Option<usize>,
131
    parked: HashMap<String, Parked>,
132
    pub gaps: Gaps,
133
}
134
135
impl Default for Layout {
136
    fn default() -> Self {
137
        let mut tags = SlotMap::with_key();
138
        let homeless = tags.insert(Tag {
139
            name: HOMELESS.into(),
140
            urgent: false,
141
        });
142
        Self {
143
            windows: SlotMap::with_key(),
144
            tags,
145
            tag_order: vec![homeless],
146
            homeless,
147
            outputs: SlotMap::with_key(),
148
            active: None,
149
            history: Vec::new(),
150
            scratchpads: Vec::new(),
151
            last_scratchpad: None,
152
            parked: HashMap::new(),
153
            gaps: Gaps::default(),
154
        }
155
    }
156
}
157
158
impl Layout {
159
    pub fn homeless(&self) -> TagId {
160
        self.homeless
161
    }
162
163
    /// Looks a tag up by name, creating it at the end of the order.
164
    pub fn tag(&mut self, name: &str) -> TagId {
165
        if let Some(id) = self.tag_id(name) {
166
            return id;
167
        }
168
        let id = self.tags.insert(Tag {
169
            name: name.into(),
170
            urgent: false,
171
        });
172
        self.tag_order.push(id);
173
        id
174
    }
175
176
    pub fn tag_id(&self, name: &str) -> Option<TagId> {
177
        self.tag_order
178
            .iter()
179
            .copied()
180
            .find(|&t| self.tags[t].name == name)
181
    }
182
183
    pub fn tags(&self) -> impl Iterator<Item = (TagId, &Tag)> {
184
        self.tag_order.iter().map(|&t| (t, &self.tags[t]))
185
    }
186
187
    pub fn reorder_tag(&mut self, tag: TagId, index: usize) {
188
        if let Some(i) = self.tag_order.iter().position(|&t| t == tag) {
189
            self.tag_order.remove(i);
190
            let index = index.min(self.tag_order.len());
191
            self.tag_order.insert(index, tag);
192
        }
193
    }
194
195
    pub fn window(&self, w: WindowId) -> Option<&Window> {
196
        self.windows.get(w)
197
    }
198
199
    pub fn windows(&self) -> impl Iterator<Item = (WindowId, &Window)> {
200
        self.windows.iter()
201
    }
202
203
    pub fn output(&self, o: OutputId) -> Option<&Output> {
204
        self.outputs.get(o)
205
    }
206
207
    pub fn outputs(&self) -> impl Iterator<Item = (OutputId, &Output)> {
208
        self.outputs.iter()
209
    }
210
211
    pub fn scratchpads(&self) -> &[Scratchpad] {
212
        &self.scratchpads
213
    }
214
215
    pub fn active_output(&self) -> Option<OutputId> {
216
        self.active
217
    }
218
219
    pub fn set_active_output(&mut self, o: OutputId) {
220
        if self.outputs.contains_key(o) {
221
            self.active = Some(o);
222
        }
223
    }
224
225
    pub fn is_viewed(&self, o: OutputId, tag: TagId) -> bool {
226
        tag == self.homeless
227
            || self
228
                .outputs
229
                .get(o)
230
                .is_some_and(|out| out.viewed.contains(&tag))
231
    }
232
233
    /// Views `tag` alone, besides the homeless tag.
234
    pub fn view(&mut self, o: OutputId, tag: TagId) {
235
        let Some(out) = self.outputs.get_mut(o) else {
236
            return;
237
        };
238
        out.viewed.clear();
239
        if tag != self.homeless {
240
            out.viewed.push(tag);
241
        }
242
        self.tags[tag].urgent = false;
243
    }
244
245
    pub fn toggle_view(&mut self, o: OutputId, tag: TagId) {
246
        if tag == self.homeless {
247
            return;
248
        }
249
        let Some(out) = self.outputs.get_mut(o) else {
250
            return;
251
        };
252
        if let Some(i) = out.viewed.iter().position(|&t| t == tag) {
253
            out.viewed.remove(i);
254
        } else {
255
            out.viewed.push(tag);
256
            self.tags[tag].urgent = false;
257
        }
258
    }
259
260
    /// Brings `w`'s tag into view beside the others when `w` is the only
261
    /// window on it there.
262
    pub fn view_if_alone(&mut self, w: WindowId) {
263
        let Some(win) = self.windows.get(w) else {
264
            return;
265
        };
266
        let (Some(o), tag) = (win.output, win.tag) else {
267
            return;
268
        };
269
        if win.scratchpad.is_some() || self.is_viewed(o, tag) {
270
            return;
271
        }
272
        let alone = !self
273
            .windows
274
            .iter()
275
            .any(|(x, v)| x != w && v.output == Some(o) && v.tag == tag && v.scratchpad.is_none());
276
        if alone {
277
            self.toggle_view(o, tag);
278
        }
279
    }
280
281
    /// The tag a launch from `o` belongs to. The focused window's tag, else
282
    /// the first viewed tag, else homeless. Homeless is always on screen, so
283
    /// focusing one of its windows does not mean the user left their tag.
284
    pub fn launch_tag(&self, o: OutputId) -> TagId {
285
        self.focused_on(o)
286
            .map(|w| &self.windows[w])
287
            .filter(|w| w.scratchpad.is_none() && !w.sticky && w.tag != self.homeless)
288
            .map(|w| w.tag)
289
            .or_else(|| self.outputs.get(o)?.viewed.first().copied())
290
            .unwrap_or(self.homeless)
291
    }
292
293
    pub fn view_everywhere(&mut self, tag: TagId) {
294
        for o in self.outputs.keys().collect::<Vec<_>>() {
295
            self.view(o, tag);
296
        }
297
    }
298
299
    pub fn visible(&self, w: WindowId) -> bool {
300
        let Some(win) = self.windows.get(w) else {
301
            return false;
302
        };
303
        let Some(o) = win.output else {
304
            return false;
305
        };
306
        match win.scratchpad {
307
            Some(s) => {
308
                let sp = &self.scratchpads[s];
309
                sp.output == Some(o) && sp.shown.map(|i| sp.windows[i]) == Some(w)
310
            }
311
            None => win.sticky || self.is_viewed(o, win.tag),
312
        }
313
    }
314
315
    /// Adds a window on `output` (default the active one) and `tag` (default
316
    /// homeless). A window landing on a tag its output is not viewing marks
317
    /// the tag urgent.
318
    pub fn add_window(
319
        &mut self,
320
        output: Option<OutputId>,
321
        tag: Option<TagId>,
322
        floating: Option<Rect>,
323
    ) -> WindowId {
324
        let output = output
325
            .filter(|&o| self.outputs.contains_key(o))
326
            .or(self.active);
327
        let tag = tag
328
            .filter(|&t| self.tags.contains_key(t))
329
            .unwrap_or(self.homeless);
330
        let w = self.windows.insert(Window {
331
            output,
332
            tag,
333
            floating: floating.is_some(),
334
            float_rect: floating,
335
            float_default: false,
336
            fullscreen: false,
337
            parent: None,
338
            sticky: false,
339
            scratchpad: None,
340
            evicted_from: None,
341
        });
342
        if let Some(o) = output {
343
            if !self.is_viewed(o, tag) {
344
                self.tags[tag].urgent = true;
345
            }
346
            self.place(w);
347
        }
348
        w
349
    }
350
351
    pub fn remove_window(&mut self, w: WindowId) {
352
        if !self.windows.contains_key(w) {
353
            return;
354
        }
355
        self.unplace(w);
356
        self.history.retain(|&h| h != w);
357
        if let Some(s) = self.windows[w].scratchpad {
358
            self.leave_scratchpad(s, w);
359
        }
360
        for win in self.windows.values_mut() {
361
            if win.parent == Some(w) {
362
                win.parent = None;
363
            }
364
        }
365
        self.windows.remove(w);
366
    }
367
368
    /// Puts a window into its output's tree or floating stack. Tiled windows
369
    /// go next to the focused window if both are visible, else next to the
370
    /// latest window of their tag, else into a new row.
371
    fn place(&mut self, w: WindowId) {
372
        let win = &self.windows[w];
373
        let Some(o) = win.output else { return };
374
        if win.scratchpad.is_some() {
375
            return;
376
        }
377
        if win.floating {
378
            if win.float_rect.is_none() {
379
                let rect = centred(self.outputs[o].area, 0.6);
380
                self.windows[w].float_rect = Some(rect);
381
                self.windows[w].float_default = true;
382
            }
383
            self.outputs[o].floating.push(w);
384
            return;
385
        }
386
        let tag = win.tag;
387
        let tree = &self.outputs[o].tree;
388
        let focused = self
389
            .focused()
390
            .filter(|&f| tree.contains(f) && self.is_viewed(o, tag));
391
        let anchor = focused.or_else(|| {
392
            self.history
393
                .iter()
394
                .rev()
395
                .copied()
396
                .find(|&h| tree.contains(h) && self.windows[h].tag == tag)
397
        });
398
        let tree = &mut self.outputs[o].tree;
399
        match anchor {
400
            Some(a) => tree.insert_after(w, a),
401
            None => tree.insert_row(w),
402
        }
403
    }
404
405
    fn unplace(&mut self, w: WindowId) {
406
        let Some(o) = self.windows[w].output else {
407
            return;
408
        };
409
        let out = &mut self.outputs[o];
410
        out.tree.remove(w);
411
        out.floating.retain(|&f| f != w);
412
    }
413
414
    pub fn focus(&mut self, w: WindowId) {
415
        let Some(win) = self.windows.get(w) else {
416
            return;
417
        };
418
        self.active = win.output.or(self.active);
419
        if let Some(o) = win.output {
420
            let stack = &mut self.outputs[o].floating;
421
            if let Some(i) = stack.iter().position(|&f| f == w) {
422
                stack.remove(i);
423
                stack.push(w);
424
            }
425
        }
426
        self.history.retain(|&h| h != w);
427
        self.history.push(w);
428
    }
429
430
    /// The most recently focused visible window on the active output. Focus
431
    /// falling back to another output would leave keybinds acting on one
432
    /// output while the keyboard and pointer sit on another.
433
    pub fn focused(&self) -> Option<WindowId> {
434
        self.focused_on(self.active?)
435
    }
436
437
    /// The most recently focused visible window on `o`.
438
    pub fn focused_on(&self, o: OutputId) -> Option<WindowId> {
439
        self.history
440
            .iter()
441
            .rev()
442
            .copied()
443
            .find(|&w| self.windows[w].output == Some(o) && self.visible(w))
444
    }
445
446
    /// Focuses the nearest visible window in `dir`, crossing to the next
447
    /// output when there is none on this one.
448
    pub fn focus_dir(&mut self, dir: Dir) -> Option<WindowId> {
449
        let o = self.active?;
450
        let from = self.focused_on(o).and_then(|f| self.rect_of(f));
451
        let target = from.and_then(|from| {
452
            let arr = self.arrange(o);
453
            let reachable = |w| {
454
                arr.fullscreen
455
                    .is_none_or(|f| w == f || arr.over.contains(&w))
456
            };
457
            let candidates = arr
458
                .tiled
459
                .iter()
460
                .filter(|p| p.visible)
461
                .map(|p| (p.window, p.rect))
462
                .chain(arr.floating.iter().copied())
463
                .filter(|&(w, _)| Some(w) != self.focused_on(o) && reachable(w));
464
            nearest(from, dir, candidates)
465
        });
466
        if let Some(w) = target {
467
            self.focus(w);
468
            return Some(w);
469
        }
470
        let next = self.output_towards(o, dir)?;
471
        self.active = Some(next);
472
        let w = self.focused_on(next);
473
        if let Some(w) = w {
474
            self.focus(w);
475
        }
476
        w
477
    }
478
479
    pub fn output_towards(&self, from: OutputId, dir: Dir) -> Option<OutputId> {
480
        let here = self.outputs.get(from)?.area;
481
        let others = self
482
            .outputs
483
            .iter()
484
            .filter(|&(id, _)| id != from)
485
            .map(|(id, out)| (id, out.area));
486
        nearest(here, dir, others)
487
    }
488
489
    fn rect_of(&self, w: WindowId) -> Option<Rect> {
490
        let o = self.windows.get(w)?.output?;
491
        let arr = self.arrange(o);
492
        arr.tiled
493
            .iter()
494
            .find(|p| p.window == w)
495
            .map(|p| p.rect)
496
            .or_else(|| arr.floating.iter().find(|f| f.0 == w).map(|f| f.1))
497
    }
498
499
    fn vis_on(&self, o: OutputId) -> impl Fn(WindowId) -> bool + '_ {
500
        move |w| self.windows[w].output == Some(o) && self.visible(w)
501
    }
502
503
    fn focused_tiled(&self) -> Option<(WindowId, OutputId)> {
504
        let w = self.focused()?;
505
        let o = self.windows[w].output?;
506
        self.outputs[o].tree.contains(w).then_some((w, o))
507
    }
508
509
    pub fn move_window(&mut self, dir: Dir) -> bool {
510
        let Some((w, o)) = self.focused_tiled() else {
511
            return false;
512
        };
513
        let mut tree = std::mem::take(&mut self.outputs[o].tree);
514
        let moved = tree.move_window(w, dir, self.vis_on(o));
515
        self.outputs[o].tree = tree;
516
        moved
517
    }
518
519
    pub fn move_container(&mut self, dir: Dir) -> bool {
520
        let Some((w, o)) = self.focused_tiled() else {
521
            return false;
522
        };
523
        let mut tree = std::mem::take(&mut self.outputs[o].tree);
524
        let moved = tree.move_container(w, dir, self.vis_on(o));
525
        self.outputs[o].tree = tree;
526
        moved
527
    }
528
529
    pub fn merge_window(&mut self, dir: Dir) -> bool {
530
        let Some((w, o)) = self.focused_tiled() else {
531
            return false;
532
        };
533
        let mut tree = std::mem::take(&mut self.outputs[o].tree);
534
        let merged = tree.merge_window(w, dir, self.vis_on(o));
535
        self.outputs[o].tree = tree;
536
        merged
537
    }
538
539
    /// Whether a tiled window fills its output through expansion.
540
    pub fn maximized(&self, w: WindowId) -> bool {
541
        let Some(o) = self.windows.get(w).and_then(|w| w.output) else {
542
            return false;
543
        };
544
        self.outputs[o].tree.maximized(w, self.vis_on(o))
545
    }
546
547
    pub fn resize(&mut self, axis: Axis, delta: f64) -> bool {
548
        let Some((w, o)) = self.focused_tiled() else {
549
            return false;
550
        };
551
        let mut tree = std::mem::take(&mut self.outputs[o].tree);
552
        let done = tree.resize(w, axis, delta, self.vis_on(o));
553
        self.outputs[o].tree = tree;
554
        done
555
    }
556
557
    pub fn expand(&mut self, scope: ExpandScope) -> bool {
558
        let Some((w, o)) = self.focused_tiled() else {
559
            return false;
560
        };
561
        let (skip, count) = match scope {
562
            ExpandScope::Window => (0, 1),
563
            ExpandScope::Container => (1, 1),
564
            ExpandScope::All => (0, usize::MAX),
565
        };
566
        let mut tree = std::mem::take(&mut self.outputs[o].tree);
567
        let expanded = tree.toggle_expand(w, skip, count, self.vis_on(o));
568
        self.outputs[o].tree = tree;
569
        expanded
570
    }
571
572
    pub fn flip(&mut self, o: OutputId) {
573
        if let Some(out) = self.outputs.get_mut(o) {
574
            out.tree.flip();
575
        }
576
    }
577
578
    /// Retags a window. It keeps its slot and disappears if the new tag is
579
    /// not viewed.
580
    pub fn set_tag(&mut self, w: WindowId, tag: TagId) {
581
        if self.tags.contains_key(tag)
582
            && let Some(win) = self.windows.get_mut(w)
583
        {
584
            win.tag = tag;
585
        }
586
    }
587
588
    /// Moves a window to `tag` on `o`, marking the tag urgent when the window
589
    /// ends up out of sight.
590
    pub fn relocate(&mut self, w: WindowId, o: Option<OutputId>, tag: TagId) {
591
        if let Some(o) = o {
592
            self.send_to_output(w, o);
593
        }
594
        self.set_tag(w, tag);
595
        self.mark_urgent(w);
596
    }
597
598
    pub fn mark_urgent(&mut self, w: WindowId) {
599
        if !self.visible(w)
600
            && let Some(tag) = self.windows.get(w).map(|win| win.tag)
601
        {
602
            self.tags[tag].urgent = true;
603
        }
604
    }
605
606
    pub fn send_to_output(&mut self, w: WindowId, o: OutputId) {
607
        let Some(win) = self.windows.get(w) else {
608
            return;
609
        };
610
        if !self.outputs.contains_key(o) || win.output == Some(o) || win.scratchpad.is_some() {
611
            return;
612
        }
613
        let from = win.output.map(|f| self.outputs[f].area);
614
        self.unplace(w);
615
        let win = &mut self.windows[w];
616
        if let (Some(from), Some(rect)) = (from, win.float_rect) {
617
            win.float_rect = Some(translate(rect, from, self.outputs[o].area));
618
        }
619
        win.output = Some(o);
620
        win.evicted_from = None;
621
        self.place(w);
622
    }
623
624
    /// Switches a window between tiled and floating. A scratchpad window
625
    /// leaves its scratchpad and tiles on the tag being viewed, since its own
626
    /// tag may well be hidden.
627
    pub fn toggle_floating(&mut self, w: WindowId) {
628
        let Some(win) = self.windows.get(w) else {
629
            return;
630
        };
631
        if let Some(s) = win.scratchpad {
632
            let o = win.output.or(self.active);
633
            let tag = o.map_or(self.homeless, |o| self.launch_tag(o));
634
            self.leave_scratchpad(s, w);
635
            let win = &mut self.windows[w];
636
            win.output = o;
637
            win.tag = tag;
638
            win.floating = false;
639
            self.place(w);
640
            return;
641
        }
642
        self.unplace(w);
643
        let win = &mut self.windows[w];
644
        win.floating = !win.floating;
645
        self.place(w);
646
    }
647
648
    pub fn set_float_rect(&mut self, w: WindowId, rect: Rect) {
649
        if let Some(win) = self.windows.get_mut(w) {
650
            win.float_rect = Some(rect);
651
            win.float_default = false;
652
        }
653
    }
654
655
    /// Places a float without settling its size, so a window still deciding
656
    /// on one is asked again after being tiled.
657
    pub fn hold_float_rect(&mut self, w: WindowId, rect: Rect) {
658
        if let Some(win) = self.windows.get_mut(w) {
659
            win.float_rect = Some(rect);
660
        }
661
    }
662
663
    pub fn toggle_fullscreen(&mut self, w: WindowId) {
664
        if let Some(win) = self.windows.get_mut(w) {
665
            win.fullscreen = !win.fullscreen;
666
        }
667
    }
668
669
    pub fn toggle_sticky(&mut self, w: WindowId) {
670
        if let Some(win) = self.windows.get_mut(w) {
671
            win.sticky = !win.sticky;
672
        }
673
    }
674
675
    /// Target geometry for everything on `o`, in output-local logical
676
    /// coordinates offset by the output's area.
677
    pub fn arrange(&self, o: OutputId) -> Arrangement {
678
        self.arrange_hiding(o, &[])
679
    }
680
681
    /// Like `arrange` with `hide` treated as hidden, which places them on the
682
    /// seeds they would grow from.
683
    pub fn arrange_hiding(&self, o: OutputId, hide: &[WindowId]) -> Arrangement {
684
        let Some(out) = self.outputs.get(o) else {
685
            return Arrangement::default();
686
        };
687
        let r = out.reserved;
688
        let usable = Rect::new(
689
            out.area.x + r.left,
690
            out.area.y + r.top,
691
            (out.area.w - r.left - r.right).max(0.0),
692
            (out.area.h - r.top - r.bottom).max(0.0),
693
        );
694
        let area = inset(usable, self.gaps.outer);
695
        let shown = self.vis_on(o);
696
        let vis = &|w: WindowId| shown(w) && !hide.contains(&w);
697
        let tiled = out
698
            .tree
699
            .arrange(area, self.gaps.inner, vis)
700
            .into_iter()
701
            .map(|(window, rect)| Placed {
702
                window,
703
                rect,
704
                visible: vis(window),
705
            })
706
            .collect();
707
        let mut floating: Vec<(WindowId, Rect)> = out
708
            .floating
709
            .iter()
710
            .copied()
711
            .filter(|&w| vis(w))
712
            .filter_map(|w| Some((w, self.windows[w].float_rect?)))
713
            .collect();
714
        for sp in &self.scratchpads {
715
            if let Some(i) = sp.shown
716
                && sp.output == Some(o)
717
                && let Some(r) = self.windows[sp.windows[i]].float_rect
718
            {
719
                floating.push((sp.windows[i], r));
720
            }
721
        }
722
        let fullscreen = self
723
            .history
724
            .iter()
725
            .rev()
726
            .copied()
727
            .find(|&w| self.windows[w].fullscreen && vis(w));
728
        let over = fullscreen
729
            .map(|f| {
730
                floating
731
                    .iter()
732
                    .map(|&(w, _)| w)
733
                    .filter(|&w| w != f && self.descends(w, f))
734
                    .collect()
735
            })
736
            .unwrap_or_default();
737
        Arrangement {
738
            tiled,
739
            floating,
740
            fullscreen,
741
            over,
742
        }
743
    }
744
745
    /// Records that `w` is a transient of `parent`.
746
    pub fn set_parent(&mut self, w: WindowId, parent: Option<WindowId>) {
747
        if parent.is_some_and(|p| p == w || self.descends(p, w)) {
748
            return;
749
        }
750
        if let Some(win) = self.windows.get_mut(w) {
751
            win.parent = parent;
752
        }
753
    }
754
755
    fn descends(&self, mut w: WindowId, of: WindowId) -> bool {
756
        for _ in 0..self.windows.len() {
757
            match self.windows.get(w).and_then(|x| x.parent) {
758
                Some(p) if p == of => return true,
759
                Some(p) => w = p,
760
                None => return false,
761
            }
762
        }
763
        false
764
    }
765
766
    /// Moves focus to a fullscreen `w` when it covers the focused window,
767
    /// unless that is one of its own transients.
768
    pub fn focus_if_covering(&mut self, w: WindowId) {
769
        let Some(win) = self.windows.get(w).filter(|win| win.fullscreen) else {
770
            return;
771
        };
772
        let covers = self.focused().is_some_and(|f| {
773
            f != w && self.windows[f].output == win.output && !self.descends(f, w)
774
        });
775
        if covers && self.visible(w) {
776
            self.focus(w);
777
        }
778
    }
779
780
    /// Whether a fullscreen window on `w`'s output hides it. The fullscreen
781
    /// window's transients are not hidden, and a window that is itself
782
    /// fullscreen takes over.
783
    pub fn covered(&self, w: WindowId) -> bool {
784
        let Some(win) = self.windows.get(w) else {
785
            return false;
786
        };
787
        let Some(o) = win.output else {
788
            return false;
789
        };
790
        let arr = self.arrange(o);
791
        !win.fullscreen && arr.fullscreen.is_some_and(|f| f != w) && !arr.over.contains(&w)
792
    }
793
794
    #[cfg(test)]
795
    pub(crate) fn check(&self) {
796
        for (o, out) in &self.outputs {
797
            out.tree.check();
798
            for w in out.tree.windows() {
799
                let win = &self.windows[w];
800
                assert_eq!(win.output, Some(o));
801
                assert!(!win.floating && win.scratchpad.is_none());
802
            }
803
            for &w in &out.floating {
804
                assert_eq!(self.windows[w].output, Some(o));
805
                assert!(self.windows[w].floating);
806
            }
807
        }
808
        for (w, win) in &self.windows {
809
            let homes = self
810
                .outputs
811
                .values()
812
                .filter(|out| out.tree.contains(w) || out.floating.contains(&w))
813
                .count();
814
            let expected = usize::from(win.output.is_some() && win.scratchpad.is_none());
815
            assert_eq!(homes, expected, "window placed {homes} times");
816
        }
817
    }
818
}
819
820
fn nearest<K: Copy>(
821
    from: Rect,
822
    dir: Dir,
823
    candidates: impl Iterator<Item = (K, Rect)>,
824
) -> Option<K> {
825
    const TOL: f64 = 0.5;
826
    let score = |r: Rect| -> Option<(bool, f64, f64)> {
827
        let (gap, overlap, offset) = match dir {
828
            Dir::Right => (
829
                r.x - from.right(),
830
                span_overlap(from.y, from.h, r.y, r.h),
831
                r.center_y() - from.center_y(),
832
            ),
833
            Dir::Left => (
834
                from.x - r.right(),
835
                span_overlap(from.y, from.h, r.y, r.h),
836
                r.center_y() - from.center_y(),
837
            ),
838
            Dir::Down => (
839
                r.y - from.bottom(),
840
                span_overlap(from.x, from.w, r.x, r.w),
841
                r.center_x() - from.center_x(),
842
            ),
843
            Dir::Up => (
844
                from.y - r.bottom(),
845
                span_overlap(from.x, from.w, r.x, r.w),
846
                r.center_x() - from.center_x(),
847
            ),
848
        };
849
        (gap > -TOL).then_some((overlap <= 0.0, gap.max(0.0), offset.abs()))
850
    };
851
    candidates
852
        .filter_map(|(k, r)| score(r).map(|s| (k, s)))
853
        .min_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))
854
        .map(|(k, _)| k)
855
}
856
857
fn span_overlap(a: f64, al: f64, b: f64, bl: f64) -> f64 {
858
    (a + al).min(b + bl) - a.max(b)
859
}
860
861
fn inset(r: Rect, by: f64) -> Rect {
862
    Rect::new(
863
        r.x + by,
864
        r.y + by,
865
        (r.w - 2.0 * by).max(0.0),
866
        (r.h - 2.0 * by).max(0.0),
867
    )
868
}
869
870
fn centred(area: Rect, frac: f64) -> Rect {
871
    let (w, h) = (area.w * frac, area.h * frac);
872
    Rect::new(
873
        area.x + (area.w - w) / 2.0,
874
        area.y + (area.h - h) / 2.0,
875
        w,
876
        h,
877
    )
878
}
879
880
/// Keeps a rect at the same relative spot when it changes output.
881
fn translate(r: Rect, from: Rect, to: Rect) -> Rect {
882
    let sx = if from.w > 0.0 { to.w / from.w } else { 1.0 };
883
    let sy = if from.h > 0.0 { to.h / from.h } else { 1.0 };
884
    Rect::new(
885
        to.x + (r.x - from.x) * sx,
886
        to.y + (r.y - from.y) * sy,
887
        r.w.min(to.w),
888
        r.h.min(to.h),
889
    )
890
}