use crate::buffer::{Buffer, Cell};
use crate::fiber::{FiberId, FiberKind, FiberTree, Rect};
use crate::props::{Overflow, TextWrap};
use crate::style::{Attrs, BorderStyle, Color, Weight};
use crate::text::{truncate_line, wrap_text};
pub(crate) fn paint(tree: &FiberTree, buf: &mut Buffer) {
if let Some(root) = tree.root {
let full = Rect {
x: 0,
y: 0,
width: buf.width(),
height: buf.height(),
};
paint_fiber(tree, root, buf, full, (0, 0));
}
}
fn intersect(a: Rect, b: Rect) -> Rect {
let x1 = a.x.max(b.x);
let y1 = a.y.max(b.y);
let x2 = a.x.saturating_add(a.width).min(b.x.saturating_add(b.width));
let y2 =
a.y.saturating_add(a.height)
.min(b.y.saturating_add(b.height));
Rect {
x: x1,
y: y1,
width: x2.saturating_sub(x1),
height: y2.saturating_sub(y1),
}
}
fn put(buf: &mut Buffer, clip: Rect, x: i32, y: i32, cell: Cell) {
let (cx, cy) = (clip.x as i32, clip.y as i32);
if x >= cx && y >= cy && x < cx + clip.width as i32 && y < cy + clip.height as i32 {
buf.set(x as u16, y as u16, cell);
}
}
fn onscreen_rect(ox: i32, oy: i32, w: u16, h: u16) -> Rect {
let x0 = ox.max(0);
let y0 = oy.max(0);
let x1 = (ox + w as i32).max(0);
let y1 = (oy + h as i32).max(0);
Rect {
x: x0.min(u16::MAX as i32) as u16,
y: y0.min(u16::MAX as i32) as u16,
width: (x1 - x0).min(u16::MAX as i32) as u16,
height: (y1 - y0).min(u16::MAX as i32) as u16,
}
}
fn paint_fiber(tree: &FiberTree, id: FiberId, buf: &mut Buffer, clip: Rect, offset: (i32, i32)) {
let fiber = tree.get(id);
let r = fiber.layout;
let (ox, oy) = (r.x as i32 + offset.0, r.y as i32 + offset.1);
match &fiber.kind {
FiberKind::View(props) => {
if props.background != Color::Reset {
for dy in 0..r.height as i32 {
for dx in 0..r.width as i32 {
put(
buf,
clip,
ox + dx,
oy + dy,
Cell {
bg: props.background,
..Cell::default()
},
);
}
}
}
if props.border_style != BorderStyle::None && r.width >= 2 && r.height >= 2 {
draw_border(
buf,
clip,
ox,
oy,
r.width,
r.height,
props.border_style,
props.border_color,
props.background,
);
}
}
FiberKind::Text(props) => {
let fallback;
let lines: &[String] = if let Some(cached) = &fiber.wrapped {
cached
} else {
fallback = match props.wrap {
TextWrap::Wrap => wrap_text(&props.content, r.width as usize),
TextWrap::Truncate => vec![truncate_line(&props.content, r.width as usize)],
};
&fallback
};
let attrs = Attrs {
bold: props.weight == Weight::Bold,
..Attrs::default()
};
for (dy, line) in lines.iter().take(r.height as usize).enumerate() {
for (dx, ch) in line.chars().take(r.width as usize).enumerate() {
let ch = if ch.is_control() { ' ' } else { ch };
let (x, y) = (ox + dx as i32, oy + dy as i32);
let bg = if x >= 0
&& y >= 0
&& (x as u16) < buf.width()
&& (y as u16) < buf.height()
{
buf.get(x as u16, y as u16).bg
} else {
Color::Reset
};
put(
buf,
clip,
x,
y,
Cell {
ch,
fg: props.color,
bg,
attrs,
},
);
}
}
}
_ => {}
}
let (child_clip, child_offset) = match &fiber.kind {
FiberKind::View(props) => {
let clip = if props.overflow != Overflow::Visible {
intersect(clip, onscreen_rect(ox, oy, r.width, r.height))
} else {
clip
};
let scroll_dy = props
.scroll
.as_ref()
.map(|s| s.offset() as i32)
.unwrap_or(0);
(clip, (offset.0, offset.1 - scroll_dy))
}
_ => (clip, offset),
};
for c in &fiber.children {
paint_fiber(tree, *c, buf, child_clip, child_offset);
}
}
fn border_chars(style: BorderStyle) -> [char; 6] {
match style {
BorderStyle::Single => ['─', '│', '┌', '┐', '└', '┘'],
BorderStyle::Round => ['─', '│', '╭', '╮', '╰', '╯'],
BorderStyle::Double => ['═', '║', '╔', '╗', '╚', '╝'],
BorderStyle::None => [' '; 6],
}
}
#[allow(clippy::too_many_arguments)]
fn draw_border(
buf: &mut Buffer,
clip: Rect,
ox: i32,
oy: i32,
w: u16,
h: u16,
style: BorderStyle,
color: Color,
bg: Color,
) {
let [hc, vc, tl, tr, bl, br] = border_chars(style);
let (x2, y2) = (ox + w as i32 - 1, oy + h as i32 - 1);
let cell = |ch| Cell {
ch,
fg: color,
bg,
attrs: Attrs::default(),
};
for dx in 1..w as i32 - 1 {
put(buf, clip, ox + dx, oy, cell(hc));
put(buf, clip, ox + dx, y2, cell(hc));
}
for dy in 1..h as i32 - 1 {
put(buf, clip, ox, oy + dy, cell(vc));
put(buf, clip, x2, oy + dy, cell(vc));
}
put(buf, clip, ox, oy, cell(tl));
put(buf, clip, x2, oy, cell(tr));
put(buf, clip, ox, y2, cell(bl));
put(buf, clip, x2, y2, cell(br));
}
#[cfg(test)]
mod tests {
use super::*;
use crate::buffer::Buffer;
use crate::element::Element;
use crate::fiber::FiberTree;
use crate::hooks::RuntimeHandle;
use crate::layout::compute_layout;
use crate::props::{
Dimension, FlexDirection, JustifyContent, Overflow, TextProps, TextWrap, ViewProps,
};
use crate::style::{BorderStyle, Color, Weight};
fn render_to_text(el: Element, w: u16, h: u16) -> String {
let (rt, _rx) = RuntimeHandle::test_handle();
let mut tree = FiberTree::new();
tree.mount_root(el, &rt);
compute_layout(&mut tree, w, h);
let mut buf = Buffer::new(w, h);
paint(&tree, &mut buf);
buf.to_text()
}
#[test]
fn bordered_box_with_text() {
let out = render_to_text(
Element::view(
ViewProps {
border_style: BorderStyle::Round,
width: Dimension::Cells(8),
height: Dimension::Cells(3),
..Default::default()
},
vec![Element::text(TextProps {
content: "hi".into(),
..Default::default()
})],
),
8,
3,
);
assert_eq!(out, "╭──────╮\n│hi │\n╰──────╯");
}
#[test]
fn column_with_wrap() {
let out = render_to_text(
Element::view(
ViewProps {
flex_direction: FlexDirection::Column,
width: Dimension::Cells(5),
height: Dimension::Cells(3),
..Default::default()
},
vec![Element::text(TextProps {
content: "one two".into(),
..Default::default()
})],
),
5,
3,
);
assert_eq!(out, "one \ntwo \n ");
}
#[test]
fn text_styles_land_in_cells() {
let (rt, _rx) = RuntimeHandle::test_handle();
let mut tree = FiberTree::new();
tree.mount_root(
Element::text(TextProps {
content: "x".into(),
color: Color::Yellow,
weight: Weight::Bold,
wrap: TextWrap::Truncate,
}),
&rt,
);
compute_layout(&mut tree, 3, 1);
let mut buf = Buffer::new(3, 1);
paint(&tree, &mut buf);
let cell = buf.get(0, 0);
assert_eq!(cell.fg, Color::Yellow);
assert!(cell.attrs.bold);
}
#[test]
fn view_background_fills_cells() {
let (rt, _rx) = RuntimeHandle::test_handle();
let mut tree = FiberTree::new();
tree.mount_root(
Element::view(
ViewProps {
background: Color::Blue,
width: Dimension::Cells(3),
height: Dimension::Cells(3),
..Default::default()
},
vec![],
),
&rt,
);
compute_layout(&mut tree, 3, 3);
let mut buf = Buffer::new(3, 3);
paint(&tree, &mut buf);
assert_eq!(buf.get(1, 1).bg, Color::Blue);
}
#[test]
fn text_control_chars_are_sanitized() {
let (rt, _rx) = RuntimeHandle::test_handle();
let mut tree = FiberTree::new();
tree.mount_root(
Element::text(TextProps {
content: "\x1b]52;c;evil\x07hi".into(),
..Default::default()
}),
&rt,
);
compute_layout(&mut tree, 20, 3);
let mut buf = Buffer::new(20, 3);
paint(&tree, &mut buf);
for y in 0..buf.height() {
for x in 0..buf.width() {
assert!(
!buf.get(x, y).ch.is_control(),
"control char leaked at ({x},{y})"
);
}
}
let out = buf.to_text();
assert!(out.contains("hi"), "visible text missing: {out:?}");
}
#[test]
fn border_color_lands_on_border_cells() {
let (rt, _rx) = RuntimeHandle::test_handle();
let mut tree = FiberTree::new();
tree.mount_root(
Element::view(
ViewProps {
border_style: BorderStyle::Single,
border_color: Color::Red,
width: Dimension::Cells(3),
height: Dimension::Cells(3),
..Default::default()
},
vec![],
),
&rt,
);
compute_layout(&mut tree, 3, 3);
let mut buf = Buffer::new(3, 3);
paint(&tree, &mut buf);
let cell = buf.get(0, 0);
assert_eq!(cell.fg, Color::Red);
assert_eq!(cell.ch, '┌');
}
#[test]
fn justify_content_end_bottom_aligns() {
let (rt, _rx) = RuntimeHandle::test_handle();
let mut tree = FiberTree::new();
tree.mount_root(
Element::view(
ViewProps {
flex_direction: FlexDirection::Column,
justify_content: JustifyContent::End,
width: Dimension::Cells(3),
height: Dimension::Cells(4),
..Default::default()
},
vec![Element::text(TextProps {
content: "x".into(),
..Default::default()
})],
),
&rt,
);
compute_layout(&mut tree, 3, 4);
let mut buf = Buffer::new(3, 4);
paint(&tree, &mut buf);
assert_eq!(buf.get(0, 3).ch, 'x', "child should sit on the bottom row");
assert_eq!(buf.get(0, 0).ch, ' ', "top row should be empty");
}
#[test]
fn overflow_clip_hides_spill() {
fn render_at(overflow: Overflow) -> Buffer {
let (rt, _rx) = RuntimeHandle::test_handle();
let mut tree = FiberTree::new();
tree.mount_root(
Element::view(
ViewProps {
overflow,
flex_direction: FlexDirection::Column,
width: Dimension::Cells(4),
height: Dimension::Cells(2),
..Default::default()
},
vec![Element::text(TextProps {
content: "abcdefghij".into(),
..Default::default()
})],
),
&rt,
);
compute_layout(&mut tree, 4, 4);
let mut buf = Buffer::new(4, 4);
paint(&tree, &mut buf);
buf
}
assert_eq!(
render_at(Overflow::Visible).get(0, 2).ch,
'i',
"spill painted"
);
let clipped = render_at(Overflow::Clip);
assert_eq!(clipped.get(0, 2).ch, ' ', "spill clipped away");
assert_eq!(clipped.get(0, 0).ch, 'a', "visible rows intact");
}
#[test]
fn scroll_offset_and_bottom_follow() {
use crate::hooks::scroll::Scroll;
let (rt, _rx) = RuntimeHandle::test_handle();
let scroll = Scroll::new(0, rt.wake.clone());
let render = |scroll: &Scroll| -> String {
let (rt, _rx) = RuntimeHandle::test_handle();
let mut tree = FiberTree::new();
let rows: Vec<Element> = (0..5)
.map(|i| {
Element::text(TextProps {
content: format!("row{i}"),
..Default::default()
})
})
.collect();
tree.mount_root(
Element::view(
ViewProps {
overflow: Overflow::Scroll,
scroll: Some(scroll.clone()),
flex_direction: FlexDirection::Column,
width: Dimension::Cells(4),
height: Dimension::Cells(2),
..Default::default()
},
rows,
),
&rt,
);
compute_layout(&mut tree, 4, 2);
let mut buf = Buffer::new(4, 2);
paint(&tree, &mut buf);
buf.to_text()
};
assert_eq!(render(&scroll), "row3\nrow4");
assert_eq!(scroll.max_offset(), 3);
assert!(scroll.at_bottom());
scroll.to_top();
assert_eq!(render(&scroll), "row0\nrow1");
scroll.scroll_to(1);
assert_eq!(render(&scroll), "row1\nrow2");
}
#[test]
fn double_border_no_root_and_text_past_buffer() {
let mut empty = Buffer::new(3, 1);
paint(&FiberTree::new(), &mut empty);
let out = render_to_text(
Element::view(
ViewProps {
border_style: BorderStyle::Double,
width: Dimension::Cells(4),
height: Dimension::Cells(3),
..Default::default()
},
vec![],
),
4,
3,
);
assert!(out.contains('\u{2554}'), "double corner");
let (rt, _rx) = RuntimeHandle::test_handle();
let mut tree = FiberTree::new();
tree.mount_root(
Element::text(TextProps {
content: "abcdef".into(),
..Default::default()
}),
&rt,
);
compute_layout(&mut tree, 6, 1);
let mut small = Buffer::new(2, 1);
paint(&tree, &mut small);
assert_eq!(small.get(0, 0).ch, 'a');
}
}