too/views/
border.rs

1use compact_str::CompactString;
2use unicode_segmentation::UnicodeSegmentation as _;
3use unicode_width::UnicodeWidthStr as _;
4
5#[allow(deprecated)]
6use crate::view::measure_text;
7
8use crate::{
9    layout::Align,
10    math::{pos2, Size, Space},
11    renderer::{Border, Grapheme, Pixel, Rgba},
12    view::{Builder, Interest, Layout, Palette, Render, StyleKind, View},
13    Str,
14};
15
16pub type BorderClass = fn(&Palette, bool, bool) -> BorderStyle;
17
18#[derive(Copy, Clone, Debug)]
19pub struct BorderStyle {
20    pub title: Rgba,
21    pub border: Rgba,
22    pub border_focused: Option<Rgba>,
23    pub border_hovered: Option<Rgba>,
24}
25
26impl BorderStyle {
27    pub fn default(palette: &Palette, _hovered: bool, _focused: bool) -> Self {
28        Self {
29            title: palette.foreground,
30            border: palette.outline,
31            border_focused: None,
32            border_hovered: None,
33        }
34    }
35
36    pub fn interactive(palette: &Palette, hovered: bool, focused: bool) -> Self {
37        Self {
38            border_focused: Some(palette.contrast),
39            border_hovered: Some(palette.secondary),
40            ..Self::default(palette, hovered, focused)
41        }
42    }
43}
44
45#[must_use = "a view does nothing unless `show()` or `show_children()` is called"]
46pub struct BorderView {
47    border: Border,
48    title: Option<CompactString>,
49    align: Align,
50    class: StyleKind<BorderClass, BorderStyle>,
51}
52
53impl std::fmt::Debug for BorderView {
54    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55        f.debug_struct("BorderView")
56            .field("title", &self.title)
57            .field("align", &self.align)
58            .field("class", &self.class)
59            .finish()
60    }
61}
62
63impl BorderView {
64    pub const fn border(mut self, border: Border) -> Self {
65        self.border = border;
66        self
67    }
68
69    pub fn title(mut self, title: impl Into<Str>) -> Self {
70        self.title = Some(title.into().into_inner());
71        self
72    }
73
74    pub const fn title_align(mut self, align: Align) -> Self {
75        self.align = align;
76        self
77    }
78
79    pub const fn class(mut self, class: BorderClass) -> Self {
80        self.class = StyleKind::deferred(class);
81        self
82    }
83
84    pub const fn style(mut self, style: BorderStyle) -> Self {
85        self.class = StyleKind::direct(style);
86        self
87    }
88}
89
90impl<'v> Builder<'v> for BorderView {
91    type View = Self;
92}
93
94impl View for BorderView {
95    type Args<'v> = Self;
96    type Response = ();
97
98    fn create(args: Self::Args<'_>) -> Self {
99        args
100    }
101
102    fn interests(&self) -> Interest {
103        Interest::MOUSE_INSIDE
104    }
105
106    fn layout(&mut self, mut layout: Layout, space: Space) -> Size {
107        let mut margin = self.border.as_margin();
108        if margin.top == 0 && self.title.is_some() {
109            margin.top = 1;
110        }
111
112        let sum = margin.sum();
113        let offset = margin.left_top();
114        let child_space = space.shrink(sum);
115
116        let node = layout.nodes.get_current();
117        let mut size = Size::ZERO;
118        for &child in &node.children {
119            size = layout.compute(child, child_space) + sum;
120            layout.set_position(child, offset);
121        }
122
123        #[allow(deprecated)]
124        let title_size = self
125            .title
126            .as_deref()
127            .map(measure_text)
128            .unwrap_or(Size::ZERO);
129
130        let max = size.max(title_size) + Size::new(1.0, 0.0);
131        space.fit(max)
132    }
133
134    fn draw(&mut self, mut render: Render) {
135        let rect = render.rect();
136        let (w, h) = (rect.width() - 1, rect.height() - 1);
137
138        let is_hovered = render.is_hovered();
139        let is_focused = render.is_focused();
140
141        let style = match self.class {
142            StyleKind::Deferred(style) => (style)(render.palette, is_hovered, is_focused),
143            StyleKind::Direct(style) => style,
144        };
145
146        let color = match (is_focused, is_hovered) {
147            (true, true) => style
148                .border_focused
149                .unwrap_or(style.border_hovered.unwrap_or(style.border)),
150            (true, false) => style.border_focused.unwrap_or(style.border),
151            (false, true) => style.border_hovered.unwrap_or(style.border),
152            (false, false) => style.border,
153        };
154
155        render
156            .horizontal_line(0, 1..=w, Pixel::new(self.border.top).fg(color))
157            .horizontal_line(h, 1..=w, Pixel::new(self.border.bottom).fg(color))
158            .vertical_line(0, 1..=h, Pixel::new(self.border.left).fg(color))
159            .vertical_line(w, 1..=h, Pixel::new(self.border.right).fg(color))
160            .set(pos2(0, 0), Pixel::new(self.border.left_top).fg(color))
161            .set(pos2(w, 0), Pixel::new(self.border.right_top).fg(color))
162            .set(pos2(0, h), Pixel::new(self.border.left_bottom).fg(color))
163            .set(pos2(w, h), Pixel::new(self.border.right_bottom).fg(color));
164
165        // XXX this is actually a valid use of `measure_text`
166        // we don't really want to delegate to the label type because we do that
167        // weird intersperse border-behind-title things
168        if let Some(title) = &self.title {
169            #[allow(deprecated)]
170            let tw = measure_text(title);
171
172            let w = w as f32;
173            let x = match self.align {
174                Align::Min => 1.0,
175                Align::Center => (w - tw.width) / 2.0,
176                Align::Max => w - tw.width,
177            };
178
179            let mut start = 0.0;
180            let fg = style.title;
181            for grapheme in title.graphemes(true) {
182                if grapheme.chars().all(|c| c.is_whitespace()) {
183                    start += grapheme.width() as f32;
184                    continue;
185                }
186                let cell = Grapheme::new(grapheme).fg(fg);
187                render.set((start + x, 0.0), cell);
188                start += grapheme.width() as f32
189            }
190        }
191
192        self.default_draw(render);
193    }
194}
195
196pub fn border(border: Border) -> BorderView {
197    BorderView {
198        border,
199        title: None,
200        align: Align::Min,
201        class: StyleKind::deferred(BorderStyle::default),
202    }
203}
204
205pub fn frame(border: Border, title: impl Into<Str>) -> BorderView {
206    BorderView {
207        border,
208        title: Some(title.into().into_inner()),
209        align: Align::Min,
210        class: StyleKind::deferred(BorderStyle::default),
211    }
212}