#![forbid(unsafe_code)]
use crate::Widget;
use ftui_core::geometry::Rect;
use ftui_render::frame::Frame;
pub struct Group<'a> {
children: Vec<Box<dyn Widget + 'a>>,
}
impl<'a> Group<'a> {
pub fn new() -> Self {
Self {
children: Vec::new(),
}
}
#[must_use]
pub fn push<W: Widget + 'a>(mut self, widget: W) -> Self {
self.children.push(Box::new(widget));
self
}
#[must_use]
pub fn push_boxed(mut self, widget: Box<dyn Widget + 'a>) -> Self {
self.children.push(widget);
self
}
#[inline]
pub fn len(&self) -> usize {
self.children.len()
}
#[inline]
pub fn is_empty(&self) -> bool {
self.children.is_empty()
}
}
impl Default for Group<'_> {
fn default() -> Self {
Self::new()
}
}
impl Widget for Group<'_> {
fn render(&self, area: Rect, frame: &mut Frame) {
if area.is_empty() {
return;
}
for child in &self.children {
child.render(area, frame);
}
}
fn is_essential(&self) -> bool {
self.children.iter().any(|c| c.is_essential())
}
}
#[cfg(test)]
mod tests {
use super::*;
use ftui_render::cell::Cell;
use ftui_render::grapheme_pool::GraphemePool;
fn buf_to_lines(buf: &ftui_render::buffer::Buffer) -> Vec<String> {
let mut lines = Vec::new();
for y in 0..buf.height() {
let mut row = String::with_capacity(buf.width() as usize);
for x in 0..buf.width() {
let ch = buf
.get(x, y)
.and_then(|c| c.content.as_char())
.unwrap_or(' ');
row.push(ch);
}
lines.push(row);
}
lines
}
#[derive(Debug, Clone, Copy)]
struct Fill(char);
impl Widget for Fill {
fn render(&self, area: Rect, frame: &mut Frame) {
for y in area.y..area.bottom() {
for x in area.x..area.right() {
frame.buffer.set(x, y, Cell::from_char(self.0));
}
}
}
}
#[derive(Debug, Clone, Copy)]
struct Dot {
ch: char,
dx: u16,
dy: u16,
}
impl Widget for Dot {
fn render(&self, area: Rect, frame: &mut Frame) {
let x = area.x.saturating_add(self.dx);
let y = area.y.saturating_add(self.dy);
if x < area.right() && y < area.bottom() {
frame.buffer.set(x, y, Cell::from_char(self.ch));
}
}
}
#[test]
fn empty_group_is_noop() {
let group = Group::new();
let area = Rect::new(0, 0, 5, 3);
let mut pool = GraphemePool::new();
let mut frame = Frame::new(5, 3, &mut pool);
group.render(area, &mut frame);
for y in 0..3 {
for x in 0..5u16 {
assert!(frame.buffer.get(x, y).unwrap().is_empty());
}
}
}
#[test]
fn single_child_renders() {
let group = Group::new().push(Fill('A'));
let area = Rect::new(0, 0, 3, 2);
let mut pool = GraphemePool::new();
let mut frame = Frame::new(3, 2, &mut pool);
group.render(area, &mut frame);
assert_eq!(buf_to_lines(&frame.buffer), vec!["AAA", "AAA"]);
}
#[test]
fn later_children_overwrite_earlier() {
let group = Group::new().push(Fill('A')).push(Dot {
ch: 'X',
dx: 1,
dy: 0,
});
let area = Rect::new(0, 0, 3, 1);
let mut pool = GraphemePool::new();
let mut frame = Frame::new(3, 1, &mut pool);
group.render(area, &mut frame);
assert_eq!(buf_to_lines(&frame.buffer), vec!["AXA"]);
}
#[test]
fn deterministic_render_order() {
let group = Group::new().push(Fill('A')).push(Fill('B'));
let area = Rect::new(0, 0, 3, 1);
let mut pool = GraphemePool::new();
let mut frame = Frame::new(3, 1, &mut pool);
group.render(area, &mut frame);
assert_eq!(buf_to_lines(&frame.buffer), vec!["BBB"]);
}
#[test]
fn multiple_dots_compose() {
let group = Group::new()
.push(Dot {
ch: '1',
dx: 0,
dy: 0,
})
.push(Dot {
ch: '2',
dx: 2,
dy: 0,
})
.push(Dot {
ch: '3',
dx: 1,
dy: 1,
});
let area = Rect::new(0, 0, 3, 2);
let mut pool = GraphemePool::new();
let mut frame = Frame::new(3, 2, &mut pool);
group.render(area, &mut frame);
assert_eq!(buf_to_lines(&frame.buffer), vec!["1 2", " 3 "]);
}
#[test]
fn zero_area_is_noop() {
let group = Group::new().push(Fill('X'));
let area = Rect::new(0, 0, 0, 0);
let mut pool = GraphemePool::new();
let mut frame = Frame::new(5, 5, &mut pool);
group.render(area, &mut frame);
for y in 0..5 {
for x in 0..5u16 {
assert!(frame.buffer.get(x, y).unwrap().is_empty());
}
}
}
#[test]
fn len_and_is_empty() {
let g0 = Group::new();
assert!(g0.is_empty());
assert_eq!(g0.len(), 0);
let g1 = Group::new().push(Fill('A'));
assert!(!g1.is_empty());
assert_eq!(g1.len(), 1);
let g3 = Group::new().push(Fill('A')).push(Fill('B')).push(Fill('C'));
assert_eq!(g3.len(), 3);
}
#[test]
fn is_essential_any_child() {
struct Essential;
impl Widget for Essential {
fn render(&self, _: Rect, _: &mut Frame) {}
fn is_essential(&self) -> bool {
true
}
}
assert!(!Group::new().push(Fill('A')).is_essential());
assert!(Group::new().push(Essential).is_essential());
assert!(Group::new().push(Fill('A')).push(Essential).is_essential());
}
#[test]
fn push_boxed_works() {
let boxed: Box<dyn Widget> = Box::new(Fill('Z'));
let group = Group::new().push_boxed(boxed);
assert_eq!(group.len(), 1);
let area = Rect::new(0, 0, 2, 1);
let mut pool = GraphemePool::new();
let mut frame = Frame::new(2, 1, &mut pool);
group.render(area, &mut frame);
assert_eq!(buf_to_lines(&frame.buffer), vec!["ZZ"]);
}
#[test]
fn nested_groups_compose() {
let inner = Group::new().push(Fill('I'));
let outer = Group::new().push(Fill('O')).push(inner);
let area = Rect::new(0, 0, 3, 1);
let mut pool = GraphemePool::new();
let mut frame = Frame::new(3, 1, &mut pool);
outer.render(area, &mut frame);
assert_eq!(buf_to_lines(&frame.buffer), vec!["III"]);
}
#[test]
fn default_group_is_empty() {
let g = Group::default();
assert!(g.is_empty());
assert_eq!(g.len(), 0);
}
#[test]
fn large_group_all_render() {
let n = 20;
let group = (0..n).fold(Group::new(), |g, _| g.push(Fill('X')));
assert_eq!(group.len(), n);
let area = Rect::new(0, 0, 3, 1);
let mut pool = GraphemePool::new();
let mut frame = Frame::new(3, 1, &mut pool);
group.render(area, &mut frame);
assert_eq!(buf_to_lines(&frame.buffer), vec!["XXX"]);
}
#[test]
fn group_with_offset_area() {
let group = Group::new().push(Fill('X'));
let area = Rect::new(2, 1, 3, 2);
let mut pool = GraphemePool::new();
let mut frame = Frame::new(6, 4, &mut pool);
group.render(area, &mut frame);
assert_eq!(
buf_to_lines(&frame.buffer),
vec![" ", " XXX ", " XXX ", " "]
);
}
}