atagen

monolith

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