tui-rs icon indicating copy to clipboard operation
tui-rs copied to clipboard

Lacking extensibility

Open simonvpe opened this issue 3 years ago • 0 comments

Problem

I need a way to display a scrolling list of widgets, similar to i.e. card collections in material design. To do this I wrote a simple widget that can draw other widgets but the implementation is a bit lacking since the widgets can't report how large of an area they "need".

Solution

I don't have a solution but I'd like to discuss the option of modifying the API to more easily extend it with custom widgets similar to the card widget.

Additional context

use tui::{
    buffer::{Buffer, Cell},
    layout::Rect,
    widgets::{Block, Widget},
};

/// A Widget that renders a list of widgets. Due to the design of tui-rs it is impossible to know
/// the size of the widget before it is drawn, so the widgets will be drawn to temporary buffers
/// whom are later analyzed and shrunk to fit before being merged into the main buffer. A drawback
/// of this design is that if you add a block with side or bottom borders the entire max size will
/// be used for that widget. So be careful with this one.
pub struct Card<'a, W: Widget> {
    items: Vec<W>,
    block: Option<Block<'a>>,
    max_card_height: u16,
}

impl<'a, W: Widget> Default for Card<'a, W> {
    fn default() -> Self {
        Self {
            items: vec![],
            block: None,
            max_card_height: 10,
        }
    }
}

impl<'a, W: Widget> Card<'a, W> {
    pub fn new(items: Vec<W>) -> Self {
        Self {
            items,
            ..Self::default()
        }
    }

    pub fn block(mut self, block: Block<'a>) -> Self {
        self.block = Some(block);
        self
    }

    pub fn max_card_height(mut self, card_height: u16) -> Self {
        self.max_card_height = card_height;
        self
    }
}

impl<'a, W: Widget> tui::widgets::Widget for Card<'a, W> {
    fn render(self, area: Rect, buf: &mut Buffer) {
        let area = self
            .block
            .map(|block: Block| {
                let inner_area = block.inner(area);
                block.render(area, buf);
                inner_area
            })
            .unwrap_or(area);

        let area = Rect {
            height: self.max_card_height,
            ..area
        };

        let buffers = self.items.into_iter().scan(area.y, |y, widget| {
            let mut buf = Buffer::empty(Rect { y: *y, ..area });
            widget.render(*buf.area(), &mut buf);
            buf = shrink_buffer(buf);
            *y += buf.area().height;
            Some(buf)
        });

        for buffer in buffers {
            buf.merge(&buffer);
        }
    }
}

/// Resizes the buffer by removing untouched rows from the bottom up
fn shrink_buffer(mut buf: Buffer) -> Buffer {
    let area = Rect {
        height: buf
            .content
            .iter()
            .enumerate()
            .fold(0u16, |max_y, (idx, cell)| {
                use std::cmp::max;
                if *cell != Cell::default() {
                    max(max_y, (idx as u16) / buf.area().width + 1)
                } else {
                    max_y
                }
            }),
        ..*buf.area()
    };
    buf.resize(area);
    buf
}

simonvpe avatar Oct 26 '21 18:10 simonvpe