Skip to main content

lv_tui/widgets/
block.rs

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
10/// 边框 + 内边距包装容器
11pub 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    /// Sets a title at the bottom border.
46    pub fn title_bottom(mut self, title: impl Into<Text>) -> Self {
47        self.title_bottom = Some(title.into());
48        self
49    }
50
51    /// Sets the title alignment (Left/Center/Right).
52    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        // Check if child is focused — use highlighted border
66        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        // 边框标题(顶部)
75        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        // 边框标题(底部)
92        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        // border(1) + padding(1) = 2 on each side
183        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        // Title should appear somewhere in the top row
195        assert!(tb.buffer.cells.iter().any(|c| c.symbol == "c"));
196    }
197}
198
199impl Block {
200    /// Returns the usable inner rect after subtracting padding and border.
201    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}