use crate::component::{Component, EventCx, LayoutCx, MeasureCx};
use crate::event::Event;
use crate::geom::{Insets, Pos, Rect, Size};
use crate::layout::Constraint;
use crate::node::Node;
use crate::render::RenderCx;
use crate::style::{Border, Color, Style};
use crate::text::Text;
pub struct Block {
child: Option<Node>,
style: Style,
title: Option<Text>,
title_bottom: Option<Text>,
title_alignment: crate::style::TextAlign,
}
impl Block {
pub fn new(child: impl Component + 'static) -> Self {
Self {
child: Some(Node::new(child)),
style: Style::default(),
title: None,
title_bottom: None,
title_alignment: crate::style::TextAlign::Left,
}
}
pub fn border(mut self, border: Border) -> Self {
self.style = self.style.border(border);
self
}
pub fn padding(mut self, value: u16) -> Self {
self.style = self.style.padding(value);
self
}
pub fn title(mut self, title: impl Into<Text>) -> Self {
self.title = Some(title.into());
self
}
pub fn title_bottom(mut self, title: impl Into<Text>) -> Self {
self.title_bottom = Some(title.into());
self
}
pub fn title_alignment(mut self, align: crate::style::TextAlign) -> Self {
self.title_alignment = align;
self
}
pub fn style(mut self, style: Style) -> Self {
self.style = style;
self
}
}
impl Component for Block {
fn render(&self, cx: &mut RenderCx) {
let child_focused = self.child.as_ref().map(|c| cx.focused_id == Some(c.id)).unwrap_or(false);
let border_style = if child_focused {
Style::default().fg(Color::White).bold()
} else {
self.style.clone()
};
cx.buffer.draw_border(cx.rect, self.style.border, &border_style);
if let Some(title) = &self.title {
let text = title.first_text();
let x = match self.title_alignment {
crate::style::TextAlign::Left => cx.rect.x.saturating_add(2),
crate::style::TextAlign::Center => {
let tw: u16 = text.chars().map(|c| unicode_width::UnicodeWidthChar::width(c).unwrap_or(0) as u16).sum();
cx.rect.x.saturating_add((cx.rect.width.saturating_sub(tw)) / 2)
}
crate::style::TextAlign::Right => {
let tw: u16 = text.chars().map(|c| unicode_width::UnicodeWidthChar::width(c).unwrap_or(0) as u16).sum();
cx.rect.x.saturating_add(cx.rect.width.saturating_sub(tw).saturating_sub(2))
}
};
cx.buffer.write_text(Pos { x, y: cx.rect.y }, cx.rect, text, &cx.style);
}
if let Some(title) = &self.title_bottom {
let text = title.first_text();
let y = cx.rect.y.saturating_add(cx.rect.height.saturating_sub(1));
cx.buffer.write_text(Pos { x: cx.rect.x.saturating_add(2), y }, cx.rect, text, &cx.style);
}
if let Some(child) = &self.child {
child.render_with_parent(cx.buffer, cx.focused_id, cx.clip_rect, cx.wrap, cx.truncate, cx.align, Some(&cx.style));
}
}
fn for_each_child(&self, f: &mut dyn FnMut(&Node)) {
if let Some(child) = &self.child {
f(child);
}
}
fn for_each_child_mut(&mut self, f: &mut dyn FnMut(&mut Node)) {
if let Some(child) = &mut self.child {
f(child);
}
}
fn measure(&self, constraint: Constraint, _cx: &mut MeasureCx) -> Size {
let pad = self.effective_padding();
let child_constraint = Constraint {
min: Size::default(),
max: Size {
width: constraint.max.width.saturating_sub(pad.left + pad.right),
height: constraint.max.height.saturating_sub(pad.top + pad.bottom),
},
};
let child_size = self
.child
.as_ref()
.map(|c| c.measure(child_constraint))
.unwrap_or_default();
Size {
width: child_size.width.saturating_add(pad.left + pad.right),
height: child_size.height.saturating_add(pad.top + pad.bottom),
}
}
fn focusable(&self) -> bool {
false
}
fn event(&mut self, event: &Event, cx: &mut EventCx) {
if matches!(event, Event::Focus | Event::Blur | Event::Tick) {
return;
}
if let Some(child) = &mut self.child {
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());
child.component.event(event, &mut child_cx);
}
}
fn layout(&mut self, rect: Rect, _cx: &mut LayoutCx) {
let inner = rect.inner(self.effective_padding());
if let Some(child) = &mut self.child {
child.layout(inner);
}
}
fn style(&self) -> Style {
self.style.clone()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::testbuffer::TestBuffer;
use crate::widgets::Label;
#[test]
fn test_block_border() {
let mut tb = TestBuffer::new(10, 3);
tb.render(&Block::new(Label::new("hi")).border(Border::Rounded).padding(0));
assert_eq!(&tb.buffer.cells[0].symbol, "╭");
}
#[test]
fn test_inner_rect() {
let block = Block::new(Label::new("x")).border(Border::Rounded).padding(1);
let r = Rect { x: 0, y: 0, width: 20, height: 10 };
let inner = block.inner_rect(r);
assert_eq!(inner.x, 2);
assert_eq!(inner.y, 2);
assert_eq!(inner.width, 16);
assert_eq!(inner.height, 6);
}
#[test]
fn test_block_title_alignment() {
let mut tb = TestBuffer::new(20, 3);
tb.render(&Block::new(Label::new("x")).border(Border::Rounded)
.title("center").title_alignment(crate::style::TextAlign::Center));
assert!(tb.buffer.cells.iter().any(|c| c.symbol == "c"));
}
}
impl Block {
pub fn inner_rect(&self, rect: Rect) -> Rect {
let p = self.effective_padding();
Rect {
x: rect.x.saturating_add(p.left),
y: rect.y.saturating_add(p.top),
width: rect.width.saturating_sub(p.left.saturating_add(p.right)),
height: rect.height.saturating_sub(p.top.saturating_add(p.bottom)),
}
}
fn effective_padding(&self) -> Insets {
let border_width: u16 = match self.style.border {
Border::None => 0,
_ => 1,
};
Insets {
top: self.style.padding.top + border_width,
right: self.style.padding.right + border_width,
bottom: self.style.padding.bottom + border_width,
left: self.style.padding.left + border_width,
}
}
}