pinnacle_api/layout/
generators.rs

1//! Various builtin generators.
2
3use std::collections::HashMap;
4
5use crate::{tag::TagHandle, util::Axis};
6
7use super::{Gaps, LayoutDir, LayoutGenerator, LayoutNode};
8
9/// A [`LayoutGenerator`] that lays out windows in a line.
10#[derive(Debug, Clone, PartialEq)]
11pub struct Line {
12    /// The gaps between the outer container and this layout.
13    pub outer_gaps: Gaps,
14    /// The gaps between windows within this layout.
15    pub inner_gaps: Gaps,
16    /// THe direction the windows should be laid out.
17    pub direction: LayoutDir,
18    /// Whether or not windows are inserted backwards.
19    pub reversed: bool,
20}
21
22impl LayoutGenerator for Line {
23    fn layout(&self, window_count: u32) -> LayoutNode {
24        let root = LayoutNode::new_with_label("builtin.line");
25        root.set_gaps(self.outer_gaps);
26        root.set_dir(self.direction);
27
28        if window_count == 0 {
29            return root;
30        }
31
32        let children = match self.reversed {
33            false => (0..window_count)
34                .map(|idx| {
35                    let node = LayoutNode::new_with_traversal_index(idx);
36                    node.set_gaps(self.inner_gaps);
37                    node
38                })
39                .collect::<Vec<_>>(),
40            true => (0..window_count)
41                .rev()
42                .map(|idx| {
43                    let node = LayoutNode::new_with_traversal_index(idx);
44                    node.set_gaps(self.inner_gaps);
45                    node
46                })
47                .collect(),
48        };
49
50        root.set_children(children);
51
52        root
53    }
54}
55
56/// Which side the master area will be.
57#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
58pub enum MasterSide {
59    /// The master area will be on the left.
60    Left,
61    /// The master area will be on the right.
62    Right,
63    /// The master area will be at the top.
64    Top,
65    /// The master area will be at the bottom.
66    Bottom,
67}
68
69/// A [`LayoutGenerator`] that has one master area to one side and a stack of windows
70/// next to it.
71#[derive(Clone, Copy, Debug, PartialEq)]
72pub struct MasterStack {
73    /// The gaps between the outer container and this layout.
74    pub outer_gaps: Gaps,
75    /// The gaps between windows within this layout.
76    pub inner_gaps: Gaps,
77    /// The proportion of the output the master area will take up.
78    ///
79    /// This will be clamped between 0.1 and 0.9.
80    pub master_factor: f32,
81    /// Which side the master area will be.
82    pub master_side: MasterSide,
83    /// How many windows will be in the master area.
84    pub master_count: u32,
85    /// Reverses the direction of window insertion i.e. new windows
86    /// are inserted at the top of the master stack instead of at the
87    /// bottom of the side stack.
88    pub reversed: bool,
89}
90
91impl Default for MasterStack {
92    fn default() -> Self {
93        Self {
94            outer_gaps: Gaps::from(4.0),
95            inner_gaps: Gaps::from(4.0),
96            master_factor: 0.5,
97            master_side: MasterSide::Left,
98            master_count: 1,
99            reversed: false,
100        }
101    }
102}
103
104impl LayoutGenerator for MasterStack {
105    fn layout(&self, window_count: u32) -> LayoutNode {
106        let root = LayoutNode::new_with_label("builtin.master_stack");
107        root.set_gaps(self.outer_gaps);
108        root.set_dir(match self.master_side {
109            MasterSide::Left | MasterSide::Right => LayoutDir::Row,
110            MasterSide::Top | MasterSide::Bottom => LayoutDir::Column,
111        });
112
113        if window_count == 0 {
114            return root;
115        }
116
117        let master_factor = self.master_factor.clamp(0.1, 0.9);
118
119        let (master_tv_idx, stack_tv_idx) = match self.reversed {
120            true => (1, 0),
121            false => (0, 1),
122        };
123
124        let master_count = u32::min(self.master_count, window_count);
125
126        let line = Line {
127            outer_gaps: 0.0.into(),
128            inner_gaps: self.inner_gaps,
129            direction: match self.master_side {
130                MasterSide::Left | MasterSide::Right => LayoutDir::Column,
131                MasterSide::Top | MasterSide::Bottom => LayoutDir::Row,
132            },
133            reversed: self.reversed,
134        };
135
136        let master_side = line.layout(master_count);
137        master_side.set_traversal_index(master_tv_idx);
138        master_side.set_size_proportion(master_factor * 10.0);
139
140        if window_count <= self.master_count {
141            root.add_child(master_side);
142            return root;
143        }
144
145        let stack_count = window_count - u32::min(self.master_count, window_count);
146        let stack_side = line.layout(stack_count);
147        stack_side.set_traversal_index(stack_tv_idx);
148        stack_side.set_size_proportion((1.0 - master_factor) * 10.0);
149
150        match self.master_side {
151            MasterSide::Left | MasterSide::Top => {
152                root.set_children([master_side, stack_side]);
153            }
154            MasterSide::Right | MasterSide::Bottom => {
155                root.set_children([stack_side, master_side]);
156            }
157        }
158
159        root
160    }
161}
162
163/// A [`LayoutGenerator`] that lays out windows in a shrinking fashion
164/// towards the bottom right corner.
165#[derive(Clone, Debug, PartialEq)]
166pub struct Dwindle {
167    /// The gaps between the outer container and this layout.
168    pub outer_gaps: Gaps,
169    /// The gaps between windows within this layout.
170    pub inner_gaps: Gaps,
171}
172
173impl Default for Dwindle {
174    fn default() -> Self {
175        Self {
176            inner_gaps: 4.0.into(),
177            outer_gaps: 4.0.into(),
178        }
179    }
180}
181
182impl LayoutGenerator for Dwindle {
183    fn layout(&self, win_count: u32) -> LayoutNode {
184        let root = LayoutNode::new_with_label("builtin.dwindle");
185        root.set_gaps(self.outer_gaps);
186
187        if win_count == 0 {
188            return root;
189        }
190
191        if win_count == 1 {
192            let child = LayoutNode::new();
193            child.set_gaps(self.inner_gaps);
194            root.add_child(child);
195            return root;
196        }
197
198        let mut current_node = root.clone();
199
200        for i in 0..win_count - 1 {
201            if current_node != root {
202                current_node.set_label(Some("builtin.dwindle.split"));
203                current_node.set_gaps(0.0);
204            }
205
206            let child1 = LayoutNode::new_with_traversal_index(0);
207            child1.set_dir(match i % 2 == 0 {
208                true => LayoutDir::Column,
209                false => LayoutDir::Row,
210            });
211            child1.set_gaps(self.inner_gaps);
212            current_node.add_child(child1);
213
214            let child2 = LayoutNode::new_with_traversal_index(1);
215            child2.set_dir(match i % 2 == 0 {
216                true => LayoutDir::Column,
217                false => LayoutDir::Row,
218            });
219            child2.set_gaps(self.inner_gaps);
220            current_node.add_child(child2.clone());
221
222            current_node = child2;
223        }
224
225        root
226    }
227}
228
229/// A [`LayoutGenerator`] that lays out windows in a spiral.
230///
231/// This is similar to the [`Dwindle`] layout but in a spiral instead of
232/// towards the bottom right corner.
233#[derive(Clone, Debug, PartialEq)]
234pub struct Spiral {
235    /// The gaps between the outer container and this layout.
236    pub outer_gaps: Gaps,
237    /// The gaps between windows within this layout.
238    pub inner_gaps: Gaps,
239}
240
241impl Default for Spiral {
242    fn default() -> Self {
243        Self {
244            inner_gaps: 4.0.into(),
245            outer_gaps: 4.0.into(),
246        }
247    }
248}
249
250impl LayoutGenerator for Spiral {
251    fn layout(&self, win_count: u32) -> LayoutNode {
252        let root = LayoutNode::new_with_label("builtin.spiral");
253        root.set_gaps(self.outer_gaps);
254
255        if win_count == 0 {
256            return root;
257        }
258
259        if win_count == 1 {
260            let child = LayoutNode::new();
261            child.set_gaps(self.inner_gaps);
262            root.add_child(child);
263            return root;
264        }
265
266        let mut current_node = root.clone();
267
268        for i in 0..win_count - 1 {
269            if current_node != root {
270                current_node.set_label(Some("builtin.spiral.split"));
271                current_node.set_gaps(0.0);
272            }
273
274            let child1 = LayoutNode::new_with_traversal_index(0);
275            child1.set_dir(match i % 2 == 0 {
276                true => LayoutDir::Column,
277                false => LayoutDir::Row,
278            });
279            child1.set_gaps(self.inner_gaps);
280            current_node.add_child(child1.clone());
281
282            let child2 = LayoutNode::new_with_traversal_index(1);
283            child2.set_dir(match i % 2 == 0 {
284                true => LayoutDir::Column,
285                false => LayoutDir::Row,
286            });
287            child2.set_gaps(self.inner_gaps);
288            current_node.add_child(child2.clone());
289
290            current_node = match i % 4 {
291                0 | 1 => child2,
292                2 | 3 => child1,
293                _ => unreachable!(),
294            };
295        }
296
297        root
298    }
299}
300
301/// Which corner the corner window will in.
302#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
303pub enum CornerLocation {
304    /// The corner window will be in the top left.
305    TopLeft,
306    /// The corner window will be in the top right.
307    TopRight,
308    /// The corner window will be in the bottom left.
309    BottomLeft,
310    /// The corner window will be in the bottom right.
311    BottomRight,
312}
313
314/// A [`LayoutGenerator`] that has one main corner window and a
315/// horizontal and vertical stack flanking it on the other two sides.
316#[derive(Debug, Clone, Copy, PartialEq)]
317pub struct Corner {
318    /// The gaps between the outer container and this layout.
319    pub outer_gaps: Gaps,
320    /// The gaps between windows within this layout.
321    pub inner_gaps: Gaps,
322    /// The proportion of the output that the width of the window takes up.
323    pub corner_width_factor: f32,
324    /// The proportion of the output that the height of the window takes up.
325    pub corner_height_factor: f32,
326    /// The location of the corner window.
327    pub corner_loc: CornerLocation,
328}
329
330impl Default for Corner {
331    fn default() -> Self {
332        Self {
333            inner_gaps: 4.0.into(),
334            outer_gaps: 4.0.into(),
335            corner_width_factor: 0.5,
336            corner_height_factor: 0.5,
337            corner_loc: CornerLocation::TopLeft,
338        }
339    }
340}
341
342impl LayoutGenerator for Corner {
343    fn layout(&self, win_count: u32) -> LayoutNode {
344        let root = LayoutNode::new_with_label("builtin.corner");
345        root.set_gaps(self.outer_gaps);
346
347        if win_count == 0 {
348            return root;
349        }
350
351        if win_count == 1 {
352            let child = LayoutNode::new();
353            child.set_gaps(self.inner_gaps);
354            root.add_child(child);
355            return root;
356        }
357
358        let corner_width_factor = self.corner_width_factor.clamp(0.1, 0.9);
359        let corner_height_factor = self.corner_height_factor.clamp(0.1, 0.9);
360
361        let corner_and_horiz_stack_node =
362            LayoutNode::new_with_label_and_index("builtin.corner.corner_and_stack", 0);
363        corner_and_horiz_stack_node.set_dir(LayoutDir::Column);
364        corner_and_horiz_stack_node.set_size_proportion(corner_width_factor * 10.0);
365
366        let vert_count = (win_count - 1).div_ceil(2);
367        let horiz_count = (win_count - 1) / 2;
368
369        let vert_stack = Line {
370            outer_gaps: 0.0.into(),
371            inner_gaps: self.inner_gaps,
372            direction: LayoutDir::Column,
373            reversed: false,
374        };
375
376        let vert_stack_node = vert_stack.layout(vert_count);
377        vert_stack_node.set_size_proportion((1.0 - corner_width_factor) * 10.0);
378        vert_stack_node.set_traversal_index(1);
379
380        root.set_children(match self.corner_loc {
381            CornerLocation::TopLeft | CornerLocation::BottomLeft => {
382                [corner_and_horiz_stack_node.clone(), vert_stack_node.clone()]
383            }
384            CornerLocation::TopRight | CornerLocation::BottomRight => {
385                [vert_stack_node.clone(), corner_and_horiz_stack_node.clone()]
386            }
387        });
388
389        if horiz_count == 0 {
390            corner_and_horiz_stack_node.set_gaps(self.inner_gaps);
391            return root;
392        }
393
394        let corner_node = LayoutNode::new_with_traversal_index(0);
395        corner_node.set_size_proportion(corner_height_factor * 10.0);
396        corner_node.set_gaps(self.inner_gaps);
397
398        let horiz_stack = Line {
399            outer_gaps: 0.0.into(),
400            inner_gaps: self.inner_gaps,
401            direction: LayoutDir::Row,
402            reversed: false,
403        };
404
405        let horiz_stack_node = horiz_stack.layout(horiz_count);
406        horiz_stack_node.set_size_proportion((1.0 - corner_height_factor) * 10.0);
407        horiz_stack_node.set_traversal_index(1);
408
409        corner_and_horiz_stack_node.set_children(match self.corner_loc {
410            CornerLocation::TopLeft | CornerLocation::TopRight => {
411                [corner_node, horiz_stack_node.clone()]
412            }
413            CornerLocation::BottomLeft | CornerLocation::BottomRight => {
414                [horiz_stack_node.clone(), corner_node]
415            }
416        });
417
418        let traversal_overrides = (0..win_count).map(|idx| (idx, vec![(idx % 2 == 1) as u32]));
419
420        root.set_traversal_overrides(traversal_overrides);
421
422        root
423    }
424}
425
426/// A [`LayoutGenerator`] that attempts to layout windows such that
427/// they are the same size.
428#[derive(Copy, Clone, Debug, PartialEq)]
429pub struct Fair {
430    /// The gaps between the outer container and this layout.
431    pub outer_gaps: Gaps,
432    /// The gaps between windows within this layout.
433    pub inner_gaps: Gaps,
434    /// Which axis the lines of windows will run.
435    pub axis: Axis,
436}
437
438impl Default for Fair {
439    fn default() -> Self {
440        Self {
441            inner_gaps: 4.0.into(),
442            outer_gaps: 4.0.into(),
443            axis: Axis::Vertical,
444        }
445    }
446}
447
448impl LayoutGenerator for Fair {
449    fn layout(&self, win_count: u32) -> LayoutNode {
450        let root = LayoutNode::new_with_label("builtin.fair");
451        root.set_gaps(self.outer_gaps);
452
453        if win_count == 0 {
454            return root;
455        }
456
457        if win_count == 1 {
458            let child = LayoutNode::new();
459            child.set_gaps(self.inner_gaps);
460            root.add_child(child);
461            return root;
462        }
463
464        if win_count == 2 {
465            let child = LayoutNode::new();
466            child.set_gaps(self.inner_gaps);
467            root.add_child(child);
468            let child2 = LayoutNode::new();
469            child2.set_gaps(self.inner_gaps);
470            root.add_child(child2);
471            return root;
472        }
473
474        let line_count = (win_count as f32).sqrt().round() as u32;
475
476        let mut wins_per_line = Vec::new();
477
478        let max_per_line = if win_count > line_count * line_count {
479            line_count + 1
480        } else {
481            line_count
482        };
483
484        for i in 1..=win_count {
485            let index = (i as f32 / max_per_line as f32).ceil() as usize - 1;
486            if wins_per_line.get(index).is_none() {
487                wins_per_line.push(0);
488            }
489            wins_per_line[index] += 1;
490        }
491
492        let line = Line {
493            outer_gaps: 0.0.into(),
494            inner_gaps: self.inner_gaps,
495            direction: match self.axis {
496                Axis::Horizontal => LayoutDir::Row,
497                Axis::Vertical => LayoutDir::Column,
498            },
499            reversed: false,
500        };
501
502        let lines = wins_per_line.into_iter().map(|win_ct| line.layout(win_ct));
503
504        root.set_children(lines);
505
506        root.set_dir(match self.axis {
507            Axis::Horizontal => LayoutDir::Column,
508            Axis::Vertical => LayoutDir::Row,
509        });
510
511        root
512    }
513}
514
515/// A [`LayoutGenerator`] that keeps track of layouts per tag and provides
516/// methods to cycle between them.
517pub struct Cycle<T> {
518    /// The layouts this generator will cycle between.
519    pub layouts: Vec<T>,
520    tag_indices: HashMap<u32, usize>,
521    current_tag: Option<TagHandle>,
522}
523
524impl<T: LayoutGenerator + ?Sized> LayoutGenerator for Box<T> {
525    fn layout(&self, window_count: u32) -> LayoutNode {
526        (**self).layout(window_count)
527    }
528}
529
530impl<T: LayoutGenerator + ?Sized> LayoutGenerator for std::sync::Arc<T> {
531    fn layout(&self, window_count: u32) -> LayoutNode {
532        (**self).layout(window_count)
533    }
534}
535
536impl<T: LayoutGenerator + ?Sized> LayoutGenerator for std::rc::Rc<T> {
537    fn layout(&self, window_count: u32) -> LayoutNode {
538        (**self).layout(window_count)
539    }
540}
541
542impl<T: LayoutGenerator> Cycle<T> {
543    /// Creates a new [`Cycle`] from the given [`LayoutGenerator`]s.
544    ///
545    /// # Examples
546    ///
547    /// ```
548    /// # use pinnacle_api::layout::generators::Cycle;
549    /// # use pinnacle_api::layout::generators::MasterStack;
550    /// # use pinnacle_api::layout::generators::Dwindle;
551    /// # use pinnacle_api::layout::generators::Corner;
552    /// # use pinnacle_api::layout::LayoutGenerator;
553    /// let cycler = Cycle::new([
554    ///     Box::<MasterStack>::default() as Box<dyn LayoutGenerator + Send>,
555    ///     Box::<Dwindle>::default() as _,
556    ///     Box::<Corner>::default() as _,
557    /// ]);
558    /// ```
559    pub fn new(layouts: impl IntoIterator<Item = T>) -> Self {
560        Self {
561            layouts: layouts.into_iter().collect(),
562            tag_indices: HashMap::default(),
563            current_tag: None,
564        }
565    }
566
567    /// Cycles the layout forward on the given tag.
568    pub fn cycle_layout_forward(&mut self, tag: &TagHandle) {
569        let index = self.tag_indices.entry(tag.id).or_default();
570        *index += 1;
571        if *index >= self.layouts.len() {
572            *index = 0;
573        }
574    }
575
576    /// Cycles the layout backward on the given tag.
577    pub fn cycle_layout_backward(&mut self, tag: &TagHandle) {
578        let index = self.tag_indices.entry(tag.id).or_default();
579        if let Some(i) = index.checked_sub(1) {
580            *index = i;
581        } else {
582            *index = self.layouts.len().saturating_sub(1);
583        }
584    }
585
586    /// Retrieves the current layout.
587    ///
588    /// Returns `None` if no layouts were given.
589    pub fn current_layout(&self, tag: &TagHandle) -> Option<&T> {
590        self.layouts
591            .get(self.tag_indices.get(&tag.id).copied().unwrap_or_default())
592    }
593
594    /// Sets the current tag to choose a layout for.
595    pub fn set_current_tag(&mut self, tag: TagHandle) {
596        self.current_tag = Some(tag);
597    }
598}
599
600impl<T: LayoutGenerator> LayoutGenerator for Cycle<T> {
601    fn layout(&self, window_count: u32) -> LayoutNode {
602        let Some(current_tag) = self.current_tag.as_ref() else {
603            return LayoutNode::new();
604        };
605        let Some(current_layout) = self.current_layout(current_tag) else {
606            return LayoutNode::new();
607        };
608        current_layout.layout(window_count)
609    }
610}