atagen

monolith

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