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}