1use crate::component::{Component, EventCx, LayoutCx, MeasureCx};
2use crate::event::Event;
3use crate::geom::{Insets, Pos, Rect, Size};
4use crate::layout::Constraint;
5use crate::node::Node;
6use crate::render::RenderCx;
7use crate::style::{Border, Color, Style};
8use crate::text::Text;
9
10pub struct Block {
12 child: Option<Node>,
13 style: Style,
14 title: Option<Text>,
15 title_bottom: Option<Text>,
16 title_alignment: crate::style::TextAlign,
17}
18
19impl Block {
20 pub fn new(child: impl Component + 'static) -> Self {
21 Self {
22 child: Some(Node::new(child)),
23 style: Style::default(),
24 title: None,
25 title_bottom: None,
26 title_alignment: crate::style::TextAlign::Left,
27 }
28 }
29
30 pub fn border(mut self, border: Border) -> Self {
31 self.style = self.style.border(border);
32 self
33 }
34
35 pub fn padding(mut self, value: u16) -> Self {
36 self.style = self.style.padding(value);
37 self
38 }
39
40 pub fn title(mut self, title: impl Into<Text>) -> Self {
41 self.title = Some(title.into());
42 self
43 }
44
45 pub fn title_bottom(mut self, title: impl Into<Text>) -> Self {
47 self.title_bottom = Some(title.into());
48 self
49 }
50
51 pub fn title_alignment(mut self, align: crate::style::TextAlign) -> Self {
53 self.title_alignment = align;
54 self
55 }
56
57 pub fn style(mut self, style: Style) -> Self {
58 self.style = style;
59 self
60 }
61}
62
63impl Component for Block {
64 fn render(&self, cx: &mut RenderCx) {
65 let child_focused = self.child.as_ref().map(|c| cx.focused_id == Some(c.id)).unwrap_or(false);
67 let border_style = if child_focused {
68 Style::default().fg(Color::White).bold()
69 } else {
70 self.style.clone()
71 };
72 cx.buffer.draw_border(cx.rect, self.style.border, &border_style);
73
74 if let Some(title) = &self.title {
76 let text = title.first_text();
77 let x = match self.title_alignment {
78 crate::style::TextAlign::Left => cx.rect.x.saturating_add(2),
79 crate::style::TextAlign::Center => {
80 let tw: u16 = text.chars().map(|c| unicode_width::UnicodeWidthChar::width(c).unwrap_or(0) as u16).sum();
81 cx.rect.x.saturating_add((cx.rect.width.saturating_sub(tw)) / 2)
82 }
83 crate::style::TextAlign::Right => {
84 let tw: u16 = text.chars().map(|c| unicode_width::UnicodeWidthChar::width(c).unwrap_or(0) as u16).sum();
85 cx.rect.x.saturating_add(cx.rect.width.saturating_sub(tw).saturating_sub(2))
86 }
87 };
88 cx.buffer.write_text(Pos { x, y: cx.rect.y }, cx.rect, text, &cx.style);
89 }
90
91 if let Some(title) = &self.title_bottom {
93 let text = title.first_text();
94 let y = cx.rect.y.saturating_add(cx.rect.height.saturating_sub(1));
95 cx.buffer.write_text(Pos { x: cx.rect.x.saturating_add(2), y }, cx.rect, text, &cx.style);
96 }
97
98 if let Some(child) = &self.child {
99 child.render_with_parent(cx.buffer, cx.focused_id, cx.clip_rect, cx.wrap, cx.truncate, cx.align, Some(&cx.style));
100 }
101 }
102
103 fn for_each_child(&self, f: &mut dyn FnMut(&Node)) {
104 if let Some(child) = &self.child {
105 f(child);
106 }
107 }
108
109 fn for_each_child_mut(&mut self, f: &mut dyn FnMut(&mut Node)) {
110 if let Some(child) = &mut self.child {
111 f(child);
112 }
113 }
114
115 fn measure(&self, constraint: Constraint, _cx: &mut MeasureCx) -> Size {
116 let pad = self.effective_padding();
117 let child_constraint = Constraint {
118 min: Size::default(),
119 max: Size {
120 width: constraint.max.width.saturating_sub(pad.left + pad.right),
121 height: constraint.max.height.saturating_sub(pad.top + pad.bottom),
122 },
123 };
124
125 let child_size = self
126 .child
127 .as_ref()
128 .map(|c| c.measure(child_constraint))
129 .unwrap_or_default();
130
131 Size {
132 width: child_size.width.saturating_add(pad.left + pad.right),
133 height: child_size.height.saturating_add(pad.top + pad.bottom),
134 }
135 }
136
137 fn focusable(&self) -> bool {
138 false
139 }
140
141 fn event(&mut self, event: &Event, cx: &mut EventCx) {
142 if matches!(event, Event::Focus | Event::Blur | Event::Tick) {
143 return;
144 }
145
146 if let Some(child) = &mut self.child {
147 let mut child_cx = EventCx::with_task_sender(&mut child.dirty, cx.global_dirty, cx.quit, cx.phase, cx.propagation_stopped, cx.task_sender.clone());
148 child.component.event(event, &mut child_cx);
149 }
150 }
151
152 fn layout(&mut self, rect: Rect, _cx: &mut LayoutCx) {
153 let inner = rect.inner(self.effective_padding());
154 if let Some(child) = &mut self.child {
155 child.layout(inner);
156 }
157 }
158
159 fn style(&self) -> Style {
160 self.style.clone()
161 }
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167 use crate::testbuffer::TestBuffer;
168 use crate::widgets::Label;
169
170 #[test]
171 fn test_block_border() {
172 let mut tb = TestBuffer::new(10, 3);
173 tb.render(&Block::new(Label::new("hi")).border(Border::Rounded).padding(0));
174 assert_eq!(&tb.buffer.cells[0].symbol, "╭");
175 }
176
177 #[test]
178 fn test_inner_rect() {
179 let block = Block::new(Label::new("x")).border(Border::Rounded).padding(1);
180 let r = Rect { x: 0, y: 0, width: 20, height: 10 };
181 let inner = block.inner_rect(r);
182 assert_eq!(inner.x, 2);
184 assert_eq!(inner.y, 2);
185 assert_eq!(inner.width, 16);
186 assert_eq!(inner.height, 6);
187 }
188
189 #[test]
190 fn test_block_title_alignment() {
191 let mut tb = TestBuffer::new(20, 3);
192 tb.render(&Block::new(Label::new("x")).border(Border::Rounded)
193 .title("center").title_alignment(crate::style::TextAlign::Center));
194 assert!(tb.buffer.cells.iter().any(|c| c.symbol == "c"));
196 }
197}
198
199impl Block {
200 pub fn inner_rect(&self, rect: Rect) -> Rect {
202 let p = self.effective_padding();
203 Rect {
204 x: rect.x.saturating_add(p.left),
205 y: rect.y.saturating_add(p.top),
206 width: rect.width.saturating_sub(p.left.saturating_add(p.right)),
207 height: rect.height.saturating_sub(p.top.saturating_add(p.bottom)),
208 }
209 }
210
211 fn effective_padding(&self) -> Insets {
212 let border_width: u16 = match self.style.border {
213 Border::None => 0,
214 _ => 1,
215 };
216 Insets {
217 top: self.style.padding.top + border_width,
218 right: self.style.padding.right + border_width,
219 bottom: self.style.padding.bottom + border_width,
220 left: self.style.padding.left + border_width,
221 }
222 }
223}