anathema_default_widgets/
border.rs

1use std::ops::ControlFlow;
2use std::str::FromStr;
3
4use anathema_geometry::{LocalPos, Pos, Region, Size};
5use anathema_value_resolver::{AttributeStorage, Attributes, ValueKind};
6use anathema_widgets::error::Result;
7use anathema_widgets::layout::{Constraints, LayoutCtx, PositionCtx};
8use anathema_widgets::paint::{Glyph, Glyphs, PaintCtx, SizePos};
9use anathema_widgets::{AnyWidget, LayoutForEach, PaintChildren, PositionChildren, Widget, WidgetId};
10
11use crate::layout::Axis;
12use crate::layout::border::BorderLayout;
13use crate::{HEIGHT, MAX_HEIGHT, MAX_WIDTH, MIN_HEIGHT, MIN_WIDTH, WIDTH};
14
15pub const BORDER_STYLE: &str = "border_style";
16
17// -----------------------------------------------------------------------------
18//     - Indices -
19//     Index into `DEFAULT_SLIM_EDGES` or `DEFAULT_THICK_EDGES`
20// -----------------------------------------------------------------------------
21pub const BORDER_EDGE_TOP_LEFT: usize = 0;
22pub const BORDER_EDGE_TOP: usize = 1;
23pub const BORDER_EDGE_TOP_RIGHT: usize = 2;
24pub const BORDER_EDGE_RIGHT: usize = 3;
25pub const BORDER_EDGE_BOTTOM_RIGHT: usize = 4;
26pub const BORDER_EDGE_BOTTOM: usize = 5;
27pub const BORDER_EDGE_BOTTOM_LEFT: usize = 6;
28pub const BORDER_EDGE_LEFT: usize = 7;
29
30// -----------------------------------------------------------------------------
31//     - Sides -
32// -----------------------------------------------------------------------------
33bitflags::bitflags! {
34    /// Border sides
35    /// ```ignore
36    /// let sides = Sides::TOP | Sides::LEFT;
37    /// ```
38    #[derive(Debug, Copy, Clone, PartialEq, Eq)]
39    pub struct Sides: u8 {
40        /// Empty
41        const EMPTY = 0x0;
42        /// Top border
43        const TOP = 0b0001;
44        /// Right border
45        const RIGHT = 0b0010;
46        /// Bottom border
47        const BOTTOM = 0b0100;
48        /// Left border
49        const LEFT = 0b1000;
50        /// All sides
51        const ALL = Self::TOP.bits() | Self::RIGHT.bits() | Self::BOTTOM.bits() | Self::LEFT.bits();
52    }
53}
54
55impl Default for Sides {
56    fn default() -> Self {
57        Self::ALL
58    }
59}
60
61impl TryFrom<&ValueKind<'_>> for Sides {
62    type Error = ();
63
64    fn try_from(value: &ValueKind<'_>) -> Result<Self, Self::Error> {
65        let mut sides = Sides::EMPTY;
66        match value {
67            ValueKind::Str(cow) => Sides::from_str(cow),
68            ValueKind::List(list) => {
69                for x in list {
70                    sides |= Sides::try_from(x)?;
71                }
72                Ok(sides)
73            }
74            ValueKind::DynList(value) => {
75                let Some(state) = value.as_state() else { return Err(()) };
76                let Some(list) = state.as_any_list() else { return Err(()) };
77                for i in 0..list.len() {
78                    if sides == Sides::ALL {
79                        break;
80                    }
81                    let value = list.lookup(i).ok_or(())?;
82                    let value = value.as_state().ok_or(())?;
83                    let s = value.as_str().ok_or(())?;
84                    sides |= Sides::from_str(s)?;
85                }
86                Ok(sides)
87            }
88            _ => Err(()),
89        }
90    }
91}
92
93impl FromStr for Sides {
94    type Err = ();
95
96    fn from_str(s: &str) -> Result<Self, Self::Err> {
97        let sides = match s {
98            "all" => Sides::ALL,
99            "top" => Sides::TOP,
100            "left" => Sides::LEFT,
101            "right" => Sides::RIGHT,
102            "bottom" => Sides::BOTTOM,
103            _ => Sides::EMPTY,
104        };
105        Ok(sides)
106    }
107}
108
109// -----------------------------------------------------------------------------
110//   - Border types -
111// -----------------------------------------------------------------------------
112pub const DEFAULT_SLIM_EDGES: [Glyph; 8] = [
113    Glyph::from_char('┌', 1),
114    Glyph::from_char('─', 1),
115    Glyph::from_char('┐', 1),
116    Glyph::from_char('│', 1),
117    Glyph::from_char('┘', 1),
118    Glyph::from_char('─', 1),
119    Glyph::from_char('└', 1),
120    Glyph::from_char('│', 1),
121];
122
123pub const DEFAULT_THICK_EDGES: [Glyph; 8] = [
124    Glyph::from_char('╔', 1),
125    Glyph::from_char('═', 1),
126    Glyph::from_char('╗', 1),
127    Glyph::from_char('║', 1),
128    Glyph::from_char('╝', 1),
129    Glyph::from_char('═', 1),
130    Glyph::from_char('╚', 1),
131    Glyph::from_char('║', 1),
132];
133
134/// The style of the border.
135#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
136pub enum BorderStyle {
137    /// ```text
138    /// ┌─────┐
139    /// │hello│
140    /// └─────┘
141    /// ```
142    #[default]
143    Thin,
144    /// ```text
145    /// ╔═════╗
146    /// ║hello║
147    /// ╚═════╝
148    /// ```
149    Thick,
150    /// ```text
151    /// 0111112
152    /// 7hello3
153    /// 6555554
154    /// ```
155    Custom([Glyph; 8]),
156}
157
158impl BorderStyle {
159    pub fn edges(&self) -> [Glyph; 8] {
160        match self {
161            BorderStyle::Thin => DEFAULT_SLIM_EDGES,
162            BorderStyle::Thick => DEFAULT_THICK_EDGES,
163            BorderStyle::Custom(edges) => *edges,
164        }
165    }
166}
167
168struct Brush {
169    glyph: Glyph,
170    width: u8,
171}
172
173impl Brush {
174    pub fn new(glyph: Glyph, width: u8) -> Self {
175        Self { width, glyph }
176    }
177}
178
179struct BorderPainter {
180    top: Line,
181    bottom: Line,
182    left: Line,
183    right: Line,
184}
185
186struct Line {
187    start_cap: Option<Brush>,
188    middle: Option<Brush>,
189    end_cap: Option<Brush>,
190    start: LocalPos,
191    end: u16,
192    axis: Axis,
193}
194
195impl Line {
196    fn will_draw(&self) -> bool {
197        self.start_cap.is_some() || self.end_cap.is_some() || self.middle.is_some()
198    }
199
200    fn draw<F>(&self, f: &mut F)
201    where
202        F: FnMut(LocalPos, Glyph),
203    {
204        let mut pos = self.start;
205        let mut end = self.end;
206
207        if let Some(brush) = &self.start_cap {
208            f(pos, brush.glyph);
209            match self.axis {
210                Axis::Horizontal => pos.x += brush.width as u16,
211                Axis::Vertical => pos.y += 1,
212            }
213        }
214
215        if let Some(brush) = &self.end_cap {
216            let pos = match self.axis {
217                Axis::Horizontal => {
218                    end -= brush.width as u16;
219                    LocalPos::new(end, pos.y)
220                }
221                Axis::Vertical => {
222                    end -= 1;
223                    LocalPos::new(pos.x, end)
224                }
225            };
226            f(pos, brush.glyph);
227        }
228
229        if let Some(brush) = &self.middle {
230            loop {
231                match self.axis {
232                    Axis::Horizontal => {
233                        if pos.x + brush.width as u16 > end {
234                            break;
235                        }
236                        f(pos, brush.glyph);
237                        pos.x += brush.width as u16;
238                    }
239                    Axis::Vertical => {
240                        if pos.y + 1 > end {
241                            break;
242                        }
243                        f(pos, brush.glyph);
244                        pos.y += 1;
245                    }
246                }
247            }
248        }
249    }
250}
251
252impl BorderPainter {
253    fn new(glyphs: &[Glyph; 8], border_size: BorderSize, size: Size) -> Self {
254        let mut height = size.height;
255
256        let top = Line {
257            start_cap: (border_size.top_left > 0).then(|| Brush::new(glyphs[0], border_size.top_left)),
258            middle: (border_size.top > 0).then(|| Brush::new(glyphs[1], border_size.top)),
259            end_cap: (border_size.top_right > 0).then(|| Brush::new(glyphs[2], border_size.top_right)),
260            start: LocalPos::ZERO,
261            axis: Axis::Horizontal,
262            end: size.width,
263        };
264
265        let bottom = Line {
266            start_cap: (border_size.bottom_left > 0).then(|| Brush::new(glyphs[6], border_size.bottom_left)),
267            middle: (border_size.bottom > 0).then(|| Brush::new(glyphs[5], border_size.bottom)),
268            end_cap: (border_size.bottom_right > 0).then(|| Brush::new(glyphs[4], border_size.bottom_right)),
269            start: LocalPos::new(0, height - 1),
270            axis: Axis::Horizontal,
271            end: size.width,
272        };
273
274        if bottom.will_draw() {
275            height -= 1;
276        }
277
278        let mut offset = 0;
279        if top.will_draw() {
280            offset += 1;
281        }
282
283        let left = Line {
284            start_cap: None,
285            middle: (border_size.left > 0).then(|| Brush::new(glyphs[7], border_size.left)),
286            end_cap: None,
287            start: LocalPos::new(0, offset),
288            axis: Axis::Vertical,
289            end: height,
290        };
291
292        let right = Line {
293            start_cap: None,
294            middle: (border_size.right > 0).then(|| Brush::new(glyphs[3], border_size.right)),
295            end_cap: None,
296            start: LocalPos::new(size.width - border_size.right as u16, offset),
297            axis: Axis::Vertical,
298            end: height,
299        };
300
301        Self {
302            top,
303            bottom,
304            left,
305            right,
306        }
307    }
308
309    fn paint<F>(&mut self, mut f: F)
310    where
311        F: FnMut(LocalPos, Glyph),
312    {
313        self.top.draw(&mut f);
314        self.bottom.draw(&mut f);
315        self.left.draw(&mut f);
316        self.right.draw(&mut f);
317    }
318}
319
320/// Width of every character that makes up the border
321#[derive(Debug, Default, Copy, Clone)]
322pub(crate) struct BorderSize {
323    pub top_left: u8,
324    pub top: u8,
325    pub top_right: u8,
326    pub right: u8,
327    pub bottom_right: u8,
328    pub bottom: u8,
329    pub bottom_left: u8,
330    pub left: u8,
331}
332
333impl BorderSize {
334    pub(crate) fn as_size(&self) -> Size {
335        let left_width = self.left.max(self.top_left).max(self.bottom_left) as u16;
336        let right_width = self.right.max(self.top_right).max(self.bottom_right) as u16;
337
338        Size::new(left_width + right_width, (self.top + self.bottom) as u16)
339    }
340}
341
342/// Draw a border around an element.
343///
344/// The border will size it self around the child if it has one.
345///
346/// If a width and / or a height is provided then the border will produce tight constraints
347/// for the child.
348///
349/// If a border has no size (width and height) and no child then nothing will be rendered.
350///
351/// To render a border with no child provide a width and a height.
352#[derive(Debug)]
353pub struct Border {
354    /// The border style decides the characters
355    /// to be used for each side of the border.
356    border_style: BorderStyle,
357    /// Which sides of the border should be rendered.
358    sides: Sides,
359    /// All the characters for the border, starting from the top left moving clockwise.
360    /// This means the top-left corner is `edges[0]`, the top if `edges[1]` and the top right is
361    /// `edges[2]` etc.
362    edges: [Glyph; 8],
363}
364
365impl Border {
366    // The additional size of the border
367    // to subtract from the constraint.
368    fn border_size(&self, sides: Sides) -> BorderSize {
369        // Get the size of the border (thickness).
370        // This is NOT including the child.
371
372        let mut border_size = BorderSize::default();
373
374        if sides.contains(Sides::LEFT | Sides::TOP) {
375            border_size.top_left = self.edges[BORDER_EDGE_TOP_LEFT].width() as u8;
376        }
377
378        if sides.contains(Sides::LEFT | Sides::BOTTOM) {
379            border_size.bottom_left = self.edges[BORDER_EDGE_BOTTOM_LEFT].width() as u8;
380        }
381
382        if sides.contains(Sides::RIGHT | Sides::BOTTOM) {
383            border_size.bottom_right = self.edges[BORDER_EDGE_BOTTOM_RIGHT].width() as u8;
384        }
385
386        if sides.contains(Sides::RIGHT | Sides::TOP) {
387            border_size.top_right = self.edges[BORDER_EDGE_TOP_RIGHT].width() as u8;
388        }
389
390        if sides.contains(Sides::LEFT) {
391            border_size.left = self.edges[BORDER_EDGE_LEFT].width() as u8;
392        }
393
394        if sides.contains(Sides::RIGHT) {
395            border_size.right = self.edges[BORDER_EDGE_RIGHT].width() as u8;
396        }
397
398        if sides.contains(Sides::TOP) {
399            border_size.top = self.edges[BORDER_EDGE_TOP].width() as u8;
400        }
401
402        if sides.contains(Sides::BOTTOM) {
403            border_size.bottom = self.edges[BORDER_EDGE_BOTTOM].width() as u8;
404        }
405
406        border_size
407    }
408}
409
410impl Widget for Border {
411    fn layout<'bp>(
412        &mut self,
413        children: LayoutForEach<'_, 'bp>,
414        constraints: Constraints,
415        id: WidgetId,
416        ctx: &mut LayoutCtx<'_, 'bp>,
417    ) -> Result<Size> {
418        let attributes = ctx.attribute_storage.get_mut(id);
419
420        self.sides = attributes.get_as::<Sides>("sides").unwrap_or_default();
421
422        self.border_style = match attributes.get(BORDER_STYLE) {
423            None => BorderStyle::Thin,
424            Some(val) => {
425                let s = val.as_str();
426                let mut edges = DEFAULT_SLIM_EDGES;
427                let mut index = 0;
428
429                match s {
430                    Some("thin") | None => BorderStyle::default(),
431                    Some("thick") => BorderStyle::Thick,
432                    Some(s) => {
433                        let mut glyphs = Glyphs::new(s);
434                        while let Some(g) = glyphs.next(ctx.glyph_map) {
435                            edges[index] = g;
436                            index += 1;
437                            if index >= DEFAULT_SLIM_EDGES.len() {
438                                break;
439                            };
440                        }
441                        BorderStyle::Custom(edges)
442                    }
443                }
444            }
445        };
446        self.edges = self.border_style.edges();
447
448        let mut layout = BorderLayout {
449            min_width: attributes.get_as::<u16>(MIN_WIDTH),
450            min_height: attributes.get_as::<u16>(MIN_HEIGHT),
451            max_width: attributes.get_as::<u16>(MAX_WIDTH),
452            max_height: attributes.get_as::<u16>(MAX_HEIGHT),
453            height: attributes.get_as::<u16>(HEIGHT),
454            width: attributes.get_as::<u16>(WIDTH),
455            border_size: self.border_size(self.sides),
456        };
457
458        layout.layout(children, constraints, ctx)
459    }
460
461    fn position<'bp>(
462        &mut self,
463        mut children: PositionChildren<'_, 'bp>,
464        _: WidgetId,
465        attribute_storage: &AttributeStorage<'bp>,
466        mut ctx: PositionCtx,
467    ) {
468        _ = children.each(|child, children| {
469            if self.sides.contains(Sides::TOP) {
470                ctx.pos.y += 1;
471            }
472
473            if self.sides.contains(Sides::LEFT) {
474                ctx.pos.x += self.edges[BORDER_EDGE_LEFT].width() as i32;
475            }
476
477            child.position(children, ctx.pos, attribute_storage, ctx.viewport);
478            ControlFlow::Break(())
479        });
480    }
481
482    fn paint<'bp>(
483        &mut self,
484        mut children: PaintChildren<'_, 'bp>,
485        _id: WidgetId,
486        attribute_storage: &AttributeStorage<'bp>,
487        mut ctx: PaintCtx<'_, SizePos>,
488    ) {
489        let border_size = self.border_size(self.sides);
490
491        _ = children.each(|child, children| {
492            let ctx = ctx.to_unsized();
493            child.paint(children, ctx, attribute_storage);
494            ControlFlow::Break(())
495        });
496
497        // Draw the border
498        // Only draw corners if there are connecting sides:
499        // e.g Sides::Left | Sides::Top
500        //
501        // Don't draw corners if there are no connecting sides:
502        // e.g Sides::Top | Sides::Bottom
503
504        if ctx.local_size.width == 0 || ctx.local_size.height == 0 {
505            return;
506        }
507
508        let mut painter = BorderPainter::new(&self.edges, border_size, ctx.local_size);
509        let paint = |pos, glyph| {
510            ctx.place_glyph(glyph, pos);
511        };
512
513        painter.paint(paint);
514    }
515
516    fn inner_bounds(&self, mut pos: Pos, mut size: Size) -> Region {
517        let bs = self.border_size(self.sides);
518        pos.x += bs.top_left.max(bs.bottom_left).max(bs.left) as i32;
519        pos.y += bs.top as i32;
520        size.width = size
521            .width
522            .saturating_sub(bs.top_right.max(bs.bottom_right).max(bs.right) as u16);
523        size.height = size.height.saturating_sub(bs.bottom as u16);
524        Region::from((pos, size))
525    }
526}
527
528pub(crate) fn make(attributes: &Attributes<'_>) -> Box<dyn AnyWidget> {
529    let sides = attributes.get_as::<Sides>("sides").unwrap_or_default();
530
531    let text = Border {
532        sides,
533        edges: DEFAULT_SLIM_EDGES,
534        border_style: BorderStyle::Thin,
535    };
536
537    Box::new(text)
538}
539//}
540
541#[cfg(test)]
542mod test {
543    use crate::testing::TestRunner;
544
545    #[test]
546    fn thin_border() {
547        let tpl = "border [width: 6, height: 4]";
548
549        let expected = "
550            ╔════════╗
551            ║┌────┐  ║
552            ║│    │  ║
553            ║│    │  ║
554            ║└────┘  ║
555            ║        ║
556            ║        ║
557            ╚════════╝
558        ";
559
560        TestRunner::new(tpl, (8, 6)).instance().render_assert(expected);
561    }
562
563    #[test]
564    fn thick_border() {
565        let tpl = "border [width: 6, height: 4, border_style: 'thick']";
566
567        let expected = "
568            ╔════════╗
569            ║╔════╗  ║
570            ║║    ║  ║
571            ║║    ║  ║
572            ║╚════╝  ║
573            ║        ║
574            ║        ║
575            ╚════════╝
576        ";
577
578        TestRunner::new(tpl, (8, 6)).instance().render_assert(expected);
579    }
580
581    #[test]
582    fn custom_border() {
583        let tpl = "border [width: 6, height: 4, border_style: '╔─╗│╝─╚│']";
584
585        let expected = "
586            ╔════════╗
587            ║╔────╗  ║
588            ║│    │  ║
589            ║│    │  ║
590            ║╚────╝  ║
591            ║        ║
592            ║        ║
593            ╚════════╝
594        ";
595
596        TestRunner::new(tpl, (8, 6)).instance().render_assert(expected);
597    }
598
599    #[test]
600    fn border_top() {
601        let tpl = "border [sides: 'top', width: 6, height: 4, border_style: '╔─╗│╝─╚│']";
602
603        let expected = "
604            ╔════════╗
605            ║──────  ║
606            ║        ║
607            ║        ║
608            ║        ║
609            ║        ║
610            ║        ║
611            ╚════════╝
612        ";
613
614        TestRunner::new(tpl, (8, 6)).instance().render_assert(expected);
615    }
616
617    #[test]
618    fn border_top_bottom() {
619        let tpl = "border [sides: 'bottom', width: 6, height: 4, border_style: '╔─╗│╝─╚│']";
620
621        let expected = "
622            ╔════════╗
623            ║        ║
624            ║        ║
625            ║        ║
626            ║──────  ║
627            ║        ║
628            ║        ║
629            ╚════════╝
630        ";
631
632        TestRunner::new(tpl, (8, 6)).instance().render_assert(expected);
633    }
634
635    #[test]
636    fn border_left() {
637        let tpl = "border [sides: 'left', width: 6, height: 4, border_style: '╔─╗│╝─╚│']";
638
639        let expected = "
640            ╔════════╗
641            ║│       ║
642            ║│       ║
643            ║│       ║
644            ║│       ║
645            ║        ║
646            ║        ║
647            ╚════════╝
648        ";
649
650        TestRunner::new(tpl, (8, 6)).instance().render_assert(expected);
651    }
652
653    #[test]
654    fn border_right() {
655        let tpl = "border [sides: 'right', width: 6, height: 4, border_style: '╔─╗│╝─╚│']";
656
657        let expected = "
658            ╔════════╗
659            ║     │  ║
660            ║     │  ║
661            ║     │  ║
662            ║     │  ║
663            ║        ║
664            ║        ║
665            ╚════════╝
666        ";
667
668        TestRunner::new(tpl, (8, 6)).instance().render_assert(expected);
669    }
670
671    #[test]
672    fn border_top_left() {
673        let tpl = "border [sides: ['top', 'left'], width: 6, height: 4, border_style: '╔─╗│╝─╚│']";
674
675        let expected = "
676            ╔════════╗
677            ║╔─────  ║
678            ║│       ║
679            ║│       ║
680            ║│       ║
681            ║        ║
682            ║        ║
683            ╚════════╝
684        ";
685
686        TestRunner::new(tpl, (8, 6)).instance().render_assert(expected);
687    }
688
689    #[test]
690    fn border_bottom_right() {
691        let tpl = "border [sides: ['bottom', 'right'], width: 6, height: 4]";
692
693        let expected = "
694            ╔════════╗
695            ║     │  ║
696            ║     │  ║
697            ║     │  ║
698            ║─────┘  ║
699            ║        ║
700            ║        ║
701            ╚════════╝
702        ";
703
704        TestRunner::new(tpl, (8, 6)).instance().render_assert(expected);
705    }
706
707    #[test]
708    fn unsized_empty_border() {
709        let tpl = "
710            border [sides: '']
711                text 'hi'
712        ";
713
714        let expected = "
715            ╔════════╗
716            ║hi      ║
717            ║        ║
718            ║        ║
719            ║        ║
720            ╚════════╝
721        ";
722
723        TestRunner::new(tpl, (8, 4)).instance().render_assert(expected);
724    }
725
726    #[test]
727    fn sized_by_child() {
728        let tpl = "
729            border 
730                text 'hello world'
731            ";
732
733        let expected = "
734            ╔════════╗
735            ║┌──────┐║
736            ║│hello │║
737            ║│world │║
738            ║└──────┘║
739            ║        ║
740            ║        ║
741            ╚════════╝
742        ";
743
744        TestRunner::new(tpl, (8, 6)).instance().render_assert(expected);
745    }
746
747    #[test]
748    fn fixed_size() {
749        let tpl = "
750            border [width: 3 + 2, height: 2 + 2]
751                text 'hello world'
752            ";
753
754        let expected = "
755            ╔════════╗
756            ║┌───┐   ║
757            ║│hel│   ║
758            ║│lo │   ║
759            ║└───┘   ║
760            ║        ║
761            ║        ║
762            ╚════════╝
763        ";
764
765        TestRunner::new(tpl, (8, 6)).instance().render_assert(expected);
766    }
767}