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 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 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}