atagen

monolith

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