atagen

monolith

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