atagen

monolith

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