Skip to main content

altui_core/widgets/
block.rs

1use crate::{
2    buffer::Buffer,
3    layout::{Alignment, Rect},
4    style::Style,
5    symbols::line,
6    text::Spans,
7    widgets::{Borders, Widget},
8};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum BorderType {
12    Plain,
13    Rounded,
14    Double,
15    Thick,
16}
17
18impl BorderType {
19    pub fn line_symbols(border_type: BorderType) -> line::Set {
20        match border_type {
21            BorderType::Plain => line::NORMAL,
22            BorderType::Rounded => line::ROUNDED,
23            BorderType::Double => line::DOUBLE,
24            BorderType::Thick => line::THICK,
25        }
26    }
27}
28
29/// Padding structure for block
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
31struct Padding {
32    pub left: u16,
33    pub right: u16,
34    pub top: u16,
35    pub bottom: u16,
36}
37
38/// Base widget to be used with all upper level ones. It may be used to display a box border around
39/// the widget and/or add a title.
40///
41/// # Examples
42///
43/// ```
44/// # use altui_core::widgets::{Block, BorderType, Borders};
45/// # use altui_core::style::{Style, Color};
46/// Block::default()
47///     .title("Block")
48///     .borders(Borders::LEFT | Borders::RIGHT)
49///     .border_style(Style::default().fg(Color::White))
50///     .border_type(BorderType::Rounded)
51///     .style(Style::default().bg(Color::Black));
52/// ```
53#[derive(Debug, Clone, PartialEq, Eq)]
54pub struct Block<'a> {
55    /// Optional title place on the upper left of the block
56    title: Option<Spans<'a>>,
57    /// Title alignment. The default is top left of the block, but one can choose to place
58    /// title in the top middle, or top right of the block
59    title_alignment: Alignment,
60    /// Visible borders
61    borders: Borders,
62    /// Border style
63    border_style: Style,
64    /// Type of the border. The default is plain lines but one can choose to have rounded corners
65    /// or doubled lines instead.
66    border_type: BorderType,
67    /// Widget style
68    style: Style,
69    /// Padding inside the block (left, right, top, bottom)
70    padding: Padding,
71}
72
73impl<'a> Default for Block<'a> {
74    fn default() -> Block<'a> {
75        Block {
76            title: None,
77            title_alignment: Alignment::Left,
78            borders: Borders::NONE,
79            border_style: Default::default(),
80            border_type: BorderType::Plain,
81            style: Default::default(),
82            padding: Default::default(),
83        }
84    }
85}
86
87impl<'a> Block<'a> {
88    pub fn styled_borders(borders: Borders, border_style: impl Into<Style>) -> Block<'a> {
89        Block {
90            borders,
91            title: None,
92            title_alignment: Alignment::Left,
93            border_type: BorderType::Plain,
94            border_style: border_style.into(),
95            ..Default::default()
96        }
97    }
98
99    pub fn bordered(borders: Borders) -> Block<'a> {
100        Block {
101            borders,
102            title: None,
103            title_alignment: Alignment::Left,
104            border_type: BorderType::Plain,
105            ..Default::default()
106        }
107    }
108
109    pub fn styled(style: Style) -> Block<'a> {
110        Block {
111            style,
112            title: None,
113            title_alignment: Alignment::Left,
114            borders: Borders::NONE,
115            border_style: Default::default(),
116            border_type: BorderType::Plain,
117            padding: Default::default(),
118        }
119    }
120
121    pub fn title<T>(mut self, title: T) -> Block<'a>
122    where
123        T: Into<Spans<'a>>,
124    {
125        self.title = Some(title.into());
126        self
127    }
128
129    pub fn title_alignment(mut self, alignment: Alignment) -> Block<'a> {
130        self.title_alignment = alignment;
131        self
132    }
133
134    pub fn border_style(mut self, style: Style) -> Block<'a> {
135        self.border_style = style;
136        self
137    }
138
139    pub fn style(mut self, style: Style) -> Block<'a> {
140        self.style = style;
141        self
142    }
143
144    pub fn borders(mut self, flag: Borders) -> Block<'a> {
145        self.borders = flag;
146        self
147    }
148
149    pub fn border_type(mut self, border_type: BorderType) -> Block<'a> {
150        self.border_type = border_type;
151        self
152    }
153
154    /// Set uniform padding on all sides
155    pub fn padding(mut self, padding: u16) -> Block<'a> {
156        self.padding = Padding {
157            left: padding,
158            right: padding,
159            top: padding,
160            bottom: padding,
161        };
162        self
163    }
164
165    /// Set specific padding values
166    pub fn padding_all(mut self, left: u16, right: u16, top: u16, bottom: u16) -> Block<'a> {
167        self.padding = Padding {
168            left,
169            right,
170            top,
171            bottom,
172        };
173        self
174    }
175
176    /// Set horizontal padding
177    pub fn padding_horizontal(mut self, padding: u16) -> Block<'a> {
178        self.padding.left = padding;
179        self.padding.right = padding;
180        self
181    }
182
183    /// Set vertical padding
184    pub fn padding_vertical(mut self, padding: u16) -> Block<'a> {
185        self.padding.top = padding;
186        self.padding.bottom = padding;
187        self
188    }
189
190    /// Set left padding only
191    pub fn padding_left(mut self, padding: u16) -> Block<'a> {
192        self.padding.left = padding;
193        self
194    }
195
196    /// Set right padding only
197    pub fn padding_right(mut self, padding: u16) -> Block<'a> {
198        self.padding.right = padding;
199        self
200    }
201
202    /// Set top padding only
203    pub fn padding_top(mut self, padding: u16) -> Block<'a> {
204        self.padding.top = padding;
205        self
206    }
207
208    /// Set bottom padding only
209    pub fn padding_bottom(mut self, padding: u16) -> Block<'a> {
210        self.padding.bottom = padding;
211        self
212    }
213
214    /// Compute the inner area of a block based on its border visibility rules and padding settings.
215    pub fn inner(&self, area: Rect) -> Rect {
216        let mut inner = area;
217
218        if self.borders.intersects(Borders::LEFT) {
219            inner.x = inner.x.saturating_add(1).min(inner.right());
220            inner.width = inner.width.saturating_sub(1);
221        }
222        if self.borders.intersects(Borders::TOP) || self.title.is_some() {
223            inner.y = inner.y.saturating_add(1).min(inner.bottom());
224            inner.height = inner.height.saturating_sub(1);
225        }
226        if self.borders.intersects(Borders::RIGHT) {
227            inner.width = inner.width.saturating_sub(1);
228        }
229        if self.borders.intersects(Borders::BOTTOM) {
230            inner.height = inner.height.saturating_sub(1);
231        }
232
233        inner.x = inner.x.saturating_add(self.padding.left).min(inner.right());
234        inner.y = inner.y.saturating_add(self.padding.top).min(inner.bottom());
235        inner.width = inner
236            .width
237            .saturating_sub(self.padding.left + self.padding.right);
238        inner.height = inner
239            .height
240            .saturating_sub(self.padding.top + self.padding.bottom);
241
242        inner
243    }
244}
245
246impl<'a> Widget for Block<'a> {
247    fn render(&mut self, area: Rect, buf: &mut Buffer) {
248        if area.area() == 0 {
249            return;
250        }
251        buf.set_style(area, self.style);
252        let symbols = BorderType::line_symbols(self.border_type);
253
254        // Sides
255        if self.borders.intersects(Borders::LEFT) {
256            for y in area.top()..area.bottom() {
257                buf.get_mut(area.left(), y)
258                    .set_symbol(symbols.vertical)
259                    .set_style(self.border_style);
260            }
261        }
262        if self.borders.intersects(Borders::TOP) {
263            for x in area.left()..area.right() {
264                buf.get_mut(x, area.top())
265                    .set_symbol(symbols.horizontal)
266                    .set_style(self.border_style);
267            }
268        }
269        if self.borders.intersects(Borders::RIGHT) {
270            let x = area.right() - 1;
271            for y in area.top()..area.bottom() {
272                buf.get_mut(x, y)
273                    .set_symbol(symbols.vertical)
274                    .set_style(self.border_style);
275            }
276        }
277        if self.borders.intersects(Borders::BOTTOM) {
278            let y = area.bottom() - 1;
279            for x in area.left()..area.right() {
280                buf.get_mut(x, y)
281                    .set_symbol(symbols.horizontal)
282                    .set_style(self.border_style);
283            }
284        }
285
286        // Corners
287        if self.borders.contains(Borders::RIGHT | Borders::BOTTOM) {
288            buf.get_mut(area.right() - 1, area.bottom() - 1)
289                .set_symbol(symbols.bottom_right)
290                .set_style(self.border_style);
291        }
292        if self.borders.contains(Borders::RIGHT | Borders::TOP) {
293            buf.get_mut(area.right() - 1, area.top())
294                .set_symbol(symbols.top_right)
295                .set_style(self.border_style);
296        }
297        if self.borders.contains(Borders::LEFT | Borders::BOTTOM) {
298            buf.get_mut(area.left(), area.bottom() - 1)
299                .set_symbol(symbols.bottom_left)
300                .set_style(self.border_style);
301        }
302        if self.borders.contains(Borders::LEFT | Borders::TOP) {
303            buf.get_mut(area.left(), area.top())
304                .set_symbol(symbols.top_left)
305                .set_style(self.border_style);
306        }
307
308        // Title
309        if let Some(title) = self.title.as_mut() {
310            let left_border_dx = if self.borders.intersects(Borders::LEFT) {
311                1
312            } else {
313                0
314            };
315
316            let right_border_dx = if self.borders.intersects(Borders::RIGHT) {
317                1
318            } else {
319                0
320            };
321
322            let title_area_width = area
323                .width
324                .saturating_sub(left_border_dx)
325                .saturating_sub(right_border_dx);
326
327            let title_dx = match self.title_alignment {
328                Alignment::Left => left_border_dx,
329                Alignment::Center => area.width.saturating_sub(title.width() as u16) / 2,
330                Alignment::Right => area
331                    .width
332                    .saturating_sub(title.width() as u16)
333                    .saturating_sub(right_border_dx),
334            };
335
336            let title_x = area.left() + title_dx;
337            let title_y = area.top();
338
339            buf.set_spans(title_x, title_y, &title, title_area_width);
340        }
341    }
342}
343
344#[cfg(test)]
345mod tests {
346    use super::*;
347    use crate::layout::Rect;
348
349    #[test]
350    fn inner_takes_into_account_the_borders() {
351        // No borders
352        assert_eq!(
353            Block::default().inner(Rect::default()),
354            Rect {
355                x: 0,
356                y: 0,
357                width: 0,
358                height: 0
359            },
360            "no borders, width=0, height=0"
361        );
362        assert_eq!(
363            Block::default().inner(Rect {
364                x: 0,
365                y: 0,
366                width: 1,
367                height: 1
368            }),
369            Rect {
370                x: 0,
371                y: 0,
372                width: 1,
373                height: 1
374            },
375            "no borders, width=1, height=1"
376        );
377
378        // Left border
379        assert_eq!(
380            Block::default().borders(Borders::LEFT).inner(Rect {
381                x: 0,
382                y: 0,
383                width: 0,
384                height: 1
385            }),
386            Rect {
387                x: 0,
388                y: 0,
389                width: 0,
390                height: 1
391            },
392            "left, width=0"
393        );
394        assert_eq!(
395            Block::default().borders(Borders::LEFT).inner(Rect {
396                x: 0,
397                y: 0,
398                width: 1,
399                height: 1
400            }),
401            Rect {
402                x: 1,
403                y: 0,
404                width: 0,
405                height: 1
406            },
407            "left, width=1"
408        );
409        assert_eq!(
410            Block::default().borders(Borders::LEFT).inner(Rect {
411                x: 0,
412                y: 0,
413                width: 2,
414                height: 1
415            }),
416            Rect {
417                x: 1,
418                y: 0,
419                width: 1,
420                height: 1
421            },
422            "left, width=2"
423        );
424
425        // Top border
426        assert_eq!(
427            Block::default().borders(Borders::TOP).inner(Rect {
428                x: 0,
429                y: 0,
430                width: 1,
431                height: 0
432            }),
433            Rect {
434                x: 0,
435                y: 0,
436                width: 1,
437                height: 0
438            },
439            "top, height=0"
440        );
441        assert_eq!(
442            Block::default().borders(Borders::TOP).inner(Rect {
443                x: 0,
444                y: 0,
445                width: 1,
446                height: 1
447            }),
448            Rect {
449                x: 0,
450                y: 1,
451                width: 1,
452                height: 0
453            },
454            "top, height=1"
455        );
456        assert_eq!(
457            Block::default().borders(Borders::TOP).inner(Rect {
458                x: 0,
459                y: 0,
460                width: 1,
461                height: 2
462            }),
463            Rect {
464                x: 0,
465                y: 1,
466                width: 1,
467                height: 1
468            },
469            "top, height=2"
470        );
471
472        // Right border
473        assert_eq!(
474            Block::default().borders(Borders::RIGHT).inner(Rect {
475                x: 0,
476                y: 0,
477                width: 0,
478                height: 1
479            }),
480            Rect {
481                x: 0,
482                y: 0,
483                width: 0,
484                height: 1
485            },
486            "right, width=0"
487        );
488        assert_eq!(
489            Block::default().borders(Borders::RIGHT).inner(Rect {
490                x: 0,
491                y: 0,
492                width: 1,
493                height: 1
494            }),
495            Rect {
496                x: 0,
497                y: 0,
498                width: 0,
499                height: 1
500            },
501            "right, width=1"
502        );
503        assert_eq!(
504            Block::default().borders(Borders::RIGHT).inner(Rect {
505                x: 0,
506                y: 0,
507                width: 2,
508                height: 1
509            }),
510            Rect {
511                x: 0,
512                y: 0,
513                width: 1,
514                height: 1
515            },
516            "right, width=2"
517        );
518
519        // Bottom border
520        assert_eq!(
521            Block::default().borders(Borders::BOTTOM).inner(Rect {
522                x: 0,
523                y: 0,
524                width: 1,
525                height: 0
526            }),
527            Rect {
528                x: 0,
529                y: 0,
530                width: 1,
531                height: 0
532            },
533            "bottom, height=0"
534        );
535        assert_eq!(
536            Block::default().borders(Borders::BOTTOM).inner(Rect {
537                x: 0,
538                y: 0,
539                width: 1,
540                height: 1
541            }),
542            Rect {
543                x: 0,
544                y: 0,
545                width: 1,
546                height: 0
547            },
548            "bottom, height=1"
549        );
550        assert_eq!(
551            Block::default().borders(Borders::BOTTOM).inner(Rect {
552                x: 0,
553                y: 0,
554                width: 1,
555                height: 2
556            }),
557            Rect {
558                x: 0,
559                y: 0,
560                width: 1,
561                height: 1
562            },
563            "bottom, height=2"
564        );
565
566        // All borders
567        assert_eq!(
568            Block::default()
569                .borders(Borders::ALL)
570                .inner(Rect::default()),
571            Rect {
572                x: 0,
573                y: 0,
574                width: 0,
575                height: 0
576            },
577            "all borders, width=0, height=0"
578        );
579        assert_eq!(
580            Block::default().borders(Borders::ALL).inner(Rect {
581                x: 0,
582                y: 0,
583                width: 1,
584                height: 1
585            }),
586            Rect {
587                x: 1,
588                y: 1,
589                width: 0,
590                height: 0,
591            },
592            "all borders, width=1, height=1"
593        );
594        assert_eq!(
595            Block::default().borders(Borders::ALL).inner(Rect {
596                x: 0,
597                y: 0,
598                width: 2,
599                height: 2,
600            }),
601            Rect {
602                x: 1,
603                y: 1,
604                width: 0,
605                height: 0,
606            },
607            "all borders, width=2, height=2"
608        );
609        assert_eq!(
610            Block::default().borders(Borders::ALL).inner(Rect {
611                x: 0,
612                y: 0,
613                width: 3,
614                height: 3,
615            }),
616            Rect {
617                x: 1,
618                y: 1,
619                width: 1,
620                height: 1,
621            },
622            "all borders, width=3, height=3"
623        );
624    }
625
626    #[test]
627    fn inner_takes_into_account_the_title() {
628        assert_eq!(
629            Block::default().title("Test").inner(Rect {
630                x: 0,
631                y: 0,
632                width: 0,
633                height: 1,
634            }),
635            Rect {
636                x: 0,
637                y: 1,
638                width: 0,
639                height: 0,
640            },
641        );
642        assert_eq!(
643            Block::default()
644                .title("Test")
645                .title_alignment(Alignment::Center)
646                .inner(Rect {
647                    x: 0,
648                    y: 0,
649                    width: 0,
650                    height: 1,
651                }),
652            Rect {
653                x: 0,
654                y: 1,
655                width: 0,
656                height: 0,
657            },
658        );
659        assert_eq!(
660            Block::default()
661                .title("Test")
662                .title_alignment(Alignment::Right)
663                .inner(Rect {
664                    x: 0,
665                    y: 0,
666                    width: 0,
667                    height: 1,
668                }),
669            Rect {
670                x: 0,
671                y: 1,
672                width: 0,
673                height: 0,
674            },
675        );
676    }
677}