Radish alpha
r
Radicle terminal user interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
lib: Rework widget render properties
Erik Kundt committed 2 years ago
commit 5365ceee8b602e6badef677d94b8362b212e9dc6
parent 921ebc00f978793129a3ff64947073a2490a6860
4 files changed +81 -74
modified src/ui/widget.rs
@@ -33,6 +33,38 @@ pub struct BaseView<S, A> {
    pub on_event: Option<EventCallback<A>>,
}

+
/// General properties that specify how a `Widget` is rendered.
+
/// They can be passed to a widgets' `render` function.
+
#[derive(Clone, Default)]
+
pub struct RenderProps {
+
    /// Focus of the render props.
+
    pub focus: bool,
+
}
+

+
impl RenderProps {
+
    /// Creates render props with focus.
+
    pub fn focused() -> Self {
+
        Self { focus: true }
+
    }
+

+
    /// Creates render props with no focus.
+
    pub fn blurred() -> Self {
+
        Self { focus: false }
+
    }
+

+
    /// Sets the focus of these render props.
+
    pub fn focus(mut self) -> Self {
+
        self.focus = true;
+
        self
+
    }
+

+
    /// Removes the focus from these render props.
+
    pub fn blur(mut self) -> Self {
+
        self.focus = false;
+
        self
+
    }
+
}
+

/// Main trait defining a `Widget` behaviour.
///
/// This is the trait that you should implement to define a custom `Widget`.
@@ -66,8 +98,8 @@ pub trait Widget {

    /// Renders a widget to the given frame in the given area.
    ///
-
    /// Optional props take precedence over the internal ones.
-
    fn render(&self, frame: &mut Frame, area: Rect, props: Option<&dyn Any>);
+
    /// Optional render props can be given.
+
    fn render(&self, frame: &mut Frame, area: Rect, props: Option<RenderProps>);

    /// Return a mutable reference to this widgets' base view.
    fn base_mut(&mut self) -> &mut BaseView<Self::State, Self::Action>;
@@ -104,7 +136,7 @@ pub trait ToRow<const W: usize> {
    fn to_row(&self) -> [Cell; W];
}

-
/// Common trait for view properties.
+
/// Common trait for widget properties.
pub trait Properties {
    fn to_boxed(self) -> Box<Self>
    where
@@ -234,11 +266,7 @@ where
        }
    }

