atagen

monolith

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