atagen

monolith

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