river_bsp_layout/
lib.rs

1pub mod user_cmd;
2
3use clap::Parser;
4use river_layout_toolkit::{GeneratedLayout, Layout, Rectangle};
5use std::fmt::Display;
6
7/// Wrapper for errors relating to the creation or operation of a `BSPLayout`
8#[non_exhaustive]
9#[derive(Debug)]
10pub enum BSPLayoutError {
11    /// Encountered when a failure occurs in `user_cmd`
12    CmdError(String),
13
14    /// Encountered when there a failure occurs when generating a layout
15    LayoutError(String),
16}
17
18impl Display for BSPLayoutError {
19    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
20        write!(f, "{:?}", self)
21    }
22}
23
24impl std::error::Error for BSPLayoutError {}
25
26/// Create a Binary Space Partitioned layout. Specifically, this layout recursively
27/// divides the screen in half. The split will alternate between vertical and horizontal
28/// based on which side of the container is longer. This will result in a grid like
29/// layout with more-or-less equal sized windows evenly distributed across the screen
30pub struct BSPLayout {
31    /// Number of pixels to put between the left inside edge of adjacent windows
32    pub ig_left: u32,
33
34    /// Number of pixels to put between the right inside edge of adjacent windows
35    pub ig_right: u32,
36
37    /// Number of pixels to put between the bottom inside edge of adjacent windows
38    pub ig_bottom: u32,
39
40    /// Number of pixels to put between the top inside edge of adjacent windows
41    pub ig_top: u32,
42
43    /// Number of pixels to put between the left screen edge and the adjacent windows
44    pub og_left: u32,
45
46    /// Number of pixels to put between the right screen edge and the adjacent windows
47    pub og_right: u32,
48
49    /// Number of pixels to put between the bottom screen edge and the adjacent windows
50    pub og_bottom: u32,
51
52    /// Number of pixels to put between the top screen edge and the adjacent windows
53    pub og_top: u32,
54
55    /// The percentage (between 0.0 and 1.0) of space that should be occupied by the primary window
56    /// when a horizontal split takes place
57    pub hsplit_perc: f32,
58
59    /// The percentage (between 0.0 and 1.0) of space that should be occupied by the primary window
60    /// when a vertical split takes place
61    pub vsplit_perc: f32,
62
63    /// Whether the first split should be horizontal or not. If true, then start by dividing the
64    /// screen in half from right to left. If false, then start by dividing the screen in half from
65    /// top to bottom
66    pub start_hsplit: bool,
67
68    /// If `true`, new views will be prepended to the list. Otherwise, new views will be appended.
69    pub reversed: bool,
70}
71
72impl BSPLayout {
73    /// Initialize a new instance of BSPLayout with inner gaps of 5 pixels and outer gaps of 10
74    /// pixels on each side, a split percent of 50%, and starting on a vertical split
75    ///
76    /// # Returns
77    ///
78    /// A new `BSPLayout`
79    pub fn new() -> BSPLayout {
80        BSPLayout {
81            ig_left: 5,
82            ig_right: 5,
83            ig_bottom: 5,
84            ig_top: 5,
85            og_left: 10,
86            og_right: 10,
87            og_top: 10,
88            og_bottom: 10,
89            hsplit_perc: 0.5,
90            vsplit_perc: 0.5,
91            reversed: false,
92            start_hsplit: false,
93        }
94    }
95
96    /// Sets all sides of outer gap to `new_gap`
97    ///
98    /// # Arguments
99    ///
100    /// * `new_gap` - The value to assign for the gap on all outer edges
101    pub fn set_all_outer_gaps(&mut self, new_gap: u32) {
102        self.og_top = new_gap;
103        self.og_bottom = new_gap;
104        self.og_left = new_gap;
105        self.og_right = new_gap;
106    }
107
108    /// Sets all inner gaps to `new_gap`
109    ///
110    /// # Arguments
111    ///
112    /// * `new_gap` - The value to assign for the gap on all inner edges between windows
113    pub fn set_all_inner_gaps(&mut self, new_gap: u32) {
114        self.ig_top = new_gap;
115        self.ig_left = new_gap;
116        self.ig_right = new_gap;
117        self.ig_bottom = new_gap;
118    }
119
120    /// Shared setup between vsplit and hsplit functions. First checks that vsplit_perc and
121    /// hsplit_perc are in range, then creates the layout variable, and finally calculates how many
122    /// views are in each half of the split
123    ///
124    /// # Arguments
125    ///
126    /// * `view_count` - The total number of views accross both splits
127    ///
128    /// # Returns
129    ///
130    /// Tuple containing - in order - `half_view_count`, `views_remaining`, and the initial layout
131    /// variable
132    ///
133    /// # Errors
134    ///
135    /// If either split percentage is not > 0.0 and < 1.0, return `BSPLayoutError`
136    fn setup_split(&self, view_count: u32) -> Result<(u32, u32, GeneratedLayout), BSPLayoutError> {
137        if self.vsplit_perc <= 0.0
138            || self.vsplit_perc >= 1.0
139            || self.hsplit_perc <= 0.0
140            || self.hsplit_perc >= 1.0
141        {
142            return Err(BSPLayoutError::LayoutError(
143                "Split percents must be > 0.0 and less than 1.0".to_string(),
144            ));
145        }
146        let layout = GeneratedLayout {
147            layout_name: "bsp-layout".to_string(),
148            views: Vec::with_capacity(view_count as usize),
149        };
150
151        let half_view_count = view_count / 2;
152        let views_remaining = view_count % 2; // In case there are odd number of views
153
154        Ok((half_view_count, views_remaining, layout))
155    }
156
157    /// Divide the screen in two by splitting from right to left first, then subsequently from
158    /// top to bottom
159    ///
160    /// # Arguments
161    ///
162    /// * `origin_x` - The x position of the top left of the space to be divided
163    /// relative to the entire display. For example, if you are dividing the entire
164    /// display, then the top left corner is 0, 0. If you are dividing the right
165    /// half of a 1920x1080 monitor, then the top left corner would be at 960, 0
166    ///
167    /// * `origin_y` - The y position of the top left of the space to be divided
168    /// relative to the entire display. For example, if you are dividing the entire
169    /// display, then the top left corner is 0, 0. If you are dividing the bottom
170    /// half of a 1920x1080 monitor, then the top left corner would be at 0, 540
171    ///
172    /// * `canvas_width` - The width in pixels of the area being divided. If you
173    /// are dividing all of a 1920x1080 monitor, then the `canvas_width` would be 1920.
174    /// If you are dividing the right half of the monitor, then the width is 960.
175    ///
176    /// * `canvas_height` - The height in pixels of the area being divided. If you
177    /// are dividing all of a 1920x1080 monitor, then the height would be 1080.
178    /// If you are dividing the bottom half of the monitor, then the height is 540.
179    ///
180    /// * `view_count` - How many windows / containers / apps / division the function
181    /// needs to make in total.
182    ///
183    /// # Returns
184    ///
185    /// A `GeneratedLayout` with `view_count` cells evenly distributed across the screen
186    /// in a grid
187    fn hsplit(
188        &self,
189        origin_x: i32,
190        origin_y: i32,
191        canvas_width: u32,
192        canvas_height: u32,
193        view_count: u32,
194    ) -> Result<GeneratedLayout, BSPLayoutError> {
195        let (half_view_count, views_remaining, mut layout) = self.setup_split(view_count)?;
196
197        // Exit condition. When there is only one window left, it should take up the
198        // entire available canvas
199        if view_count == 1 {
200            layout.views.push(Rectangle {
201                x: origin_x,
202                y: origin_y,
203                width: canvas_width,
204                height: canvas_height,
205            });
206
207            return Ok(layout);
208        }
209
210        let mut prime_split = (canvas_height as f32 * self.hsplit_perc) as u32;
211        if prime_split == 0 {
212            prime_split = 1;
213        }
214        if prime_split >= canvas_height {
215            prime_split = canvas_height - 1;
216        }
217        let sec_split = canvas_height - prime_split;
218
219        let (prime_sub, sec_sub) = if !self.reversed {
220            (self.ig_bottom, self.ig_top)
221        } else {
222            (self.ig_top, self.ig_bottom)
223        };
224
225        let (prime_y, sec_y) = if !self.reversed {
226            (origin_y, prime_split as i32 + origin_y + sec_sub as i32)
227        } else {
228            (sec_split as i32 + origin_y + prime_sub as i32, origin_y)
229        };
230
231        let mut prime_layout = self.vsplit(
232            origin_x,
233            prime_y,
234            canvas_width,
235            if prime_sub < prime_split {
236                prime_split - prime_sub
237            } else {
238                1
239            },
240            half_view_count,
241        )?;
242
243        let mut sec_layout = self.vsplit(
244            origin_x,
245            sec_y,
246            canvas_width,
247            if sec_sub < sec_split {
248                sec_split - sec_sub
249            } else {
250                1
251            },
252            half_view_count + views_remaining,
253        )?;
254
255        layout.views.append(&mut prime_layout.views);
256        layout.views.append(&mut sec_layout.views);
257
258        Ok(layout)
259    }
260
261    /// Divide the screen in two by splitting from top to bottom first, then subsequently from
262    /// right to left
263    ///
264    /// # Arguments
265    ///
266    /// * `origin_x` - The x position of the top left of the space to be divided
267    /// relative to the entire display. For example, if you are dividing the entire
268    /// display, then the top left corner is 0, 0. If you are dividing the right
269    /// half of a 1920x1080 monitor, then the top left corner would be at 960, 0
270    ///
271    /// * `origin_y` - The y position of the top left of the space to be divided
272    /// relative to the entire display. For example, if you are dividing the entire
273    /// display, then the top left corner is 0, 0. If you are dividing the bottom
274    /// half of a 1920x1080 monitor, then the top left corner would be at 0, 540
275    ///
276    /// * `canvas_width` - The width in pixels of the area being divided. If you
277    /// are dividing all of a 1920x1080 monitor, then the `canvas_width` would be 1920.
278    /// If you are dividing the right half of the monitor, then the width is 960.
279    ///
280    /// * `canvas_height` - The height in pixels of the area being divided. If you
281    /// are dividing all of a 1920x1080 monitor, then the height would be 1080.
282    /// If you are dividing the bottom half of the monitor, then the height is 540.
283    ///
284    /// * `view_count` - How many windows / containers / apps / division the function
285    /// needs to make in total.
286    ///
287    /// # Returns
288    ///
289    /// A `GeneratedLayout` with `view_count` cells evenly distributed across the screen
290    /// in a grid
291    fn vsplit(
292        &self,
293        origin_x: i32,
294        origin_y: i32,
295        canvas_width: u32,
296        canvas_height: u32,
297        view_count: u32,
298    ) -> Result<GeneratedLayout, BSPLayoutError> {
299        let (half_view_count, views_remaining, mut layout) = self.setup_split(view_count)?;
300
301        // Exit condition. When there is only one window left, it should take up the
302        // entire available canvas
303        if view_count == 1 {
304            layout.views.push(Rectangle {
305                x: origin_x,
306                y: origin_y,
307                width: canvas_width,
308                height: canvas_height,
309            });
310
311            return Ok(layout);
312        }
313
314        let mut prime_split = (canvas_width as f32 * self.vsplit_perc) as u32;
315        if prime_split == 0 {
316            prime_split = 1;
317        }
318        if prime_split >= canvas_width {
319            prime_split = canvas_width - 1;
320        }
321
322        let sec_split = canvas_width - prime_split;
323
324        let (prime_sub, sec_sub) = if !self.reversed {
325            (self.ig_right, self.ig_left)
326        } else {
327            (self.ig_left, self.ig_right)
328        };
329
330        let (prime_x, sec_x) = if !self.reversed {
331            (origin_x, prime_split as i32 + origin_x + sec_sub as i32)
332        } else {
333            (sec_split as i32 + origin_x + prime_sub as i32, origin_x)
334        };
335
336        let mut prime_layout = self.hsplit(
337            prime_x,
338            origin_y,
339            if prime_sub < prime_split {
340                prime_split - prime_sub
341            } else {
342                1
343            },
344            canvas_height,
345            half_view_count,
346        )?;
347
348        let mut sec_layout = self.hsplit(
349            sec_x,
350            origin_y,
351            if sec_sub < sec_split {
352                sec_split - sec_sub
353            } else {
354                1
355            },
356            canvas_height,
357            half_view_count + views_remaining,
358        )?;
359
360        layout.views.append(&mut prime_layout.views);
361        layout.views.append(&mut sec_layout.views);
362
363        Ok(layout)
364    }
365}
366
367impl Layout for BSPLayout {
368    type Error = BSPLayoutError;
369
370    const NAMESPACE: &'static str = "bsp-layout";
371
372    /// Handle commands passed to the layout with `send-layout-cmd`. Supports individually setting
373    /// the gaps on each side of the screen as well as inner edges. Also supports setting all outer
374    /// and inner gaps at the same time
375    ///
376    /// # Examples
377    ///
378    /// ```
379    /// use river_bsp_layout::BSPLayout;
380    /// use river_layout_toolkit::Layout;
381    ///
382    /// // Initialize layout with 0 gaps
383    /// let mut bsp = BSPLayout::new();
384    /// bsp.set_all_inner_gaps(0);
385    /// bsp.set_all_outer_gaps(0);
386    ///
387    /// // Set gap between windows and the monitor edge to be 5 pixels
388    /// let res = bsp.user_cmd("--outer-gap 5".to_string(), None, "eDP-1").unwrap();
389    /// assert_eq!(bsp.og_top, 5);
390    /// assert_eq!(bsp.og_bottom, 5);
391    /// assert_eq!(bsp.og_right, 5);
392    /// assert_eq!(bsp.og_left, 5);
393    /// ```
394    ///
395    /// # Errors
396    ///
397    /// Will return `BSPLayoutError::CmdError` if an unrecognized command is passed
398    /// or if an invalid argument is passed to a valid command.
399    fn user_cmd(
400        &mut self,
401        cmd: String,
402        _tags: Option<u32>,
403        _output: &str,
404    ) -> Result<(), Self::Error> {
405        let mut cmd: Vec<&str> = cmd.split(" ").collect();
406        cmd.insert(0, "");
407        let cmd = match user_cmd::UserCmd::try_parse_from(cmd) {
408            Ok(c) => c,
409            Err(e) => {
410                eprintln!("{}", e);
411                return Ok(());
412            }
413        };
414
415        cmd.handle_outer_gaps(self);
416        cmd.handle_inner_gaps(self);
417        cmd.handle_start_split(self)?;
418        cmd.handle_set_split(self);
419        cmd.handle_ch_split(self);
420        cmd.handle_reverse(self);
421
422        Ok(())
423    }
424
425    /// Create the geometry for the `BSPLayout`
426    ///
427    /// # Arguments
428    ///
429    /// * `view_count` - The number of views / windows / containers to divide the screen into
430    /// * `usable_width` - How many pixels wide the whole display is
431    /// * `usable_height` - How many pixels tall the whole display is
432    /// * `_tags` - Int representing which tags are currently active based on which
433    /// bit is toggled
434    /// * `_output` - The name of the output to generate the layout on
435    ///
436    /// # Examples
437    ///
438    /// ```
439    /// use river_bsp_layout::BSPLayout;
440    /// use river_layout_toolkit::Layout;
441    ///
442    /// let mut bsp = BSPLayout::new();
443    /// bsp.generate_layout(2, 1920, 1080, 0b000000001, "eDP-1").unwrap();
444    /// ```
445    fn generate_layout(
446        &mut self,
447        view_count: u32,
448        usable_width: u32,
449        usable_height: u32,
450        _tags: u32,
451        _output: &str,
452    ) -> Result<GeneratedLayout, Self::Error> {
453        if !self.start_hsplit {
454            Ok(self.vsplit(
455                self.og_left as i32,
456                self.og_top as i32,
457                usable_width - self.og_left - self.og_right,
458                usable_height - self.og_top - self.og_bottom,
459                view_count,
460            ))?
461        } else {
462            Ok(self.hsplit(
463                self.og_left as i32,
464                self.og_top as i32,
465                usable_width - self.og_left - self.og_right,
466                usable_height - self.og_top - self.og_bottom,
467                view_count,
468            ))?
469        }
470    }
471}