atagen

monolith

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