-
    fn render(&self, frame: &mut ratatui::Frame, _area: Rect, props: Option<&dyn Any>) {
-
        let _props = props
-
            .and_then(|props| props.downcast_ref::<WindowProps<Id>>())
-
            .unwrap_or(&self.props);
-

+
    fn render(&self, frame: &mut ratatui::Frame, _area: Rect, _props: Option<RenderProps>) {
        let area = frame.size();

        let page = self
@@ -335,21 +363,18 @@ impl<S, A> Widget for Shortcuts<S, A> {
            ShortcutsProps::from_callback(self.base.on_update, state).unwrap_or(self.props.clone());
    }

-
    fn render(&self, frame: &mut ratatui::Frame, area: Rect, props: Option<&dyn Any>) {
+
    fn render(&self, frame: &mut ratatui::Frame, area: Rect, _props: Option<RenderProps>) {
        use ratatui::widgets::Table;

-
        let props = props
-
            .and_then(|props| props.downcast_ref::<ShortcutsProps>())
-
            .unwrap_or(&self.props);
-

-
        let mut shortcuts = props.shortcuts.iter().peekable();
+
        let mut shortcuts = self.props.shortcuts.iter().peekable();
        let mut row = vec![];

        while let Some(shortcut) = shortcuts.next() {
            let short = Text::from(shortcut.0.clone()).style(style::gray());
            let long = Text::from(shortcut.1.clone()).style(style::gray().dim());
            let spacer = Text::from(String::new());
-
            let divider = Text::from(format!(" {} ", props.divider)).style(style::gray().dim());
+
            let divider =
+
                Text::from(format!(" {} ", self.props.divider)).style(style::gray().dim());

            row.push((shortcut.0.chars().count(), short));
            row.push((1, spacer));
@@ -606,11 +631,7 @@ where
        }
    }

-
    fn render(&self, frame: &mut ratatui::Frame, area: Rect, props: Option<&dyn Any>) {
-
        let props = props
-
            .and_then(|props| props.downcast_ref::<TableProps<R, W>>())
-
            .unwrap_or(&self.props);
-

+
    fn render(&self, frame: &mut ratatui::Frame, area: Rect, _props: Option<RenderProps>) {
        let widths: Vec<Constraint> = self
            .props
            .columns
@@ -618,19 +639,23 @@ where
            .filter_map(|c| if !c.skip { Some(c.width) } else { None })
            .collect();

-
        let widths = if area.width < props.cutoff as u16 {
-
            widths.iter().take(props.cutoff_after).collect::<Vec<_>>()
+
        let widths = if area.width < self.props.cutoff as u16 {
+
            widths
+
                .iter()
+
                .take(self.props.cutoff_after)
+
                .collect::<Vec<_>>()
        } else {
            widths.iter().collect::<Vec<_>>()
        };

-
        if !props.items.is_empty() {
-
            let rows = props
+
        if !self.props.items.is_empty() {
+
            let rows = self
+
                .props
                .items
                .iter()
                .map(|item| {
                    let mut cells = vec![];
-
                    let mut it = props.columns.iter();
+
                    let mut it = self.props.columns.iter();

                    for cell in item.to_row() {
                        if let Some(col) = it.next() {
modified src/ui/widget/container.rs
@@ -1,4 +1,3 @@
-
use std::any::Any;
use std::fmt::Debug;

use tokio::sync::mpsc::UnboundedSender;
@@ -11,7 +10,7 @@ use ratatui::widgets::{Block, BorderType, Borders, Row};
use crate::ui::ext::{FooterBlock, FooterBlockType, HeaderBlock};
use crate::ui::theme::style;

-
use super::{BaseView, BoxedWidget, Column, Properties, Widget};
+
use super::{BaseView, BoxedWidget, Column, Properties, RenderProps, Widget};

#[derive(Clone, Debug)]
pub struct HeaderProps<'a> {
@@ -102,12 +101,9 @@ impl<'a: 'static, S, A> Widget for Header<'a, S, A> {
            .unwrap_or(self.props.clone());
    }

-
    fn render(&self, frame: &mut ratatui::Frame, area: Rect, props: Option<&dyn Any>) {
-
        let props = props
-
            .and_then(|props| props.downcast_ref::<HeaderProps>())
-
            .unwrap_or(&self.props);
-

-
        let widths: Vec<Constraint> = props
+
    fn render(&self, frame: &mut ratatui::Frame, area: Rect, _props: Option<RenderProps>) {
+
        let widths: Vec<Constraint> = self
+
            .props
            .columns
            .iter()
            .filter_map(|column| {
@@ -118,7 +114,8 @@ impl<'a: 'static, S, A> Widget for Header<'a, S, A> {
                }
            })
            .collect();
-
        let cells = props
+
        let cells = self
+
            .props
            .columns
            .iter()
            .filter_map(|column| {
@@ -130,8 +127,11 @@ impl<'a: 'static, S, A> Widget for Header<'a, S, A> {
            })
            .collect::<Vec<_>>();

-
        let widths = if area.width < props.cutoff as u16 {
-
            widths.iter().take(props.cutoff_after).collect::<Vec<_>>()
+
        let widths = if area.width < self.props.cutoff as u16 {
+
            widths
+
                .iter()
+
                .take(self.props.cutoff_after)
+
                .collect::<Vec<_>>()
        } else {
            widths.iter().collect::<Vec<_>>()
        };
@@ -139,7 +139,7 @@ impl<'a: 'static, S, A> Widget for Header<'a, S, A> {
        // Render header
        let block = HeaderBlock::default()
            .borders(Borders::ALL)
-
            .border_style(style::border(props.focus))
+
            .border_style(style::border(self.props.focus))
            .border_type(BorderType::Rounded);

        let header_layout = Layout::default()
@@ -275,12 +275,9 @@ impl<'a: 'static, S, A> Widget for Footer<'a, S, A> {
            .unwrap_or(self.props.clone());
    }

-
    fn render(&self, frame: &mut ratatui::Frame, area: Rect, props: Option<&dyn Any>) {
-
        let props = props
-
            .and_then(|props| props.downcast_ref::<FooterProps>())
-
            .unwrap_or(&self.props);
-

-
        let widths = props
+
    fn render(&self, frame: &mut ratatui::Frame, area: Rect, _props: Option<RenderProps>) {
+
        let widths = self
+
            .props
            .columns
            .iter()
            .map(|c| match c.width {
@@ -290,7 +287,8 @@ impl<'a: 'static, S, A> Widget for Footer<'a, S, A> {
            .collect::<Vec<_>>();

        let layout = Layout::horizontal(widths).split(area);
-
        let cells = props
+
        let cells = self
+
            .props
            .columns
            .iter()
            .map(|c| c.text.clone())
@@ -307,7 +305,7 @@ impl<'a: 'static, S, A> Widget for Footer<'a, S, A> {
                _ if i == last => FooterBlockType::End,
                _ => FooterBlockType::Repeat,
            };
-
            self.render_cell(frame, *area, block_type, cell.clone(), props.focus);
+
            self.render_cell(frame, *area, block_type, cell.clone(), self.props.focus);
        }
    }

@@ -411,13 +409,9 @@ impl<S, A> Widget for Container<S, A> {
        }
    }

-
    fn render(&self, frame: &mut ratatui::Frame, area: Rect, props: Option<&dyn Any>) {
-
        let props = props
-
            .and_then(|props| props.downcast_ref::<ContainerProps>())
-
            .unwrap_or(&self.props);
-

+
    fn render(&self, frame: &mut ratatui::Frame, area: Rect, _props: Option<RenderProps>) {
        let header_h = if self.header.is_some() { 3 } else { 0 };
-
        let footer_h = if self.footer.is_some() && !props.hide_footer {
+
        let footer_h = if self.footer.is_some() && !self.props.hide_footer {
            3
        } else {
            0
@@ -432,7 +426,7 @@ impl<S, A> Widget for Container<S, A> {

        let borders = match (
            self.header.is_some(),
-
            (self.footer.is_some() && !props.hide_footer),
+
            (self.footer.is_some() && !self.props.hide_footer),
        ) {
            (false, false) => Borders::ALL,
            (true, false) => Borders::BOTTOM | Borders::LEFT | Borders::RIGHT,
@@ -441,7 +435,7 @@ impl<S, A> Widget for Container<S, A> {
        };

        let block = Block::default()
-
            .border_style(style::border(props.focus))
+
            .border_style(style::border(self.props.focus))
            .border_type(BorderType::Rounded)
            .borders(borders);
        frame.render_widget(block.clone(), content_area);
modified src/ui/widget/input.rs
@@ -1,5 +1,3 @@
-
use std::any::Any;
-

use termion::event::Key;

use tokio::sync::mpsc::UnboundedSender;
@@ -9,7 +7,7 @@ use ratatui::prelude::Rect;
use ratatui::style::Stylize;
use ratatui::text::{Line, Span};

-
use super::{BaseView, Properties, Widget, WidgetState};
+
use super::{BaseView, Properties, RenderProps, Widget, WidgetState};

#[derive(Clone)]
pub struct TextFieldProps {
@@ -191,20 +189,16 @@ impl<S, A> Widget for TextField<S, A> {
        }
    }

-
    fn render(&self, frame: &mut ratatui::Frame, area: Rect, props: Option<&dyn Any>) {
-
        let props = props
-
            .and_then(|props| props.downcast_ref::<TextFieldProps>())
-
            .unwrap_or(&self.props);
-

+
    fn render(&self, frame: &mut ratatui::Frame, area: Rect, _props: Option<RenderProps>) {
        let layout = Layout::vertical(Constraint::from_lengths([1, 1])).split(area);

        let text = self.state.text.clone().unwrap_or_default();
        let input = text.as_str();
-
        let label = format!(" {} ", props.title);
+
        let label = format!(" {} ", self.props.title);
        let overline = String::from("▔").repeat(area.width as usize);
        let cursor_pos = self.state.cursor_position as u16;

-
        if props.inline_label {
+
        if self.props.inline_label {
            let top_layout = Layout::horizontal([
                Constraint::Length(label.chars().count() as u16),
                Constraint::Length(1),
@@ -221,7 +215,7 @@ impl<S, A> Widget for TextField<S, A> {
            frame.render_widget(input, top_layout[2]);
            frame.render_widget(overline, layout[1]);

-
            if props.show_cursor {
+
            if self.props.show_cursor {
                frame.set_cursor(top_layout[2].x + cursor_pos, top_layout[2].y)
            }
        } else {
@@ -237,7 +231,7 @@ impl<S, A> Widget for TextField<S, A> {
            frame.render_widget(top, layout[0]);
            frame.render_widget(bottom, layout[1]);

-
            if props.show_cursor {
+
            if self.props.show_cursor {
                frame.set_cursor(area.x + cursor_pos, area.y)
            }
        }
modified src/ui/widget/text.rs
@@ -1,5 +1,3 @@
-
use std::any::Any;
-

use tokio::sync::mpsc::UnboundedSender;

use termion::event::Key;
@@ -7,7 +5,7 @@ use termion::event::Key;
use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::text::Text;

-
use super::{BaseView, Properties, Widget, WidgetState};
+
use super::{BaseView, Properties, RenderProps, Widget, WidgetState};

#[derive(Clone)]
pub struct ParagraphProps<'a> {
@@ -205,15 +203,11 @@ impl<'a: 'static, S, A> Widget for Paragraph<'a, S, A> {
            ParagraphProps::from_callback(self.base.on_update, state).unwrap_or(self.props.clone());
    }

-
    fn render(&self, frame: &mut ratatui::Frame, area: Rect, props: Option<&dyn Any>) {
-
        let props = props
-
            .and_then(|props| props.downcast_ref::<ParagraphProps>())
-
            .unwrap_or(&self.props);
-

+
    fn render(&self, frame: &mut ratatui::Frame, area: Rect, _props: Option<RenderProps>) {
        let [content_area] = Layout::horizontal([Constraint::Min(1)])
            .horizontal_margin(1)
            .areas(area);
-
        let content = ratatui::widgets::Paragraph::new(props.content.clone())
+
        let content = ratatui::widgets::Paragraph::new(self.props.content.clone())
            .scroll((self.state.offset as u16, 0));

        frame.render_widget(content, content_area);