river_dwindle/
lib.rs

1use regex::Regex;
2use river_layout_toolkit::{GeneratedLayout, Layout, Rectangle};
3use std::cmp::Ordering;
4use std::convert::Infallible;
5use std::iter::zip;
6
7#[derive(Copy, Clone)]
8pub enum Orientation {
9    Horizontal,
10    Vertical,
11}
12
13pub struct Canvas {
14    pub rectangle: Rectangle,
15}
16
17impl Canvas {
18    pub fn new(outer_padding: u32, width: u32, height: u32) -> Canvas {
19        Canvas {
20            rectangle: Rectangle {
21                x: outer_padding as i32,
22                y: outer_padding as i32,
23                width: width - 2 * outer_padding,
24                height: height - 2 * outer_padding,
25            },
26        }
27    }
28
29    pub fn get_orientation(&self) -> Orientation {
30        match self.rectangle.width.cmp(&self.rectangle.height) {
31            Ordering::Greater => Orientation::Horizontal,
32            Ordering::Less => Orientation::Vertical,
33            Ordering::Equal => Orientation::Horizontal,
34        }
35    }
36
37    pub fn get_rectangle(self) -> Rectangle {
38        self.rectangle
39    }
40
41    pub fn update(&mut self, view: &Rectangle, view_padding: u32) {
42        let orientation = self.get_orientation();
43
44        self.rectangle = Rectangle {
45            x: match orientation {
46                Orientation::Horizontal => self.rectangle.x + ((view.width + view_padding) as i32),
47                Orientation::Vertical => self.rectangle.x,
48            },
49            y: match orientation {
50                Orientation::Horizontal => self.rectangle.y,
51                Orientation::Vertical => self.rectangle.y + ((view.height + view_padding) as i32),
52            },
53            width: match orientation {
54                Orientation::Horizontal => self.rectangle.width - view.width - view_padding,
55                Orientation::Vertical => self.rectangle.width,
56            },
57            height: match orientation {
58                Orientation::Horizontal => self.rectangle.height,
59                Orientation::Vertical => self.rectangle.height - view.height - view_padding,
60            },
61        };
62    }
63}
64
65pub struct DwindleLayout {
66    pub view_padding: u32,
67    pub outer_padding: u32,
68    pub ratio: f32,
69}
70
71impl DwindleLayout {
72    pub fn new(view_padding: u32, outer_padding: u32, ratio: f32) -> DwindleLayout {
73        DwindleLayout {
74            view_padding,
75            outer_padding,
76            ratio,
77        }
78    }
79
80    fn create_normal_view(&self, canvas: &Canvas) -> Rectangle {
81        let orientation = canvas.get_orientation();
82        let padding = self.view_padding / 2;
83
84        Rectangle {
85            x: canvas.rectangle.x,
86            y: canvas.rectangle.y,
87            width: match orientation {
88                Orientation::Horizontal => {
89                    (canvas.rectangle.width as f32 * self.ratio - padding as f32) as u32
90                }
91                Orientation::Vertical => canvas.rectangle.width,
92            },
93            height: match orientation {
94                Orientation::Horizontal => canvas.rectangle.height,
95                Orientation::Vertical => {
96                    (canvas.rectangle.height as f32 * self.ratio - padding as f32) as u32
97                }
98            },
99        }
100    }
101}
102
103impl Layout for DwindleLayout {
104    type Error = Infallible;
105
106    const NAMESPACE: &'static str = "river-dwindle";
107
108    fn generate_layout(
109        &mut self,
110        view_count: u32,
111        usable_width: u32,
112        usable_height: u32,
113        _tags: u32,
114        _output: &str,
115    ) -> Result<GeneratedLayout, Self::Error> {
116        let mut layout = GeneratedLayout {
117            layout_name: "river-dwindle".to_string(),
118            views: Vec::with_capacity(view_count as usize),
119        };
120
121        let mut canvas = Canvas::new(self.outer_padding, usable_width, usable_height);
122
123        for _view_n in 0..view_count - 1 {
124            let view = self.create_normal_view(&canvas);
125            canvas.update(&view, self.view_padding);
126            layout.views.push(view);
127        }
128
129        layout.views.push(canvas.get_rectangle());
130
131        Ok(layout)
132    }
133
134    fn user_cmd(
135        &mut self,
136        cmd: String,
137        _tags: Option<u32>,
138        _output: &str,
139    ) -> Result<(), Self::Error> {
140        let outer_matcher = Regex::new(r"^outer-padding (?P<px>\d+)$").unwrap();
141        let view_matcher = Regex::new(r"^view-padding (?P<px>\d+)$").unwrap();
142
143        for (matcher, field) in zip(
144            [outer_matcher, view_matcher],
145            [&mut self.outer_padding, &mut self.view_padding],
146        ) {
147            let Some(cmd_match) = matcher.captures(&cmd) else {
148                continue;
149            };
150
151            match cmd_match["px"].parse::<u32>() {
152                Ok(new_value) => {
153                    *field = new_value;
154                }
155                Err(_) => {
156                    eprintln!("Could not parse u32 from argument: {}", &cmd_match["px"]);
157                }
158            };
159
160            return Ok(());
161        }
162
163        let ratio_matcher =
164            Regex::new(r"^ratio (?P<sign>[-+])?(?P<ratio>(?:\d*[.])?\d+)$").unwrap();
165
166        if let Some(cmd_match) = ratio_matcher.captures(&cmd) {
167            match cmd_match["ratio"].parse::<f32>() {
168                Ok(requested_value) => {
169                    let mut new_value = self.ratio;
170
171                    // determine the new value based on the operation requested
172                    match cmd_match.name("sign") {
173                        Some(sign) => {
174                            match sign.as_str() {
175                                "+" => {
176                                    new_value = self.ratio + requested_value;
177                                }
178                                "-" => {
179                                    new_value = self.ratio - requested_value;
180                                }
181                                _ => {
182                                    eprintln!("Couldn't parse the ratio sign: {}", sign.as_str());
183                                }
184                            };
185                        }
186                        None => {
187                            new_value = requested_value;
188                        }
189                    }
190
191                    // check that the new value requested is a valid number
192                    if !(0.5..1.0).contains(&new_value) {
193                        eprintln!(
194                            "Invalid ratio: {} (give a ratio between 0.5 (incl.) and 1)",
195                            new_value,
196                        );
197                    } else {
198                        self.ratio = new_value;
199                    };
200                }
201
202                Err(_) => {
203                    eprintln!("Could not parse f32 from argument: {}", &cmd_match["ratio"]);
204                }
205            };
206
207            return Ok(());
208        };
209
210        eprintln!("Command not recognized: {cmd}");
211
212        Ok(())
213    }
214}
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219
220    const DISPLAY_WIDTH: u32 = 1920;
221    const DISPLAY_HEIGHT: u32 = 1080;
222    const DISPLAY_NAME: &str = "A_DISPLAY";
223
224    fn test_layout(
225        view_padding: u32,
226        outer_padding: u32,
227        ratio: f32,
228        view_count: u32,
229        correct_layout: Vec<Rectangle>,
230    ) {
231        let mut dwindle = DwindleLayout::new(view_padding, outer_padding, ratio);
232        let generated_layout = dwindle
233            .generate_layout(view_count, DISPLAY_WIDTH, DISPLAY_HEIGHT, 1, DISPLAY_NAME)
234            .unwrap();
235
236        assert_eq!(generated_layout.views.len(), view_count as usize);
237
238        for (generated_view, correct_view) in
239            generated_layout.views.iter().zip(correct_layout.iter())
240        {
241            assert_eq!(
242                (
243                    generated_view.x,
244                    generated_view.y,
245                    generated_view.width,
246                    generated_view.height
247                ),
248                (
249                    correct_view.x,
250                    correct_view.y,
251                    correct_view.width,
252                    correct_view.height
253                ),
254            )
255        }
256    }
257
258    #[test]
259    fn single_view() {
260        test_layout(
261            0,
262            0,
263            0.5,
264            1,
265            vec![Rectangle {
266                x: 0,
267                y: 0,
268                width: 1920,
269                height: 1080,
270            }],
271        )
272    }
273
274    #[test]
275    fn multiple_views_normal() {
276        test_layout(
277            0,
278            0,
279            0.5,
280            4,
281            vec![
282                Rectangle {
283                    x: 0,
284                    y: 0,
285                    width: 960,
286                    height: 1080,
287                },
288                Rectangle {
289                    x: 960,
290                    y: 0,
291                    width: 960,
292                    height: 540,
293                },
294                Rectangle {
295                    x: 960,
296                    y: 540,
297                    width: 480,
298                    height: 540,
299                },
300                Rectangle {
301                    x: 1440,
302                    y: 540,
303                    width: 480,
304                    height: 540,
305                },
306            ],
307        )
308    }
309
310    #[test]
311    fn multiple_views_ratio70() {
312        test_layout(
313            0,
314            0,
315            0.7,
316            4,
317            vec![
318                Rectangle {
319                    x: 0,
320                    y: 0,
321                    width: 1344,
322                    height: 1080,
323                },
324                Rectangle {
325                    x: 1344,
326                    y: 0,
327                    width: 576,
328                    height: 756,
329                },
330                Rectangle {
331                    x: 1344,
332                    y: 756,
333                    width: 403,
334                    height: 324,
335                },
336                Rectangle {
337                    x: 1747,
338                    y: 756,
339                    width: 173,
340                    height: 324,
341                },
342            ],
343        )
344    }
345
346    #[test]
347    fn multiple_views_padded() {
348        test_layout(
349            16,
350            16,
351            0.5,
352            4,
353            vec![
354                Rectangle {
355                    x: 16,
356                    y: 16,
357                    width: 936,
358                    height: 1048,
359                },
360                Rectangle {
361                    x: 968,
362                    y: 16,
363                    width: 936,
364                    height: 516,
365                },
366                Rectangle {
367                    x: 968,
368                    y: 548,
369                    width: 460,
370                    height: 516,
371                },
372                Rectangle {
373                    x: 1444,
374                    y: 548,
375                    width: 460,
376                    height: 516,
377                },
378            ],
379        )
380    }
381
382    #[test]
383    fn send_padding() {
384        const INITIAL_PADDING: u32 = 0;
385        const NEW_PADDING: u32 = 16;
386
387        let mut dwindle = DwindleLayout::new(INITIAL_PADDING, INITIAL_PADDING, 0.5);
388
389        assert_eq!(dwindle.view_padding, INITIAL_PADDING);
390        dwindle
391            .user_cmd(
392                format!("view-padding {NEW_PADDING}").to_string(),
393                None,
394                DISPLAY_NAME,
395            )
396            .unwrap();
397        assert_eq!(dwindle.view_padding, NEW_PADDING);
398
399        assert_eq!(dwindle.outer_padding, 0);
400        dwindle
401            .user_cmd(
402                format!("outer-padding {NEW_PADDING}").to_string(),
403                None,
404                DISPLAY_NAME,
405            )
406            .unwrap();
407        assert_eq!(dwindle.outer_padding, NEW_PADDING);
408    }
409
410    #[test]
411    fn send_ratio() {
412        const INITIAL_RATIO: f32 = 0.5;
413        const NEW_RATIO: f32 = 0.75;
414
415        let mut dwindle = DwindleLayout::new(0, 0, INITIAL_RATIO);
416
417        assert_eq!(dwindle.ratio, INITIAL_RATIO);
418        dwindle
419            .user_cmd(format!("ratio {NEW_RATIO}").to_string(), None, DISPLAY_NAME)
420            .unwrap();
421        assert_eq!(dwindle.ratio, NEW_RATIO);
422    }
423
424    #[test]
425    fn send_ratio_invalid() {
426        const INITIAL_RATIO: f32 = 0.5;
427        const NEW_RATIO: f32 = 0.49;
428
429        let mut dwindle = DwindleLayout::new(0, 0, INITIAL_RATIO);
430
431        assert_eq!(dwindle.ratio, INITIAL_RATIO);
432        dwindle
433            .user_cmd(format!("ratio {NEW_RATIO}").to_string(), None, DISPLAY_NAME)
434            .unwrap();
435        assert_eq!(dwindle.ratio, INITIAL_RATIO);
436    }
437
438    #[test]
439    fn send_ratio_add() {
440        const INITIAL_RATIO: f32 = 0.5;
441        const RATIO_ADDITION: f32 = 0.05;
442
443        let mut dwindle = DwindleLayout::new(0, 0, INITIAL_RATIO);
444
445        assert_eq!(dwindle.ratio, INITIAL_RATIO);
446        dwindle
447            .user_cmd(
448                format!("ratio +{RATIO_ADDITION}").to_string(),
449                None,
450                DISPLAY_NAME,
451            )
452            .unwrap();
453        assert_eq!(dwindle.ratio, INITIAL_RATIO + RATIO_ADDITION);
454    }
455
456    #[test]
457    fn send_ratio_subtract_invalid() {
458        const INITIAL_RATIO: f32 = 0.5;
459        const RATIO_SUBTRACTION: f32 = 0.05;
460
461        let mut dwindle = DwindleLayout::new(0, 0, INITIAL_RATIO);
462
463        assert_eq!(dwindle.ratio, INITIAL_RATIO);
464        dwindle
465            .user_cmd(
466                format!("ratio -{RATIO_SUBTRACTION}").to_string(),
467                None,
468                DISPLAY_NAME,
469            )
470            .unwrap();
471        assert_eq!(dwindle.ratio, INITIAL_RATIO);
472    }
473
474    #[test]
475    fn send_nonsense() {
476        let mut dwindle = DwindleLayout::new(0, 0, 0.5);
477
478        dwindle
479            .user_cmd(format!("lorem ipsum").to_string(), None, DISPLAY_NAME)
480            .unwrap();
481    }
482}