use std::cell::Cell;
use std::rc::Rc;
use crate::base::{Point, Rect, Rgba};
use crate::layout::{Edges, Style as LayoutStyle};
use crate::theme::TokenSet;
use crate::ui::{Canvas, Element, View};
#[path = "block_close.rs"]
mod close;
#[cfg(test)]
use close::CLOSE_GLYPH;
use close::{close_child, close_run, close_text, panel_rect};
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum BorderKind {
Plain,
Rounded,
Double,
Heavy,
None,
}
impl BorderKind {
fn glyphs(self) -> Option<[char; 8]> {
match self {
BorderKind::Plain => Some(['┌', '─', '┐', '│', '│', '└', '─', '┘']),
BorderKind::Rounded => Some(['╭', '─', '╮', '│', '│', '╰', '─', '╯']),
BorderKind::Double => Some(['╔', '═', '╗', '║', '║', '╚', '═', '╝']),
BorderKind::Heavy => Some(['┏', '━', '┓', '┃', '┃', '┗', '━', '┛']),
BorderKind::None => None,
}
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum TitleAlign {
Left,
Center,
Right,
}
pub struct Block {
border: BorderKind,
title: Option<String>,
title_align: TitleAlign,
focused: bool,
fill: Option<Rgba>,
shadow: Option<Rgba>,
layout: LayoutStyle,
children: Vec<View>,
on_close: Option<Box<dyn FnMut()>>,
}
impl Block {
pub fn new() -> Block {
Block {
border: BorderKind::Plain,
title: None,
title_align: TitleAlign::Left,
focused: false,
fill: None,
shadow: None,
layout: LayoutStyle::default(),
children: Vec::new(),
on_close: None,
}
}
pub fn border(mut self, kind: BorderKind) -> Block {
self.border = kind;
self
}
pub fn title(mut self, title: impl Into<String>) -> Block {
self.title = Some(title.into());
self
}
pub fn title_align(mut self, align: TitleAlign) -> Block {
self.title_align = align;
self
}
pub fn focused(mut self, focused: bool) -> Block {
self.focused = focused;
self
}
pub fn fill(mut self, ground: Rgba) -> Block {
self.fill = Some(ground);
self
}
pub fn shadow(mut self, ground: Rgba) -> Block {
self.shadow = Some(ground);
self
}
pub fn layout(mut self, style: LayoutStyle) -> Block {
self.layout = style;
self
}
pub fn child(mut self, view: impl Into<View>) -> Block {
self.children.push(view.into());
self
}
pub fn on_close(mut self, f: impl FnMut() + 'static) -> Block {
self.on_close = Some(Box::new(f));
self
}
pub fn view(self, cx: crate::reactive::Scope) -> crate::ui::View {
let t = crate::widgets::theme_tokens(cx);
self.element(&t).build()
}
pub fn element(self, t: &TokenSet) -> Element {
let stroke = if self.focused {
t.border_focus
} else {
t.border
};
let title_fg = if self.focused { t.text } else { t.text_muted };
let close_muted = t.text_muted;
let close_hot = t.error;
let border = self.border;
let title = self.title;
let access_title = if self.on_close.is_some() {
title.clone()
} else {
None
};
let align = self.title_align;
let fill = self.fill;
let shadow = self.shadow;
let bordered = border.glyphs().is_some();
let closable = self.on_close.is_some();
let geom: Option<Rc<Cell<Rect>>> = closable.then(|| Rc::new(Cell::new(Rect::ZERO)));
let mut chrome = Edges::ZERO;
if bordered {
chrome = Edges::all(1);
}
if shadow.is_some() {
chrome.right += 1;
chrome.bottom += 1;
}
let layout = self.layout;
let geom_probe = geom.clone();
let mut el =
Element::new()
.style(layout)
.padding_floor(chrome)
.draw(move |canvas, rect| {
let panel = panel_rect(rect, shadow.is_some());
if let Some(g) = &geom_probe {
g.set(panel);
}
if rect.w <= 0 || rect.h <= 0 {
return;
}
if let Some(ground) = shadow {
for y in (rect.y + 1)..rect.bottom() {
canvas.put(Point::new(rect.right() - 1, y), ' ', ground, ground);
}
for x in (rect.x + 1)..rect.right() {
canvas.put(Point::new(x, rect.bottom() - 1), ' ', ground, ground);
}
}
let close = if closable {
close_run(panel, bordered).map(|run| (run, close_muted))
} else {
None
};
draw_block(
canvas,
panel,
border,
stroke,
fill,
title.as_deref(),
title_fg,
align,
close,
);
});
if closable {
el = el.probe_when_culled();
}
for child in self.children {
el = el.child(child);
}
if let (Some(geom), Some(on_close)) = (geom, self.on_close) {
el = el.child(close_child(
geom,
bordered,
access_title,
close_hot,
on_close,
));
}
el
}
}
impl Default for Block {
fn default() -> Self {
Block::new()
}
}
#[allow(clippy::too_many_arguments)]
fn draw_block(
canvas: &mut dyn Canvas,
rect: Rect,
border: BorderKind,
stroke: Rgba,
fill: Option<Rgba>,
title: Option<&str>,
title_fg: Rgba,
align: TitleAlign,
close: Option<(Rect, Rgba)>,
) {
if rect.w <= 0 || rect.h <= 0 {
return;
}
if let Some(ground) = fill {
canvas.fill(rect, ' ', ground, ground);
}
let keep = Rgba::TRANSPARENT; let bg = fill.unwrap_or(keep);
let Some([tl, top, tr, left, right, bl, bottom, br]) = border.glyphs() else {
if let Some((run, ink)) = close {
canvas.print(Point::new(run.x, run.y), close_text(run.w), ink, bg);
}
return;
};
let (x0, y0) = (rect.x, rect.y);
let (x1, y1) = (rect.right() - 1, rect.bottom() - 1);
for x in (x0 + 1)..x1 {
canvas.put(Point::new(x, y0), top, stroke, bg);
canvas.put(Point::new(x, y1), bottom, stroke, bg);
}
for y in (y0 + 1)..y1 {
canvas.put(Point::new(x0, y), left, stroke, bg);
canvas.put(Point::new(x1, y), right, stroke, bg);
}
canvas.put(Point::new(x0, y0), tl, stroke, bg);
canvas.put(Point::new(x1, y0), tr, stroke, bg);
if rect.h > 1 {
canvas.put(Point::new(x0, y1), bl, stroke, bg);
canvas.put(Point::new(x1, y1), br, stroke, bg);
}
if let Some((run, ink)) = close {
canvas.print(Point::new(run.x, run.y), close_text(run.w), ink, bg);
}
let Some(title) = title else { return };
let title_end = close.map(|(run, _)| run.x).unwrap_or(x1); let avail = (title_end - (x0 + 1) - 2).max(0) as usize;
if avail == 0 || title.is_empty() {
return;
}
let shown: String = title.chars().take(avail).collect();
let w = shown.chars().count() as i32 + 2; let tx = match align {
TitleAlign::Left => x0 + 1,
TitleAlign::Center => x0 + (rect.w - w).max(0) / 2,
TitleAlign::Right => title_end - w,
}
.clamp(x0 + 1, (title_end - w).max(x0 + 1));
canvas.print(Point::new(tx, y0), &format!(" {shown} "), title_fg, bg);
}
#[cfg(test)]
#[path = "block_tests.rs"]
mod tests;