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