atagen

monolith

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